Commits

beyzend committed 1457981 Draft

-server side changes for proof-of-concept. See client commit log.

  • Participants
  • Parent commits 33f82aa

Comments (0)

Files changed (13)

 version: 1
 runtime: python
 api_version: 1
+#threadsafe: no
+
+builtins:
+- remote_api: on
 
 handlers:
 
 # PyAMF Flash Remoting Gateway
-- url: /gateway
-  script: gateway.py    
+#- url: /gateway
+#  script: gateway.py    
 
 # Static: Flash files.
 - url: /swfs
 # Everything else goes to the main app.
 - url: /.*
   script: main.py
+
+#instead of CGI based handler use a WSGI based. See python 2.7 GAE migration guide. NVM. Need to upgrade GAE on dev machine first.
+#- url: /.*
+#  script: main.application
   
 skip_files: |
  ^(.*/)?(
  ^(flex/.*)|
  (psd/.*)|
  (pyamf/tests/.*)|
- (tools/.*)|
+ (tools/.*)
+ #(datastore_script.py) |
+ #(starttown_spawn.json) 
  )$
 

File datastore_script.py

+'''
+Created on Sep 23, 2012
+
+@author: gnulinux
+'''
+import sys
+import json
+import os
+from examples.model import *
+import math as math
+import random as random
+import datetime as datetime
+import copy as copy
+class DataStoreInit():
+    def __init__(self):
+        self.mob_spawns =[]
+        self.MAX_ENTITY = 50
+        self.START_REGION=u'START_REGION_KEY'
+        return
+    
+    def randomDirection(self):
+        randTheta = random.random() * math.pi * 2.0
+        randPhi = random.random() * math.pi * 2.0 
+        return (math.cos(randTheta), math.cos(randPhi))
+        
+    def writeModelDefaultState(self, amodel, spawnX, spawnY):
+        amodel.pos_x = float(spawnX)
+        amodel.pos_y = float(spawnY)
+        amodel.spawn_x = float(spawnX)
+        amodel.spawn_y = float(spawnY)
+        randDir = self.randomDirection()
+        amodel.dir_x = randDir[0]
+        amodel.dir_y = randDir[1]
+        amodel.acceleration_x = 0.0
+        amodel.acceleration_y = 0.0
+        amodel.maxAcceleration = 8.0
+        amodel.state = 1
+        amodel.invulnerable = False
+        amodel.health = 100 #get from templates
+        amodel.thinktime = 0.0
+        timenow = datetime.datetime.now()
+        amodel.timesincesync = timenow
+        amodel.timerespawn = timenow
+        amodel.ordercount = long(0)
+        
+        #amodelInitState = MobInitState(amodel)
+        
+        amodel.put()
+        #amodelInitState.put()
+    def initDB(self):
+        #get home. If it doesn't exist create it.
+        map = Map.get_or_insert(self.START_REGION)
+        
+        #randomize spawns
+        #spawn_sample = random.sample(self.mob_spawns, self.MAX_ENTITY)
+        for i in range(0, self.MAX_ENTITY):
+            spawn = random.choice(self.mob_spawns)
+            amodel = MobEntity(homebase=map)
+        
+            self.writeModelDefaultState(amodel, spawn['x'], spawn['y'])
+            
+    
+    def resetState(self, mob):
+        spawn = random.choice(self.mob_spawns)
+        self.writeModelDefaultState(mob, spawn['x'], spawn['y'])
+    
+    def resetStateAll(self):
+        query = MobEntity.all()
+        mobs = query.fetch()
+        for mob in mobs:
+            spawn = random.choice(self.mob_spawns)
+            self.writeModelDefaultState(mob, spawn['x'], spawn['y'])
+    
+    def parse(self):
+       
+        #full_path = os.path.join(os.path.split(__file__)[:-1], "starttown_spawn.json")
+        print "full_path: ", os.path
+    
+        f = open("data/starttown_spawn.json")
+        value = json.loads(f.read())
+        
+        layers = value["layers"]
+        for layer in layers:
+            if layer["name"] == "spawn_points":
+                spawns = layer
+        #spawns = layers["spawn_points"]["objects"]
+        spawns = spawns["objects"]
+        for spawn in spawns:
+            if spawn["name"] != "Spawn_Player":
+                
+                self.mob_spawns.append({"name":spawn["name"]
+                                        ,"x":spawn["x"]
+                                        ,"y":spawn["y"]})
+        
+        print "MobSpawns: "
+        print self.mob_spawns
+        
+
+if __name__== '__main__':
+    sys.path.append('.')
+    print sys.path
+    dataStoreScript = DataStoreInit()
+    dataStoreScript.parse()
+    dataStoreScript.initDB()
+    

