A single-file, self-contained darts scoring application for playing 301 and 501 games. The entire application—server, game logic, and user interface—lives in one Python file (server.py) with no external dependencies beyond Python's standard library.
The app runs a local HTTP server that serves an embedded HTML/CSS/JavaScript frontend. All game state is managed server-side in Python, while the browser handles rendering and user interaction. Players and win statistics persist to a JSON file, surviving server restarts.
Key characteristics:
- Zero dependencies — Uses only Python standard library
- Single file — Everything in
server.py(711 lines) - Three languages — Python (backend), JavaScript (frontend logic), HTML/CSS (UI)
- Stateful server — Game state lives in Python memory, synced via REST API
- Persistent leaderboard — Win counts saved to
darts_data.json
python3 server.py
# Open http://localhost:8000server.py # The entire application (711 lines)
darts_data.json # Auto-created persistence file for players & wins
| Section | Language | Lines | Purpose |
|---|---|---|---|
| Backend server | Python | 1-261 | HTTP handling, game logic, persistence |
| Frontend UI | HTML/CSS/JS | 265-699 | Embedded in Python string literal |
| Server bootstrap | Python | 701-711 | Entry point |
File: server.py
Language: Python 3
Lines: 1-261, 701-711
DATA_FILE = "darts_data.json" # Line 14
load_data() # Lines 16-20: Load players & wins from disk
save_data() # Lines 22-24: Write players & wins to disk
- Players and win counts persist across server restarts
- Auto-creates file on first save
- Simple JSON format:
{"players": {...}, "wins": {...}}
players = {} # id -> {id, name}
wins = {} # playerId -> win count (integer)
game = None # Current game state (dict or None)All state is held in Python global variables. The game object structure:
{
"variant": 501, # 301 or 501
"playerIds": ["1", "2"], # Turn order
"scores": {"1": 501, "2": 501},
"currentPlayerIndex": 0, # Index into playerIds
"currentTurn": [], # List of {segment, score}
"turnHistory": [], # Completed turns
"phase": "playing", # "playing" | "finished"
"winnerId": None # Set when game ends
}| Function | Lines | Purpose |
|---|---|---|
new_game(variant, player_ids) |
33-43 | Create fresh game state |
segment_score(seg) |
45-61 | Parse segment string ("T20") to points |
is_double(seg) |
63-64 | Check if segment is a double |
current_player_id() |
66-69 | Get active player's ID |
apply_throw(segment) |
71-131 | Core scoring logic — handles bust, win, turn advancement |
advance_player() |
133-135 | Move to next player (round-robin) |
undo_throw() |
137-165 | Undo last throw or restore previous turn |
skip_turn() |
167-189 | End turn early, advance to next player |
A throw busts if:
new_score < 0— Went below zeronew_score == 1— Can't finish (need double, minimum 2)new_score == 0 && !is_double(segment)— Must finish on a double
On bust, score resets to start-of-turn value (line 100-101).
When new_score == 0 (and not busted):
- Set phase to "finished"
- Record winner ID
- Increment win counter
- Persist to disk
Class: DartsHandler (extends SimpleHTTPRequestHandler)
| Path | Lines | Response |
|---|---|---|
/ |
195-199 | Serves the embedded HTML frontend |
/api/state |
200-201 | Returns {players, game, wins} as JSON |
| Path | Lines | Purpose |
|---|---|---|
/api/player |
212-216 | Create new player |
/api/player/delete |
218-223 | Delete player by ID |
/api/game/start |
225-230 | Start new game with selected players |
/api/game/throw |
232-236 | Record a dart throw |
/api/game/undo |
238-240 | Undo last throw |
/api/game/skip |
242-244 | Skip remaining throws, advance turn |
/api/game/end |
246-248 | End current game |
All API responses use json_response() which:
- Sets
Content-Type: application/json - Adds CORS header for cross-origin requests
- JSON-encodes the response data
if __name__ == "__main__":
port = 8000
server = HTTPServer(("", port), DartsHandler)
server.serve_forever()Location: Embedded in HTML string literal (lines 265-699)
Language: HTML5, CSS3, vanilla JavaScript (ES6+)
- Single
<div id="app">container - All UI rendered dynamically via JavaScript
- Mobile-optimized viewport meta tag
| Component | Lines | Description |
|---|---|---|
| Base reset | 272-275 | Box-sizing, dark background |
| Buttons | 277-288 | Color variants (yellow, green, blue, red, etc.) |
| Player rows | 290-299 | Flexbox layout, selected/active states |
| Throw display | 306-314 | Three boxes showing current turn's darts |
| Bed selector | 316-321 | Single/Double/Treble toggle buttons |
| Number grid | 323-327 | 5x4 grid of numbers 1-20 |
| Checkout hint | 337-340 | Green box showing finish path |
| Win screen | 342-345 | Centered layout with large text |
let players = {}; // Synced from server
let game = null; // Current game state
let wins = {}; // Win counts per player
let selectedIds = []; // Players selected for next game
let variant = 501; // Selected game type
let bed = "S"; // Current bed selection (S/D/T)
let confettiInterval; // Animation frame IDapi(path, data) // POST request, updates state, re-renders
loadState() // GET /api/state on page loadAll API calls automatically update local state and trigger re-render.
const CHECKOUTS = {
170: "T20 T20 DB",
167: "T20 T19 DB",
// ... 70+ entries for scores 2-170
};
getCheckout(score, dartsLeft) // Returns path or nullStandard dart checkout combinations. Only shows if achievable with remaining darts.
| Function | Lines | Purpose |
|---|---|---|
startConfetti() |
422-468 | Creates 150 falling rectangles with physics |
stopConfetti() |
471-477 | Cancels animation, clears particles |
Uses HTML5 Canvas with requestAnimationFrame for smooth 60fps animation.
The render() function handles three screens:
| Screen | Lines | Condition |
|---|---|---|
| Game Over | 483-525 | game.phase === "finished" |
| Game In Progress | 527-605 | game.phase === "playing" |
| Setup/Lobby | 607-661 | game === null |
- Winner announcement
- Final scores for all players
- Leaderboard sorted by wins
- Confetti animation trigger
- "New Game" button
- Player list with scores (active player highlighted)
- Three throw boxes showing current turn
- Checkout hint (when applicable)
- Bed selector (Single/Double/Treble)
- Number grid (1-20)
- Bull buttons (25/50)
- Action buttons (Undo, Miss, Next)
- Last turn summary
- Player name input
- Player list (tap to select)
- Win count badges on players
- Game type selector (501/301)
- Start game button
- All-time leaderboard (top 5)
| Function | Lines | Purpose |
|---|---|---|
addPlayer() |
665-672 | Create player from input field |
togglePlayer(id) |
674-681 | Select/deselect player for game |
setBed(b) |
684-687 | Change bed selection |
doThrow(segment) |
689-692 | Record throw, reset bed to Single |
loadState(); // Fetch initial state on page loadUser clicks "T20"
↓
doThrow("T20") called
↓
POST /api/game/throw {segment: "T20"}
↓
Python: apply_throw("T20")
↓
Game state updated in memory
↓
JSON response: {game: {...}}
↓
JavaScript updates local state
↓
render() rebuilds entire UI
| Code | Meaning | Points |
|---|---|---|
S1-S20 |
Single 1-20 | 1-20 |
D1-D20 |
Double 1-20 | 2-40 |
T1-T20 |
Treble 1-20 | 3-60 |
SB |
Single Bull | 25 |
DB |
Double Bull | 50 |
MISS |
Miss | 0 |
File: darts_data.json
{
"players": {
"1": {"id": "1", "name": "Alice"},
"2": {"id": "2", "name": "Bob"}
},
"wins": {
"1": 5,
"2": 3
}
}| Section | Lines | Count |
|---|---|---|
| Python imports & persistence | 1-24 | 24 |
| Game state & logic | 26-189 | 164 |
| HTTP server | 191-261 | 71 |
| HTML/CSS (embedded) | 265-351 | 87 |
| JavaScript (embedded) | 356-696 | 341 |
| Server bootstrap | 701-711 | 11 |
| Total | 711 |