The Handbill Ledger

A Django game, played start to finish — the walkthrough


A bounty-hunter game that plays out like Django Unchained and is built so that every mechanic is a Django-framework feature doing its clearest possible job. What follows is one complete playthrough — a fresh hunter from the login handbill to “free at last” — with each beat read three ways: what is on the screen, where it comes from in the picture, and what the framework is doing underneath.

Animated capture of the game: a hunter walking the pixel-art town of Daughtrey, opening the wanted board, and claiming a handbill
The trade, in motion
Act I · Taking up the trade

From the ledger’s first page

1

The ledger opens

Login page styled as a wanted handbill nailed to a dark wood wall

/login/ — the wanted-poster auth screen

On the screen
The front door is a handbill tacked to lamplit planks: name, word, and a brass-ringed ledger line. Typed values render in a struck-into-the-book monospace.
In the picture
Schultz travels on paperwork — bills of sale, warrants, handbills. Nothing in his world happens until the right paper says so, and neither does anything here.
Under the hood
Django’s own contrib.auth views and AuthenticationForm, restyled but never rewritten — field names, validators, and error slots are the framework’s. A wrong password answers in character (“That name and word don’t match our ledger.”) with Django’s literal message beneath in fine print, because an inactive-account refusal must never be dressed up as a typo.
2

A name goes in the book

Signup page with password rules rendered in the handbill style

/signup/ — registration

On the screen
Signing up mints the character: $75 in the purse, rank Freedman, standing on Daughtrey’s spawn tile.
In the picture
The film opens with a man being freed and handed a trade. That is literally this form.
Under the hood
A CreateView whose form_valid runs in transaction.atomic: the User and the Hunter are born together or not at all. Placement coordinates come off the seeded Location row — moving the spawn is a map edit, not a code change.
3

Daughtrey, Texas

The pixel-art town of Daughtrey with HUD showing $75 and rank Freedman

/game/ — first spawn, canvas world, server-rendered HUD

On the screen
A hand-pixeled town — mud ruts in the road, clapboard walls, doors glowing lamplight. Arrow keys walk tile to tile; the HUD above is not part of the canvas at all.
In the picture
Daughtrey is the film’s first town — where Schultz demonstrates how the paperwork of the trade beats the men with guns.
Under the hood
The whole world is rows: territories, locations, and trigger tiles are models, seeded by data migrations and compiled from ASCII map art by manage.py buildmaps — which flood-fills every map from its spawn and refuses to write a world where any door is unreachable. The client gets tiles and coordinates only; every meaning stays server-side.
4

The doctor makes his pitch

Speech bubble from Dr. King Schultz over the pixel town

A DIALOGUE trigger tile, stepped on

On the screen
Stand beside the grey-bowlered figure and he speaks — the mentor pointing you at the marshal’s office.
In the picture
Schultz’s recruitment speech — the bounty trade explained by the man who lives on it.
Under the hood
The client posts only (x, y). The server looks that coordinate up in TriggerTile against the location it believes you occupy, and answers with explicit keys — a payload spread was rejected in review because a key named action could impersonate a travel instruction.
Act II · The trade

Paper, powder, and the price of a head

5

The wanted board

The marshal's office wanted board with two posters, portraits, and reward bands

/board/ — swapped into the panel by the marshal’s-office door

On the screen
Texas paper only — the wall in front of you, richest reward first, each poster carrying its sketch and its licence: DEAD OR ALIVE or ALIVE ONLY, printed in words on the paper itself.
In the picture
The Smitty Bacall handbill, read aloud in the film with its per-head arithmetic, is this screen’s blueprint — down to the gang roster, which the game corrected from the actual script (Gerald Nash rides with Michaels and Koons).
Under the hood
Bounty.objects.open() is a custom QuerySet where expiry is a WHERE clause: a lapsed handbill vanishes with nobody writing a row. The poster is a template inclusion tag — the board and the claim endpoint render the same partial by calling the same function, so they cannot drift. Query cost is pinned by a test that renders one poster and eight for the same budget.
6

Taking the paper down

Gerald Nash's poster stamped YOURS with the ribbon 'Paper taken. Ride.'

The claim, the instant after the click

On the screen
One click and the head is yours — the poster restamps itself YOURS, and every other poster’s button withdraws: one hunt at a time.
In the picture
A handbill claimed is a contract. Two hunters cannot collect on one head, which in the film is settled with pistols and here is settled by the database.
Under the hood
The project’s crown jewel: claim() locks the row with select_for_update, re-checks under the lock, and is backstopped by partial unique constraints in Postgres. The test spins two real threads at one bounty and proves exactly one wins — and a mutation check proved that removing the lock makes it fail 15 times out of 15.
7

The general store

The store shelf with prices, a disabled too-expensive item, and a reputation-gated rifle

/store/ — the shelf, with gates showing