File examples/__init__.py

+# Google App Engine for Flash projects with PyAMF. 
+# Copyright (c) 2008 Aral Balkan (http://aralbalkan.com)
+# Released under the Open Source MIT License
+#
+# Blog post: http://aralbalkan.com/1307 
+# Google App Engine: http://code.google.com/appengine/
+# PyAMF: http://pyamf.org
+
+import wsgiref.handlers
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+import logging
+from urlparse import urlparse
+import os
+
+from gaeswf import BaseSWFHandler
+
+class InitialFlashExample(BaseSWFHandler):
+
+	def get(self, path):		
+		# This is the root app URL that maps to this application in app.yaml.
+		# If this application is accessed from root, set appUrl = '/'. 
+		# In all other cases, set appUrl to the URL defined in your app.yaml 
+		# file with a trailing forward slash but _without_ a forward slashe
+		# at the end (e.g. /examples/swfaddress is correct).
+		appUrl = "/examples/initial/flash"
+
+		# Path to the example SWF
+		#swf = 'http://' + urlparse(self.request.url).netloc + '/swfs/initial.swf';
+		swf = 'http://' + urlparse(self.request.url).netloc + '/swfs/Projectace3.swf'
+		# Handle deep links
+		self.handleDeepLinks(appUrl);
+
+		template_values = {
+			'type': 'Flash',
+			'title': 'Initial Google App Engine Flash client proof-of-concept',
+			'description': '',
+			'appUrl': appUrl,
+			'swf': swf,
+			'width': '800',
+			'height': '600',
+			'basepath': 'http://' + urlparse(self.request.url).netloc			
+		}
+
+		# Write out the HTML file to embed the SWF.
+		# Thanks to Javier for pointing out the static_dir does _not_ work for templates.  
+		# (see http://aralbalkan.com/1307#comment-135454)
+		path = os.path.join(os.path.dirname(__file__), '../templates/example_initial.html')
+		self.response.out.write(template.render(path, template_values, debug=True))
+		
+class InitialFlexExample(BaseSWFHandler):
+
+	def get(self, path):
+		
+		# See comments, above, for explanations.
+				
+		appUrl = "/examples/initial/flex"
+		swf = 'http://' + urlparse(self.request.url).netloc + '/swfs/InitialFlex.swf';
+		#swf = 'http://' + urlparse(self.request.url).netloc + '/swfs/'
+		self.handleDeepLinks(appUrl);
+
+		template_values = {
+			'type': 'Flex',
+			'title': 'Initial Google App Engine Flex 3 client proof-of-concept',
+			'description': '',
+			'appUrl': appUrl,
+			'swf': swf,
+			'width': '550',
+			'height': '550'			
+		}
+
+		path = os.path.join(os.path.dirname(__file__), '../templates/example_initial.html')
+		self.response.out.write(template.render(path, template_values, debug=True))

File examples/__init__.pyc

Binary file removed.

File examples/model.py

