Add game logic design spec
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
commit
85045f2a5f
@@ -0,0 +1,248 @@
|
|||||||
|
# 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`:
|
||||||
|
|
||||||
|
1. **Input**: `category_id`, `board_size` (default 10), `word_count` (default 8)
|
||||||
|
2. **Query**: Fetch all `CategoryWord` rows for the category. Filter to words that fit within `board_size`. Randomly select `word_count` words.
|
||||||
|
3. **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.
|
||||||
|
4. **Fill**: Remaining empty cells get random uppercase letters (A-Z).
|
||||||
|
5. **Output**: Returns a `Game` object and list of `Word` objects (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 in `alembic/versions/`
|
||||||
|
- **Seed data**: An initial migration or seed script populates `Category` and `CategoryWord` tables 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:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"id": 1, "name": "animals"},
|
||||||
|
{"id": 2, "name": "colors"},
|
||||||
|
{"id": 3, "name": "food"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/game/new
|
||||||
|
|
||||||
|
**Request body:**
|
||||||
|
```json
|
||||||
|
{"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>):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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):
|
||||||
|
```json
|
||||||
|
{"word": "TIGER"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body** (click guess):
|
||||||
|
```json
|
||||||
|
{"cells": [{"row": 0, "col": 3}, {"row": 0, "col": 4}, ...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation (text guess)**:
|
||||||
|
1. Uppercase the input
|
||||||
|
2. Check if the word exists in this game's `Word` list
|
||||||
|
3. Check if it hasn't already been found
|
||||||
|
4. If valid: mark `found=true`, set `found_at`
|
||||||
|
|
||||||
|
**Validation (click guess)**:
|
||||||
|
1. Extract the letter at each (row, col) from the grid
|
||||||
|
2. Concatenate to form a word string
|
||||||
|
3. Check if that word exists and the cells match the stored placement coordinates
|
||||||
|
4. If valid: mark found
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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/categories` endpoint)
|
||||||
|
- 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}/guess` with `{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}/guess` with 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 `Game` model 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_id` or session token to `Game`.
|
||||||
|
- To support leaderboards: query `Game` rows with `status=completed`, ordered by `completed_at - started_at`.
|
||||||
|
- Categories and words in the DB means they can be managed via an admin interface or API later.
|
||||||
Reference in New Issue
Block a user