Add framework for entity serialization

This commit is contained in:
Jaculabilis 2017-10-02 17:19:09 -05:00
parent d400883be1
commit 61e15cff47
22 changed files with 448 additions and 223 deletions

58
Jormungand.py Normal file
View File

@ -0,0 +1,58 @@
import sys
from collections import defaultdict
from util.Util import logout
class Jormungand():
def __init__(self, argv):
command = argv[1] if len(argv) >= 2 else "help"
registry = defaultdict(lambda: Jormungand.help, {
"help": Jormungand.help,
"run": Jormungand.run,
"send": Jormungand.send
})
exec_func = registry[command]
self.execute = lambda: exec_func(argv)
@staticmethod
def help(argv):
print("Hello, world!")
@staticmethod
def run(argv):
if len(argv) < 3:
print("run requires a port")
sys.exit(-1)
port = int(argv[2])
from world.Server import Server
server = Server(port)
logout("Launching server on port {}".format(port), "Jormungand run")
server.run(argv[3:])
@staticmethod
def send(argv):
if len(argv) < 4:
print("send requires a target and payload")
sys.exit(-1)
address = argv[2]
import zmq, json, pygame
pygame.display.set_mode((1,1), pygame.NOFRAME)
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://{}".format(address))
for i in range(3, len(argv)):
if argv[i] == "-ball":
from entity.dummy.DebugBall import DebugBall
e = DebugBall()
s = json.dumps(e.serialize())
print("Sending {}".format(str(e)))
for i in range(100):
socket.send_string(s)
message = socket.recv()
print("Reply: {}".format(message))
def main():
j = Jormungand(sys.argv)
j.execute()
if __name__ == "__main__":
main()

BIN
assets/ball64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

BIN
assets/tube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

83
entity/Entity.py Normal file
View File

@ -0,0 +1,83 @@
import sys
from random import randrange
class Entity(object):
"""
An Entity is something that exists in the Fishtank entities list. The Entity class provides
some basic structure to the behavior of entities, including position and velocity,
serialization, and update and draw. Each entity has a random 8-digit hex id for identification
purposes.
"""
def __init__(self):
self.fishtank = None
self.id = randrange(16**8)
self.x = 0
self.y = 0
self.z = 0
self.vx = 0
self.vy = 0
def __repr__(self):
return "[{}#{:8x} p{},{} z{} v{},{}]".format(type(self).__name__,
self.id, self.x, self.y, self.z, self.vx, self.vy)
def __str__(self):
return "[{}#{:8x}]".format(type(self).__name__, self.id)
def serialize(self):
"""
Returns a JSON-compatbible representation of the current state of this entity.
Subclasses should override this method, call it from their superclass, then update the
returned representation with its own information, including the subclass's module and
class and any idiomatic fields of that class.
Output: serialized representation of this entity
"""
return {
"module":"entity.Entity",
"class":"Entity",
"id":"{:8x}".format(self.id),
"p":[self.x,self.y],
"z":self.z,
"v":[self.vx,self.vy]
}
@staticmethod
def deserialize(serial):
"""
Reconstructs an Entity from a serialized representation as returned by Entity.serialize().
Subclasses should reimplement this method with e as their own class.
Input: serial, a serialized representation of an entity
Output: an entity reproducing the state represented in serial
"""
e = Entity()
return Entity.rebuild(e, serial)
@staticmethod
def rebuild(e, serial):
"""
Helper function for Entity.deserialize().
Subclasses should override this method, call it from their superclass, then add
deserialization for their idiomatic fields.
Input: e, a newly initialized entity
serial, a serialized representation of an entity as passed to deserialize()
Output: an entity reproducing the state represented in serial
"""
e.id = int(serial['id'], 16)
e.x, e.y = serial['p']
e.z = serial['z']
e.vx, e.vy = serial['v']
return e
def update(self, delta):
"""
Updates this entity during the update pass of the game loop.
Input: delta, the number of seconds since the last tick
"""
pass
def draw(self, screen):
"""
Draws this entity during the draw pass of the game loop.
Input: screen, a Surface object to draw this entity on.
"""
pass

32
entity/Tube.py Normal file
View File

@ -0,0 +1,32 @@
import sys
from entity.Entity import Entity
from util.Util import logout, load_image
class Tube(Entity):
def __init__(self, network_gate):
Entity.__init__(self)
self.gate = network_gate
self.inbox = []
self.texture = load_image("tube.png")
self.x, self.y = 200, 200
self.z = -1
def __repr__(self):
return "[Tube gate={} inbox={}]".format(repr(self.gate), repr(self.inbox))
def accept(self, entity):
self.fishtank.remove_entity(entity)
self.inbox.append(entity)
logout("Accepted: {}".format(str(entity), "Tube#{}".format(self.id)))
def update(self, delta):
Entity.update(self, delta)
if self.inbox:
entity = self.inbox.pop(0)
self.gate.transmit(entity.serialize())
def draw(self, screen):
Entity.draw(self, screen)
rect = self.texture.get_rect()
rect.center = (int(self.x), int(self.y))
screen.blit(self.texture, rect)

