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