games: you buy the deck, and win it back a card at a time
Solitaire, Vegas rules — the only shape solitaire has ever had as a gambling game. You don't win or lose the deal: the stake buys the deck outright, and every card you get home to a foundation pays a fifty-second of the tier's multiple back. Cash the board whenever you like and keep what you've banked, so a board that has gone dead is a decision rather than a wall. No undo: the stake is spent the moment the deck is bought, and an undo would be a way to walk a losing board backwards until it wins. Three deals, and the two dials are the whole difficulty of Klondike. Patient draws one with unlimited passes and pays 1.4x, so it takes 38 cards home to get square. Vegas draws three, three times round, 2.2x, square at 24. Cutthroat draws three and gives you one pass, 3.4x, square at 16 — most of those boards never clear, and you're ahead long before they would. internal/games/klondike is the same pure reducer as the other two, and Pays() is one function for the same reason hangman's is. Two fuzzers hold the deck together: no sequence of moves can lose or duplicate a card, and the board stays well-formed. They earned their keep immediately — the first thing they caught was a recycle that reversed the waste. It flips as a block, so the card drawn first comes out first, and reversing it would have dealt a different game on every pass and quietly broken the seed in the audit log. The browser never sees the stock or a face-down card, which here is most of the deck rather than blackjack's one hole card: a column sends how many cards are under it, never which. The table re-renders and animates the difference. Blackjack plays back a script because a hand only ever grows at one end; solitaire moves runs from anywhere to anywhere and an auto-finish moves eleven cards at once, so a script of "append this card there" would be a second engine over here and it would be the one that's wrong. Instead the board on screen is always exactly the board the server says exists, and each card is played from where it just was to where it now is. The events supply only what a diff can't: where a newly-revealed card came from, and what the board is worth. The rules are mirrored in JS on purpose, and only to light up the columns a held card can go to. Being shown where a card goes is the game teaching you; being told no after you commit is the game scolding you. The server still decides, and a disagreement snaps the board back to what it says. Two things came out into the open rather than being copied, which is the rule this room runs on: casino-cards.js (the deck — faces, pips, the flip) and PeteFX.spot() (the pile of chips and the number under it, which now owns the rule that the number is a readout of the pile). Blackjack uses both. Not yet driven in a browser.
This commit is contained in:
172
internal/web/static/js/casino-cards.js
Normal file
172
internal/web/static/js/casino-cards.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// The deck, as the room draws it.
|
||||
//
|
||||
// A card is drawn, not typed. The first attempt set the pips as text — "♠" in a
|
||||
// span — and at the size a card actually is, a suit character renders as a speck:
|
||||
// the shape is whatever font happened to answer, it doesn't scale, and it can't
|
||||
// be positioned to the half-row a real pip layout needs.
|
||||
//
|
||||
// So each face is one SVG on a 100×140 field (the proportions of a real card),
|
||||
// with the suits as vector shapes. Everything below is coordinates on that field,
|
||||
// which is why the pips land where a printed deck puts them instead of where a
|
||||
// flexbox felt like putting them.
|
||||
//
|
||||
// This started life inside blackjack.js. It's out here because solitaire deals
|
||||
// off the same deck, and the second table in a casino is exactly the moment a
|
||||
// copied card renderer becomes two card renderers that drift.
|
||||
//
|
||||
// Exposed as window.PeteCards. Nothing in here knows what game is being played.
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var SUIT_ART = {
|
||||
"♠": '<path d="M50 6C50 6 16 34 16 58a17 17 0 0 0 28 13c-1 12-5 19-12 25h36c-7-6-11-13-12-25a17 17 0 0 0 28-13C84 34 50 6 50 6Z"/>',
|
||||
"♥": '<path d="M50 96C50 96 8 66 8 38A22 22 0 0 1 50 24 22 22 0 0 1 92 38c0 28-42 58-42 58Z"/>',
|
||||
"♦": '<path d="M50 4 88 50 50 96 12 50Z"/>',
|
||||
"♣": '<g><circle cx="50" cy="28" r="19"/><circle cx="24" cy="62" r="19"/><circle cx="76" cy="62" r="19"/>' +
|
||||
'<path d="M44 60h12l7 36H37Z"/></g>',
|
||||
};
|
||||
|
||||
// Pip layouts, the way a real deck lays them out — which is not "N suits in a
|
||||
// row". [x, y] on the 100×140 field. The seven canonical rows sit at y = 27,
|
||||
// 41, 56, 70, 84, 99, 113; sevens, eights and tens carry a pip *between* two of
|
||||
// them, which is the whole reason this is a table of coordinates and not a
|
||||
// grid. Anything below the middle is printed upside down, so it is.
|
||||
var R = [0, 27, 41.4, 55.7, 70, 84.3, 98.6, 113]; // 1-indexed, R[4] is the middle
|
||||
var L = 30, C = 50, Rr = 70; // the three columns
|
||||
|
||||
var PIPS = {
|
||||
"A": [[C, 70, 2.1]],
|
||||
"2": [[C, R[1]], [C, R[7]]],
|
||||
"3": [[C, R[1]], [C, R[4]], [C, R[7]]],
|
||||
"4": [[L, R[1]], [Rr, R[1]], [L, R[7]], [Rr, R[7]]],
|
||||
"5": [[L, R[1]], [Rr, R[1]], [C, R[4]], [L, R[7]], [Rr, R[7]]],
|
||||
"6": [[L, R[1]], [Rr, R[1]], [L, R[4]], [Rr, R[4]], [L, R[7]], [Rr, R[7]]],
|
||||
"7": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[4]], [Rr, R[4]], [L, R[7]], [Rr, R[7]]],
|
||||
"8": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[4]], [Rr, R[4]], [C, 91.5], [L, R[7]], [Rr, R[7]]],
|
||||
"9": [[L, R[1]], [Rr, R[1]], [L, R[3]], [Rr, R[3]], [C, R[4]], [L, R[5]], [Rr, R[5]], [L, R[7]], [Rr, R[7]]],
|
||||
"10": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[3]], [Rr, R[3]], [L, R[5]], [Rr, R[5]], [C, 91.5], [L, R[7]], [Rr, R[7]]],
|
||||
};
|
||||
|
||||
var COURT = { "J": "Jack", "Q": "Queen", "K": "King" };
|
||||
var SUIT_NAMES = { "♠": "spades", "♥": "hearts", "♦": "diamonds", "♣": "clubs" };
|
||||
|
||||
// One pip: the suit art, scaled and dropped at [x, y], turned over if it sits
|
||||
// below the middle of the card.
|
||||
function pipAt(suit, x, y, scale) {
|
||||
var s = (scale || 1) * 0.17;
|
||||
var turn = y > 70 ? " rotate(180 50 50)" : "";
|
||||
return '<g transform="translate(' + x + ' ' + y + ') scale(' + s + ') translate(-50 -50)' + turn + '">' +
|
||||
SUIT_ART[suit] + "</g>";
|
||||
}
|
||||
|
||||
// The corner index: rank over suit. Printed in both corners, the second one
|
||||
// upside down, which is what lets you read a card from a fanned hand — and in
|
||||
// solitaire, from a column where all you can see is the top eighth of it.
|
||||
function index(face) {
|
||||
var g =
|
||||
'<g>' +
|
||||
'<text x="12" y="24" class="pete-card-idx">' + face.rank + "</text>" +
|
||||
'<g transform="translate(12 36) scale(0.13) translate(-50 -50)">' + SUIT_ART[face.suit] + "</g>" +
|
||||
"</g>";
|
||||
return g + '<g transform="rotate(180 50 70)">' + g + "</g>";
|
||||
}
|
||||
|
||||
// paint draws the face. Every table uses this one, because they all deal out of
|
||||
// the same deck.
|
||||
function paint(front, face) {
|
||||
front.dataset.red = face.red ? "1" : "0";
|
||||
|
||||
var body = "";
|
||||
if (COURT[face.rank]) {
|
||||
// Court cards: a framed panel, the suit above the letter and again below it
|
||||
// the other way up. A real court mirrors a *figure*; mirroring a letter just
|
||||
// stacks two of them into a blob, which is exactly what the first attempt
|
||||
// did. No portrait either — a drawn king would fight the room, and this
|
||||
// reads instantly at the size a card actually is.
|
||||
body =
|
||||
'<rect x="20" y="22" width="60" height="96" rx="6" class="pete-card-panel"/>' +
|
||||
pipAt(face.suit, 50, 38, 0.95) +
|
||||
'<text x="50" y="82" class="pete-card-court">' + face.rank + "</text>" +
|
||||
pipAt(face.suit, 50, 102, 0.95);
|
||||
} else {
|
||||
var spots = PIPS[face.rank] || [];
|
||||
for (var i = 0; i < spots.length; i++) {
|
||||
body += pipAt(face.suit, spots[i][0], spots[i][1], spots[i][2]);
|
||||
}
|
||||
}
|
||||
|
||||
front.innerHTML =
|
||||
'<svg class="pete-card-svg" viewBox="0 0 100 140" xmlns="http://www.w3.org/2000/svg" ' +
|
||||
'role="img" aria-label="' + aria(face) + '">' + index(face) + body + "</svg>";
|
||||
}
|
||||
|
||||
// "A♠" is not something a screen reader should be asked to pronounce.
|
||||
function aria(face) {
|
||||
var name = COURT[face.rank] || (face.rank === "A" ? "Ace" : face.rank);
|
||||
var suit = SUIT_NAMES[face.suit];
|
||||
return suit ? name + " of " + suit : face.label;
|
||||
}
|
||||
|
||||
var dealt = 0; // how many cards this page has put down, ever — the tilt seed
|
||||
|
||||
// el builds one card. face === null means face-down: the card is on the table,
|
||||
// but this browser has not been told what it is.
|
||||
//
|
||||
// opts.deal — fly it in from the shoe (blackjack). Solitaire turns this off:
|
||||
// it animates its own cards from wherever they actually came from,
|
||||
// and a board that re-renders would otherwise re-deal itself out of
|
||||
// the corner on every single move.
|
||||
// opts.tilt — a degree or two of resting angle. A dealt hand wants it; a
|
||||
// solitaire column does not, because thirteen tilted cards stacked
|
||||
// an eighth of an inch apart read as a mistake rather than as a
|
||||
// hand that was handled.
|
||||
function el(face, opts) {
|
||||
opts = opts || {};
|
||||
var deal = opts.deal !== false;
|
||||
var tilt = opts.tilt !== false;
|
||||
|
||||
var wrap = document.createElement("div");
|
||||
wrap.className = "pete-card";
|
||||
wrap.dataset.face = face ? "up" : "down";
|
||||
if (face) wrap.dataset.key = face.label; // one deck, so the label is an id
|
||||
|
||||
if (deal) {
|
||||
// Every card flies out of the shoe, which sits in the top-right of the felt.
|
||||
wrap.style.setProperty("--deal-x", "14rem");
|
||||
wrap.style.setProperty("--deal-y", "-6rem");
|
||||
} else {
|
||||
wrap.style.animation = "none";
|
||||
}
|
||||
wrap.style.setProperty("--tilt", tilt ? window.PeteFX.jitter(dealt++, 2.4).toFixed(2) + "deg" : "0deg");
|
||||
|
||||
var inner = document.createElement("div");
|
||||
inner.className = "pete-card-inner";
|
||||
|
||||
var front = document.createElement("div");
|
||||
front.className = "pete-card-front";
|
||||
var back = document.createElement("div");
|
||||
back.className = "pete-card-back";
|
||||
|
||||
inner.appendChild(front);
|
||||
inner.appendChild(back);
|
||||
wrap.appendChild(inner);
|
||||
if (face) paint(front, face);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// turnOver flips a face-down card up, now that we've been told what it is.
|
||||
function turnOver(wrap, face) {
|
||||
if (!wrap) return;
|
||||
paint(wrap.querySelector(".pete-card-front"), face);
|
||||
wrap.dataset.face = "up";
|
||||
wrap.dataset.key = face.label;
|
||||
}
|
||||
|
||||
window.PeteCards = {
|
||||
el: el,
|
||||
paint: paint,
|
||||
turnOver: turnOver,
|
||||
aria: aria,
|
||||
art: SUIT_ART,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user