+######################################################################
+#
+# The GAE SWF Project (http://gaeswf.appspot.com)
+#
+# Model: Defines the persisted data types.
+# 
+# Copyright (c) 2008 Aral Balkan. Released under the MIT license.
+#
+# Learn more about Google App Engine and other cool stuff at
+# the Singularity Web Conference: Online on October 24-26, 2008
+# http://singularity08.com
+#
+# Blog: http://aralbalkan.com
+#
+######################################################################
+
+from google.appengine.ext import db
+from google.appengine.api import users
+import copy
+
+class Photo(db.Model):
+	user = db.UserProperty()
+	fileBlob = db.BlobProperty()
+	fileName = db.StringProperty(default=None)
+	fileType = db.StringProperty(default=None)
+	fileSize = db.IntegerProperty(default=0)
+	modifiedAt = db.DateTimeProperty(auto_now=True)	
+	authToken = db.StringProperty(default=None)
+	ip = db.StringProperty(default=None)
+
+class UserProfile(db.Model):
+	user = db.UserProperty()
+	name = db.StringProperty()
+	url = db.LinkProperty()
+	description = db.StringProperty()
+	photo = db.ReferenceProperty(Photo)
+	hasPhoto = db.BooleanProperty(default=False)
+	createdAt = db.DateTimeProperty(auto_now_add=True)
+	modifiedAt = db.DateTimeProperty(auto_now=True)
+"""
+MapRegion.
+"""
+class Map(db.Model):
+	def __init__(self, key_name=None, **kwargs):
+		db.Model.__init__(self, key_name=key_name, **kwargs)
+		return
+#	mapDescription = db.StringProperty() #this should be "unique" but unique_constraint not enforced right now
+
+"""
+This remembers a mob's default state so it can return to it. 
+There is a one-to-one relationship between a MobInstance and it's MobDefaultState
+Need to research if there are better ways to store this default state instead of create two 
+identical Model in the DB.
+"""
+"""
+NOTE: Forget default stae for now. Just have a entity instance for testing.
+"""
+class MobEntity(db.Model):
+	homebase = db.ReferenceProperty(Map, collection_name="mobs_in_map")
+	pos_x = db.FloatProperty()
+	pos_y = db.FloatProperty()
+	dir_x = db.FloatProperty()
+	dir_y = db.FloatProperty()
+	spawn_x = db.FloatProperty()
+	spawn_y = db.FloatProperty()
+	acceleration_x = db.FloatProperty()
+	acceleration_y = db.FloatProperty()
+	maxAcceleration = db.FloatProperty()
+	state = db.IntegerProperty()
+	invulnerable = db.BooleanProperty()
+	health = db.IntegerProperty()
+	thinktime = db.FloatProperty()
+	timesincesync = db.DateTimeProperty() #use to determine whether to reset to original state.
+	timerespawn = db.DateTimeProperty()
+	ordercount = db.IntegerProperty() #for testing only. There is not transactions here.
+	
+	
+	
+"""
+Temp. model class for Mob. We need to think HARD about the data model in a real MMO type game.
+
+class MobInstance(db.Model):
+	pos_x = db.FloatProperty()
+	pos_y = db.FloatProperty()
+	dir_x = db.FloatProperty()
+	dir_y = db.FloatProperty()
+	acceleration = db.FloatProperty()
+	maxAcceleration = db.FloatProperty()
+	state = db.IntegerProperty()
+	invulnerable= db.BooleanProperty()
+	health = db.IntegerProperty()
+	
+	def resetState(self, defaultState):
+		self.pos_x = defaultState.pos_x
+		self.pos_y = defaultState.pos_y
+		self.pos_
+"""
+#class Stats(db.Model):
+#	date = db.DateProperty()
+#	newUsers = db.IntegerProperty() 
+#	pageViews = db.IntegerProperty()
+#	logins = db.IntegerProperty()
+#	uniqueVisitors = db.IntegerProperty()

File examples/model.pyc

Binary file removed.

File examples/photo.py

