import json from datetime import datetime, timezone import tornado.web from game.factory import BoardFactory, DIRECTIONS from models.category import Category from models.game import Game, Word class BaseAPIHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Content-Type", "application/json") def get_session(self): Session = self.application.settings["session_factory"] return Session() def write_error(self, status_code, **kwargs): self.write(json.dumps({"error": self._reason})) class CategoriesHandler(BaseAPIHandler): def get(self): session = self.get_session() try: categories = session.query(Category).all() self.write(json.dumps([{"id": c.id, "name": c.name} for c in categories])) finally: session.close() class NewGameHandler(BaseAPIHandler): def post(self): session = self.get_session() try: data = json.loads(self.request.body) category_id = data["category_id"] board_size = data.get("board_size", 10) board_size = max(8, min(20, board_size)) factory = BoardFactory(session) game, words = factory.create( category_id=category_id, board_size=board_size, ) session.add(game) for w in words: session.add(w) session.commit() self.write(json.dumps(self._game_to_dict(game))) finally: session.close() def _game_to_dict(self, game): return { "id": game.id, "board_size": game.board_size, "category": game.category.name, "grid": game.grid, "words": [{"text": w.text, "found": w.found} for w in game.words], "started_at": game.started_at.isoformat() + "Z", "status": game.status, "completed_at": None, } class GameStateHandler(BaseAPIHandler): def get(self, game_id): session = self.get_session() try: game = session.query(Game).filter_by(id=game_id).first() if not game: self.set_status(404) self.write(json.dumps({"error": "Game not found"})) return self.write( json.dumps( { "id": game.id, "board_size": game.board_size, "category": game.category.name, "grid": game.grid, "words": [ {"text": w.text, "found": w.found} for w in game.words ], "started_at": game.started_at.isoformat() + "Z", "status": game.status, "completed_at": ( game.completed_at.isoformat() + "Z" if game.completed_at else None ), } ) ) finally: session.close() class GuessHandler(BaseAPIHandler): def post(self, game_id): session = self.get_session() try: game = session.query(Game).filter_by(id=game_id).first() if not game: self.set_status(404) self.write(json.dumps({"error": "Game not found"})) return data = json.loads(self.request.body) if "word" in data: result = self._handle_text_guess(session, game, data["word"]) elif "cells" in data: result = self._handle_click_guess(session, game, data["cells"]) else: self.set_status(400) self.write(json.dumps({"error": "Provide 'word' or 'cells'"})) return self.write(json.dumps(result)) finally: session.close() def _handle_text_guess(self, session, game, guess_text): guess_text = guess_text.upper().strip() word = ( session.query(Word) .filter_by(game_id=game.id, text=guess_text, found=False) .first() ) if not word: return self._miss_response(game, session) return self._mark_found(session, game, word) def _handle_click_guess(self, session, game, cells): grid = game.grid try: letters = "".join(grid[c["row"]][c["col"]] for c in cells) except (IndexError, KeyError): return self._miss_response(game, session) word = ( session.query(Word) .filter_by(game_id=game.id, text=letters, found=False) .first() ) if not word: return self._miss_response(game, session) dr, dc = DIRECTIONS[word.direction] expected_cells = [ {"row": word.start_row + i * dr, "col": word.start_col + i * dc} for i in range(len(word.text)) ] if cells != expected_cells: return self._miss_response(game, session) return self._mark_found(session, game, word) def _mark_found(self, session, game, word): word.found = True word.found_at = datetime.now(timezone.utc) unfound = session.query(Word).filter_by(game_id=game.id, found=False).count() if unfound == 0: game.status = "completed" game.completed_at = datetime.now(timezone.utc) session.commit() dr, dc = DIRECTIONS[word.direction] cells = [ {"row": word.start_row + i * dr, "col": word.start_col + i * dc} for i in range(len(word.text)) ] return { "correct": True, "word": word.text, "cells": cells, "words": [{"text": w.text, "found": w.found} for w in game.words], "status": game.status, "completed_at": ( game.completed_at.isoformat() + "Z" if game.completed_at else None ), } def _miss_response(self, game, session): return { "correct": False, "word": None, "cells": [], "words": [{"text": w.text, "found": w.found} for w in game.words], "status": game.status, "completed_at": None, }