Three things, and the first one was a bug. Your own hand didn't move until the lap ended. bump() keeps the bots' fans honest and has always refused seat zero, and nothing else touched yours — so a +4 landing on you at the top of a lap put four backs into your hand and then nothing, and the cards themselves turned up seconds later when the script finished and paint() finally ran. You spent the whole lap looking at a hand you no longer held. The engine now stamps your hand onto every event that changes it (Event.Hand, seat zero only, which is the one hand the browser is already entitled to see) and the table redraws as the cards land. Measured in the running app: 2 -> 3 cards at 414ms into a 1791ms lap. You couldn't call UNO, and not because the button was missing: going down to one card *was* the call. discard() fired the uno event by itself, which made it a thing that happened to you rather than a thing you did, and a rule nobody can fail is not a rule. So now you say it or you don't (Move.Uno), and if you don't, every bot still in the game gets one look at you before any of them plays — because a bot that has moved on is a bot that has stopped watching your hand. It runs the other way too, and that half is the fun one: a bot forgets often enough to be worth watching for, and when it does it says *nothing*. No event, no badge, no tell on the felt except the count beside its fan reading "1 card". Catch it and it takes two; call a seat that had nothing to hide and you take two yourself, which is what stops the catch button from being a thing you simply mash. Which cards owe the call is the engine's answer, not a count of your hand: No Mercy's "discard all" takes every card of its colour with it, so a six-card hand can land on one, and a browser subtracting one from six walks you into a catch it never warned you about. And the room was silent. Every sound in here is *made* — an oscillator, a burst of filtered noise, an envelope — the same bargain the weather engine takes with its clouds. A card is a slap of noise through a bandpass, a chip is two detuned sines with a knock on the front, a win is four notes going up. No asset files, no round trips, and a sound can be pitched and detuned per call instead of being the same wav three hundred times. Hooked into the FX layer rather than into the games, so every table that throws a chip or turns a card got it at once. The multiples moved, and the test that exists to catch that caught it. The naive strategy now calls UNO, because calling is a button and not a strategy — what these tiers price is bad card play, not a player who ignores the felt shouting at them — and on that footing the normal tables come back to where they were (40.1 / 28.5 / 23.1). No Mercy Full House did not: it was paying a *negative* house edge, which is the house paying you to sit down. Re-priced 3.8 -> 3.5. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
97 lines
5.0 KiB
HTML
97 lines
5.0 KiB
HTML
{{define "layout"}}<!doctype html>
|
|
<html lang="en" data-room="{{.Room.Slug}}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{block "title" .}}{{.Room.Name}}{{end}}</title>
|
|
<meta name="robots" content="noindex">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/static/css/output.css">
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,3 27,9.5 27,22.5 16,29 5,22.5 5,9.5' fill='%23f2b53d' stroke='%232b2118' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E" type="image/svg+xml">
|
|
<meta name="theme-color" content="#17231d">
|
|
<script>
|
|
// Which room you're standing in, from your clock rather than the server's:
|
|
// Casinopolis in daylight, Casino Night Zone once the lights come on at six.
|
|
// Same rule as roomAt() in games_pages.go — keep the two in step.
|
|
//
|
|
// The server already painted its best guess into the markup, so this only
|
|
// corrects a player whose evening isn't the server's.
|
|
(function () {
|
|
var ROOMS = {
|
|
"casinopolis": "Casinopolis",
|
|
"casino-night": "Casino Night Zone",
|
|
};
|
|
function roomAt(h) { return (h >= 6 && h < 18) ? "casinopolis" : "casino-night"; }
|
|
function apply() {
|
|
var slug = roomAt(new Date().getHours());
|
|
// The palette can be set from <head> — the sign can't: this script runs
|
|
// before the header exists. So set the attribute now (no flash of the
|
|
// wrong room) and always re-stamp the name, which is a no-op until the
|
|
// header is there to stamp. Bailing out early when the slug already
|
|
// matches would leave the server's name under the browser's palette.
|
|
document.documentElement.dataset.room = slug;
|
|
document.querySelectorAll("[data-room-name]").forEach(function (el) {
|
|
el.textContent = ROOMS[slug];
|
|
});
|
|
}
|
|
apply();
|
|
document.addEventListener("DOMContentLoaded", apply);
|
|
setInterval(apply, 60000);
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)]">
|
|
<div class="pointer-events-none fixed inset-0 -z-10 cs-room" aria-hidden="true"></div>
|
|
|
|
<header class="mx-auto max-w-5xl px-4 pt-6 pb-4 sm:pt-10">
|
|
<div class="flex items-center justify-between gap-3">
|
|
<a href="/games" class="group flex min-w-0 items-center gap-3">
|
|
{{template "_comb" .}}
|
|
<span class="min-w-0">
|
|
<span data-room-name class="block font-display text-2xl sm:text-3xl font-bold leading-none tracking-tight">{{.Room.Name}}</span>
|
|
<span class="mt-1 block text-xs font-semibold uppercase tracking-[0.18em] text-[color:var(--ink)]/45">Chips are euros</span>
|
|
</span>
|
|
</a>
|
|
|
|
{{if .User}}
|
|
<a href="/auth/logout" title="Signed in as {{.User.Display}} — sign out"
|
|
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
|
<span class="grid h-6 w-6 place-items-center rounded-full bg-[color:var(--accent)] text-xs font-bold text-[#20180c]">{{.User.Initial}}</span>
|
|
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mx-auto max-w-5xl px-4 pb-20">
|
|
{{block "main" .}}{{end}}
|
|
</main>
|
|
|
|
<footer class="mx-auto max-w-5xl px-4 pb-10 text-center text-xs text-[color:var(--ink)]/40">
|
|
Play for what you can lose. Cash out whenever you like.
|
|
</footer>
|
|
|
|
<!-- The room's noise. Loaded here rather than per-table because every table
|
|
wants it and none of them owns it, and because it has to be defined before
|
|
casino-fx.js — which reaches for it on every chip it throws. `defer` runs
|
|
these in document order, so that's guaranteed. -->
|
|
<script src="/static/js/casino-sfx.js" defer></script>
|
|
{{block "scripts" .}}{{end}}
|
|
</body>
|
|
</html>{{end}}
|
|
|
|
{{/* The house mark: a honeycomb cell struck like a chip. Stands in for a logo,
|
|
and deliberately isn't a face — nobody is watching you play. */}}
|
|
{{define "_comb"}}
|
|
<span class="cs-comb grid h-11 w-11 shrink-0 place-items-center transition-transform group-hover:-rotate-6" aria-hidden="true">
|
|
<svg viewBox="0 0 32 32" class="h-11 w-11">
|
|
<polygon points="16,2.5 27.5,9.25 27.5,22.75 16,29.5 4.5,22.75 4.5,9.25"
|
|
fill="var(--accent)" stroke="var(--ink)" stroke-width="2.5" stroke-linejoin="round"/>
|
|
<polygon points="16,9 21.5,12.25 21.5,18.75 16,22 10.5,18.75 10.5,12.25"
|
|
fill="none" stroke="var(--ink)" stroke-width="1.75" stroke-linejoin="round" opacity="0.5"/>
|
|
</svg>
|
|
</span>
|
|
{{end}}
|