games: a table of bots you have to beat to the last card
UNO, played for chips. You stake once, sit down against one to three bots, and going out first pays the table: 2.2x heads up, 3.6x against a full house. Anybody else going out first takes the stake. The table size is the tier, because it is the only dial UNO has. The bots move inside ApplyMove. A game with opponents is normally where you reach for a socket, and the plan says solo UNO must not — so one request plays your move and every bot turn behind it, and hands back the whole lap as a script the felt plays in order. The RNG is in the state rather than an argument to it: the bots choose and a spent deck reshuffles, so the engine needs randomness mid-game, and there is no generator alive across requests to pass in. The seed rides in the state and each step derives its own. The game still replays exactly as it fell. The zero value of Color is Wild, and that is the whole point of it: a wild played with the colour field missing from the JSON must be refused, not quietly played as a red one. It was red for an hour. The browser never sees a bot's card — not the deck, not a hand, not the face of a card a bot drew, which is most of the deck. Seats cross the wire as a name and a count. The multiples are measured, not guessed: playing the first legal card you hold wins 43/32/27% of the time against these bots, so the tiers price that to lose about 8% a game and leave good play worth roughly the house's edge. PeteFX.flyNode is the throw with the chip taken out of it, so a card can be thrown across the felt the same way. fly() is now that with a chip in it. Not yet driven in a browser, which in this room means not yet finished. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -1421,6 +1421,346 @@ html[data-phase="night"] {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---- uno ------------------------------------------------------------------
|
||||
The card is the whole look: a coloured rectangle with a white oval laid
|
||||
across it at an angle. Drop the oval and it reads as a button, so the oval
|
||||
is not decoration — it is what makes the thing a card.
|
||||
|
||||
The bots are drawn from a *count*. That is all the server sends, and the fan
|
||||
of backs up there is generated from a number rather than from cards, because
|
||||
there are no cards to draw. */
|
||||
|
||||
/* The felt takes a tint of the colour in play. After a wild, the card on the
|
||||
pile is not the colour you have to follow — this is the table saying so. */
|
||||
.pete-uno[data-c="red"] { --play: #d64545; }
|
||||
.pete-uno[data-c="blue"] { --play: #3d7fd6; }
|
||||
.pete-uno[data-c="yellow"] { --play: #e0b02c; }
|
||||
.pete-uno[data-c="green"] { --play: #46a86b; }
|
||||
.pete-uno::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(80% 55% at 50% 45%, var(--play, transparent), transparent 70%);
|
||||
opacity: 0.16;
|
||||
transition: opacity 0.5s ease, background 0.5s ease;
|
||||
}
|
||||
|
||||
.pete-uno-card {
|
||||
position: relative;
|
||||
height: var(--uno-h, 6.4rem);
|
||||
width: var(--uno-w, 4.3rem);
|
||||
border-radius: 0.55rem;
|
||||
padding: 0.28rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 0 rgba(0,0,0,0.22), 0 6px 14px rgba(0,0,0,0.18);
|
||||
flex: none;
|
||||
}
|
||||
.pete-uno-face {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.38rem;
|
||||
overflow: hidden;
|
||||
background: var(--uno, #2b2118);
|
||||
color: #fff;
|
||||
}
|
||||
.pete-uno-card[data-c="red"] { --uno: #d64545; }
|
||||
.pete-uno-card[data-c="blue"] { --uno: #3d7fd6; }
|
||||
.pete-uno-card[data-c="yellow"] { --uno: #e0b02c; }
|
||||
.pete-uno-card[data-c="green"] { --uno: #46a86b; }
|
||||
.pete-uno-card[data-c="wild"] { --uno: #2b2118; }
|
||||
|
||||
/* A wild that has been played wears the colour it was called as — the pile has
|
||||
to show what you must follow, not what was printed. */
|
||||
.pete-uno-card[data-named="red"] { box-shadow: 0 0 0 3px #d64545, 0 3px 0 rgba(0,0,0,0.22); }
|
||||
.pete-uno-card[data-named="blue"] { box-shadow: 0 0 0 3px #3d7fd6, 0 3px 0 rgba(0,0,0,0.22); }
|
||||
.pete-uno-card[data-named="yellow"] { box-shadow: 0 0 0 3px #e0b02c, 0 3px 0 rgba(0,0,0,0.22); }
|
||||
.pete-uno-card[data-named="green"] { box-shadow: 0 0 0 3px #46a86b, 0 3px 0 rgba(0,0,0,0.22); }
|
||||
|
||||
/* The oval. Tilted, because it is tilted on the card in your hand. */
|
||||
.pete-uno-oval {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 78%;
|
||||
width: 62%;
|
||||
border-radius: 50%;
|
||||
transform: rotate(-24deg);
|
||||
background: #fff;
|
||||
color: var(--uno, #2b2118);
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
.pete-uno-card[data-c="wild"] .pete-uno-oval { color: #2b2118; font-size: 1.15rem; }
|
||||
|
||||
.pete-uno-corner {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,0.35);
|
||||
}
|
||||
.pete-uno-corner[data-at="tl"] { top: 0.22rem; left: 0.28rem; }
|
||||
.pete-uno-corner[data-at="br"] { bottom: 0.22rem; right: 0.28rem; transform: rotate(180deg); }
|
||||
|
||||
/* The four quadrants of a wild, under the oval. */
|
||||
.pete-uno-wheel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
conic-gradient(#d64545 0 25%, #e0b02c 0 50%, #46a86b 0 75%, #3d7fd6 0);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
/* The back. Every card you are not entitled to see is one of these. */
|
||||
.pete-uno-card-back { padding: 0.28rem; }
|
||||
.pete-uno-back {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.38rem;
|
||||
background:
|
||||
radial-gradient(120% 80% at 50% 0%, rgba(255,255,255,0.16), transparent 60%),
|
||||
#2b2118;
|
||||
}
|
||||
.pete-uno-back::after {
|
||||
content: "UNO";
|
||||
display: block;
|
||||
transform: rotate(-24deg);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 999px;
|
||||
background: #e0b02c;
|
||||
color: #2b2118;
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Your hand. It fans, it wraps, and a card you can play stands up out of it —
|
||||
being shown what goes is the game teaching you, and the server still decides. */
|
||||
.pete-uno-hand {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 6.8rem;
|
||||
padding-top: 0.6rem;
|
||||
}
|
||||
.pete-uno-hand .pete-uno-card {
|
||||
animation: pete-uno-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards;
|
||||
animation-delay: calc(var(--i, 0) * 45ms);
|
||||
transition: transform 0.16s ease, filter 0.16s ease;
|
||||
}
|
||||
.pete-uno-hand .pete-uno-card[data-on="1"] {
|
||||
cursor: pointer;
|
||||
transform: translateY(-0.5rem);
|
||||
}
|
||||
.pete-uno-hand .pete-uno-card[data-on="1"]:hover { transform: translateY(-1.1rem) scale(1.03); }
|
||||
.pete-uno-hand .pete-uno-card[data-on="0"] { filter: brightness(0.62) saturate(0.7); }
|
||||
@keyframes pete-uno-in {
|
||||
from { transform: translateY(2rem) rotate(6deg); opacity: 0; }
|
||||
to { transform: translateY(-0.5rem); opacity: 1; }
|
||||
}
|
||||
|
||||
/* A seat: a fan of backs, a name, a count. The fan overlaps, so eight cards
|
||||
read as a hand rather than a row. */
|
||||
.pete-uno-seat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
position: relative;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 1rem;
|
||||
transition: background 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
.pete-uno-seat[data-turn="1"] {
|
||||
background: rgba(255,255,255,0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.pete-uno-seat[data-uno="1"] .pete-uno-count { color: #ffd76e; }
|
||||
|
||||
.pete-uno-fan {
|
||||
display: flex;
|
||||
--uno-h: 3.2rem;
|
||||
--uno-w: 2.15rem;
|
||||
}
|
||||
.pete-uno-fan .pete-uno-card {
|
||||
margin-left: -1.35rem;
|
||||
transform: rotate(calc((var(--i, 0) - (var(--n, 1) - 1) / 2) * 4deg));
|
||||
transform-origin: bottom center;
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,0.22);
|
||||
}
|
||||
.pete-uno-fan .pete-uno-card:first-child { margin-left: 0; }
|
||||
.pete-uno-fan .pete-uno-back::after { font-size: 0.4rem; padding: 0.06rem 0.22rem; }
|
||||
|
||||
.pete-uno-name {
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
.pete-uno-count {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,0.55);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* The deck you draw from, and the pile you play onto. */
|
||||
.pete-uno-deck {
|
||||
position: relative;
|
||||
--uno-h: 6.4rem;
|
||||
--uno-w: 4.3rem;
|
||||
height: var(--uno-h);
|
||||
width: var(--uno-w);
|
||||
border-radius: 0.55rem;
|
||||
padding: 0.28rem;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 3px 0 rgba(0,0,0,0.22),
|
||||
0.28rem 0.28rem 0 -0.06rem rgba(255,255,255,0.35),
|
||||
0.5rem 0.5rem 0 -0.12rem rgba(255,255,255,0.18);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.pete-uno-deck:not(:disabled):hover { transform: translateY(-3px); }
|
||||
.pete-uno-deck:disabled { opacity: 0.75; }
|
||||
.pete-uno-deck-count {
|
||||
position: absolute;
|
||||
bottom: -0.55rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0,0,0,0.55);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pete-uno-shuffle { animation: pete-uno-shuffle 0.42s ease; }
|
||||
@keyframes pete-uno-shuffle {
|
||||
0%, 100% { transform: none; }
|
||||
30% { transform: translateY(-4px) rotate(-3deg); }
|
||||
65% { transform: translateY(2px) rotate(2deg); }
|
||||
}
|
||||
|
||||
.pete-uno-discard {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 6.4rem;
|
||||
width: 4.3rem;
|
||||
border-radius: 0.55rem;
|
||||
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.14);
|
||||
}
|
||||
.pete-uno-land { animation: pete-uno-land 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||
@keyframes pete-uno-land {
|
||||
from { transform: scale(1.14) rotate(-4deg); }
|
||||
to { transform: none; }
|
||||
}
|
||||
|
||||
.pete-uno-colour {
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.3);
|
||||
}
|
||||
.pete-uno-colour[data-c="red"] { background: #d64545; }
|
||||
.pete-uno-colour[data-c="blue"] { background: #3d7fd6; }
|
||||
.pete-uno-colour[data-c="yellow"] { background: #e0b02c; color: #2b2118; }
|
||||
.pete-uno-colour[data-c="green"] { background: #46a86b; }
|
||||
|
||||
/* A word thrown at a seat: SKIPPED, UNO!, +4. */
|
||||
.pete-uno-badge {
|
||||
position: absolute;
|
||||
top: -0.4rem;
|
||||
left: 50%;
|
||||
z-index: 5;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #2b2118;
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 3px 0 rgba(0,0,0,0.2);
|
||||
animation: pete-uno-badge 0.9s ease forwards;
|
||||
}
|
||||
.pete-uno-badge[data-tone="bad"] { background: #cc3d4a; color: #fff; }
|
||||
.pete-uno-badge[data-tone="uno"] { background: #e0b02c; }
|
||||
@keyframes pete-uno-badge {
|
||||
0% { transform: translate(-50%, 0.4rem) scale(0.7); opacity: 0; }
|
||||
25% { transform: translate(-50%, -0.5rem) scale(1.06); opacity: 1; }
|
||||
75% { transform: translate(-50%, -0.9rem) scale(1); opacity: 1; }
|
||||
100% { transform: translate(-50%, -1.6rem) scale(0.95); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Naming a colour. It covers the felt because until it's answered there is no
|
||||
legal move on the table underneath it. */
|
||||
.pete-uno-wild {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(10, 20, 15, 0.55);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.pete-uno-wild-box {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-radius: 1.25rem;
|
||||
background: rgba(20, 40, 30, 0.92);
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.35);
|
||||
text-align: center;
|
||||
}
|
||||
.pete-uno-swatch {
|
||||
padding: 0.7rem 1.4rem;
|
||||
border-radius: 0.75rem;
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: var(--sw, #666);
|
||||
box-shadow: 0 3px 0 rgba(0,0,0,0.3);
|
||||
transition: transform 0.12s ease, filter 0.12s ease;
|
||||
}
|
||||
.pete-uno-swatch:hover { transform: translateY(-2px); filter: brightness(1.08); }
|
||||
.pete-uno-swatch:active { transform: translateY(1px); }
|
||||
.pete-uno-swatch[data-c="red"] { --sw: #d64545; }
|
||||
.pete-uno-swatch[data-c="blue"] { --sw: #3d7fd6; }
|
||||
.pete-uno-swatch[data-c="yellow"] { --sw: #e0b02c; color: #2b2118; }
|
||||
.pete-uno-swatch[data-c="green"] { --sw: #46a86b; }
|
||||
|
||||
/* A phone. The cards get smaller so a hand of ten still fits without the felt
|
||||
scrolling sideways, which is the thing to check at 390px with a game on it. */
|
||||
@media (max-width: 639px) {
|
||||
.pete-uno-hand { --uno-h: 5.2rem; --uno-w: 3.5rem; gap: 0.3rem; }
|
||||
.pete-uno-deck { --uno-h: 5.2rem; --uno-w: 3.5rem; }
|
||||
.pete-uno-discard { height: 5.2rem; width: 3.5rem; }
|
||||
.pete-uno-hand .pete-uno-card { padding: 0.22rem; }
|
||||
.pete-uno-oval { font-size: 1.15rem; }
|
||||
.pete-uno-seat { padding: 0.35rem 0.45rem; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pete-card,
|
||||
.pete-card::after,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user