Defines data models (Category, CategoryWord, Game, Word), board factory, REST API endpoints, and dual-input frontend design for the word search game. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.0 KiB
Word Search Game Logic Design
Context
The project has a Tornado web server with two stub pages (main, game) and a dark teal Bootstrap theme. No game logic exists yet. This spec defines the data layer (SQLAlchemy + Alembic + SQLite), board generation, REST API, and frontend interaction needed to make the word search game playable.
The design prioritizes server-side game logic for testability and cheat resistance, while keeping the frontend interactive enough for both text and click-based input.
Data Models
All models use SQLAlchemy ORM with a SQLite database file (e.g. wordsearch.db).
Category
| Column | Type | Notes |
|---|---|---|
| id | Integer PK | Auto-increment |
| name | String(50) | Unique, e.g. "animals" |
CategoryWord
| Column | Type | Notes |
|---|---|---|
| id | Integer PK | Auto-increment |
| category_id | Integer FK | References Category.id |
| word | String(20) | Uppercase, e.g. "TIGER" |
Unique constraint on (category_id, word).
Game
| Column | Type | Notes |
|---|---|---|
| id | String(36) PK | UUID as string |
| category_id | Integer FK | References Category.id |
| board_size | Integer | Grid dimension (N x N), default 10 |
| grid | JSON | 2D array of single uppercase chars |
| status | String(20) | "in_progress" or "completed" |
| started_at | DateTime | Set on creation (UTC) |
| completed_at | DateTime | Nullable, set when all words found |
Word
| Column | Type | Notes |
|---|---|---|
| id | Integer PK | Auto-increment |
| game_id | String(36) FK | References Game.id |
| text | String(20) | The placed word, uppercase |
| start_row | Integer | 0-indexed row of first letter |
| start_col | Integer | 0-indexed column of first letter |
| direction | String(2) | One of: E, W, N, S, NE, NW, SE, SW |
| found | Boolean | Default false |
| found_at | DateTime | Nullable, set when word is matched |
Direction Encoding
Directions map to (row_delta, col_delta):
- E = (0, +1), W = (0, -1)
- S = (+1, 0), N = (-1, 0)
- SE = (+1, +1), SW = (+1, -1)
- NE = (-1, +1), NW = (-1, -1)
Board Factory
Module: game/factory.py
Class BoardFactory:
- Input:
category_id,board_size(default 10),word_count(default 8) - Query: Fetch all
CategoryWordrows for the category. Filter to words that fit withinboard_size. Randomly selectword_countwords. - Placement loop: For each word, try random (row, col, direction) positions. A position is valid if:
- The word fits within grid bounds
- Each cell is either empty or already contains the same letter (overlap is allowed)
- Max placement attempts per word: 100. If exceeded, skip the word.
- Fill: Remaining empty cells get random uppercase letters (A-Z).
- Output: Returns a
Gameobject and list ofWordobjects (not yet committed to DB).
Database Setup
- Engine: SQLAlchemy with
sqlite:///wordsearch.db(file in project root) - Session: A session factory created at app startup, passed to handlers via
Application.settings - Alembic: Configured with
alembic init, migrations inalembic/versions/ - Seed data: An initial migration or seed script populates
CategoryandCategoryWordtables with at least 3 categories (~20-30 words each):- Animals (TIGER, DOLPHIN, EAGLE, etc.)
- Colors (CRIMSON, VIOLET, AMBER, etc.)
- Food (PIZZA, SUSHI, MANGO, etc.)
API Endpoints
All API handlers inherit from a base that sets Content-Type: application/json.
GET /api/categories
Returns all available categories.
Response:
[
{"id": 1, "name": "animals"},
{"id": 2, "name": "colors"},
{"id": 3, "name": "food"}
]
POST /api/game/new
Request body:
{"category_id": 1, "board_size": 12}
board_size is optional (default 10, clamped to 8-20).
Behavior: Creates a new game via BoardFactory, persists to DB, returns game state.
Response (same format as GET /api/game/):
{
"id": "uuid",
"board_size": 12,
"category": "animals",
"grid": [["A","B",...], ...],
"words": [
{"text": "TIGER", "found": false},
...
],
"started_at": "2026-05-06T12:00:00Z",
"status": "in_progress"
}
Note: word positions (start_row, start_col, direction) are NOT sent to the client. The client only knows the word text and whether it's been found.
GET /api/game/{id}
Returns the current game state in the same format as above. Includes completed_at if finished.
POST /api/game/{id}/guess
Request body (text guess):
{"word": "TIGER"}
Request body (click guess):
{"cells": [{"row": 0, "col": 3}, {"row": 0, "col": 4}, ...]}
Validation (text guess):
- Uppercase the input
- Check if the word exists in this game's
Wordlist - Check if it hasn't already been found
- If valid: mark
found=true, setfound_at
Validation (click guess):
- Extract the letter at each (row, col) from the grid
- Concatenate to form a word string
- Check if that word exists and the cells match the stored placement coordinates
- If valid: mark found
Response:
{
"correct": true,
"word": "TIGER",
"cells": [{"row": 0, "col": 3}, {"row": 0, "col": 4}, ...],
"words": [...updated word list...],
"status": "in_progress",
"completed_at": null
}
When correct is true, cells contains the word's actual grid positions so the frontend can highlight them. When the last word is found, status changes to "completed" and completed_at is set.
Frontend (game.html)
Game Setup
On the /game page, before a game starts:
- A dropdown to pick a category (populated from a
GET /api/categoriesendpoint) - A board size selector (range input or dropdown, 8-20, default 10)
- A "New Game" button
Game Board
After starting a game:
- The grid renders as an HTML
<table>with one letter per<td>, styled with the teal theme - A word list sidebar shows all target words, with found words crossed off and highlighted
- A timer displays elapsed seconds since
started_at
Text Input
- An input field below the grid with a "Submit" button
- On submit:
POST /api/game/{id}/guesswith{word: input_value} - If correct: highlight the word's cells on the grid in accent color, cross off the word in the list
- If incorrect: brief shake animation or "not found" flash message
Click Selection
- Clicking a cell starts a selection, subsequent clicks extend it
- Selected cells highlight in a temporary color
- A "Submit Selection" button sends
POST /api/game/{id}/guesswith the cell coordinates - Clear selection on successful match or via a "Clear" button
- Constraint: clicks must form a straight line (horizontal, vertical, or diagonal). Frontend validates this before submitting.
Game Complete
When all words are found:
- Stop the timer
- Show a completion banner with elapsed time
- Offer a "Play Again" button that returns to game setup
Project Structure (new/modified files)
ai_demo_backend/
├── app.py # Add db session setup, new API routes
├── db.py # SQLAlchemy engine + session factory
├── models/
│ ├── __init__.py
│ ├── category.py # Category, CategoryWord
│ └── game.py # Game, Word
├── game/
│ ├── __init__.py
│ └── factory.py # BoardFactory
├── handlers/
│ ├── api.py # API handlers (new game, guess, categories)
│ ├── game.py # (existing, updated)
│ └── main.py # (existing, unchanged)
├── templates/
│ ├── game.html # (existing, rewritten with game UI)
│ └── ...
├── static/
│ ├── css/theme.css # (existing, extended with grid styles)
│ └── js/game.js # Frontend game logic
├── alembic/ # Alembic migrations directory
├── alembic.ini # Alembic config
└── seed.py # Optional: seed script for categories
Future Scaling Considerations
- The
Gamemodel already supports multiple concurrent games via UUID. The current UI only shows one game at a time, but the API is stateless per game ID. - To support multiple users: add a
player_idor session token toGame. - To support leaderboards: query
Gamerows withstatus=completed, ordered bycompleted_at - started_at. - Categories and words in the DB means they can be managed via an admin interface or API later.