server.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/env python2
  2. import sys
  3. import logging
  4. import time
  5. from hashlib import sha1
  6. from wspy import AsyncServer, TextMessage, DeflateFrame, WebkitDeflateFrame
  7. from game import Board
  8. class BadRequest(RuntimeError):
  9. pass
  10. def check(condition, error='invalid format'):
  11. if not condition:
  12. raise BadRequest(error)
  13. class Msg:
  14. def __init__(self, mtype, *args):
  15. self.mtype = mtype
  16. self.args = args
  17. @classmethod
  18. def decode(cls, message):
  19. check(isinstance(message, TextMessage))
  20. parts = message.payload.split(';')
  21. check(parts)
  22. mtype = parts[0]
  23. args = [int(p) if p.isdigit() else p for p in parts[1:]]
  24. return cls(mtype, *args)
  25. def encode(self):
  26. return TextMessage(';'.join([self.mtype] + map(str, self.args)))
  27. STATE_JOINING = 0
  28. STATE_STARTED = 1
  29. STATE_FINISHED = 2
  30. class Session:
  31. def __init__(self, w, h, owner):
  32. self.sid = sha1(str(time.time())).hexdigest()
  33. self.clients = [owner]
  34. owner.player = 1
  35. self.player_counter = 2
  36. self.state = STATE_JOINING
  37. self.board = Board(w, h)
  38. self.turn = owner
  39. owner.send(Msg('newgame', self.sid, w, h).encode())
  40. owner.send(Msg('turn', self.turn.player).encode())
  41. def __str__(self):
  42. return '<Session %s state=%d size=%dx%d>' % \
  43. (self.sid, self.state, self.board.w, self.board.h)
  44. def click_wall(self, client, x, y, direction):
  45. check(self.turn is client, 'not your turn')
  46. check(self.state < STATE_FINISHED, 'already finished')
  47. self.state = STATE_STARTED
  48. occupied = self.board.click_wall(x, y, direction, client.player)
  49. self.bcast('clickwall', x, y, direction)
  50. if occupied:
  51. for x, y in occupied:
  52. self.bcast('occupy', x, y, client.player)
  53. if self.board.is_finished():
  54. scores = self.board.scores()
  55. for player in xrange(1, self.player_counter):
  56. scores.setdefault(player, 0)
  57. scores = scores.items()
  58. scores.sort(key=lambda (player, score): score, reverse=True)
  59. self.state = STATE_FINISHED
  60. self.bcast('finish', *['%d:%d' % s for s in scores])
  61. else:
  62. index = (self.clients.index(self.turn) + 1) % len(self.clients)
  63. self.turn = self.clients[index]
  64. self.bcast('turn', self.turn.player)
  65. def bcast(self, mtype, *args):
  66. encoded = Msg(mtype, *args).encode()
  67. for client in self.clients:
  68. client.send(encoded)
  69. def join(self, client):
  70. client.player = self.player_counter
  71. self.player_counter += 1
  72. self.bcast('join', client.player)
  73. client.send(Msg('newgame', self.sid, self.board.w, self.board.h,
  74. client.player).encode())
  75. client.send(Msg('turn', self.turn.player).encode())
  76. for other in self.clients:
  77. client.send(Msg('join', other.player).encode())
  78. self.clients.append(client)
  79. def leave(self, client):
  80. self.clients.remove(client)
  81. self.bcast('leave', client.player)
  82. def is_dead(self):
  83. return not self.clients
  84. class GameServer(AsyncServer):
  85. def __init__(self, *args, **kwargs):
  86. super(GameServer, self).__init__(*args, **kwargs)
  87. self.sessions = {}
  88. def onmessage(self, client, message):
  89. try:
  90. msg = Msg.decode(message)
  91. if msg.mtype == 'newgame':
  92. check(len(msg.args) == 2)
  93. w, h = msg.args
  94. client.session = session = Session(w, h, client)
  95. self.sessions[session.sid] = session
  96. logging.info('%s created session %s' % (client, session))
  97. elif msg.mtype == 'join':
  98. check(len(msg.args) == 1)
  99. sid = msg.args[0]
  100. check(not hasattr(client, 'session'), 'already in a session')
  101. check(sid in self.sessions, 'no such session')
  102. session = self.sessions[sid]
  103. check(session.state == STATE_JOINING, 'game already started')
  104. session.join(client)
  105. client.session = session
  106. logging.info('%s joined %s' % (client, session))
  107. elif msg.mtype == 'clickwall':
  108. check(len(msg.args) == 3)
  109. x, y, direction = msg.args
  110. check(client.session, 'no session associated with client')
  111. client.session.click_wall(client, x, y, direction)
  112. if client.session.state == STATE_FINISHED:
  113. logging.info('session %s finished' % client.session.sid)
  114. else:
  115. raise BadRequest('unknown message type')
  116. except BadRequest as e:
  117. logging.warning('bad request: %s' % e.message)
  118. client.send(Msg('error', e.message).encode())
  119. def onclose(self, client, code, reason):
  120. if hasattr(client, 'session'):
  121. client.session.leave(client)
  122. logging.info('%s left %s' % (client, client.session))
  123. if client.session.is_dead():
  124. logging.info('deleting session %s' % client.session)
  125. del self.sessions[client.session.sid]
  126. if __name__ == '__main__':
  127. port = int(sys.argv[1]) if len(sys.argv) > 1 else 8099
  128. GameServer(('localhost', port),
  129. extensions=[DeflateFrame(), WebkitDeflateFrame()],
  130. loglevel=logging.DEBUG).run()