A casino-style spinning wheel game with a fish mascot, streaks and a fishing minigame, running on a Python/Flask backend with PostgreSQL persistence and user authentication.
π Patch Notes Β· π Season Museum: every season, with a clip of each
Lucky Wheel is a browser-based gambling wheel built with a Python/Flask backend and a React frontend. Spin the wheel, build streaks, fish for Surge, and race the rest of the server to the top of the weekly tide.
The current season is Season 9 Β· Tides π. Instead of one long season that a single player can run away with, the race resets every week, and the top three each week take a medal that never resets.
All game state is stored server-side in PostgreSQL. Progress persists across devices and sessions, and client-side cheating is prevented.
Season 9 runs in weekly tides: 9.1, 9.2, 9.3 and so on. Every Friday at 21:00 UK time (Europe/London) the tide turns:
- Wins, losses, streaks, Charts, Surge and πͺ stake chips reset. Everyone starts the new tide level.
- The top three are posted in chat, saved to the Hall of Fame and awarded a medal.
- A new community goal starts.
What carries over from tide to tide: cosmetics (owned and equipped), your Encyclopaedia and record weights, medals, chat and your account.
The tide banner at the top of the screen shows the current tide, a countdown to the next turn and last tide's podium. Tap it to open the Hall of Fame.
Finish a tide in the top three to earn π₯ π₯ π₯. Medals are the long chase of the season. The Hall of Fame shows the medal table and the podium of every past tide.
- Spinning wheel: WIN or LOSE, styled as a neon casino wheel with smooth CSS rotation
- Win/loss counter: persisted in PostgreSQL across sessions and devices
- Streak bonus: 3+ consecutive wins or losses pays a scaling bonus. Γ2 per step up to streak 15, then cubic and linear growth, with a hard cap at streak 150 (113,096 raw bonus)
- Streak panel: appears in the left sidebar only when a streak is active (fire emoji for wins, skull for losses)
- Stats popup: the π button shows total spins, wins, losses, win rate, fish bucks, fastest catch, and your Season History
- Leaderboard: top 10 players of the current tide, ranked by wins
- π² Dice: roll two dice (three with Third Die) to add the sum to a win streak of 3+. Snake eyes halves your streak, double sixes doubles it; with three dice, triple 1s Γ·3 and triple 6s Γ3. One charge to start, recharging every 10 minutes. Riptide's Loaded Dice holds more
The functional shop is gone. Gear comes from Charts: three talent trees, and not enough points for all of them.
- You start each tide with 1 point and get one free point every day (days start at 21:00 UK time, in step with the tide).
- Level up to get points sooner. Each level costs wins: 1,000 for the first, then Γ6 each time (6k, 36k, 216k, β¦).
- The cap is 14 points: one full tree and a splash of another, never all three. Levels reset when the tide turns.
- Higher rows need points in that tree first (row 2 needs 2, row 3 needs 4, the keystone needs 6), and you can hold one keystone.
- Adding points is always free. A re-chart (taking points back) is allowed once a day.
| Tree | Talent | Ranks | Effect |
|---|---|---|---|
| π Swell: ride the streak | π«§ Undertow | 3 | Streak bonuses Γ2 / Γ4 / Γ8 |
| π Rising Tide | 2 | Every win pays Γ2 / Γ4 | |
| β Steady Keel | 1 | Sometimes a loss only knocks your streak back one | |
| π Fortune Charm | 1 | Streak bonuses sometimes pay +25% | |
| π Echo | 1 | Wins sometimes pay twice | |
| π‘οΈ Breakwater | 1 | Blocks one loss, recharges after 25 wins | |
| π Spring Tide (keystone) | 1 | Streak bonuses Γ2 again, but you can't stake or roll dice | |
| π Riptide: bet the tide | πͺ Open Water | 1 | Stake up to 30% of your wins. Each staked spin costs 1 πͺ |
| π² Loaded Dice | 3 | Hold 2 / 3 / 4 dice charges | |
| π± Deep Water | 2 | Stake up to 35% / 40% | |
| π Treasure | 1 | 1% of wins are jackpots: Γ25, or Γ5 on a staked spin | |
| π Safety Line | 1 | Staked losses refund a little; arm insurance | |
| π― Third Die | 1 | Roll three dice | |
| βοΈ Double or Nothing | 1 | Re-stake your last win in one go | |
| πͺοΈ Rogue Wave (keystone) | 1 | Dice come back twice as fast and stack two higher, but no Surge | |
| π£ Angler: read the water | π Rich Waters | 3 | Surge spins pay Γ25 / Γ50 / Γ100 (base Γ5) |
| πͺ± Better Bait | 2 | Faster bites, bigger catches, +25% / +50% Surge | |
| β΅ Deckhand | 2 | Auto-fish while you're away; catches more often | |
| β Steady Hands | 3 | Wider reel bar; bar moves 50% faster; a slipping fish escapes 25% slower | |
| π Auto-Cast | 1 | Recast automatically | |
| π§ Old Salt | 1 | Auto-fish catches rares, and more often | |
| π Deep Sea (keystone) | 1 | Junk and commons stop biting, rares and legendaries bite three times as often, and bites take longer |
Some talents need another first: Deep Water, Safety Line and Double or Nothing need Open Water; Old Salt needs Deckhand.
- Click π£ CAST to drop your line. When the fish bites, tap to hook it.
- Then keep the π inside your green reel bar. Hold (mouse, touch or Space) to push the bar right; let go and it drifts back. The catch meter fills while the fish is in the bar and drains while it isn't. If it empties, the fish gets away.
- Rarer fish swim faster, dart more often and take longer to land.
- A cleaner fight lands a heavier fish, and heavier fish are worth more π. Your heaviest catch of each species is saved as a record that never resets.
- Your first catch of each day pays Γ5.
- Auto-fish (Angler: Deckhand) catches commons and uncommons every few seconds, rares with Old Salt, never legendaries. It keeps fishing while you're away.
- All timing is server-authoritative: the bite, the fight and the catch are validated server-side.
The Fish Encyclopaedia (π) tracks 46 species across junk, common, uncommon, rare and legendary. Many only bite at dawn, day, dusk or night (UK time), at high or low tide (the tide turns every 6ΒΌ hours), or are migrants that visit for a week at a time. The Encyclopaedia shows the current tide, what's biting now, a hint for every fish and your record weights.
Fishing powers the wheel. Every catch charges Surge spins: more for rarer and heavier fish, a quarter as much from auto-fishing. While you have Surge, each spin uses one and multiplies its wins: Γ5 for everyone, up to Γ100 with Rich Waters. The chip under your score shows how many Surge spins you have left and what they pay.
With Open Water you can stake part of your wins on a spin. The stake panel raises your stake in 5% steps (30% cap, 40% with Deep Water) and shows what you'd win or lose before you spin.
- Stake escrow: the stake is debited up front and held at risk. Win it back plus your payout, or lose it.
- Stake chips: claim 3 free chips a day. Each staked spin costs one chip. Community goals pay chips too.
- Safety Line: a staked loss refunds 25% of the stake, and you can arm insurance to cap a spin's loss and refund the stake.
- Double or Nothing: put your entire last win on the line. All-or-nothing: no insurance or safety net applies.
Switch the wheel's odds profile at will. Steady and Volatile are always available; one more mode rotates weekly, turning with the tide on Friday at 21:00.
| Mode | Win % | Loss % | Jackpot % | Jackpot Γ | Notes |
|---|---|---|---|---|---|
| Steady (default) | 70% | 28% | 2% | 25Γ | Small wins, rare losses |
| Volatile | 45% | 50% | 5% | 50Γ | High variance, double jackpot payout |
| Inverted (rotates) | 35% | 60% | 5% | 25Γ | Losses become small wins; loss streaks still build bonus |
| Gravity (rotates) | 55% | 40% | 5% | 25Γ | Outcomes drift toward the last result |
| Long Shot (rotates) | 20% | 60% | 20% | 10Γ | Most spins lose; jackpots hit often but pay less |
The rotating slot cycles Inverted β Gravity β Long Shot by week.
Three bounties a day, the same three all day, resetting at midnight UTC. They are streaks and fishing only, so every build can finish them: reach a 10-spin win streak, catch 10 fish, land 5 fish in a fight, land a rare or legendary, land a trophy-sized fish. Each one pays Surge: 100, 200 and 300 spins.
One server-wide goal per tide: catch fish, land jackpots, or wager wins. Everyone's progress counts toward one shared target, with a per-player cap so no one can solo it. Completing it pays every contributor 10 πͺ chips and lifts everyone's win chance to 55% for a week.
- Free for everyone. Tick auto-spin and the wheel spins every 3 seconds.
- It keeps going when you close the tab or log out. Come back within 24 hours and your missed spins are played out, with a "While you were away" card showing the time, spins and wins (and fish, with auto-fish).
- Manual spinning is locked while auto-spin runs.
New players don't get every panel at once. Fishing opens at 10 spins (or your first catch), Bounties at 25, Dice at your first 3-streak, and the Community Goal at 50. Once a panel opens it stays open, even after the tide turns.
To keep the weekly race fair, systems built for endless seasons are retired: Prestige, Loadouts, the Singularity and the Aquarium (their API routes return 410 Gone). Classes and the functional shop are gone too: gear comes from Charts. The leaderboard ranks by wins alone.
- Register with a username (3β32 alphanumeric) and password (6+ chars)
- One account per device (enforced via a long-lived
device_idcookie; multiple users on the same IP are fine) - Strict single-session enforcement β logging in on a new device boots the previous session
- 30-day persistent login sessions (signed HTTP-only cookies)
- Brute-force protection: escalating lockouts after 5/10/20 failed attempts per username (1min/5min/1hr)
- All login and registration attempts are logged with IP, normalised username, User-Agent, and rejection reason
A persistent chat channel (bottom-right panel, resizable) where players can talk, alongside automatic announcements for big wins, double-down wins, new players, community goal milestones and each tide's podium.
- A full-viewport canvas fire effect rises behind all game UI, scaling with win streak intensity
- Mix mode (default) β embers and a cellular automaton inferno layered with additive blending
- Embers appear from streak 3; inferno ignites from streak 10; screen fills around streak 30
- Intensity lerps smoothly β wins cause the fire to grow, a loss makes it fall gradually rather than cutting out
- Suppressed automatically in Low-Spec Mode and when OS
prefers-reduced-motionis set
- Fully playable on phones and tablets (β€ 768 px breakpoint); the desktop layout is unchanged
- Bottom toolbar: Shop πͺ, Leaderboard π, Fishing π£ (once unlocked), Chat π¬, Backpack π and Stats π
- Backpack drawer: stake chips, Bounties and the Community Goal in one scrollable column
- Tap-to-dismiss backdrop: tapping outside any open panel closes it
- Low-Spec Mode (β‘ button in the top bar) β disables infinite CSS animations, GPU-heavy drop-shadows, confetti, fish aura, and fire effect; respects OS
prefers-reduced-motion - Preference is saved per user in the database and synced across devices
- All game logic runs server-side; clients cannot submit win/loss outcomes, fish catches, or spin results
- Stakes, chips and Chart allocations are re-validated server-side rather than trusting client-supplied amounts
- Replay strings are HMAC-signed so a hand-crafted string can't impersonate a real win
- Only one tab plays at a time: a second tab is paused and offers Play here to move over
- Rate limiter keys on user account rather than IP (prevents shared-network collisions)
In Season 9 the shop sells cosmetics only, paid for in losses. Gameplay gear comes from Charts. The shop shows your Chart at a glance; tap it to open the full Charts. Cosmetics are kept forever, through every tide.
- Wins: your score for the tide, and what Chart levels cost.
- Losses: spent on cosmetics (skins, trails, themes, backgrounds).
- Fish Bucks π: earned from fishing.
- Surge spins π: earned from fishing and bounties; each one multiplies a spin's wins.
- Stake chips πͺ: 3 free a day, plus community goals; spent on staked spins.
| Skin | Cost | Emoji |
|---|---|---|
| Tropical Fish | 25 | π |
| Pufferfish | 50 | π‘ |
| Octopus | 75 | π |
| Shark | 100 | π¦ |
| Dolphin | 150 | π¬ |
| Squid | 200 | π¦ |
| Turtle | 350 | π’ |
| Crab | 600 | π¦ |
| Lobster | 1,000 | π¦ |
| Whale | 2,000 | π³ |
| Seal | 3,500 | π¦ |
| Shrimp | 6,000 | π¦ |
| Coral | 10,000 | πͺΈ |
| Mermaid | 17,500 | π§ |
| Crocodile | 30,000 | π |
| Rocket | 50,000 | π |
| Comet | 85,000 | βοΈ |
| Saturn | 145,000 | πͺ |
| Alien | 250,000 | π½ |
| UFO | 425,000 | πΈ |
| Lucky Dice | 600,000 | π² |
| Joker | 850,000 | π |
| Diamond | 1,200,000 | π |
| Poker | 1,700,000 | |
| Slot Machine | 2,400,000 | π° |
Each skin has custom idle/win/loss speech. Buy and equip to change the fish.
Visual trail effect on the fish. Trail and streak aura effects coexist independently.
| Tier | Cost | Effect |
|---|---|---|
| Sparkle Trail | 125 | β¨ Gold shimmer |
| Fire Trail | 500 | π₯ Flame glow |
| Rainbow Trail | 2,000 | π Rainbow hue |
| Frost Trail | 7,000 | βοΈ Ice crystal aura |
| Thunder Trail | 22,000 | β‘ Electric sparks |
| Galaxy Trail | 70,000 | π Cosmic swirl |
Changes the canvas colour palette of the wheel. Two independent chains β own and switch between either freely.
| Theme | Cost | Look |
|---|---|---|
| Fire Theme | 250 | π₯ Red/orange |
| Ice Theme | 1,000 | βοΈ Blue/cyan |
| Neon Theme | 4,000 | π Purple/neon |
| Void Theme | 12,000 | π Deep void |
| Gold Theme | 40,000 | β¨ Pure gold |
| Tidal Theme | 250 | π Cool blue/teal, wave animation |
| Ember Theme | 1,000 | π₯ Warm orange, spark animation |
| Frost Theme | 4,000 | βοΈ Ice-crystal palette, crack animation |
| Aurora Theme | 12,000 | π Shifting greens/purples, northern lights |
| Vintage Theme | 40,000 | πΌ Retro sepia tones |
| Golden Wheel | 300 | β¨ Radiant glow ring (independent of theme) |
Resizes the fishing panel. Priced at 1 loss each as an accessibility option, not a progression item.
| Tier | Cost | Panel Size |
|---|---|---|
| Compact | 1 | 50% |
| Big Panel | 1 | 130% |
| Giant Panel | 1 | 160% |
| Colossal | 1 | 200% |
Ocean Casino (an animated seabed, static in Low-Spec Mode) is the free default. Buying and equipping another background overrides it.
| Theme | Cost | Look |
|---|---|---|
| Royal Casino | 400 | Rich purple |
| Inferno Casino | 1,600 | Blazing red |
| Forest | 5,000 | π² Lush green |
| Abyss | 15,000 | π Deep dark ocean |
| Cosmic | 50,000 | π Space nebula |
Each season's page theme is granted to everyone automatically; older ones can be bought.
| Theme | Cost | Look |
|---|---|---|
| Season 1 | 1,000 | Classic gold & orange |
| Season 2 | 1,000 | Green & red |
| Season 3 | 1,000 | Purple & orange |
| Season 4 | 1,000 | Deep violet |
| Season 5 | 1,000 | Bioluminescent cyan & coral |
| Season 6 π | 1,000 | Night ocean: deep indigo & violet |
| Season 7 | 1,000 | Sepia-tinted |
| Season 8 π° | 1,000 | Casino floor |
| Season 9 π | 1,000 | Tides: current season default (auto-granted to all players) |
| Tier | Cost | Count |
|---|---|---|
| Confetti+ | 75 | Γ2 |
| Confetti++ | 300 | Γ5 |
| Confetti MAX | 1,200 | Γ15 |
| Party Mode | 150 | Confetti on every result |
- Python 3.8+
- PostgreSQL 14+
- Node.js (for the one-time JSX build step)
pip install -r requirements.txt# Create DB user and database
sudo -u postgres psql -c "CREATE USER wheelapp WITH PASSWORD '<your-password>';"
sudo -u postgres psql -c "CREATE DATABASE wheeldb OWNER wheelapp;"Then apply the baseline schema and run migrations:
PGPASSWORD='<your-password>' psql -U wheelapp -d wheeldb -h localhost -f schema.sql
DATABASE_URL="postgresql://wheelapp:<your-password>@localhost/wheeldb" python migrate.pyBoth variables are required β the server will refuse to start without them.
export DATABASE_URL="postgresql://wheelapp:<your-password>@localhost/wheeldb"
export WHEEL_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
export PORT=5000 # optional, defaults to 5000For convenience, copy .env.example to .env β python-dotenv will load it automatically.
The JSX source must be transpiled once (and again after any app.jsx changes):
npx babel static/app.jsx -o static/app.jsPresets are loaded from babel.config.json in the repo root, so no
--presets flag is required.
Production (recommended):
gunicorn -c gunicorn.conf.py server:appDevelopment:
python server.pyOpen http://localhost:5000 in your browser. You'll be prompted to register or log in.
A separate staging environment runs on port 5001 against a wheeldb_staging database, using a git worktree on the staging branch.
/home/user/wheel-app/ β master (production, port 5000, wheeldb)
/home/user/wheel-app-staging/ β staging (port 5001, wheeldb_staging)
Start staging dev server:
cd /home/user/wheel-app-staging && PORT=5001 python server.pyPromote to production:
cd /home/user/wheel-app && ./deploy.shdeploy.sh merges staging β master, applies pending migrations, rebuilds the frontend, and reloads gunicorn.
The tide turns automatically every Friday at 21:00 UK time, driven by a systemd timer (deploy/wheel-rollover.timer β deploy/wheel-rollover.service) that runs bin/rollover.sh:
bin/advance_tide.py --check-only: is a tide due? If not, exit quietly.- Clone the live database (
bin/clone-prod-to.sh) and rehearse the rollover on the clone, then verify it withbin/post_rollover_check.py. - Back up the live database.
- Advance the live tide (
seasons.advance_season) and verify it again, including the live/api/seasonand/api/hall-of-fame.
The script holds a lock so two runs can't overlap. On any failure it stops, leaves a ROLLOVER_FAILED marker in the app directory, and keeps the rehearsal database for inspection. APP_DIR, PROD_DB, REHEARSAL_DB, LIVE_URL and BACKUP_CMD can be overridden to point it at staging.
Schema changes are managed with numbered SQL files and a lightweight migration runner.
python migrate.py # apply pending migrations
python migrate.py --status # show applied / pending migrations
python migrate.py --dry-run # preview without executingMigration files live in migrations/NNN_description.sql. Applied versions are tracked in the schema_migrations table in each database.
The test suite uses pytest. Run it via the Makefile target or directly:
make test # equivalent to: python3 -m pytest -q
python3 -m pytest -q # run from the repo root
python3 -m pytest tests/test_models.py -q # single filePrerequisites: a reachable PostgreSQL instance is required for the
DB-backed tests. The test suite now runs against wheeldb_test (a
clone of the production schema), NOT the production wheeldb β
T246's conftest safety check refuses to run if DATABASE_URL points
at the prod database. Set up the test DB once:
make test-db-reset # drops, recreates, and migrates wheeldb_test
make test # runs the suiteThe connection string is read from the DATABASE_URL environment
variable β set it in your shell or in .env. The make test target
auto-rewrites a wheeldb URL to wheeldb_test so a developer's
local .env works as-is. (T234 moves the staging credentials out
of the test files into .env, so a missing
DATABASE_URL will fail with a clear error rather than silently using a
baked-in credential). The safety check also refuses to run if
DATABASE_URL points at the production wheeldb (it must be
wheeldb_test or wheeldb_staging).
The unit tests in tests/test_models.py and
tests/test_format_wins_python.py are pure and need no DB.
wheel-app/
βββ server.py # Thin entry point: create_app() β gunicorn target
βββ app.py # Flask app factory: config, extensions, blueprints, error handlers
βββ auth.py # Blueprint: /api/me, /api/register, /api/login, /api/logout
βββ game.py # Blueprint: state, spin, tick, auto-spin, dice, charts, fishing,
β # shop, wager, bounties, community goal, hall of fame,
β # leaderboard, stats, season, health
βββ season_config.py # Current season: name, number, page theme, rollover day/time
βββ seasons.py # Tide labels, next rollover time, advance_season()
βββ talents.py # Charts: the three trees, points, level cost, Surge rules
βββ fish.py # Cast, bite, fight and land; auto-fish
βββ fish_catalog.py # The 46 species: rarity, time-of-day, tide and migrant windows, weights
βββ dice.py # Dice charges, recharge and rolls
βββ shop.py # Buying and equipping cosmetics
βββ db.py # psycopg2 ThreadedConnectionPool + db_connection() context manager
βββ models.py # FISH_SKINS, SHOP_ITEMS, streak bonus and other game constants
βββ wagers.py # Stake validation, escrow risk calculation
βββ wheel_modes.py # Wheel mode definitions + weekly rotation
βββ bounties.py # Daily bounty selection, progress, Surge rewards
βββ community_goals.py # Per-tide community goal lifecycle
βββ chat.py # Blueprint: /api/chat, system message posting
βββ chat_triggers.py # System announcement text (big wins, tide podium, goal milestones)
βββ security.py # check_lockout(), record_attempt(), clear_attempts(), require_json()
βββ extensions.py # Flask-Limiter and Flask-Login instances
βββ migrate.py # SQL migration runner (apply / status / dry-run)
βββ deploy.sh # Production deploy: merge staging β migrate β build β reload
βββ gunicorn.conf.py # Gunicorn config: 4 gthread workers Γ 4 threads, PORT from env
βββ schema.sql # PostgreSQL baseline schema
βββ migrations/ # Numbered SQL migration files (NNN_description.sql)
βββ bin/ # Tide rollover: rollover.sh, advance_tide.py, post_rollover_check.py,
β # clone-prod-to.sh
βββ deploy/ # systemd units for the weekly rollover timer
βββ requirements.txt # Python dependencies
βββ .env.example # Required environment variable template
βββ static/
βββ index.html # Slim HTML shell
βββ app.jsx # React source (edit this)
βββ app.js # Compiled output (generated by Babel β do not edit directly)
βββ styles.css # All CSS
All game endpoints require authentication (session cookie). POST endpoints require Content-Type: application/json. Routes without a listed limit share the default of 200/min.
| Endpoint | Method | Rate Limit | Description |
|---|---|---|---|
/api/me |
GET | β | Returns {username} or {username: null} |
/api/register |
POST | 5/hr | Create account |
/api/login |
POST | 10/min | Authenticate |
/api/logout |
POST | β | Clear session |
| Endpoint | Method | Rate Limit | Description |
|---|---|---|---|
/api/health |
GET | β | DB connectivity check β {"status":"ok"} or 503 (no login needed) |
/api/state |
GET | β | Full game state |
/api/season |
GET | 60/min | Current season and tide label |
/api/settings |
POST | β | Persist user preferences (low_spec_mode) |
/api/spin |
POST | 10/sec | Server determines outcome, updates DB. Body: {stake, tab_id} |
/api/tab/heartbeat |
POST | 30/min | Claim or keep the playing tab. Body: {tab_id, takeover} |
/api/auto-spin/start |
POST | β | Start auto-spin |
/api/auto-spin/stop |
POST | β | Stop auto-spin |
/api/tick |
POST | 30/min | Play out due auto-spins; summarises a catch-up after time away |
/api/roll-dice |
POST | 3/sec | Roll dice onto a 3+ win streak |
/api/charts |
GET / POST | POST 10/sec | Get your Chart / save it. Body: {alloc} |
/api/charts/level-up |
POST | 10/sec | Buy a Chart point with wins |
/api/cast |
POST | 5/sec | Cast the line |
/api/bite-poll |
POST | 8/sec | Check for a bite |
/api/reel |
POST | 5/sec | Hook the fish and start the fight |
/api/land |
POST | 5/sec | Finish the fight. Body: {landed, quality} |
/api/fish-catalog |
GET | β | All species, what's biting now, your records |
/api/auto-fish-tick |
POST | 1/5sec | One automated catch (needs Deckhand) |
/api/auto-fish-enabled |
POST | 10/min | Toggle auto-fish. Body: {enabled} |
/api/buy |
POST | β | Buy a cosmetic. Body: {item_id} |
/api/equip |
POST | β | Equip a fish skin. Body: {fish_id} |
/api/equip-cosmetic |
POST | β | Toggle a cosmetic on/off. Body: {item_id} |
/api/wager/stake |
POST | β | Set your stake. Body: {stake} |
/api/wager/double-down |
POST | β | Arm Double or Nothing for the next spin |
/api/wager/double-down/cancel |
POST | β | Disarm it |
/api/insurance/arm |
POST | β | Arm insurance for the next spin |
/api/insurance/cancel |
POST | β | Disarm it |
/api/insurance/claim-free |
POST | β | Claim today's 3 stake chips (409 if already claimed) |
/api/wheel-mode |
POST | β | Set active wheel mode. Body: {mode} |
/api/bounties |
GET | β | Today's 3 bounties with progress |
/api/bounties/claim |
POST | β | Claim a completed bounty's Surge. Body: {bounty_id} |
/api/community-goal |
GET | β | This tide's goal, progress and your contribution |
/api/hall-of-fame |
GET | 30/min | Every past tide's podium and the medal table |
/api/leaderboard |
GET | 30/min | Top 10 players of the current tide |
/api/stats |
GET | β | Personal stats, including Season History |
/api/patch-notes |
GET | 20/min | Patch notes |
/api/chat |
GET / POST | GET 30/min, POST 1/sec | Read / post chat messages |
/api/admin/advance-season |
POST | β | Admin only (X-Admin-Secret header) |
Retired in Season 9 (return 410 Gone): /api/prestige, /api/singularity, /api/singularity/contribute, /api/loadout, /api/loadout/apply, /api/aquarium. /api/wins-exchange returns 403.
/api/spin response (abridged; see _RESPONSE_KEYS in game.py for the full set):
{
"result": "win",
"wins_delta": 40,
"losses_delta": 0,
"streak": 4,
"bonus_earned": 4,
"effective_win_mult": 5,
"jackpot_hit": false,
"echo_triggered": false,
"stake": 0,
"insurance_tokens": 3,
"active_wheel_mode": "steady",
"surge_spins": 41,
"surge_used": true,
"message": "..."
}wins_delta and losses_delta are the change from this spin (net of any stake escrow). The client adds these to its local state to avoid race conditions.
The frontend is a pre-compiled React app. Edit static/app.jsx and run the Babel build step to update static/app.js. Key components:
| Component | Purpose |
|---|---|
App |
Root: checks /api/me, renders AuthPage or GameApp |
AuthPage |
Login/register form with error handling |
GameApp |
Main game: wheel, fish, panels, all API calls |
TideBanner |
Current tide, countdown to the turn, last tide's podium; opens the Hall of Fame |
HallOfFamePanel |
Medal table and every past tide's podium |
TidesBackground |
The Season 9 animated sea background |
ChartsPanel |
The three Chart trees: spend, level up, re-chart |
ChartStrip |
Your Chart at a glance, inside the shop |
FishingPanel |
Cast, bite, and the reel-bar fight; Auto-Cast/Auto-Fish toggles |
FishEncyclopedia |
All 46 species, what's biting now, hints and record weights |
StreakPanel |
Sidebar streak display |
DicePanel |
Dice charges and the roll button |
WagerPanel |
Stake panel, Double or Nothing and insurance |
FreeTokensPanel |
Claim today's stake chips |
BountiesPanel / CommunityGoalPanel |
Progress bars and claim buttons |
ShopPanel |
Cosmetics shop with the Chart strip; collapsible |
Leaderboard |
Top 10 players of the tide |
StatsPanel |
Personal stats modal (π) |
PatchNotesPanel |
Patch notes and What's New |
FireEffect |
Full-viewport canvas fire behind the UI, scaled by win streak |
ChatPanel |
Resizable bottom-right chat panel |
GuardWheel |
Mini wheel overlay when Breakwater blocks a loss |
drawWheel |
Canvas rendering with theme support (default / fire / ice / neon / void / gold / tidal / ember / frost / aurora / vintage) |
Mobile layout is handled in CSS (@media (max-width: 768px)) and a small amount of React state (isMobile, mobilePanel) in GameApp. The same components are reused, positioned via CSS class toggles.
Minimal localStorage: game state lives in PostgreSQL, but UI preferences (low-spec mode, chat panel size/open state, patch-notes-seen, one-time hints) persist in localStorage.
- Backend: Python, Flask, flask-login, flask-limiter, bcrypt
- Database: PostgreSQL (psycopg2 with
ThreadedConnectionPool) - WSGI: Gunicorn (gthread workers)
- Frontend: React 18 (CDN UMD), pre-compiled JSX via Babel CLI, vanilla CSS
- Auth: Server-side sessions via signed HTTP-only cookies (SameSite=Lax)