+######################################################################
+#
+# The GAE SWF Project (http://gaeswf.appspot.com)
+#
+# Photo upload/download handlers. These manage photo requests.
+# (Note that the authToken scheme exists because Flash's FileReference
+# feature for uploads/downloads has a bug where it doesn't send along
+# session information. We need the authToken to authenticate users
+# for uploads.)
+# 
+# Copyright (c) 2008 Aral Balkan. Released under the MIT license.
+#
+# Learn more about Google App Engine and other cool stuff at
+# the Singularity Web Conference: Online on October 24-26, 2008
+# http://singularity08.com
+#
+# Blog: http://aralbalkan.com
+#
+######################################################################
+
+import wsgiref.handlers
+from google.appengine.ext import webapp
+
+from examples.model import Photo
+from examples.model import UserProfile
+
+from google.appengine.api import users
+from google.appengine.ext import db
+
+import logging
+
+import string
+
+class PhotoUploadHandler(webapp.RequestHandler):
+	
+	def post(self):
+		authToken = self.request.get('authToken')
+		
+		photo = Photo.all().filter("authToken = ", authToken).get()
+
+		#if photo == None:
+		#	photo = Photo()
+		#	photo.user = user
+
+		if photo:
+			logging.info("Storing photo...")
+			photo.ip = self.request.remote_addr
+		
+			image = self.request.get('upload')
+
+			photo.fileBlob = db.Blob(image)	
+			photo.put()
+			
+			# Until the user crops the photo, set their hasPhoto to false 
+			# so that uncropped photos don't show up in the stream.
+			userProfile = UserProfile.all().filter("user = ", photo.user).get()
+			userProfile.hasPhoto = False
+			userProfile.put()
+
+			# Return something for complete event to fire in Flash. (Thank you, Abdul Qabiz).
+			# http://www.abdulqabiz.com/blog/archives/flash_and_actionscript/workaround_file_1.php
+			# This gets returned as the data in the uploadCompleteData event.
+			self.response.out.write(True)
+		else:
+			logging.info("Error: No such photo.")
+			# Unauthorized	
+			self.error(401)
+
+						
+class PhotoDownloadHandler(webapp.RequestHandler):
+
+	def get(self):
+		user = users.get_current_user()
+		
+		if user:
+			photo = Photo.all().filter("user = ", user).get()
+			
+			if photo:
+				logging.info("Returning image!")
+				# TODO: We really need to return the right content-type. Right now I'm returning JPEG for everything.
+				self.response.headers['Content-Type'] = "image/jpeg"
+				
+				#logging.info(photo.fileBlob)
+				
+				self.response.out.write(photo.fileBlob)
+			else:
+				logging.info("No photo found for user, returning default.")
+				self.redirect('/images/no_image.jpg')
+		else:
+			# Unauthorized
+			self.error(401)

File examples/photo.pyc

Binary file removed.
 
 # PyAMF Flash Remoting (RPC) gateway.
 
+import logging
 import wsgiref.handlers
-from pyamf.remoting.gateway.wsgi import WSGIGateway
 
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+#from pyamf.remoting.gateway.wsgi import WSGIGateway
+from pyamf.remoting.gateway.google import WebAppGateway
 # You can also use a wildcard to import services here.
 #from services import user
 #from services import photo
 from services import mob
 # Service mappings
+
+debug_enabled=True
 s = {
 	#'user': user,
 	#'photo': photo
 	'mob': mob.mob
-}
-
+	}
+gateway = WebAppGateway(s, logger=logging, debug=debug_enabled)
+application_path=[('', gateway)]
+application = webapp.WSGIApplication(application_path
+									, debug=debug_enabled)
+run_wsgi_app(application)
+"""
 def main():
+  
+  
   application = WSGIGateway(s)
   wsgiref.handlers.CGIHandler().run(application)
+  
 
 if __name__ == '__main__':
-  main()
+  main()
+"""
 import wsgiref.handlers
 from google.appengine.ext import webapp
 from google.appengine.ext.webapp import template
+from google.appengine.ext.webapp.util import run_wsgi_app
+
 
 from urlparse import urlparse
 
 from google.appengine.ext import db
 from examples.model import Photo
 
+from pyamf.remoting.gateway.google import WebAppGateway
+from services import mob
+
 class IndexHandler(webapp.RequestHandler):
 
 	def get(self):
 		path = os.path.join(os.path.dirname(__file__), 'templates/simple.html')
 		self.response.out.write(template.render(path, template_vars, debug=True))
 
+#NO WEBAPP2 installed. Upgrade GAE!
+debug_enabled=False
+s = {
+	#'user': user,
+	#'photo': photo
+	'mob': mob.mob
+	,'mob.allmobs': mob.allmobs
+	, 'mob.updatemob': mob.updateMob
+	}
+gateway = WebAppGateway(s, logger=logging, debug=debug_enabled)
+
+application = webapp.WSGIApplication([
+		('/', IndexHandler),
+		('/gateway', gateway),
+		#('/test', TestUploadHandler),
+		#('/photo/upload', photo.PhotoUploadHandler),
+		#('/photo/download', photo.PhotoDownloadHandler),
+		('/examples/initial/flash(/.*)?', InitialFlashExample),
+		#('/examples/initial/flex(/.*)?', InitialFlexExample),
+		('/.*', NotFoundHandler)
+	], debug=True)
+
 def main():