62
entity/dummy/DebugBall.py Normal file
View File

@ -0,0 +1,62 @@
import sys
import random
import pygame
from entity.Entity import Entity
from entity.Tube import Tube
from util.Util import load_image
class DebugBall(Entity):
"""
A debug entity that bounces around and jumps through network tubes.
"""
def __init__(self):
Entity.__init__(self)
self.vx = 32
self.vy = 128
self.dummy = "DUMMY"
self.texture = load_image("ball64.png")
self.texture = pygame.transform.smoothscale(self.texture, (16,16))
self.rect = pygame.Rect(0, 0, 16, 16)
def serialize(self):
e = Entity.serialize(self)
e.update({
"module":"entity.dummy.DebugBall",
"class":"DebugBall",
"dummy":self.dummy
})
return e
@staticmethod
def deserialize(serial):
e = DebugBall()
return DebugBall.rebuild(e, serial)
@staticmethod
def rebuild(e, serial):
Entity.rebuild(e, serial)
e.dummy = serial["dummy"]
return e
def update(self, delta):
Entity.update(self, delta)
self.x += self.vx * delta
self.y += self.vy * delta
if self.x < 0 or self.x > self.fishtank.size[0]:
self.vx = -self.vx
self.x = 0 if self.x < 0 else self.fishtank.size[0] if self.x > self.fishtank.size[0] else self.x
if self.y < 0 or self.y > self.fishtank.size[1]:
self.vy = -self.vy
self.y = 0 if self.y < 0 else self.fishtank.size[1] if self.y > self.fishtank.size[1] else self.y
for entity in self.fishtank.entities:
if type(entity) is Tube and abs(self.x - entity.x) < 64 and abs(self.y - entity.y) < 64:
entity.accept(self)
break
def draw(self, screen):
Entity.draw(self, screen)
#rect = self.texture.get_rect()
self.rect.center = (int(self.x), int(self.y))
screen.blit(self.texture, self.rect)

View File

@ -1,41 +1,41 @@
import sys
from entity.Entity import Entity
from entity.Tube import Tube
class DebugJumper(Entity):
def __init__(self, message):
Entity.__init__(self)
self._message = message
self._counter = 0
def __repr__(self):
return "[DebugJumper message='{}' counter={}]".format(self._message, self._counter)
def serialize(self):
sup = Entity.serialize(self)
sup.update({
"module":"entity.dummy.DebugJumper",
"class":"DebugJumper",
"message":self._message,
"counter":self._counter
})
return sup
@staticmethod
def deserialize(serial):
e = DebugJumper(serial["message"])
e._counter = serial["counter"]
return e
def update(self):
Entity.update(self)
self._counter += 1
if self._counter % 5 == 0:
for entity in self._fishtank._entities:
if type(entity) is Tube:
entity.accept(self)
break
def draw(self):
Entity.draw(self)
sys.stdout.write(self._message + " ({})\n".format(self._counter))
import sys
from entity.Entity import Entity
from entity.Tube import Tube
class DebugJumper(Entity):
def __init__(self, message):
Entity.__init__(self)
self.message = message
self.counter = 0
def __repr__(self):
return "[DebugJumper @{},{} message='{}' counter={}]".format(self.x, self.y, self.message, self.counter)
def serialize(self):
sup = Entity.serialize(self)
sup.update({
"module":"entity.dummy.DebugJumper",
"class":"DebugJumper",
"message":self.message,
"counter":self.counter
})
return sup
@staticmethod
def deserialize(serial):
e = DebugJumper(serial["message"])
e.counter = serial["counter"]
return e
def update(self, delta):
Entity.update(self)
self.counter += 1
if self.counter % 5 == 0:
for entity in self.fishtank.entities:
if type(entity) is Tube:
entity.accept(self)
break
def draw(self, screen):
Entity.draw(self)
sys.stdout.write(self.message + " ({})\n".format(self.counter))

View File

