server.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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. 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. else:
  113. raise BadRequest('unknown message type')
  114. except BadRequest as e:
  115. logging.warning('bad request: %s' % e.message)
  116. client.send(Msg('error', e.message).encode())
  117. def onclose(self, client, code, reason):
  118. if hasattr(client, 'session'):
  119. client.session.leave(client)
  120. logging.info('%s left %s' % (client, client.session))
  121. if client.session.is_dead():
  122. logging.info('deleting session %s' % client.session)
  123. del self.sessions[client.session.sid]
  124. if __name__ == '__main__':
  125. if len(sys.argv) < 2:
  126. print >>sys.stderr, 'usage: % PORT' % sys.argv[0]
  127. sys.exit(1)
  128. port = int(sys.argv[1])
  129. GameServer(('', port)).run()
  130. #GameServer(('', port), loglevel=logging.DEBUG).run()