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:
@@ -73,19 +73,22 @@
|
||||
return ((n - Math.floor(n)) * 2 - 1) * (spread || 1);
|
||||
}
|
||||
|
||||
// fly moves one chip from one place to another and resolves when it lands.
|
||||
// flyNode moves *anything* from one place to another and resolves when it
|
||||
// lands: a chip, a playing card, a card face down.
|
||||
//
|
||||
// The arc is the point. A straight line between two rects reads as a UI
|
||||
// transition; a chip that rises, travels and drops reads as a throw. WAAPI does
|
||||
// this in one keyframe list because it can interpolate a midpoint — a CSS
|
||||
// transition; something that rises, travels and drops reads as a throw. WAAPI
|
||||
// does this in one keyframe list because it can interpolate a midpoint — a CSS
|
||||
// transition can't, which is why this isn't a class toggle.
|
||||
function fly(from, to, opts) {
|
||||
//
|
||||
// The node is yours: build it, hand it over, and it is gone when the promise
|
||||
// resolves. Nothing in here knows or cares what it was.
|
||||
function flyNode(node, from, to, opts) {
|
||||
opts = opts || {};
|
||||
var a = centre(from);
|
||||
var b = centre(to);
|
||||
var el = disc(opts.denom || 25);
|
||||
el.className += " pete-fly";
|
||||
stage().appendChild(el);
|
||||
node.classList.add("pete-fly");
|
||||
stage().appendChild(node);
|
||||
|
||||
var dur = opts.duration || 420;
|
||||
if (reduced) dur = 1;
|
||||
@@ -96,12 +99,14 @@
|
||||
var midX = (a.x + b.x) / 2;
|
||||
var midY = (a.y + b.y) / 2 - lift;
|
||||
var spin = opts.spin == null ? jitter(opts.index || 0, 90) : opts.spin;
|
||||
var s0 = opts.fromScale == null ? 0.85 : opts.fromScale;
|
||||
var s1 = opts.toScale == null ? 1 : opts.toScale;
|
||||
|
||||
var anim = el.animate(
|
||||
var anim = node.animate(
|
||||
[
|
||||
{ transform: t(a.x, a.y, 0.85, 0), opacity: 1, offset: 0 },
|
||||
{ transform: t(midX, midY, 1.12, spin * 0.6), opacity: 1, offset: 0.5 },
|
||||
{ transform: t(b.x, b.y, 1, spin), opacity: opts.fade ? 0 : 1, offset: 1 },
|
||||
{ transform: t(a.x, a.y, s0, opts.fromSpin || 0), opacity: 1, offset: 0 },
|
||||
{ transform: t(midX, midY, (s0 + s1) / 2 * 1.12, spin * 0.6), opacity: 1, offset: 0.5 },
|
||||
{ transform: t(b.x, b.y, s1, spin), opacity: opts.fade ? 0 : 1, offset: 1 },
|
||||
],
|
||||
{ duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: reduced ? 0 : opts.delay || 0, fill: "both" }
|
||||
);
|
||||
@@ -109,10 +114,17 @@
|
||||
return anim.finished
|
||||
.catch(function () {})
|
||||
.then(function () {
|
||||
el.remove();
|
||||
node.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// fly throws one chip. It is flyNode with a chip in it, which is what it always
|
||||
// was — UNO wanted the same throw with a card in it, so the throw moved out.
|
||||
function fly(from, to, opts) {
|
||||
opts = opts || {};
|
||||
return flyNode(disc(opts.denom || 25), from, to, opts);
|
||||
}
|
||||
|
||||
function t(x, y, scale, rot) {
|
||||
return "translate(" + x + "px," + y + "px) scale(" + scale + ") rotate(" + rot + "deg)";
|
||||
}
|
||||
@@ -313,6 +325,7 @@
|
||||
disc: disc,
|
||||
jitter: jitter,
|
||||
fly: fly,
|
||||
flyNode: flyNode,
|
||||
flyMany: flyMany,
|
||||
spot: spot,
|
||||
burst: burst,
|
||||
|
||||
Reference in New Issue
Block a user