2026-10-21 13:26:16 -04:00
2026-10-21 13:26:16 -04:00
idk
2026-10-18 14:23:38 -04:00
idk
2026-10-18 14:23:38 -04:00
idk
2026-10-18 14:23:38 -04:00
2026-10-21 11:53:46 -04:00
2026-10-21 13:26:16 -04:00
2026-10-21 13:26:16 -04:00
2026-10-21 13:26:16 -04:00

The Button

A web game of patience and nerve. A 24-hour countdown resets every time any player presses the button. If the countdown ever reaches zero, the game is over forever, and the final global stats are published.

Inspired by Reddit's The Button experiment.

How to play

  1. Sign up with a name and password (or log back in).
  2. Press the button. The countdown resets to 24 hours.
  3. You score points based on the zone the countdown was in when you pressed. The longer you wait, the more you score, but if everyone waits too long, the game ends.

Zones

Higher-value zones are shorter, so scoring big is risky.

Zone Points Duration (24h timer)
Green 0 12h 11m
Blue 2 6h 6m
Yellow 4 3h 3m
Red 8 1h 31m
Black 16 46m
White 32 23m

Every press records the zone, the points, and the exact remaining time in milliseconds. Players can view their own score, presses per zone, and press history at any time. A public live leaderboard shows all players ranked by total points, with press counts; ties are sorted alphabetically. Scores refresh every five seconds and immediately after you press or join. Detailed global stats (presses per zone, closest call, full press log) stay hidden until the game ends.

Logged-in players can also join the mailing list.

The Chat section is a simple public chatroom. Logged-in players can post plain-text messages (up to 200 characters, one message every two seconds). The latest 100 messages are displayed oldest first and refresh every five seconds while Chat is open. Messages persist in SQLite across restarts, including after the game ends. There are no editing, deletion, or moderation controls yet.

Every successful button press also posts an automatic chat announcement with the player's name, exact time remaining, and points earned. These announcements are saved with the press and do not count toward the player's chat cooldown.

Engagement roadmap

The goal is to make waiting social and consequential without rewarding button spam or changing the current game's rules. Planned delivery order:

  1. Optional live presence — implemented. Show participating players watching the clock and who is viewing Chat. Off by default; ephemeral heartbeats, automatic expiry, and no activity history. See the details below.
  2. Personal bests and rivalries — implemented. Your score shows closest call, highest-scoring press, leaderboard position, and the player immediately ahead with points needed to catch up. Existing history is credited automatically; no data migration is needed.
  3. Achievements — implemented. Close call, Full spectrum, and Keeping us alive appear under Your score with unlock dates and progress. Existing press history counts, with no bonus points or press-volume rewards.
  4. Community milestones — implemented. Eight one-time shared goals post gold announcements in chat. Unlocks persist across restarts, and historical milestones are silently credited on upgrade.
  5. Optional zone alerts. Let players choose a zone and opt into sound or browser notifications. Request permissions only after an explicit action, alert on transitions rather than polls, and handle resets and denied permissions. Closed-browser delivery is separate work, not promised by basic tab notifications.
  6. Separate one-life mode — exploratory. Allow one press per player per round in a separate mode with its own rounds and leaderboard. Never silently change the current game or reset its data.

Keep new features consistent with the Catppuccin Mocha styling and selectable-section navigation. Preserve visible countdown/button controls on desktop and mobile, authenticated mutations, bounded input, safe text rendering, and server-authoritative scoring. Verify changes using isolated databases rather than live game data.

Personal bests and rivalries

Your score includes private personal records derived from your existing press history. Closest call means the least time remaining; highest-scoring press means the most points in a single press. Ties prefer the lower remaining time, then the earlier press. A zero-point press is still a valid record; new players see placeholders until their first press.

