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
  7. from game import Board
  8. class BadRequest(RuntimeError):
  9. pass
  10. def check(condition, error='invalid type or args'):
  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.bcast('finish', *['%d:%d' % s for s in scores])
  60. logging.info('finishing session %s' % self.sid)
  61. self.state = STATE_FINISHED
  62. else:
  63. index = (self.clients.index(self.turn) + 1) % len(self.clients)
  64. self.turn = self.clients[index]
  65. self.bcast('turn', self.turn.player)
  66. def bcast(self, mtype, *args):
  67. encoded = Msg(mtype, *args).encode()
  68. for client in self.clients:
  69. client.send(encoded)
  70. def join(self, client):
  71. client.player = self.player_counter
  72. self.player_counter += 1
  73. self.bcast('join', client.player)
  74. client.send(Msg('newgame', self.sid, self.board.w, self.board.h,
  75. client.player).encode())
  76. client.send(Msg('turn', self.turn.player).encode())
  77. for other in self.clients:
  78. client.send(Msg('join', other.player).encode())
  79. self.clients.append(client)
  80. def leave(self, client):
  81. self.clients.remove(client)
  82. self.bcast('leave', client.player)
  83. def is_dead(self):
  84. return not self.clients
  85. class GameServer(AsyncServer):
  86. def __init__(self, *args, **kwargs):
  87. super(GameServer, self).__init__(*args, **kwargs)
  88. self.sessions = {}
  89. def onmessage(self, client, message):
  90. try:
  91. msg = Msg.decode(message)
  92. if msg.mtype == 'newgame':
  93. check(len(msg.args) == 2)
  94. w, h = msg.args
  95. client.session = session = Session(w, h, client)
  96. self.sessions[session.sid] = session
  97. logging.info('%s created session %s' % (client, session))
  98. elif msg.mtype == 'join':
  99. check(len(msg.args) == 1)
  100. sid = msg.args[0]
  101. check(not hasattr(client, 'session'), 'already in a session')
  102. check(sid in self.sessions, 'no such session')
  103. session = self.sessions[sid]
  104. check(session.state == STATE_JOINING, 'game already started')
  105. session.join(client)
  106. client.session = session
  107. logging.info('%s joined %s' % (client, session))
  108. elif msg.mtype == 'clickwall':
  109. check(len(msg.args) == 3)
  110. x, y, direction = msg.args
  111. check(client.session, 'no session associated with client')
  112. client.session.click_wall(client, x, y, direction)
  113. else:
  114. raise BadRequest('unknown message type')
  115. except BadRequest as e:
  116. logging.warning('bad request: %s' % e.message)
  117. client.send(Msg('error', e.message).encode())
  118. def onclose(self, client, code, reason):
  119. if hasattr(client, 'session'):
  120. client.session.leave(client)
  121. logging.info('%s left %s' % (client, client.session))
  122. if client.session.is_dead():
  123. logging.info('deleting session %s' % client.session)
  124. del self.sessions[client.session.sid]
  125. if __name__ == '__main__':
  126. if len(sys.argv) < 2:
  127. print >>sys.stderr, 'usage: % PORT' % sys.argv[0]
  128. sys.exit(1)
  129. port = int(sys.argv[1])
  130. GameServer(('', port)).run()
  131. #GameServer(('', port), loglevel=logging.DEBUG).run()