On the screen
Five items, honest labels: what you cannot afford says “Beyond your purse”, and the Sharps rifle sits greyed until word of you travels — reputation-gated, visible but unbuyable.
In the picture
Outfitting for the winter hunts — the montage where the trade starts paying for its own tools.
Under the hood
The form’s queryset is the gate — an item you may not buy is not in the form at all. The friendly refusal lives in clean_item; the guarantee lives in a conditional UPDATE … WHERE cash >= price whose row count decides. Thirteen mutations were run against this form’s tests; all thirteen were caught.
8

$75 becomes $25

The Colt purchased, price struck through, HUD showing the new balance

The purchase response — shelf restocked, purse redrawn

On the screen
The Colt joins your kit, its price struck through, and the purse in the top bar changes without the page moving.
In the picture
Money on the counter. The film counts every dollar out loud; so does the HUD.
Under the hood
An htmx out-of-band swap re-renders the HUD partial inside the purchase response — after refresh_from_db(), because middleware snapshots the hunter before the view runs and an unrefreshed include would show pre-purchase money. A review caught that exact trap before any panel shipped it.
9

The saloon knows everything

The saloon panel: barkeep rumors, the Fastest Guns leaderboard, and career stats

/saloon/ — rumors, standings, and your own ledger

On the screen
The barkeep’s talk is local and earned — some lines only surface once your reputation warrants them. On the wall: THE FASTEST GUNS, and your own career figures.
In the picture
Saloons are where the film moves its information — who is wanted, who is dangerous, what the winter pays ($1,500–$3,000 a gang, per the script, and per the barkeep here).
Under the hood
The leaderboard is one SQL statement: a filtered aggregate ranked by a RANK() window function — chosen over DENSE_RANK() because a saloon counts heads, so two hunters tied at $300 are both 1st and the next man is honestly 3rd. Career stats are a single aggregate() over the fight ledger.
Act III · The hunt

Out where the paper points

10

The Texas trail

The wilderness trail: grass, trees, a mud road and water

Through the town gate — the wilderness between towns

On the screen
The road west: churned mud, animated water, ambush ground that looks like ordinary trail — because it is. The trail warns you quietly, which is to say not at all.
In the picture
The riding. Half the film is two men crossing country toward a name on a piece of paper.
Under the hood
Doors are database rows whose payloads the browser never sees; walking is free and client-side, but travel happens only when the server answers a trigger POST by moving the row. A review found the one crack — an in-flight position save racing a door — and the fix now provably drains every save before any trigger fires.
11

The standoff

The showdown panel: a taunt, the pinned poster, and the DRAW moment

/hunt/showdown/ — the ENCOUNTER tile, with your paper matching this territory

On the screen
He speaks first — the same man says the same line every time you meet him — then a tension delay, then DRAW. Click fast and the roll gets up to +3.
In the picture
Every standoff in the film is a beat of stillness, then speed. The taunts are the script’s own villain lines, cleaned to the letter of the reference’s content rules.
Under the hood
The stopwatch lives in the session: both ends of the interval are server-observed, and the number the browser reports is read by nothing — a test time-travels ten seconds and posts elapsed_ms=0 to prove the client’s own clock buys zero. Dice, gear, reputation, and reaction are snapshotted onto the fight row so every verdict re-checks from its own arithmetic, forever.
12

“$200, paid on proof”

The winning verdict: HE IS DOWN, $200 paid, HUD updated

The win — a 16 against a 12, purse and repute move together

On the screen
HE IS DOWN. The reward lands, reputation follows, and the HUD redraws out of band — the one panel that moves money is the one panel that refreshes the bar.
In the picture
Schultz’s Daughtrey line — “In other words, marshal, you owe me two hundred dollars” — is the entire economy of this game in one sentence.
Under the hood
Collection is one transaction: a conditional UPDATE whose row count is the permission slip to pay, cash moved by F() expressions, and the project’s single custom signal — bounty_collected — carrying reputation across the app boundary. Sent inside the transaction on purpose: fame is part of the payout, so a failing receiver rolls the money back too.
13

Catching lead

The hurt refusal: LYING LOW with a live countdown

The other verdict — and what re-entering while wounded looks like

On the screen
Lose the roll and you caught lead: two minutes lying low, with a countdown that is a number, not a promise.
In the picture
People get shot in this story. The ones who live wait it out.
Under the hood
hurt_until is never cleared — the wound heals by the clock passing it, the same lazy-time trick as bounty expiry: no job, no worker, just a comparison. The gate is enforced in the engine under the claim lock (raising a distinct HunterHurt), because a review proved two simultaneous DRAWs could otherwise both roll off one arming.
14

The Marshal’s Office is the Django admin

The reskinned second AdminSite: parchment ledger of bounties with filters and actions

/marshal/ — a second, playable AdminSite; /admin/ stays stock for ops

On the screen
Staff play the marshal: the bounty ledger with filters, and actions that post outlaws to the board, double rewards on live paper, and re-hang lapsed handbills.
In the picture
The office Schultz walks into with a corpse and walks out of with a receipt. His ledger of handbills is, structurally, an admin changelist.
Under the hood
A second AdminSite instance, skinned through a one-line template fork proven safe under the production cached loader — the stock /admin/ renders byte-identical with the fork in place. The actions obey the same database constraints as the game: posting a man already wanted is refused politely, per row, by the constraint’s own name.
Act IV · Candyland

