Explorar o código

Mostly implemented backend (still need a gui)

Taddeus Kroes %!s(int64=11) %!d(string=hai) anos
pai
achega
28cdcaa5fb
Modificáronse 5 ficheiros con 240 adicións e 32 borrados
  1. 5 1
      Makefile
  2. 72 22
      game.coffee
  3. 1 1
      game.py
  4. 161 7
      server.py
  5. 1 1
      www/index.html

+ 5 - 1
Makefile

@@ -1,4 +1,4 @@
-.PHONY: all clean
+.PHONY: all check clean
 
 all: www/style.css www/game.js
 
@@ -8,5 +8,9 @@ www/%.css: %.sass
 www/%.js: %.coffee
 	coffee -o $(@D) $<
 
+check:
+	pyflakes *.py
+	pep8 *.py
+
 clean:
 	rm -f www/*.js www/*.css

+ 72 - 22
game.coffee

@@ -18,7 +18,7 @@ divin = (parent, cls) ->
     parent.appendChild elem
     elem
 
-class Game
+class Board
     constructor: (@w, @h, elem) ->
         @render elem if elem
 
@@ -75,24 +75,74 @@ class Game
                 divin row, "wall-h#{clicked}"
             divin row, 'dot'
 
-game = new Game 6, 6
-game.render document.getElementById 'game'
-game.click_wall 2, 2, WALL_RIGHT
-game.click_wall 2, 2, WALL_BOTTOM
-game.click_wall 1, 2, WALL_RIGHT
-game.click_wall 2, 2, WALL_TOP
-game.occupy 2, 2, 2
-
-#ws = new WebSocket URL
-#
-#ws.onopen = ->
-#    console.log 'open'
-#
-#ws.onclose = ->
-#    console.log 'close'
-#
-#ws.onerror = (e) ->
-#    console.log 'error', e
-#
-#ws.onmessage = (msg) ->
-#    console.log 'msg', msg
+    destroy: ->
+        @board.parentNode.removeChild @board
+
+show_error = alert
+
+set_this_player = (id) ->
+
+add_other_player = (id) ->
+
+remove_other_player = (id) ->
+
+set_turn_player = (id) ->
+
+finish = (scores) ->
+    console.log 'scores:', scores
+
+ws = new WebSocket URL
+
+ws.send_msg = (mtype, args...) ->
+    @send [mtype].concat(args).join ';'
+
+ws.onopen = ->
+    console.debug 'open'
+
+    if location.hash
+        @send_msg 'join', location.hash.substr 1
+    else
+        @send_msg 'newgame', 5, 6
+
+ws.onclose = ->
+    console.debug 'close'
+    @board?.destroy()
+
+ws.onerror = (e) ->
+    console.debug 'error', e
+
+ws.onmessage = (msg) ->
+    [mtype, args...] = msg.data.split ';'
+    args = ((if s.match /^\d+$/ then parseInt s else s) for s in args)
+    console.debug 'msg:', mtype, args
+
+    switch mtype
+        when 'newgame'
+            [@sid, w, h, player] = args
+            @board = new Board w, h
+            @board.render document.getElementById 'board'
+            location.hash = @sid
+            set_this_player player
+        when 'join'
+            add_other_player args[0]
+        when 'leave'
+            remove_other_player args[0]
+        when 'clickwall'
+            [x, y, direction] = args
+            @board.click_wall x, y, direction
+        when 'occupy'
+            [x, y, player] = args
+            @board.occupy x, y, player
+        when 'turn'
+            set_turn_player args[0]
+        when 'finish'
+            finish (s.split(':').map parseInt for s in args)
+        when 'error'
+            error = args[0]
+
+            if error == 'no such session'
+                @send_msg 'newgame', 5, 6
+            else
+                show_error error
+        else
+            show_error 'received invalid message from server'

+ 1 - 1
game.py

@@ -6,7 +6,7 @@ WALL_LEFT   = 8
 WALL_ALL = WALL_TOP | WALL_RIGHT | WALL_BOTTOM | WALL_LEFT
 
 
-class Game:
+class Board:
     def __init__(self, w, h):
         assert w > 1 and h > 1
         self.w = w

+ 161 - 7
server.py

@@ -1,20 +1,174 @@
 #!/usr/bin/env python2
+import sys
+import logging
 import time
 from hashlib import sha1
 
-import wspy
+from wspy import AsyncServer, TextMessage
+from game import Board
+
+
+class BadRequest(RuntimeError):
+    pass
+
+
+def check(condition, error='invalid type or args'):
+    if not condition:
+        raise BadRequest(error)
+
+
+class Msg:
+    def __init__(self, mtype, *args):
+        self.mtype = mtype
+        self.args = args
+
+    @classmethod
+    def decode(cls, message):
+        check(isinstance(message, TextMessage))
+        parts = message.payload.split(';')
+        check(parts)
+        mtype = parts[0]
+        args = [int(p) if p.isdigit() else p for p in parts[1:]]
+        return cls(mtype, *args)
+
+    def encode(self):
+        return TextMessage(';'.join([self.mtype] + map(str, self.args)))
+
+
+STATE_JOINING = 0
+STATE_STARTED = 1
+STATE_FINISHED = 2
 
 
 class Session:
-    def __init__(self):
-        self.sid = sha1(str(time.time()))
-        self.clients = []
+    def __init__(self, w, h, owner):
+        self.sid = sha1(str(time.time())).hexdigest()
+        self.clients = [owner]
+        owner.player = 1
+        self.player_counter = 2
+        self.state = STATE_JOINING
+        self.board = Board(w, h)
+        self.turn = owner
+
+        owner.send(Msg('newgame', self.sid, w, h).encode())
+        owner.send(Msg('turn', self.turn.player).encode())
+
+    def __str__(self):
+        return '<Session %s state=%d size=%dx%d>' % \
+                (self.sid, self.state, self.board.w, self.board.h)
+
+    def click_wall(self, client, x, y, direction):
+        check(self.turn is client, 'not your turn')
+        check(self.state < STATE_FINISHED, 'already finished')
+        self.state = STATE_STARTED
+
+        occupied = self.board.click_wall(x, y, direction, client.player)
+        self.bcast('clickwall', x, y, direction)
+
+        if occupied:
+            for x, y in occupied:
+                self.bcast('occupy', x, y, client.player)
+
+            if self.board.is_finished():
+                scores = self.board.scores()
+
+                for player in xrange(1, self.player_counter):
+                    scores.setdefault(player, 0)
+
+                scores = scores.items()
+                scores.sort(key=lambda (player, score): score, reverse=True)
+
+                self.bcast('finish', *['%d:%d' % s for s in scores])
+                logging.info('finishing session %s' % self.sid)
+                self.state = STATE_FINISHED
+        else:
+            index = (self.clients.index(self.turn) + 1) % len(self.clients)
+            self.turn = self.clients[index]
 
+    def bcast(self, mtype, *args):
+        encoded = Msg(mtype, *args).encode()
+
+        for client in self.clients:
+            client.send(encoded)
+
+    def join(self, client):
+        client.player = self.player_counter
+        self.player_counter += 1
+
+        self.bcast('join', client.player)
+
+        client.send(Msg('newgame', self.sid, self.board.w, self.board.h,
+                        client.player).encode())
+        client.send(Msg('turn', self.turn.player).encode())
+
+        for other in self.clients:
+            client.send(Msg('join', other.player).encode())
+
+        self.clients.append(client)
+
+    def leave(self, client):
+        self.clients.remove(client)
+        self.bcast('leave', client.player)
+
+    def is_dead(self):
+        return not self.clients
+
+
+class GameServer(AsyncServer):
+    def __init__(self, *args, **kwargs):
+        super(GameServer, self).__init__(*args, **kwargs)
+        self.sessions = {}
 
-class GameServer(wspy.AsyncServer):
     def onmessage(self, client, message):
-        pass
+        try:
+            msg = Msg.decode(message)
+
+            if msg.mtype == 'newgame':
+                check(len(msg.args) == 2)
+                w, h = msg.args
+                client.session = session = Session(w, h, client)
+                self.sessions[session.sid] = session
+                logging.info('%s created session %s' % (client, session))
+
+            elif msg.mtype == 'join':
+                check(len(msg.args) == 1)
+                sid = msg.args[0]
+                check(not hasattr(client, 'session'), 'already in a session')
+                check(sid in self.sessions, 'no such session')
+                session = self.sessions[sid]
+                check(session.state == STATE_JOINING, 'game already started')
+                session.join(client)
+                client.session = session
+                logging.info('%s joined %s' % (client, session))
+
+            elif msg.mtype == 'clickwall':
+                check(len(msg.args) == 3)
+                x, y, direction = msg.args
+                check(client.session, 'no session associated with client')
+                client.session.click_wall(client, x, y, direction)
+
+            else:
+                raise BadRequest('unknown message type')
+
+        except BadRequest as e:
+            logging.warning('bad request: %s' % e.message)
+            client.send(Msg('error', e.message).encode())
+
+    def onclose(self, client, code, reason):
+        if hasattr(client, 'session'):
+            client.session.leave(client)
+            logging.info('%s left %s' % (client, client.session))
+
+            if client.session.is_dead():
+                logging.info('deleting session %s' % client.session)
+                del self.sessions[client.session.sid]
 
 
 if __name__ == '__main__':
-    GameServer(('', 8099)).run()
+    if len(sys.argv) < 2:
+        print >>sys.stderr, 'usage: % PORT' % sys.argv[0]
+        sys.exit(1)
+
+    port = int(sys.argv[1])
+    GameServer(('', port)).run()
+    #GameServer(('', port), loglevel=logging.DEBUG).run()

+ 1 - 1
www/index.html

@@ -5,7 +5,7 @@
         <link href="style.css" rel="stylesheet" type="text/css">
     </head>
     <body>
-        <div id="game" class="game"></div>
+        <div id="board"></div>
         <script src="game.js"></script>
     </body>
 </html>