-	
-	
+	run_wsgi_app(application)
 
-	os.environ['DJANGO_SETTINGS_MODULE'] = 'myapp.settings'
-
-	
+if __name__== '__main__':
+	main()
+"""
+def main():
+		
 	application = webapp.WSGIApplication([
 		('/', IndexHandler),
 		#('/test', TestUploadHandler),
 
 if __name__ == '__main__':
   main()
+"""

File services/mob.py

 from pyamf import flex
 from pyamf import amf3
 
-from math import log
-from random import *;
+import math as math
+from random import *
+import datetime as datetime
+
+
+from examples.model import MobEntity 
+
 
 XRANGE=32*32 #there are 32 tiles in the first test map
 YRANGE=32*32
-MEAN_TIME=1.0 / 2.0 #6 seconds mean think time using a exp. distribution random process.
+MEAN_TIME= 1.0 / 6.0 #6 seconds mean think time using a exp. distribution random process.
+
+
+  
 
 def mob(id):
     #assume currentPos is a tuple representing a vector2 where in the order is x,y
     #assume a Poisson variable
     #return {"id":id, "randtime":-log(1.0 - random.random()) / MEAN_TIME}
-    return [id, -log(1.0 - random()) / MEAN_TIME]
+    return [id, -math.log(1.0 - random()) / MEAN_TIME]
+
+def getThinkTime():
+    return max(0.5, -math.log(1.0 - random()) / MEAN_TIME)
+
+def getRandomDir():
+    theta = random() * math.pi
+    phi = random() * math.pi
+    randomDir = (math.cos(theta), math.sin(phi))
+    return randomDir
+
+def checkAndResetMob(mob, timenow, elapsed, sinceRespawn):
+    mob.id = mob.key().id()
+    #if this time is greater than timeout, reset
+    if(float(elapsed) > 50.0 or float(sinceRespawn) > 50.0):
+            mob.pos_x = mob.spawn_x
+            mob.pos_y = mob.spawn_y
+            mob.thinktime = getThinkTime()
+            mob.timesincesync = timenow
+            mob.timerespawn = timenow
+            mob.put()
+            return True
+    return False
+
+def allmobs():
+    #this method will get mobs from db.
+    mobs = MobEntity.all().fetch(100) #should we hardcode limit for maps per map?
+    #we need to update think time
+    for mob in mobs:
+        timenow = datetime.datetime.now()
+        elapsed = (timenow - mob.timesincesync).seconds
+        sinceRespawn = (timenow - mob.timerespawn).seconds
+        checkAndResetMob(mob, timenow, elapsed, 0)
+            #actually need to reset position also
+        
+    return mobs
+#NO INTERPOLATION DONE!!! CAN BECOMEOUT OF SYNC!
+def writeMobToClientSide(mob, mobcs):
+    #we need to update the sync time to reflect elapsed time.
+    timenow = datetime.datetime.now()
+    elapsed = timenow - mob.timesincesync
+    #we can continuesly be out of sync
+    elapsedInSeconds = (float(elapsed.seconds) + float(elapsed.microseconds) / 1000000.0)
+    if(elapsedInSeconds <= mob.thinktime):
+        mobcs['thinktime'] = mob.thinktime  - elapsedInSeconds #conver to seconds
+    else:
+        mobcs['thinktime'] = 0.1; 
+    mobcs['pos_x'] = mob.pos_x
+    mobcs['pos_y'] = mob.pos_y
+    mobcs['dir_x'] = mob.dir_x
+    mobcs['dir_y'] = mob.dir_y
+    #mobcs['acceleration_x'] = mob.acceleration_x
+    #mobcs['acceleration_y'] = mob.acceleration_y
+    mobcs['health'] = mob.health
+    mobcs['timesincesync'] = mob.timesincesync
+    mobcs['ordercount'] = mob.ordercount
+    mobcs['id'] = mob.key().id()
+
+def updateServerMob(mob, mobcs):
+    #think up new think time
+    mob.ordercount += long(1)
+    mob.thinktime = getThinkTime()
+    mob.timesincesync = datetime.datetime.now()
+    mob.pos_x = float(mobcs['pos_x'])
+    mob.pos_y = float(mobcs['pos_y'])
+    #get random direction
+    randDir = getRandomDir()
+    dirTheta = math.atan2(mob.dir_y * -1.0, mob.dir_x * -1.0);
+    mob.dir_x = randDir[0] * math.cos(dirTheta) - randDir[1] * math.sin(dirTheta)
+    mob.dir_y = randDir[1] * math.cos(dirTheta) + randDir[0] * math.sin(dirTheta)
+    #mob.acceleration_x = 
+    
+def updateMob(clientId, mobClientSide):
+    mob = MobEntity.get_by_id(mobClientSide['id'])
+    timenow = datetime.datetime.now()
+    elapsed = (timenow - mob.timesincesync).seconds
+    sinceRespawn = (timenow - mob.timerespawn).seconds
+    if checkAndResetMob(mob, timenow, elapsed, sinceRespawn) == True:
+        return [clientId, mob]
+    #if ordercount is less 
+    #if (mobClientSide['ordercount'] < mob.ordercount): #this means someone before this client has already update the server view. refresh client.
+    if (mobClientSide['timesincesync'] < mob.timesincesync):
+        writeMobToClientSide(mob, mobClientSide)
+        return [clientId, mobClientSide]
+    #Due to the way the calls are a Poisson process, they're semi ordered!
+    #So who ever gets here gets to update server's view. They may become out of sync. But we'll need to eventually
+    #make a MOB go back to spawn point. We can also do server side prediction to mitigate any problems.
+    updateServerMob(mob, mobClientSide)
+    mob.put()
+    mob.id = mob.key().id()
+    return [clientId, mob] 
+    
+    
+    