@ -1,43 +0,0 @@
import sys
import time
import json
class Fishtank():
def __init__(self, recv_queue):
self._entities = []
self._to_remove = []
self._recv_queue = recv_queue
def run(self):
while True:
# Delete flagged entities
if self._to_remove:
for entity in self._to_remove:
self._entities.remove(entity)
self._to_remove = []
# Update and draw
for entity in self._entities:
entity.update()
for entity in self._entities:
entity.draw()
#time.sleep(1)
# Intake queue
if not self._recv_queue.empty():
serial = self._recv_queue.get(False)
sys.stdout.write("Fishtank dequeued a {}\n".format(serial["class"]))
mod = __import__(serial["module"], fromlist=[serial["class"]])
klass = getattr(mod, serial["class"])
e = klass.deserialize(serial)
self.add_entity(e)
def add_entity(self, entity):
entity._fishtank = self
self._entities.append(entity)
sys.stdout.write("Added: {}\n".format(repr(entity)))
def remove_entity(self, entity):
if entity not in self._entities:
sys.stderr.write(
"WARN: remove called for entity '{}', but it isn't in the eneityt list\n".format(entity.__name__))
return
self._to_remove.append(entity)

View File

@ -1,25 +0,0 @@
import sys
class Entity(object):
def __init__(self):
pass
def __repr__(self):
return "[Entity]"
def serialize(self):
return {
"module":"entity.Entity",
"class":"Entity"
}
@staticmethod
def deserialize(serial):
e = Entity()
return e
def update(self):
pass
def draw(self):
pass

View File

@ -1,22 +0,0 @@
import sys
from entity.Entity import Entity
class Tube(Entity):
def __init__(self, network_gate):
Entity.__init__(self)
self._gate = network_gate
self._inbox = []
def __repr__(self):
return "[Tube gate={} inbox={}]".format(repr(self._gate), repr(self._inbox))
def accept(self, entity):
self._fishtank.remove_entity(entity)
self._inbox.append(entity)
def update(self):
Entity.update(self)
if self._inbox:
entity = self._inbox.pop(0)
sys.stdout.write("Sending: {}\n".format(repr(entity)))
self._gate.transmit(entity.serialize())

View File

@ -1,19 +0,0 @@
import sys
from entity.Entity import Entity
class DebugWaiter(Entity):
def __init__(self, message):
Entity.__init__(self)
self._message = message
self._counter = 0
def __repr__(self):
return "[DebugWaiter message='{}' counter={}]".format(self._message, self._counter)
def update(self):
Entity.update(self)
self._counter += 1
def draw(self):
Entity.draw(self)
sys.stdout.write(self._message + " ({})\n".format(self._counter))

View File

@ -1,19 +0,0 @@
import sys
import zmq
import json
class NetworkGate():
def __init__(self, address, port):
self._address = address
self._port = port
context = zmq.Context()
self._socket = context.socket(zmq.REQ)
self._socket.connect("tcp://{}:{}".format(address, port))
def __repr__(self):
return "[NetworkGate {}:{}]".format(self._address, self._port)
def transmit(self, serial):
s = json.dumps(serial)
self._socket.send_string(s)
message = self._socket.recv()

View File

@ -1,54 +0,0 @@
import sys
import zmq
import json
import time
from multiprocessing import Process, Queue
from Fishtank import Fishtank
def socket_listener(port, recv_queue):
sys.stdout.write("Socket listener starting...\n")
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:{}".format(port))
while True:
message = socket.recv()
response = b"Undefined response"
try:
serial = json.loads(message)
if "class" in serial:
sys.stdout.write("Listener received a {}\n".format(serial["class"]))
recv_queue.put(serial, False)
response = b"Received"
except:
response = b"Error"
socket.send(response)
def main():
port = int(sys.argv[1])
sys.stdout.write("Launching on port {}\n".format(port))
# Spawn the socket thread
q = Queue()
socket_proc = Process(target=socket_listener, args=(port,q))
socket_proc.start()
sys.stdout.write("Socket thread started\n")
time.sleep(1)
# Build the world
fishtank = Fishtank(q)
for i in range(1, len(sys.argv)):
if sys.argv[i] == "-w":
from entity.dummy.DebugWaiter import DebugWaiter
fishtank.add_entity(DebugWaiter("DebugWaiter"))
if sys.argv[i] == "-j":
from entity.dummy.DebugJumper import DebugJumper
fishtank.add_entity(DebugJumper("DebugJumper"))
if sys.argv[i] == "-t":
pipe_port = int(sys.argv[i+1])
from network.NetworkGate import NetworkGate
network_gate = NetworkGate("localhost", pipe_port)
from entity.Tube import Tube
fishtank.add_entity(Tube(network_gate))
fishtank.run()
if __name__ == "__main__":
main()

26
util/Util.py Normal file
View File

