PageRenderTime 54ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/weiqi/services/games.py

https://gitlab.com/sphaso/weiqi.gs
Python | 397 lines | 280 code | 93 blank | 24 comment | 71 complexity | cc2e5dd9ab80b4e086a088849e819e46 MD5 | raw file
  1. # weiqi.gs
  2. # Copyright (C) 2016 Michael Bitzi
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as
  6. # published by the Free Software Foundation, either version 3 of the
  7. # License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. from sqlalchemy.orm import undefer
  17. from contextlib import contextmanager
  18. import random
  19. from datetime import datetime
  20. from tornado import gen
  21. from weiqi import settings
  22. from weiqi.db import transaction, session
  23. from weiqi.services import BaseService, ServiceError, UserService, RatingService, RoomService, CorrespondenceService
  24. from weiqi.models import Game, Timing
  25. from weiqi.board import RESIGN, BLACK, SYMBOL_TRIANGLE, SYMBOL_CIRCLE, SYMBOL_SQUARE
  26. from weiqi.scoring import count_score
  27. from weiqi.timing import update_timing, update_timing_after_move
  28. class InvalidPlayerError(ServiceError):
  29. pass
  30. class InvalidStageError(ServiceError):
  31. pass
  32. class GameHasNotStartedError(ServiceError):
  33. pass
  34. class NotAllowedError(ServiceError):
  35. pass
  36. class GameService(BaseService):
  37. __service_name__ = 'games'
  38. @BaseService.register
  39. def open_game(self, game_id):
  40. game = self.db.query(Game).filter_by(id=game_id).one()
  41. if game.is_private and game.black_user != self.user and game.white_user != self.user:
  42. raise NotAllowedError('this game is private')
  43. RoomService(self.db, self.socket, self.user).join_room(game.room_id, True)
  44. self.subscribe(game.id)
  45. self.socket.send('game_data', game.to_frontend(full=True))
  46. if game.is_demo and game.demo_owner == self.user:
  47. self.socket.publish('game_started', game.to_frontend())
  48. @BaseService.register
  49. def close_game(self, game_id):
  50. game = self.db.query(Game).filter_by(id=game_id).one()
  51. self.unsubscribe(game.id)
  52. RoomService(self.db, self.socket, self.user).leave_room(game.room_id)
  53. if game.is_demo and game.demo_owner == self.user:
  54. self.socket.publish('game_finished', game.to_frontend())
  55. def subscribe(self, game_id):
  56. self.socket.subscribe('game_data/'+str(game_id))
  57. self.socket.subscribe('game_update/'+str(game_id))
  58. self.socket.subscribe('game_info/'+str(game_id))
  59. self.socket.subscribe('demo_current_node_id/'+str(game_id))
  60. def unsubscribe(self, game_id):
  61. self.socket.unsubscribe('game_data/'+str(game_id))
  62. self.socket.unsubscribe('game_update/'+str(game_id))
  63. self.socket.unsubscribe('game_info/'+str(game_id))
  64. self.socket.unsubscribe('demo_current_node_id/'+str(game_id))
  65. def publish_demos(self):
  66. if not self.user:
  67. return
  68. for demo in self.user.open_demos(self.db):
  69. if self.user.is_online:
  70. self.socket.publish('game_started', demo.to_frontend())
  71. else:
  72. self.socket.publish('game_finished', demo.to_frontend())
  73. @BaseService.authenticated
  74. @BaseService.register
  75. def move(self, game_id, move):
  76. with self._game_for_update(game_id) as game:
  77. if game.is_demo:
  78. self._game_move_demo(game, move)
  79. else:
  80. self._game_move(game, move)
  81. if game.stage == 'finished':
  82. self._finish_game(game)
  83. game.apply_board_change()
  84. self.db.commit()
  85. if game.is_demo or game.stage != 'finished':
  86. self._publish_game_update(game)
  87. if game.is_correspondence and game.stage != 'finished':
  88. CorrespondenceService(self.db, self.socket).notify_move_played(game, self.user)
  89. @BaseService.authenticated
  90. @BaseService.register
  91. def resume_from_counting(self, game_id):
  92. with self._game_for_update(game_id) as game:
  93. if self.user not in [game.black_user, game.white_user]:
  94. raise InvalidPlayerError()
  95. if game.stage != 'counting':
  96. raise InvalidStageError()
  97. game.stage = 'playing'
  98. # In order to reset board changes (such as for point marks) we insert an empty edit node.
  99. # This also has the effect that the pass-counter is reset.
  100. game.board.add_edits([], [], [])
  101. game.apply_board_change()
  102. # To prevent loss of time we need to reset the last update time.
  103. game.timing.timing_updated_at = datetime.utcnow()
  104. self.db.commit()
  105. self._publish_game_update(game)
  106. @contextmanager
  107. def _game_for_update(self, game_id):
  108. with transaction(self.db):
  109. game = self.db.query(Game).options(undefer('board')).with_for_update().get(game_id)
  110. yield game
  111. def _game_move_demo(self, game, move):
  112. if not self.user == game.demo_control:
  113. raise InvalidPlayerError()
  114. if move == RESIGN:
  115. raise ServiceError('cannot resign in demo games')
  116. game.board.play(move)
  117. def _game_move(self, game, move):
  118. if self.user not in [game.black_user, game.white_user]:
  119. raise InvalidPlayerError()
  120. if game.stage == 'finished':
  121. raise InvalidStageError()
  122. if not game.timing.has_started:
  123. raise GameHasNotStartedError()
  124. if move == RESIGN:
  125. self._resign(game)
  126. return
  127. if game.stage != 'playing':
  128. raise InvalidStageError()
  129. if game.current_user != self.user:
  130. raise InvalidPlayerError()
  131. if not update_timing(game.timing, game.board.current == BLACK):
  132. self._win_by_time(game)
  133. return
  134. game.board.play(move)
  135. update_timing_after_move(game.timing, game.board.current != BLACK)
  136. if game.board.both_passed:
  137. game.stage = 'counting'
  138. self._update_score(game)
  139. def _resign(self, game):
  140. game.stage = 'finished'
  141. if self.user == game.black_user:
  142. game.result = 'W+R'
  143. elif self.user == game.white_user:
  144. game.result = 'B+R'
  145. else:
  146. raise InvalidPlayerError()
  147. def _win_by_time(self, game):
  148. game.stage = 'finished'
  149. if game.board.current == BLACK:
  150. game.result = 'W+T'
  151. else:
  152. game.result = 'B+T'
  153. def _update_score(self, game):
  154. score = count_score(game.board, game.komi)
  155. game.result = score.result
  156. game.board.current_node.score_points = score.points
  157. def _publish_game_update(self, game):
  158. self.socket.publish('game_update/'+str(game.id), {
  159. 'game_id': game.id,
  160. 'stage': game.stage,
  161. 'result': game.result,
  162. 'timing': game.timing.to_frontend() if game.timing else None,
  163. 'node': game.board.current_node.to_dict() if game.board.current_node else {},
  164. })
  165. @BaseService.authenticated
  166. @BaseService.register
  167. def toggle_marked_dead(self, game_id, coord):
  168. with self._game_for_update(game_id) as game:
  169. if self.user not in [game.black_user, game.white_user]:
  170. raise InvalidPlayerError()
  171. if game.stage != 'counting':
  172. raise InvalidStageError()
  173. game.board.toggle_marked_dead(coord)
  174. self._update_score(game)
  175. game.apply_board_change()
  176. self.db.commit()
  177. self._publish_game_update(game)
  178. @BaseService.authenticated
  179. @BaseService.register
  180. def confirm_score(self, game_id, result):
  181. with self._game_for_update(game_id) as game:
  182. if self.user not in [game.black_user, game.white_user]:
  183. raise InvalidPlayerError()
  184. if game.stage != 'counting':
  185. raise InvalidStageError()
  186. if result != game.result:
  187. raise ServiceError('got incorrect result: {}'.format(result))
  188. if self.user == game.black_user:
  189. game.result_black_confirmed = game.result
  190. else:
  191. game.result_white_confirmed = game.result
  192. if game.result_black_confirmed == game.result_white_confirmed:
  193. game.stage = 'finished'
  194. self._finish_game(game)
  195. def _finish_game(self, game):
  196. if game.is_demo or game.stage != 'finished':
  197. return
  198. if game.board.moves_played <= game.board.size:
  199. game.result = 'aborted'
  200. if game.is_ranked:
  201. RatingService(self.db).update_ratings(game)
  202. self.socket.publish('game_finished', game.to_frontend())
  203. self._publish_game_data(game)
  204. UserService(self.db, self.socket, game.black_user).publish_status()
  205. UserService(self.db, self.socket, game.white_user).publish_status()
  206. if game.is_correspondence:
  207. CorrespondenceService(self.db, self.socket).notify_game_finished(game)
  208. def _publish_game_data(self, game):
  209. self.socket.publish('game_data/'+str(game.id), game.to_frontend(full=True))
  210. @BaseService.authenticated
  211. @BaseService.register
  212. def set_current_node(self, game_id, node_id):
  213. game = self.db.query(Game).get(game_id)
  214. if not game.demo_control == self.user:
  215. raise InvalidPlayerError()
  216. if node_id >= len(game.board.tree):
  217. raise ServiceError('invalid node_id')
  218. game.board.current_node_id = node_id
  219. game.apply_board_change()
  220. self.socket.publish('demo_current_node_id/'+str(game.id), {
  221. 'game_id': game.id,
  222. 'node_id': game.board.current_node_id,
  223. })
  224. @BaseService.authenticated
  225. @BaseService.register
  226. def demo_tool_triangle(self, game_id, coord):
  227. with self._demo_tool(game_id) as (game, node):
  228. node.toggle_symbol(coord, SYMBOL_TRIANGLE)
  229. @BaseService.authenticated
  230. @BaseService.register
  231. def demo_tool_square(self, game_id, coord):
  232. with self._demo_tool(game_id) as (game, node):
  233. node.toggle_symbol(coord, SYMBOL_SQUARE)
  234. @BaseService.authenticated
  235. @BaseService.register
  236. def demo_tool_circle(self, game_id, coord):
  237. with self._demo_tool(game_id) as (game, node):
  238. node.toggle_symbol(coord, SYMBOL_CIRCLE)
  239. @BaseService.authenticated
  240. @BaseService.register
  241. def demo_tool_label(self, game_id, coord):
  242. with self._demo_tool(game_id) as (game, node):
  243. node.toggle_label(coord)
  244. @BaseService.authenticated
  245. @BaseService.register
  246. def demo_tool_number(self, game_id, coord):
  247. with self._demo_tool(game_id) as (game, node):
  248. node.toggle_number(coord)
  249. @BaseService.authenticated
  250. @BaseService.register
  251. def demo_tool_edit(self, game_id, coord, color):
  252. with self._demo_tool(game_id) as (game, node):
  253. game.board.toggle_edit(coord, color)
  254. @BaseService.authenticated
  255. @BaseService.register
  256. def demo_tool_edit_cycle(self, game_id, coord):
  257. with self._demo_tool(game_id) as (game, node):
  258. game.board.edit_cycle(coord)
  259. @contextmanager
  260. def _demo_tool(self, game_id):
  261. game = self.db.query(Game).options(undefer('board')).get(game_id)
  262. if not game.is_demo or not game.demo_control == self.user:
  263. raise InvalidPlayerError()
  264. if not game.board.current_node:
  265. game.board.add_edits([], [], [])
  266. node = game.board.current_node
  267. yield game, node
  268. game.apply_board_change()
  269. self._publish_game_update(game)
  270. @BaseService.authenticated
  271. @BaseService.register
  272. def edit_info(self, game_id, title, black_display, white_display):
  273. game = self.db.query(Game).filter_by(id=game_id, is_demo=True, demo_owner_id=self.user.id).one()
  274. game.title = title
  275. game.black_display = black_display
  276. game.white_display = white_display
  277. self.socket.publish('game_info/'+str(game.id), {
  278. 'game_id': game.id,
  279. 'title': game.title,
  280. 'black_display': game.black_display,
  281. 'white_display': game.white_display
  282. })
  283. def check_due_moves(self):
  284. """Checks and updates all timings which are due for a move being played."""
  285. timings = self.db.query(Timing).with_for_update().join('game').options(undefer('game.board')).filter(
  286. (Game.is_demo.is_(False)) & (Game.stage == 'playing') & (Timing.next_move_at <= datetime.utcnow()))
  287. for timing in timings:
  288. if not update_timing(timing, timing.game.board.current == BLACK):
  289. self._win_by_time(timing.game)
  290. self._finish_game(timing.game)
  291. def resume_all_games(self):
  292. """Gracefully resumes all games on startup.
  293. Resets the timings to reduce lost time after a server downtime.
  294. The overtime for each player is reset and a pre-defined amount of time is added to the player's maintime.
  295. """
  296. for timing in self.db.query(Timing).join(Game).filter(Game.stage == 'playing'):
  297. timing.black_main += settings.RESUME_TIMING_ADD_TIME
  298. timing.white_main += settings.RESUME_TIMING_ADD_TIME
  299. if timing.system != 'fischer':
  300. timing.black_overtime = timing.overtime * timing.overtime_count
  301. timing.white_overtime = timing.overtime * timing.overtime_count