Palabrícula is a Spanish word puzzle game built with:
- FastAPI backend
- PostgreSQL
- Vanilla JavaScript frontend
- SQLAlchemy ORM
- Custom board generator and solver
- Docker Compose deployment
The game generates a 4x4 letter grid and computes all valid Spanish words that can be formed by adjacent tiles (including diagonals).
Players create a session per puzzle and submit words in real time.
src/
├── db/ # Database models and session setup
├── scripts/ # Puzzle generation and import tools
├── services/ # Game logic (PuzzleService)
└── main.py # FastAPI application
frontend/
├── index.html
├── css/
└── js/
docker-compose.yml
Dockerfile
docker compose up --buildRun in detached mode:
docker compose up -d --builddocker compose downAll services:
docker compose logs -fBackend only:
docker compose logs -f apiFrontend:
http://localhost:5500
Backend API:
http://localhost:8000
API documentation:
http://localhost:8000/docs
Health check:
http://localhost:8000/health
uv syncuvicorn src.main:app --reload --port 8000From the frontend directory:
python -m http.server 5500Open:
http://localhost:5500
python -m src.scripts.generate_puzzle --board MOCI/IENE/SQID/OUBFFormat:
- 4 rows
- Rows separated by
/ - 4x4 grid required
python -m src.scripts.generate_puzzleThis will:
- Generate a random board
- Validate solvability
- Ensure tile coverage
- Enforce word count constraints
After generation:
daily_puzzle.json
daily_solution.json
Public game data:
{
"id": "...",
"size": 4,
"board": [["A", "B", "C", "D"]],
"word_count": 62
}Contains:
- All valid words
- Display and normalized forms
- Tile paths
- Puzzle statistics
python -m src.scripts.import_puzzle daily_puzzle.json daily_solution.jsonThis will:
- Create a Puzzle row
- Store solution JSON
- Make the puzzle available through the API
GET /healthGET /puzzle/today
GET /puzzle/{puzzle_id}Returns puzzle data without solution information.
POST /sessionRequest:
{
"puzzle_id": "uuid"
}Response:
{
"session_id": "uuid"
}POST /submit-wordRequest:
{
"session_id": "uuid",
"word": "string"
}Response:
{
"success": true,
"normalized": "PALABRA",
"display": "Palabra",
"score_added": 3,
"total_score": 10,
"found_count": 5,
"total_words": 62,
"completion": 8.12
}GET /progress/{session_id}Response:
{
"session_id": "uuid",
"puzzle_id": "uuid",
"found_words": 5,
"total_words": 62,
"score": 10,
"completed": false,
"words": ["PALABRA"],
"display_words": ["Palabra"],
"username": "joe"
}GET /dictionary/rae?q=PALABRAReturns RAE definitions for discovered words.
Example:
{
"word": "CENÉ",
"definitions": [
"Tomar la cena",
"Comer en la cena una cosa"
]
}GET /leaderboard/today
GET /leaderboard/{puzzle_id}The API was designed using a capability-driven approach inspired by Arnaud Lauret's API design methodology. Rather than exposing database operations, endpoints correspond to user capabilities required by the Palabrícula game. Multiple user interactions may map to the same capability, and each capability is implemented by one or more HTTP operations.
The API was designed by first identifying the capabilities required by the game rather than starting from endpoints. Each user interaction was decomposed into inputs, successful outcomes, failure cases, and the underlying API capability.
| User | Use Case | Step | Input | Success | Failure | API Capability |
|---|---|---|---|---|---|---|
| Player | Play today's puzzle | Load today's puzzle | — | Puzzle returned | No puzzle published | Retrieve puzzle |
| Player | Start a game | Create a play session | puzzle_id, optional player_id | Session created | Puzzle not found | Create session |
| Anonymous player | Choose a username | Register player | session_id, username | Player created and linked to session | Username already exists | Register player |
| Player | Resume a game | Restore progress | session_id | Found words and score returned | Session not found | Retrieve session progress |
| Player | Submit a word | Validate word | session_id, word | Word accepted and recorded | Invalid word, duplicate word, session not found | Submit word |
| Player | Track progress | Refresh progress | session_id | Current progress returned | Session not found | Retrieve progress |
| Player | View rankings | Display today's leaderboard | — | Leaderboard returned | Puzzle unavailable | Retrieve leaderboard |
| Player | View rankings | Display leaderboard for a puzzle | puzzle_id | Leaderboard returned | Puzzle not found | Retrieve leaderboard |
| Player | Read a definition | Lookup a discovered word | word | Definitions returned | Definition unavailable | Retrieve dictionary entry |
| Monitoring | Check service availability | Health probe | — | Service healthy | Service unavailable | Health check |
The identified capabilities map directly to the public API.
| Capability | Endpoint |
|---|---|
| Retrieve puzzle | GET /puzzle/today |
| Retrieve puzzle | GET /puzzle/{puzzle_id} |
| Create session | POST /session |
| Retrieve session | GET /session/{session_id} |
| Register player | POST /player |
| Submit word | POST /submit-word |
| Retrieve progress | GET /progress/{session_id} |
| Retrieve leaderboard | GET /leaderboard/today |
| Retrieve leaderboard | GET /leaderboard/{puzzle_id} |
| Retrieve dictionary entry | GET /dictionary/rae |
| Health check | GET /health |
A word is valid if:
- Exists in the Spanish dictionary
- Exists in the puzzle solution set
- Is formed by adjacent tiles (diagonals allowed)
- Uses each tile at most once per word
Palabrícula includes progressive hints that unlock during the daily challenge.
Hints become available when the player reaches 50% of the maximum puzzle score.
Each tile can display additional information:
- A number in the corner indicates how many remaining non-bonus words start from that tile.
- Tiles that no longer belong to any remaining non-bonus word become gray.
- Gray tiles can still be used to discover bonus words.
Once hints are unlocked, players can optionally enable additional assistance:
Shows all undiscovered non-bonus words in alphabetical order, keeping discovered words visible as a reference. Missing words are hidden using placeholders.
Example:
- ASAR
- A***
- CASA
- C***
Shows the beginning of undiscovered words.
The amount of information revealed depends on word length:
- Short words show the first letter.
- Medium words show the first two letters.
- Longer words may also reveal ending letters.
Example:
- A***
- CA***
- PE***OS
Hints never reveal the complete solution immediately and are only available after reaching half of the puzzle progress.
- Sessions persist per puzzle
- Found words are stored in the database
- Scoring formula:
max(len(word) - 3, 1) - Progress is computed from database state and solution data
- Solution data is never exposed through the API
- Hint generation happens client-side using puzzle metadata already required for gameplay. Complete solutions are not exposed through the API.
The wordlist used in this project is derived from:
Short word list:
https://github.com/eymenefealtun/all-words-in-all-languages/tree/main/Spanish
Long word list:
The game can display definitions from the RAE dictionary when hovering over discovered words.
This feature uses the public RAE API:
An API key is optional. The application works without one, but providing a key may help avoid rate limits.
Create a .env file in the project root:
RAE_API_KEY=your_api_key_hereIf using Docker Compose, ensure the environment variable is passed to the API container:
services:
api:
environment:
- RAE_API_KEY=${RAE_API_KEY}Then start or restart the stack:
docker compose up -d --buildGET /dictionary/rae?q=PALABRAExample response:
{
"word": "CENÉ",
"definitions": [
"Tomar la cena",
"Comer en la cena una cosa"
]
}If the RAE service is unavailable, gameplay is unaffected. Dictionary lookups are optional and independent from word validation.
- Frontend is stateless except for
session_id - Backend is the source of truth
- Puzzles are immutable after import
- Solution JSON is never exposed through the API
Pending features:
- Leaderboard:
- Ranking by Completion Speed
- For this we need to add a mechanism for estimating play time based on /submit-word calls
- Current leaderboard is limited to 100 entries. Improve current user display in the leaderboard.
- Ranking by Completion Speed
- Leaderboard history
- Daily express 3x3
- Weekly puzzle (maybe 5x5 or bigger)
- Obtain a proper domain name (maybe squaredle.ar)
- Ranking cache (invalidate with submit)
- BG music