@ -0,0 +1,26 @@
import datetime
from os.path import join
import sys
import pygame
def logout(s, tag=""):
sys.stdout.write("[{:%Y-%m-%d %H:%M:%S} {}] {}\n".format(datetime.datetime.now(), tag, s))
def logerr(s, tag=""):
sys.stderr.write("[{:%Y-%m-%d %H:%M:%S} {}] {}\n".format(datetime.datetime.now(), tag, s))
def load_image(filename):
path = join("assets", filename)
try:
image = pygame.image.load(path)
except:
logerr("ERROR: Image '{}' not found".format(filename), "load_image")
sys.exit(-1)
#try:
if image.get_alpha() is None:
image = image.convert()
else:
image = image.convert_alpha()
#except:
# logerr("ERROR: Image '{}' not converted".format(filename))
return image

71
world/Fishtank.py Normal file
View File

@ -0,0 +1,71 @@
import sys
import time
import json
import pygame
from util.Util import logout, logerr
class Fishtank():
def __init__(self, recv_queue):
self.entities = []
self.to_remove = []
self.recv_queue = recv_queue
self.size = (480, 360)
self.screen = pygame.display.set_mode(self.size)
def run(self):
"""Begins the game loop. Does not return."""
clock = pygame.time.Clock()
while True:
# Upkeep
milli = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
# Delete flagged entities
if self.to_remove:
for entity in self.to_remove:
self.entities.remove(entity)
self.to_remove = []
# Update
for entity in self.entities:
entity.update(milli)
# Intake queue
if not self.recv_queue.empty():
serial = self.recv_queue.get(False)
logout("Dequeued: {}".format(serial["class"]), "Fishtank")
mod = __import__(serial["module"], fromlist=[serial["class"]])
klass = getattr(mod, serial["class"])
e = klass.deserialize(serial)
self.add_entity(e)
# Draw
self.screen.fill((100, 149, 237))
for entity in sorted(self.entities, key=lambda e:e.z):
entity.draw(self.screen)
pygame.display.flip()
def add_entity(self, entity):
"""
Adds an entity to the entity list and sets its fishtank property to this.
Input: entity, the entity to add
"""
entity.fishtank = self
self.entities.append(entity)
logout("Added: {}".format(repr(entity)), "Fishtank")
def remove_entity(self, entity):
"""
Marks an entity to be removed before the next update pass.
Input: entity, the entity to remove
"""
if entity not in self.entities:
logerr("WARN: remove called for entity '{}',"\
"but it isn't in the entity list".format(entity.__name__), "Fishtank")
return
self.to_remove.append(entity)

21
world/NetworkGate.py Normal file
View File

@ -0,0 +1,21 @@
import sys
import zmq
import json
from util.Util import logout
class NetworkGate():
def __init__(self, address, port):
self.address = address
self.port = port
context = zmq.Context()
self.socket = context.socket(zmq.REQ)
self.socket.connect("tcp://{}:{}".format(address, port))
def __repr__(self):
return "[NetworkGate {}:{}]".format(self.address, self.port)
def transmit(self, serial):
s = json.dumps(serial)
logout("Sending: {}".format(serial["class"]))
self.socket.send_string(s)
message = self.socket.recv()

54
world/Server.py Normal file
View File

@ -0,0 +1,54 @@
import zmq
import json
import time
from multiprocessing import Process, Queue
from world.Fishtank import Fishtank
from util.Util import logout
class Server(object):
def __init__(self, recv_port):
self.recv_port = recv_port
self.recv_queue = Queue()
self.recv_proc = Process(
target=self.socket_listener,
args=(self.recv_port, self.recv_queue))
def socket_listener(self, port, recv_queue):
logout("Socket thread starting", "Server.socket_listener")
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:{}".format(port))
while True:
message = socket.recv()
response = b"Undefined response"
try:
serial = json.loads(message)
if "class" in serial:
logout("Received: {}".format(serial["class"]), "Server.socket_listener")
recv_queue.put(serial, False)
response = b"Received"
except:
response = b"Error"
socket.send(response)
def run(self, argv):
# Launch the recv process
self.recv_proc.start()
logout("Socket thread launched", "Server")
time.sleep(0.2)
# Build the world
fishtank = Fishtank(self.recv_queue)
for i in range(len(argv)):
if argv[i] == "-ball":
from entity.dummy.DebugBall import DebugBall
fishtank.add_entity(DebugBall())
if argv[i] == "-tube":
tube_port = int(argv[i+1])
from world.NetworkGate import NetworkGate
network_gate = NetworkGate("localhost", tube_port)
from entity.Tube import Tube
fishtank.add_entity(Tube(network_gate))
fishtank.run()

0
world/__init__.py Normal file
View File