Files
wordsearch/handlers/api.py
T
Mathew Sir Guest the bestandClaude Opus 4.6 4d681a92ca docs: add Google-style docstrings and Sphinx autodoc setup
Add concise docstrings to all Python modules, classes, and public
functions. Configure Sphinx with napoleon extension for Google-style
docstring parsing and autodoc pages for each module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 23:06:37 -06:00

220 lines
6.9 KiB
Python

"""REST API handlers for game operations."""
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):
"""Base handler for JSON API endpoints."""
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):
"""``GET /api/categories`` -- list all word categories."""
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):
"""``POST /api/game/new`` -- create a new game.
Expects JSON body with ``category_id`` and optional ``board_size`` (8-20).
"""
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):
"""``GET /api/game/<id>`` -- retrieve current game state."""
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):
"""``POST /api/game/<id>/guess`` -- submit a word guess.
Accepts either ``{"word": "..."}`` for text input or
``{"cells": [{"row": N, "col": N}, ...]}`` for click selection.
"""
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,
}