openvibers/openvibe.casino Commits

WCAG AA: game tones read in every theme; wide tables reachable by keyboard (#24)

OP OpenVibers committed 11 days ago · b401b0b4bd · 2 files, +9 −8

Changes

ddiff --git a/public/css/casino.css b/public/css/casino.css
iindex 3a34f2a..c97f573 100644
---- a/public/css/casino.css
++++ b/public/css/casino.css
@@@ -66,7 +66,8 @@
.cs-ladder a { grid-template-columns: minmax(0, 1fr) 64px; grid-template-areas: "bet edge" "bar bar"; gap: 4px 10px; }
.cs-l-game { display: none; }
.cs-l-bet { grid-area: bet; white-space: normal; }
-- .cs-l-g2 { display: inline; color: var(--tone); font-weight: 700; }
++ /* The game's tone, mixed toward the text so it reads in every theme (the raw tones were 3.4-3.6:1, WCAG sweep 2026-11-01). */
++ .cs-l-g2 { display: inline; color: color-mix(in srgb, var(--tone) 55%, var(--text-primary, #e6edf7)); font-weight: 700; }
.cs-l-bar { grid-area: bar; height: 8px; }
.cs-l-edge { grid-area: edge; }
}
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 004bbc0..ee2593b 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -94,7 +94,7 @@ function hardChart({ compact = false } = {}) {
const totals = compact ? [9, 10, 11, 12, 13, 16] : [8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
const handFor = (t) => (t <= 10 ? [card(2), card(t - 2)] : t <= 19 ? [card(10), card(t - 10)] : [card(10), card(10)]);
const label = { H: 'H', S: 'S', D: 'D', Ds: 'D', P: 'P' };
-- return `<div class="cs-chart-wrap"><table class="cs-chart"><caption>Hard totals: what basic strategy says against the dealer's card</caption><thead><tr><th scope="col">You</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${totals.map((t) => `<tr><th scope="row">${t}</th>${ups.map((u) => { const c = blackjack.chart(handFor(t), u, false); return `<td class="is-${label[c]}">${label[c]}</td>`; }).join('')}</tr>`).join('')}</tbody></table><p class="cs-fine">H hit · S stand · D double (hit when you cannot)</p></div>`;
++ return `<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="Basic strategy chart"><table class="cs-chart"><caption>Hard totals: what basic strategy says against the dealer's card</caption><thead><tr><th scope="col">You</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${totals.map((t) => `<tr><th scope="row">${t}</th>${ups.map((u) => { const c = blackjack.chart(handFor(t), u, false); return `<td class="is-${label[c]}">${label[c]}</td>`; }).join('')}</tr>`).join('')}</tbody></table><p class="cs-fine">H hit · S stand · D double (hit when you cannot)</p></div>`;
}
function home({ sims = 0 } = {}) {
@@@ -167,7 +167,7 @@ ${safeLine()}
}
function edgeTable(g) {
-- return `<div class="cs-chart-wrap"><table class="cs-bets"><caption>What each bet costs</caption><thead><tr><th scope="col">Bet</th><th scope="col">House edge</th><th scope="col">How we know</th></tr></thead><tbody>${g.bets.slice().sort((a, b) => a.edge - b.edge).map((b) => `<tr><th scope="row">${esc(b.name)}</th><td>${pct(b.edge, 3)}</td><td>${esc(b.how)}</td></tr>`).join('')}</tbody></table></div>`;
++ return `<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="What each bet costs"><table class="cs-bets"><caption>What each bet costs</caption><thead><tr><th scope="col">Bet</th><th scope="col">House edge</th><th scope="col">How we know</th></tr></thead><tbody>${g.bets.slice().sort((a, b) => a.edge - b.edge).map((b) => `<tr><th scope="row">${esc(b.name)}</th><td>${pct(b.edge, 3)}</td><td>${esc(b.how)}</td></tr>`).join('')}</tbody></table></div>`;
}
const LEARN = {
@@@ -176,20 +176,20 @@ const LEARN = {
const soft = [13, 14, 15, 16, 17, 18, 19];
const ups = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const lab = { H: 'H', S: 'S', D: 'D', Ds: 'Ds', P: 'P' };
-- const softTable = `<div class="cs-chart-wrap"><table class="cs-chart"><caption>Soft totals (an ace counted as 11)</caption><thead><tr><th scope="col">You</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${soft.map((t) => `<tr><th scope="row">A,${t - 11}</th>${ups.map((u) => { const c = blackjack.chart([{ rank: 1, suit: 0 }, { rank: t - 11, suit: 0 }], u, false); return `<td class="is-${c === 'Ds' ? 'D' : c}">${lab[c]}</td>`; }).join('')}</tr>`).join('')}</tbody></table><p class="cs-fine">Ds double, or stand when you cannot</p></div>`;
++ const softTable = `<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="Soft totals chart"><table class="cs-chart"><caption>Soft totals (an ace counted as 11)</caption><thead><tr><th scope="col">You</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${soft.map((t) => `<tr><th scope="row">A,${t - 11}</th>${ups.map((u) => { const c = blackjack.chart([{ rank: 1, suit: 0 }, { rank: t - 11, suit: 0 }], u, false); return `<td class="is-${c === 'Ds' ? 'D' : c}">${lab[c]}</td>`; }).join('')}</tr>`).join('')}</tbody></table><p class="cs-fine">Ds double, or stand when you cannot</p></div>`;
const pairs = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
-- const pairTable = `<div class="cs-chart-wrap"><table class="cs-chart"><caption>Pairs: split or not</caption><thead><tr><th scope="col">Pair</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${pairs.map((p) => { const r = p === 11 ? 1 : p; return `<tr><th scope="row">${p === 11 ? 'A,A' : `${p},${p}`}</th>${ups.map((u) => { const c = blackjack.chart([{ rank: r, suit: 0 }, { rank: r, suit: 1 }], u, true); return `<td class="is-${c === 'P' ? 'P' : '-'}">${c === 'P' ? 'P' : ''}</td>`; }).join('')}</tr>`; }).join('')}</tbody></table><p class="cs-fine">P split; an empty cell plays the hand as a total</p></div>`;
++ const pairTable = `<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="Pairs chart"><table class="cs-chart"><caption>Pairs: split or not</caption><thead><tr><th scope="col">Pair</th>${ups.map((u) => `<th scope="col">${u === 11 ? 'A' : u}</th>`).join('')}</tr></thead><tbody>${pairs.map((p) => { const r = p === 11 ? 1 : p; return `<tr><th scope="row">${p === 11 ? 'A,A' : `${p},${p}`}</th>${ups.map((u) => { const c = blackjack.chart([{ rank: r, suit: 0 }, { rank: r, suit: 1 }], u, true); return `<td class="is-${c === 'P' ? 'P' : '-'}">${c === 'P' ? 'P' : ''}</td>`; }).join('')}</tr>`; }).join('')}</tbody></table><p class="cs-fine">P split; an empty cell plays the hand as a total</p></div>`;
return `<h2>The rules here</h2><p>You and the dealer each get two cards; one of the dealer's is face down. Closest to 21 without going over wins. Face cards count 10, an ace 1 or 11. The dealer must draw to 16 and stand on 17. A blackjack, an ace and a ten, pays 3 to 2.</p>
<h2>Basic strategy</h2><p>For every hand you can hold against every card the dealer can show, one move loses least over time. These tables come from the engine the tables play with; the hint at the table shows the same move.</p>${hardChart()}${softTable}${pairTable}`;
},
roulette: () => `<h2>One price for everything</h2><p>A bet on n numbers pays 36 ÷ n − 1 to one, as if there were only 36 pockets. The zeros are what the house keeps: 1 pocket in 37 on a European wheel (2.70%), 2 in 38 on an American one (5.26%). The American top line, 0, 00, 1, 2 and 3, pays 6 to 1 for five numbers and costs 7.89%.</p>
--<h2>What a bet covers and pays</h2><div class="cs-chart-wrap"><table class="cs-bets"><thead><tr><th scope="col">Bet</th><th scope="col">Numbers</th><th scope="col">Pays</th><th scope="col">Wins on a European wheel</th></tr></thead><tbody>${[['Straight', 1], ['Split', 2], ['Street', 3], ['Corner', 4], ['Six line', 6], ['Dozen or column', 12], ['Red, black, odd, even, low, high', 18]].map(([n, k]) => `<tr><th scope="row">${n}</th><td>${k}</td><td>${36 / k - 1} to 1</td><td>${(k / 37 * 100).toFixed(2)}%</td></tr>`).join('')}</tbody></table></div>`,
++<h2>What a bet covers and pays</h2><div class="cs-chart-wrap" tabindex="0" role="region" aria-label="What a bet covers and pays"><table class="cs-bets"><thead><tr><th scope="col">Bet</th><th scope="col">Numbers</th><th scope="col">Pays</th><th scope="col">Wins on a European wheel</th></tr></thead><tbody>${[['Straight', 1], ['Split', 2], ['Street', 3], ['Corner', 4], ['Six line', 6], ['Dozen or column', 12], ['Red, black, odd, even, low, high', 18]].map(([n, k]) => `<tr><th scope="row">${n}</th><td>${k}</td><td>${36 / k - 1} to 1</td><td>${(k / 37 * 100).toFixed(2)}%</td></tr>`).join('')}</tbody></table></div>`,
baccarat: () => `<h2>Nothing to decide but the bet</h2><p>Two hands, the player and the banker, get two cards each; only the last digit of the total counts. An 8 or 9 is a natural and stands. Otherwise the player draws on 0 to 5, and the banker draws by the tableau below. The banker bet wins a little more often, so it pays 0.95 to 1.</p>
--<div class="cs-chart-wrap"><table class="cs-chart"><caption>When the banker draws, by the player's third card</caption><thead><tr><th scope="col">Banker</th>${['none', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((v) => `<th scope="col">${v}</th>`).join('')}</tr></thead><tbody>${[0, 1, 2, 3, 4, 5, 6, 7].map((b) => `<tr><th scope="row">${b}</th>${[null, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((p) => { const d = games.engines.baccarat.bankerDraws(b, p); return `<td class="is-${d ? 'H' : 'S'}">${d ? 'Draw' : ''}</td>`; }).join('')}</tr>`).join('')}</tbody></table></div>`,
++<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="When the banker draws"><table class="cs-chart"><caption>When the banker draws, by the player's third card</caption><thead><tr><th scope="col">Banker</th>${['none', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((v) => `<th scope="col">${v}</th>`).join('')}</tr></thead><tbody>${[0, 1, 2, 3, 4, 5, 6, 7].map((b) => `<tr><th scope="row">${b}</th>${[null, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((p) => { const d = games.engines.baccarat.bankerDraws(b, p); return `<td class="is-${d ? 'H' : 'S'}">${d ? 'Draw' : ''}</td>`; }).join('')}</tr>`).join('')}</tbody></table></div>`,
craps: () => { const { WAYS } = games.engines.craps; return `<h2>Two dice, 36 ways</h2><p>Seven is the most likely total, six ways in 36. The pass line wins at once on 7 or 11 and loses on 2, 3 or 12; any other total becomes the point, and the point must come again before a 7. Free odds behind the line pay exactly the true odds, so they cost nothing.</p>
<div class="cs-dice-odds">${[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((t) => `<span style="--h:${WAYS[t]}"><i></i><b>${t}</b><small>${WAYS[t]}/36</small></span>`).join('')}</div>`; },
slots: () => { const sl = games.engines.slots; const x = sl.exact(); return `<h2>The machine, opened</h2><p>Three reels of 24 stops, every stop equally likely, so every one of the 13,824 spins can be counted. This one pays back ${(x.rtp * 100).toFixed(2)}% of what goes in over time and wins something on ${(x.hitRate * 100).toFixed(1)}% of spins; most of those wins are smaller than the bet.</p>
--<div class="cs-chart-wrap"><table class="cs-bets"><caption>The paytable and how often each win comes</caption><thead><tr><th scope="col">Pays</th><th scope="col">Chance on a spin</th><th scope="col">About once every</th></tr></thead><tbody>${x.wins.map((w) => `<tr><th scope="row">${w.pay} to 1</th><td>${(w.chance * 100).toFixed(3)}%</td><td>${Math.round(1 / w.chance).toLocaleString('en-US')} spins</td></tr>`).join('')}</tbody></table></div>
++<div class="cs-chart-wrap" tabindex="0" role="region" aria-label="The paytable"><table class="cs-bets"><caption>The paytable and how often each win comes</caption><thead><tr><th scope="col">Pays</th><th scope="col">Chance on a spin</th><th scope="col">About once every</th></tr></thead><tbody>${x.wins.map((w) => `<tr><th scope="row">${w.pay} to 1</th><td>${(w.chance * 100).toFixed(3)}%</td><td>${Math.round(1 / w.chance).toLocaleString('en-US')} spins</td></tr>`).join('')}</tbody></table></div>
<div class="cs-reels">${sl.STRIPS.map((s, i) => `<div class="cs-reel"><b>Reel ${i + 1}</b><ol>${s.map((sym) => `<li class="sym-${sym}">${sym}</li>`).join('')}</ol></div>`).join('')}</div>`; },
};