diff --git a/Jormungand.py b/Jormungand.py new file mode 100644 index 0000000..b43824f --- /dev/null +++ b/Jormungand.py @@ -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() \ No newline at end of file diff --git a/src/__init__.py b/__init__.py similarity index 100% rename from src/__init__.py rename to __init__.py diff --git a/assets/ball64.png b/assets/ball64.png new file mode 100644 index 0000000..60d11b5 Binary files /dev/null and b/assets/ball64.png differ diff --git a/assets/tube.png b/assets/tube.png new file mode 100644 index 0000000..b2a11f6 Binary files /dev/null and b/assets/tube.png differ diff --git a/entity/Entity.py b/entity/Entity.py new file mode 100644 index 0000000..2f726a3 --- /dev/null +++ b/entity/Entity.py @@ -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 \ No newline at end of file diff --git a/entity/Tube.py b/entity/Tube.py new file mode 100644 index 0000000..f67e388 --- /dev/null +++ b/entity/Tube.py @@ -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) \ No newline at end of file diff --git a/src/entity/__init__.py b/entity/__init__.py similarity index 100% rename from src/entity/__init__.py rename to entity/__init__.py diff --git a/entity/dummy/DebugBall.py b/entity/dummy/DebugBall.py new file mode 100644 index 0000000..6324b7e --- /dev/null +++ b/entity/dummy/DebugBall.py @@ -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) + \ No newline at end of file diff --git a/src/entity/dummy/DebugJumper.py b/entity/dummy/DebugJumper.py similarity index 53% rename from src/entity/dummy/DebugJumper.py rename to entity/dummy/DebugJumper.py index 7f2dafd..f2590bd 100644 --- a/src/entity/dummy/DebugJumper.py +++ b/entity/dummy/DebugJumper.py @@ -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)) \ No newline at end of file +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)) \ No newline at end of file diff --git a/src/entity/dummy/__init__.py b/entity/dummy/__init__.py similarity index 100% rename from src/entity/dummy/__init__.py rename to entity/dummy/__init__.py diff --git a/src/Fishtank.py b/src/Fishtank.py deleted file mode 100644 index 7ca2a5c..0000000 --- a/src/Fishtank.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/src/entity/Entity.py b/src/entity/Entity.py deleted file mode 100644 index bc7ad90..0000000 --- a/src/entity/Entity.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/entity/Tube.py b/src/entity/Tube.py deleted file mode 100644 index 50af4d4..0000000 --- a/src/entity/Tube.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/src/entity/dummy/DebugWaiter.py b/src/entity/dummy/DebugWaiter.py deleted file mode 100644 index 743610f..0000000 --- a/src/entity/dummy/DebugWaiter.py +++ /dev/null @@ -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)) \ No newline at end of file diff --git a/src/network/NetworkGate.py b/src/network/NetworkGate.py deleted file mode 100644 index 8d8ccf4..0000000 --- a/src/network/NetworkGate.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/src/server.py b/src/server.py deleted file mode 100644 index 7b4d45e..0000000 --- a/src/server.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/util/Util.py b/util/Util.py new file mode 100644 index 0000000..54fdf3e --- /dev/null +++ b/util/Util.py @@ -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 \ No newline at end of file diff --git a/src/network/__init__.py b/util/__init__.py similarity index 100% rename from src/network/__init__.py rename to util/__init__.py diff --git a/world/Fishtank.py b/world/Fishtank.py new file mode 100644 index 0000000..73a0381 --- /dev/null +++ b/world/Fishtank.py @@ -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) \ No newline at end of file diff --git a/world/NetworkGate.py b/world/NetworkGate.py new file mode 100644 index 0000000..3b1cd8d --- /dev/null +++ b/world/NetworkGate.py @@ -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() \ No newline at end of file diff --git a/world/Server.py b/world/Server.py new file mode 100644 index 0000000..103e0a5 --- /dev/null +++ b/world/Server.py @@ -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() \ No newline at end of file diff --git a/world/__init__.py b/world/__init__.py new file mode 100644 index 0000000..e69de29