File templates/base.html

 				<div class="content">
 					<div class="t"></div>
 					<p>
-						<a id="sourceLink" href="http://aralbalkan.com/downloads/The_GAE_SWF_Project_1.63.zip">Source code</a><br/>
-						<span id="additionalInfo">Version 1.63, 4MB.</span><br>
+						<a id="sourceLink" href="https://bitbucket.org/beyzend/projectace3/src">Client Source Code</a><br/>		
+						<span id="additionalInfo">bitbucket repo.</span><br>
+						<a id="sourceLink" href="https://bitbucket.org/beyzend/projectace3_gae_app/src">App Engine Source Code</a><br />
+						<span id="additionalInfo">bitbucket engine.</span><br>
 					</p>
 				</div>
 				<div class="b"><div></div></div>

File templates/example_initial.html

 {% block column2 %}
 <h2>Things to try</h2>
 <ul>
-	<li>The current app base path: {{basepath}}</li>
-	<li>Login to the application using your Google Accounts account (Users API)</li>
-	<li>Add and update a profile (DataStore API)</li>
-	<li>Navigate between Home and Profile and watch the Address Bar of your browser (SWFAddress)</li>
-	<li>Hit <a href="{{appUrl}}/profile" title="Initial Google App Engine {{type}} example, profile page.">the profile page directly</a>. (<em>/profile</em> automatically gets translated to SWFAddress URL, <em>#/profile</em>.)</li>
-	<li>Hit <a href="{{appUrl}}/profile" title="Initial Google App Engine {{type}} example, profile page.">the profile page directly</a> when logged out. Now login. (Intelligent redirection on login.)</li>
-	<li>Change your browser's font size up and down and watch the Flash app. (Making Flash obey browser font-size changes.)</li>
-	{% ifequal type 'Flex' %}
-	<li>Try out the mouse wheel, it works on OS X too. (ExternalMouseWheelSupport).</li>
-	<li>Upload a photo and crop it (File Upload, the new PhotoCropper component, and ByteArray).</li>
-	{% endifequal %}
+	<li>This game is a 2 day proof of concept test using only http request/response to make a game.</li>
+	<li>Proof of concept using Google App Engine for game development</li>
+	<li>The game state exists in the datastore and client-side models. No persistent connection between client & server is maintained.  
+	<li>Still needs work--there is no server side prediction at all, for example.</li>	
+</ul>
+<ul>
+	<li>How to play:</li>
+	<li>up,down,left,right arrow keys to control your main character.</li>
 </ul>
 {% endblock %}