games: a gallows you can bet on
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.
This commit is contained in:
@@ -877,6 +877,207 @@ html[data-phase="night"] {
|
||||
50% { opacity: 1; transform: translateY(-1px); }
|
||||
}
|
||||
|
||||
/* ---- hangman -------------------------------------------------------------
|
||||
The gallows is the meter: it counts down your lives and your winnings at the
|
||||
same time. So a miss has to *land* — the limb draws itself in along its own
|
||||
length, the whole thing flinches, and the multiple falls. Three parts of one
|
||||
event, and the game is about watching it happen to you. */
|
||||
|
||||
.pete-gallows {
|
||||
overflow: visible;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.pete-gallows-frame path {
|
||||
stroke: rgba(0, 0, 0, 0.35);
|
||||
stroke-width: 6;
|
||||
}
|
||||
/* The rope. Same post, thinner, so it reads as something that would hold. */
|
||||
.pete-gallows-frame path:last-child {
|
||||
stroke: rgba(0, 0, 0, 0.3);
|
||||
stroke-width: 3.5;
|
||||
}
|
||||
.pete-gallows-body > * {
|
||||
stroke: #fff;
|
||||
stroke-width: 5;
|
||||
opacity: 0;
|
||||
filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
.pete-gallows-body > [data-on="1"] { opacity: 1; }
|
||||
|
||||
/* Drawn, not faded in: the stroke runs on along its own path. 260 is longer
|
||||
than any limb here, which is all a dash offset needs to be. */
|
||||
.pete-part-draw {
|
||||
stroke-dasharray: 260;
|
||||
animation: pete-part 0.45s cubic-bezier(0.22, 1, 0.36, 1) backwards;
|
||||
}
|
||||
@keyframes pete-part {
|
||||
from { stroke-dashoffset: 260; }
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.pete-shake { animation: pete-shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
|
||||
@keyframes pete-shake {
|
||||
10%, 90% { transform: translateX(-1.5px); }
|
||||
20%, 80% { transform: translateX(3px); }
|
||||
30%, 50%, 70% { transform: translateX(-5px); }
|
||||
40%, 60% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
/* The phrase. Tiles wrap between words, never inside one. */
|
||||
.pete-board {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 1.1rem;
|
||||
min-height: 5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.pete-word {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.pete-tile {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 2.9rem;
|
||||
width: 2.2rem;
|
||||
border-radius: 0.5rem;
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0.28);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
/* A letter you've earned sits proud of the ones you haven't. */
|
||||
.pete-tile[data-up="1"] {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #2b2118;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
/* Punctuation is scaffolding: it was never yours to guess, so it gets no tile
|
||||
to guess it into. */
|
||||
.pete-tile[data-punct="1"] {
|
||||
background: none;
|
||||
border: 0;
|
||||
width: auto;
|
||||
min-width: 0.7rem;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.pete-tile-hit { animation: pete-tile-hit 0.34s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||
@keyframes pete-tile-hit {
|
||||
0% { transform: rotateX(90deg) scale(1.1); }
|
||||
100% { transform: rotateX(0) scale(1); }
|
||||
}
|
||||
|
||||
.pete-missed {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 1.5rem;
|
||||
min-width: 1.5rem;
|
||||
padding: 0 0.3rem;
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: line-through;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* What a win is worth, right now. */
|
||||
.pete-meter {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
padding: 0.4rem 0.9rem;
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.pete-meter-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
.pete-meter-value {
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--accent);
|
||||
}
|
||||
/* Down at the floor: a win now hands back the stake and nothing else. */
|
||||
.pete-meter[data-cold="1"] .pete-meter-value { color: rgba(255, 255, 255, 0.55); }
|
||||
.pete-meter[data-hit="1"] {
|
||||
box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.9);
|
||||
animation: pete-meter-hit 0.4s ease;
|
||||
}
|
||||
@keyframes pete-meter-hit {
|
||||
0% { transform: scale(1); }
|
||||
35% { transform: scale(0.94); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* The keyboard. */
|
||||
.pete-keys { display: grid; gap: 0.35rem; }
|
||||
.pete-key-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.pete-key-row[data-digits="1"] { margin-top: 0.25rem; opacity: 0.75; }
|
||||
.pete-key-row[data-digits="1"] .pete-key {
|
||||
height: 2rem;
|
||||
min-width: 1.8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.pete-key {
|
||||
height: 2.75rem;
|
||||
min-width: 2.2rem;
|
||||
flex: 0 1 2.4rem;
|
||||
border-radius: 0.6rem;
|
||||
background: color-mix(in srgb, var(--ink) 6%, transparent);
|
||||
border: 2px solid color-mix(in srgb, var(--ink) 10%, transparent);
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
transition: transform 0.08s ease, background 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
.pete-key:hover:not(:disabled) { background: color-mix(in srgb, var(--ink) 12%, transparent); }
|
||||
.pete-key:active:not(:disabled) { transform: translateY(1px) scale(0.96); }
|
||||
.pete-key:disabled { cursor: default; }
|
||||
/* A key that's been spent looks spent — otherwise you spend it twice. */
|
||||
.pete-key[data-state="hit"] {
|
||||
background: #4caf7d;
|
||||
border-color: #3d9367;
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
.pete-key[data-state="miss"] {
|
||||
background: color-mix(in srgb, var(--ink) 4%, transparent);
|
||||
color: color-mix(in srgb, var(--ink) 30%, transparent);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.pete-key:disabled[data-state=""] { opacity: 0.4; }
|
||||
|
||||
/* Picking a length. The one you're on is lit. */
|
||||
.pete-tier {
|
||||
background: color-mix(in srgb, var(--ink) 4%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ink) 10%, transparent);
|
||||
}
|
||||
.pete-tier:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
||||
.pete-tier[data-on="1"] {
|
||||
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pete-card,
|
||||
.pete-card::after,
|
||||
@@ -884,6 +1085,10 @@ html[data-phase="night"] {
|
||||
.pete-dealer-think { animation: none; }
|
||||
.pete-card-inner { transition: none; }
|
||||
.pete-hand[data-won="1"] .pete-card { animation: none; }
|
||||
.pete-part-draw,
|
||||
.pete-shake,
|
||||
.pete-tile-hit,
|
||||
.pete-meter[data-hit="1"] { animation: none; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user