The house at the end of the paper trail

15

Turned away at the gate

Stephen turns away a low-reputation hunter, quoting the toll and their actual numbers

/finale/gate/ — reputation 400 and $500, or no entry

On the screen
Ride the whole map — Daughtrey, the trail, El Paso, Greenville — and the gate still reads your ledger back to you: not enough name, not enough money, come back when you are somebody.
In the picture
Candyland cannot be raided; it has to be bought into, with a cover story expensive enough to be believed.
Under the hood
A view gate on real columns — and one more condition a plan can’t foresee: you must not be mid-hunt, because the finale’s own paper rides the same one-active-claim constraint as everything else.
16

Stephen’s questions

The interrogation panel: question one of three

Stage one — the cover story, $100 per slip

On the screen
Three questions. A wrong answer costs $100 of cover story and starts you over; go broke and the gate closes behind you.
In the picture
Stephen is the film’s real security system — the one nobody budgeted for. His suspicion scene is this stage.
Under the hood
Every stage transition is a guarded UPDATE (filter(finale_stage=N)), so a re-POST or a second tab cannot skip a beat; the toll is the same race-safe conditional-UPDATE shape as the store, so it cannot overdraw.
17

The parlor, then the house

The parlor stand: Butch Pooch's zero-dollar poster and Candie's line

Stages two and three — $0 handbills; this was never about the money

On the screen
Two stands, worth nothing on paper — Butch Pooch in the parlor, then Stephen in the house, greeting you with his own gloat turned back on him.
In the picture
“You had my curiosity. Now you got my attention.” opens the parlor; “that’s when you knew your goose was cooked” opens the last door.
Under the hood
The finale reuses the whole encounter engine unchanged, hanging its paper OPEN-and-claimed inside one transaction so no other query ever sees it on a board. The single-occupancy parlor is not code being careful — it is one_active_posting_per_outlaw doing its job: a second hunter at this stage is told the parlor is occupied, by the constraint, translated by name.
18

Free at last

The epilogue: FREE AT LAST, career ledger, and the standings

The epilogue — career ledger closed, standings crowned

On the screen
The closing page of the ledger: eight hunts, the money, the fame — and the standings board with your name where it now belongs.
In the picture
The mansion, the ending, the horse taking its bow.
Under the hood
Freedom and its +300 fame land in a single UPDATE — a review caught that two statements left a crash window where a hunter could be free but unfamous, unrecoverably. The career figures are one aggregate(); the crown is the same window function that ranks the saloon wall.
The reckoning

How the picture maps to the framework

The README carries the full thirteen-row table; this is the short ledger.

The pictureThe gameThe framework
Schultz’s ledger of handbillsThe Marshal’s OfficeA second, reskinned AdminSite
Claiming a head firstBounty.claim()select_for_update + partial unique constraints
Telegrams between townsmanage.py telegraphOne idempotent management command — no Celery
Paper going staleBounty expiry, wound recoveryTime as a WHERE clause — lazy reads, one clock
Counting the reward out loudEvery dollar that movesF() expressions and conditional UPDATEs
Word travelsReputation on collectionThe one custom signal, sent inside the transaction
The handbill itself{% wanted_poster %}An inclusion tag both board and claim render through
Saloon talkEarned rumorsReputation-gated querysets
The fastest gunTHE FASTEST GUNS boardRANK() window function, one statement
Candyland’s one parlorSingle-occupancy finale standsone_active_posting_per_outlaw, translated by name
The proof

This walkthrough was also the test

The demo run doubled as a verification pass: every beat above was played live, with the browser console and the server log watched at each step.

Played clean
Zero 500s and zero tracebacks across the full run; the only console entry all session was a missing favicon. All eight fight rows re-check from their own snapshots: roll + sum(snapshot) ≥ target equals the recorded verdict, every time.
Suite behind it
370 tests: the two-thread claim race, a 13-mutation store kill-matrix, an 840-action seeded economy walk where the books must balance after every step, query-count budgets, and a Playwright smoke that signs up and claims through the real browser.

One bug found — and fixed — during this demo

The playthrough surfaced a stranded handbill: deleting a hunter cascades their claim away but left the bounty CLAIMED forever — and the very constraint that keeps the boards honest then barred that outlaw from ever being posted again. Big John Brittle was sitting in exactly that state.

The fix went to the game’s reaper: the telegraph now re-hangs claimed paper that has no living claim, before its expiry pass, so a lapsed orphan retires in the same run. The first attempt used .exclude(claims__resolved_at=None) — which a failing test caught compiling to a LEFT JOIN that excluded the zero-claim rows it existed to catch. The committed version names real claims by primary key. Running it against the live database re-hung Big John on the spot: telegraph: 1 re-hung, 0 expired, 0 posted.

Take up the trade

Everything above is playable now — the same $75, the same first town, the same paper on the wall. Give a name and a word and the ledger opens.

Take up the trade

Already in the book? Open the ledger