Hangman, and it plays for chips — which the plan had down as a free game, on the grounds that trivia has no euro coupling in gogobee. But a free game in a casino reads as a demo, so it stakes like everything else. The idea that makes it a casino game rather than hangman with a wager stapled on: the gallows is the payout meter. A wrong guess draws a limb *and* takes a tenth off what a win is worth, because those are the same event and showing them as one is the entire reason to bet on this. Short phrases pay 2.6x (fewer letters, less to go on), long ones 1.6x — the floor is 1x, so a win never hands back less than the stake, and the rake still comes out of winnings only. State.Pays() is the number the felt quotes and the number settle() lands on. They were briefly two sums, and the table spent an afternoon advertising a pre-rake payout it didn't honour. Two things the storage layer had already decided for us, and one it hadn't: game_live_hands is keyed on the player, so "one game at a time" holds across games for free (a live hangman 409s a blackjack deal, stake intact). But table() unmarshalled every live row as a blackjack hand, which does not fail on a hangman row — it quietly yields an empty hand. It dispatches on the game now. commit() is the settle path both games share, and casinoRoutes() the one route list, since the dev rig wires its own mux and a second copy is a copy that stops including the newest game. Driven in a browser, win and loss: 200 at 2.34x paid 455 and the bar landed on it; six wrong took the stake and no more; a reload mid-phrase brought back the board, the limbs, the multiple, the spent keys and the chips on the spot. The browser found the two bugs a Go test can't — a lives counter under the house rack, and a word wrapping early because the rack's clearance was on the whole column instead of the one row beside it.
177 lines
8.4 KiB
HTML
177 lines
8.4 KiB
HTML
{{define "title"}}Hangman · {{.Room.Name}}{{end}}
|
||
|
||
{{define "main"}}
|
||
<div class="space-y-6" data-hangman>
|
||
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div class="flex items-center gap-3 min-w-0">
|
||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||
</svg>
|
||
<span class="sr-only">Back to the casino</span>
|
||
</a>
|
||
<h1 class="font-display text-3xl font-bold">Hangman</h1>
|
||
</div>
|
||
<p class="text-sm text-[color:var(--ink)]/50">{{.MaxWrong}} lives · every wrong guess costs you a tenth of the win</p>
|
||
</div>
|
||
|
||
{{template "_chipbar" .}}
|
||
|
||
<!-- The felt. The gallows is on it because the gallows is the meter: it counts
|
||
down your lives and your winnings at the same time. -->
|
||
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
||
|
||
<div class="pete-rack" data-house aria-hidden="true">
|
||
<span data-chip="500" style="--stack: 5"></span>
|
||
<span data-chip="100" style="--stack: 7"></span>
|
||
<span data-chip="25" style="--stack: 4"></span>
|
||
<span data-chip="5" style="--stack: 6"></span>
|
||
</div>
|
||
|
||
<!-- The gallows and the stake stack up on the left; the phrase and what it's
|
||
worth take the rest. The right column keeps clear of the rack in the
|
||
corner, which is why nothing in it is pushed to the far edge. -->
|
||
<div class="relative grid gap-x-10 gap-y-8 lg:grid-cols-[auto,1fr] lg:items-start">
|
||
|
||
<div class="flex flex-col items-center gap-6 lg:items-start">
|
||
<svg data-gallows viewBox="0 0 130 150" class="pete-gallows h-48 w-auto sm:h-56" role="img"
|
||
aria-labelledby="gallows-title">
|
||
<title id="gallows-title">The gallows</title>
|
||
<g class="pete-gallows-frame">
|
||
<path d="M8 145 H72" />
|
||
<path d="M28 145 V8" />
|
||
<path d="M28 8 H92" />
|
||
<path d="M92 8 V26" />
|
||
</g>
|
||
<g class="pete-gallows-body">
|
||
<circle data-part="0" cx="92" cy="38" r="12" />
|
||
<path data-part="1" d="M92 50 V88" />
|
||
<path data-part="2" d="M92 58 L74 76" />
|
||
<path data-part="3" d="M92 58 L110 76" />
|
||
<path data-part="4" d="M92 88 L76 114" />
|
||
<path data-part="5" d="M92 88 L108 114" />
|
||
</g>
|
||
</svg>
|
||
|
||
<!-- The stake sits under the gallows it's riding on, the same spot the
|
||
blackjack table puts your bet on. -->
|
||
<div class="flex items-center gap-4">
|
||
<div class="pete-spot" data-spot>
|
||
<span class="pete-spot-label">Bet</span>
|
||
<div class="pete-stack" data-stack></div>
|
||
<span data-spot-total class="pete-spot-total hidden"></span>
|
||
</div>
|
||
<p data-lives class="text-xs font-bold uppercase tracking-wider text-white/50"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="min-w-0 space-y-6">
|
||
|
||
<!-- What a win is worth right now. It falls as the figure fills in.
|
||
This row is the only one level with the house rack in the corner, so
|
||
it is the only one that has to keep clear of it — the board below
|
||
gets the full width, and needs it. -->
|
||
<div class="flex flex-wrap items-center gap-3 pr-24 sm:pr-28">
|
||
<div class="pete-meter" data-meter>
|
||
<span class="pete-meter-label">Pays</span>
|
||
<span data-multiple class="pete-meter-value">—</span>
|
||
</div>
|
||
<p class="text-sm text-white/60">
|
||
<span data-stands class="font-bold tabular-nums text-white/90">—</span>
|
||
<span data-stands-label>if you get it</span>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- The phrase. -->
|
||
<div data-board class="pete-board" aria-live="polite" aria-label="The phrase"></div>
|
||
|
||
<!-- Letters that missed. -->
|
||
<div class="flex min-h-[1.75rem] flex-wrap items-center gap-1.5">
|
||
<span data-wrong-label class="hidden text-xs font-bold uppercase tracking-wider text-white/40">Missed</span>
|
||
<div data-wrong class="flex flex-wrap gap-1.5"></div>
|
||
</div>
|
||
|
||
<!-- What just happened. -->
|
||
<div class="flex min-h-[2.75rem] items-center">
|
||
<p data-verdict class="hidden rounded-full bg-white/95 px-5 py-2 font-display text-lg font-bold text-[#2b2118] shadow-pete"></p>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Guessing: shown while a game is live. -->
|
||
<section data-guessing class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
||
<div data-keyboard class="pete-keys"></div>
|
||
|
||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||
<label for="solve" class="sr-only">Guess the whole phrase</label>
|
||
<input id="solve" data-solve-input type="text" autocomplete="off" enterkeyhint="go"
|
||
placeholder="Or just say what it is…"
|
||
class="min-w-0 flex-1 rounded-full bg-[color:var(--ink)]/5 px-4 py-2.5 text-sm font-semibold
|
||
border-2 border-[color:var(--ink)]/10 focus:outline-none focus:border-[color:var(--accent)]">
|
||
<button type="button" data-solve
|
||
class="rounded-full bg-[color:var(--accent)] px-6 py-2.5 font-display font-bold text-white shadow-pete
|
||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Solve
|
||
</button>
|
||
</div>
|
||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
||
Type a letter to guess it. A wrong solve costs a life, same as a wrong letter.
|
||
</p>
|
||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
||
</section>
|
||
|
||
<!-- Betting: shown between games. -->
|
||
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
||
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">How long a phrase?</div>
|
||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||
{{range .Tiers}}
|
||
<button type="button" data-tier="{{.Slug}}"
|
||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||
<div class="flex items-baseline justify-between gap-2">
|
||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{printf "%.1f" .Base}}×</span>
|
||
</div>
|
||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
||
</button>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
||
<div>
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your bet</div>
|
||
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
{{range .Denominations}}
|
||
<button type="button" data-chip="{{.}}" aria-label="Bet {{.}} more"
|
||
class="pete-chip pete-disc grid h-12 w-12 place-items-center font-display text-sm font-bold text-white">
|
||
<span>{{.}}</span>
|
||
</button>
|
||
{{end}}
|
||
<button type="button" data-bet-clear
|
||
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
||
</div>
|
||
|
||
<button type="button" data-start
|
||
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Play
|
||
</button>
|
||
</div>
|
||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
||
</section>
|
||
|
||
</div>
|
||
{{end}}
|
||
|
||
{{define "scripts"}}
|
||
<script src="/static/js/casino-fx.js" defer></script>
|
||
<script src="/static/js/games.js" defer></script>
|
||
<script src="/static/js/hangman.js" defer></script>
|
||
{{end}}
|