Leaderboard positions match the public ordering (points descending, then name alphabetically), not shared competition ranks. The rival is the player immediately above you, including alphabetical ties. The score card shows the gap to match their total and the minimum additional whole-number points to exceed it; actual presses still award fixed zone values, so you may jump past that threshold. Equal top scores are described as a shared lead; solo players and outright leaders receive their own messages. These cards refresh every five seconds while Your score is open and after pressing. Rival information exposes only public names and totals, never another player's records or press history.

Achievements

Your score includes eight private achievements, derived from the game's existing press history:

Achievement Requirement
Close call A successful press with strictly less than 60 seconds remaining (and more than zero).
Full spectrum At least one press in each of the six scoring zones; repeated presses in one zone do not advance it.
Red alert Press in the red zone.
Into the dark Press in the black zone.
White knuckle Press in the white zone.
Photo finish Press with strictly less than ten seconds remaining, and more than zero.
Last second Press with strictly less than one second remaining, and more than zero.
Keeping us alive Press after a global quiet stretch of at least half the saved game countdown: 12 hours for the standard 24-hour game. Scales with shorter test games.

Quiet time is measured from the previous press by any player, or from game start if there was no previous press. It is not the time since your own last press. The earliest qualifying press determines each unlock date. Progress is based on completed presses, not idle time accumulating while the page is open. These are badges only: they do not change scores, publish chat announcements, or reward repeated resets.

Historical JSON imports and existing SQLite history are credited automatically. No new table or manual migration is required for personal achievements; badges are recomputed from history after restart. They remain available after game over and refresh with Your score. Personal achievements are returned only by authenticated GET /api/me, not by the public leaderboard or final leaderboard.

Community milestones

There are eight community milestones, matching the eight personal achievements. Each posts once to the shared chat, with distinct gold styling:

Milestone Community goal
One day together Survive 24 hours.
A week of nerve Survive 7 days.
Thirty days strong Survive 30 days.
A growing crowd Reach 10 registered players.
Fifty strong Reach 50 registered players.
A hundred second chances Reach 100 presses.
A thousand moments Reach 1,000 presses.
Our first photo finish First successful press with more than zero but less than 10 seconds left.

Survival milestones use real elapsed time since game start, not scaled countdown durations, and never count time after the game deadline/end. They are checked by the server watcher and normal state requests, even without presses. The photo finish is a single community first, not an announcement for every improved record. No milestone gives bonus points.

Milestone unlocks are stored in SQLite separately from player chat, and share the latest-100-message feed. They have no player dependency and do not affect chat cooldowns. Registration/press milestone unlocks commit with the corresponding game update. Repeated requests, watchers, and restarts cannot announce a goal twice. The first upgrade/import silently records already-reached goals to avoid flooding chat; future newly-reached goals announce normally. On later restarts, newly crossed survival milestones are announced once. Missed goals are never replayed repeatedly.

Optional zone alerts are the next roadmap step.

Live presence

Logged-in players can enable Show me online in their score section. Presence is off by default. The count near the clock includes only opted-in players with a visible tab; Chat also lists opted-in players currently viewing that section. Multiple tabs/sessions count as one player. Updates run every 15 seconds, and stale presence expires after 45 seconds. Hiding a tab or opting out removes that tab's presence; logging out removes all presence for that session. Failed disconnect updates fall back to expiry. Presence lives only in server memory and is cleared on restart. Guests and players who leave the setting off are not tracked, so the count is not a total visitor count.

Public GET /api/presence returns {online_count, chat_players}. Authenticated POST /api/presence accepts {tab_id, active, in_chat} with a 32-character lowercase hexadecimal tab ID and boolean flags. At most 10 active tabs per login session are accepted. No player IDs, session IDs, or last-seen timestamps are returned publicly.

Presence tests cover authentication, validation, expiry and heartbeat renewal, tab/session deduplication, chat membership, revoked sessions, bounded entries, and logout. Browser checks cover opt-in/out, tab visibility changes, chat membership, logout, and responsive layout.

Running

