- JavaScript 77.3%
- CSS 14.6%
- HTML 3.9%
- Python 3.8%
- Dockerfile 0.4%
| static | ||
| app.py | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| requirements.txt | ||
| server.py | ||
Santorini · Oracle of the Gods
A tiny web toy for the board game Santorini: draw a sign, and an in-browser AI reads your doodle and deals you a god card.
Canvas → 400×400 bitmap → DoodleNet (tiny CNN, runs in-browser) → label
→ Santorini god mapping → god card revealed
No account, no backend classification, nothing uploaded anywhere — the recognition model runs entirely on the visitor's device.
How it works
- Drawing — a p5.js canvas, driven by native Pointer Events so mouse, trackpad, touchscreen, and stylus all draw identically.
- Recognition — ml5.js's
DoodleNet, a small CNN pretrained on Google's Quick, Draw! dataset (~345 doodle categories). Classification happens client-side; the Python backend never sees the drawing. - Mapping —
static/godcards.jsmaps a recognized label (e.g.hammer,snake,eye) to a Santorini god and a one-line power description. Anything the oracle doesn't recognize falls back to a generic Mortal card. It covers the full base-game roster of 32 gods, keyed across 51 doodle labels so recognition has decent coverage (e.g. bothowlandbookpoint to Athena). Power text is paraphrased for a small card — check the printed rulebook for exact wording. - Portraits (optional) — see Custom card art below. Every card works out of the box with just an emoji glyph.
- Backend — a minimal Flask
app whose only job is serving
static/index.htmland its assets. There is no database, no API, no user data.
Project layout
santorini/
├── app.py Flask app — serves static/ from the site root
├── server.py Production entrypoint (waitress)
├── requirements.txt
├── Dockerfile
├── docker-compose.yml OMV / reverse-proxy deployment
└── static/
├── index.html Page shell
├── style.css Dark Aegean-night theme, card reveal styling
├── sketch.js Drawing, classification, and card-reveal logic
├── godcards.js Doodle-label → Santorini god mapping
└── images/ Optional god portrait PNGs (see below)
Everything the app serves lives in one static/ folder — there's no
separate templates/ directory to keep in sync.
Running locally (no Docker)
pip install -r requirements.txt
python server.py # http://localhost:8080
Set DEBUG=true to run Flask's dev server with auto-reload instead
of the production waitress server:
DEBUG=true python server.py
Running with Docker
docker build -t santorini-oracle .
docker run -p 8080:8080 santorini-oracle
Running with Docker Compose (OMV deployment)
docker-compose.yml is set up for a reverse-proxy setup where
Caddy sits in front of the container on a
shared external Docker network (proxy-network), rather than
publishing the port directly to the host.
Expected host path:
/srv/dev-disk-by-uuid-a920d9c0-dfc1-4a58-ae4d-92cf88ff04a5/docker-app/santorini
The compose file bind-mounts that same directory into the container's
/app, so editing anything under static/ (HTML, CSS, JS, or a
portrait PNG) takes effect on browser refresh — no rebuild needed.
Editing app.py or server.py needs a container restart, since
Python only reads those at process start.
docker compose up -d --build
Matching Caddy site block:
santorini.studio-umzu.de {
reverse_proxy santorini:8080
}
(santorini resolves via Docker's internal DNS because the container
is named santorini and joined to the same proxy-network Caddy
uses — no host port needs to be published.)
Custom card art
By default every god card shows an emoji glyph (🔨, 🐍, 👁️, …). To
use real artwork instead, drop a PNG into static/images/ named after
the god:
static/images/hephaestus.png
static/images/athena.png
static/images/mortal.png ← the no-match fallback card
Naming rule: lowercase the god's name, replace anything that isn't a
letter or number with a hyphen, add .png. No code changes and no
per-card configuration needed — the app looks for the matching file
automatically, and quietly falls back to the emoji glyph if the file
is missing.
Adding or editing gods
All mapping lives in static/godcards.js as a plain object keyed by
the doodle label ml5/DoodleNet returns (lowercase, matching Quick,
Draw!'s category names — e.g. "hammer", "wine glass", "sailboat").
Each entry looks like:
hammer: {
name: "Hephaestus",
glyph: "🔨",
tagline: "The Smith",
power: "After moving, you may build one additional time on the same space.",
},
Add homebrew: true if the power is a fan invention rather than an
approximation of an official Santorini god's ability — it adds a
small "homebrew power" tag to the revealed card. (The current roster
is the full base-game set, so none are flagged this way today — this
is for if you extend it with expansion or custom gods later.)
Notes & limitations
- DoodleNet's vocabulary is fixed to Quick, Draw!'s ~345 categories, so recognition works best with quick, simple, single-subject doodles — the same style the original Quick, Draw! game encourages. Thick single strokes drawn quickly tend to classify better than careful, detailed drawings.
- Nothing is persisted anywhere: no accounts, no database, no server-side logging of drawings. Refreshing the page clears everything.
- The god-power text is intentionally simplified/paraphrased for a quick browser toy and shouldn't be treated as an accurate rules reference for actual gameplay.