games: draw the card, don't type it

The last attempt built a card face out of text: a "♠" in a span for every
pip. At the size a card actually is, a suit character renders as a speck —
the shape is whatever font answered, it doesn't scale, and it can't be put
on the half-row a real pip layout needs. The result read worse than the
plain rank it replaced.

So each face is one SVG on a 100×140 field, suits as vector shapes, pips at
the coordinates a printed deck puts them. Courts get a framed panel with the
suit above the letter and again below it upside down — mirroring a letter,
which is what the first pass did, just stacks two of them into a blob; a real
court mirrors a figure.

Also restores .pete-card-back, which went out with the text rules it was
sitting among: without it a face-down card had no back at all, so the
dealer's hole card was invisible on the felt. Caught by driving a hand.
This commit is contained in:
prosolis
2026-07-13 23:49:17 -07:00
parent 8ec13eab5b
commit b00da21a47
3 changed files with 112 additions and 129 deletions

View File

@@ -73,96 +73,96 @@
return wrap;
}
// Pip layouts, the way a real deck lays them out — which is not "N suits in a
// row". Each entry is [column, row] on a 3×7 grid: three columns, seven rows,
// the same skeleton every printed deck has used for about four hundred years.
// A pip below the halfway line is printed upside down, so it does that here.
// ---- the face ------------------------------------------------------------
//
// Sevens and tens have a pip that sits *between* two rows, so those span a pair
// of rows and centre themselves across the pair.
// 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.
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": [[2, 4]],
"2": [[2, 1], [2, 7]],
"3": [[2, 1], [2, 4], [2, 7]],
"4": [[1, 1], [3, 1], [1, 7], [3, 7]],
"5": [[1, 1], [3, 1], [2, 4], [1, 7], [3, 7]],
"6": [[1, 1], [3, 1], [1, 4], [3, 4], [1, 7], [3, 7]],
"7": [[1, 1], [3, 1], [2, "2/4"], [1, 4], [3, 4], [1, 7], [3, 7]],
"8": [[1, 1], [3, 1], [2, "2/4"], [1, 4], [3, 4], [2, "4/6"], [1, 7], [3, 7]],
"9": [[1, 1], [3, 1], [1, 3], [3, 3], [2, 4], [1, 5], [3, 5], [1, 7], [3, 7]],
"10": [[1, 1], [3, 1], [2, "2/4"], [1, 3], [3, 3], [1, 5], [3, 5], [2, "4/6"], [1, 7], [3, 7]],
"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" };
function corner(face, where) {
var el = document.createElement("span");
el.className = "pete-card-corner pete-card-corner-" + where;
var r = document.createElement("span");
r.className = "pete-card-corner-rank";
r.textContent = face.rank;
var s = document.createElement("span");
s.className = "pete-card-corner-suit";
s.textContent = face.suit;
el.appendChild(r);
el.appendChild(s);
return el;
// 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>";
}
function pip(face, col, row) {
var el = document.createElement("span");
el.className = "pete-card-pip";
el.textContent = face.suit;
el.style.gridColumn = String(col);
// A row given as "2/4" spans that pair and centres between them.
el.style.gridRow = String(row);
// The bottom half of a real card is printed the other way up.
var mid = typeof row === "number" ? row > 4 : row === "4/6";
if (mid) el.dataset.flip = "1";
return el;
// 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.
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>";
}
// paintFace draws an actual playing card: index in two opposite corners, and
// either the pips or a court figure in the middle. The dealer's cards and
// yours use the same face, because they came out of the same shoe.
// paintFace draws the card. The dealer's cards and yours use the same face,
// because they came out of the same shoe.
function paintFace(front, face) {
front.dataset.red = face.red ? "1" : "0";
front.innerHTML = "";
front.appendChild(corner(face, "tl"));
var body = document.createElement("span");
var body = "";
if (COURT[face.rank]) {
// Court cards: the letter, big, with the suit sitting over each shoulder.
// No portrait — a drawn face would fight the room, and this reads instantly.
body.className = "pete-card-court";
var top = document.createElement("span");
top.className = "pete-card-court-suit";
top.textContent = face.suit;
var letter = document.createElement("span");
letter.className = "pete-card-court-letter";
letter.textContent = face.rank;
var bot = document.createElement("span");
bot.className = "pete-card-court-suit";
bot.dataset.flip = "1";
bot.textContent = face.suit;
body.appendChild(top);
body.appendChild(letter);
body.appendChild(bot);
// 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 {
body.className = "pete-card-pips";
// An ace gets one big pip in the middle, the way an ace does.
if (face.rank === "A") body.dataset.ace = "1";
var spots = PIPS[face.rank] || [];
for (var i = 0; i < spots.length; i++) {
body.appendChild(pip(face, spots[i][0], spots[i][1]));
body += pipAt(face.suit, spots[i][0], spots[i][1], spots[i][2]);
}
}
front.appendChild(body);
front.appendChild(corner(face, "br"));
front.setAttribute("aria-label", ariaFor(face));
front.innerHTML =
'<svg class="pete-card-svg" viewBox="0 0 100 140" xmlns="http://www.w3.org/2000/svg" ' +
'role="img" aria-label="' + ariaFor(face) + '">' + index(face) + body + "</svg>";
}
// "A♠" is not something a screen reader should be asked to pronounce.