docker compose up -d --build

Open http://localhost:8000. The SQLite game database and the mailing list are stored in the button-data volume. Existing JSON game data in that volume is automatically migrated on the first startup after upgrading.

docker compose logs -f                                    # view logs
docker compose cp thebutton:/data/mailing_list.csv .      # export the mailing list
docker compose down                                       # stop (data is kept)
docker compose down -v                                    # stop and delete all data (new game)

Without Docker

Requires Python 3.9+. No third-party packages.

python server.py

Open http://127.0.0.1:8000. Game state is stored in data.sqlite3.

Migrating existing data

On startup, if the database has no saved game and data.json exists, the server automatically imports the countdown, players, password hashes, active sessions, subscription flags, and complete press history in one transaction. Older single-token player records are also supported. No manual conversion or additional dependencies are needed.

The JSON file is left untouched as a backup. Once the database contains a game, it is authoritative: subsequent restarts never re-import the JSON file. Invalid source data causes startup to fail rather than silently starting a new game; fix the source and restart to retry. The mailing-list CSV is unchanged.

Custom BUTTON_DATA_FILE paths continue to locate legacy JSON data; by default, the SQLite database is created alongside it with a .sqlite3 suffix. BUTTON_DATABASE_FILE can override the database path. Docker keeps both paths under /data, so existing named volumes migrate automatically.

To start a new game, stop the server and delete the SQLite database and remove or move the legacy JSON backup; leaving the JSON file in place would import the old game again.

Configuration

Set via environment variables (in Docker, add them under environment: in compose.yaml).

Variable Default Description
BUTTON_DURATION_SECONDS 86400 Countdown length. Zones scale with it.
BUTTON_HOST 127.0.0.1 (0.0.0.0 in Docker) Address to listen on.
BUTTON_PORT 8000 Port to listen on.
BUTTON_DATA_FILE data.json Legacy JSON file to import if no saved SQLite game exists.
BUTTON_DATABASE_FILE data.sqlite3 SQLite game database; defaults to BUTTON_DATA_FILE with a .sqlite3 suffix.
BUTTON_MAILING_LIST_FILE mailing_list.csv Mailing list file.

Tip: BUTTON_DURATION_SECONDS=60 makes for quick testing.

API

Method Path Auth Description
GET /api/state Deadline, server time, zones, whether ended
POST /api/register {name, password} → {name, token}
POST /api/login {name, password} → {name, token}
POST /api/logout ✓ Revoke the current session token
POST /api/press ✓ Press the button
GET /api/me ✓ Your score, zone counts, and press history
GET /api/leaderboard Public player names, total points, and press counts
GET /api/chat Latest 100 public chat messages
POST /api/chat ✓ {message} → saved message; 200-character limit
GET /api/presence Opted-in online count and chat display names
POST /api/presence ✓ {tab_id, active, in_chat} presence heartbeat or removal
GET /api/stats Global stats (403 until the game ends)
POST /api/subscribe ✓ {first_name, email, last_name?, phone?}

Authenticated requests use Authorization: Bearer <token>.

Security notes

  • Passwords are hashed with PBKDF2-SHA256 (600k iterations); session tokens are stored only as SHA-256 hashes.
  • Logins lock for 15 minutes after 5 failed attempts per name.
  • The server speaks plain HTTP. Put it behind a TLS-terminating reverse proxy (e.g. Caddy or nginx) before exposing it to the internet.
  • Registration and mailing-list signup are not rate limited.

Project layout

server.py          Game server and API (Python standard library only)
static/            Frontend (HTML, CSS, vanilla JS)
Dockerfile         Container image
compose.yaml       Docker Compose service and data volume

Credits

Created by Eric Meehan. Built with GitHub Copilot.

S
Description
The Button Game
Readme
2.6 MiB
Languages
Python 65.1%
JavaScript 16.6%
CSS 10.1%
HTML 8%
Dockerfile 0.2%