Files
Poker/server/cards.js
T
agomizi1cmandClaude Opus 5.5 8de55f7ca5 Implement Texas Hold'em play: dealing, betting, side pots, showdown
- Hand engine (server/hand.js): crypto-shuffled deck kept server-side,
  blinds (heads-up button = SB), min-raise rules incl. incomplete all-in,
  uncalled bet refunds, side pots, split pots, all-in runout
- Hand evaluator (server/cards.js) and node:test suite (npm test)
- Table flow: hands auto-continue after host START, 30s action clock
  (timeout = check/fold + sit out), sit out / sit in, rebuy, mid-hand leave
  folds and keeps the seat reserved until the hand ends, pause freezes all clocks
- Only each player's own hole cards are sent until a showdown/runout reveal;
  all actions validated server-side
- Play UI: board, hole cards, bets, action tags, turn timer, raise panel with
  presets, showdown results with winning cards highlighted, compact 7+ layout
- Remove the host crown and the temporary "next phase" control

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:34:25 +09:00

97 lines
2.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const crypto = require('crypto');
// カードは "As"(スペードのA), "Td"(ダイヤの10)のような2文字
const RANKS = '23456789TJQKA';
const SUITS = 'shdc';
// 暗号論的乱数で Fisher-Yates シャッフル(山札はサーバの外に出さない)
function newDeck() {
const deck = [];
for (const r of RANKS) for (const s of SUITS) deck.push(r + s);
for (let i = deck.length - 1; i > 0; i--) {
const j = crypto.randomInt(i + 1);
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
const rankValue = (card) => RANKS.indexOf(card[0]) + 2;
// 5枚の役の強さ。配列を先頭から比較して大きいほうが強い
// [役の種類(0〜8), 比較用の数字...]
function score5(cards) {
const vals = cards.map(rankValue).sort((a, b) => b - a);
const flush = cards.every((c) => c[1] === cards[0][1]);
let straightHigh = 0;
if (new Set(vals).size === 5) {
if (vals[0] - vals[4] === 4) straightHigh = vals[0];
else if (vals[0] === 14 && vals[1] === 5) straightHigh = 5; // A-2-3-4-5
}
if (straightHigh && flush) return [8, straightHigh];
// 枚数の多い順、同じ枚数なら数字の大きい順
const counts = new Map();
for (const v of vals) counts.set(v, (counts.get(v) || 0) + 1);
const groups = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
const byGroup = groups.map((g) => g[0]);
if (groups[0][1] === 4) return [7, ...byGroup];
if (groups[0][1] === 3 && groups[1][1] === 2) return [6, ...byGroup];
if (flush) return [5, ...vals];
if (straightHigh) return [4, straightHigh];
if (groups[0][1] === 3) return [3, ...byGroup];
if (groups[0][1] === 2 && groups[1][1] === 2) return [2, ...byGroup];
if (groups[0][1] === 2) return [1, ...byGroup];
return [0, ...vals];
}
function compareScore(a, b) {
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const d = (a[i] || 0) - (b[i] || 0);
if (d) return d;
}
return 0;
}
const HAND_NAMES = [
'ハイカード',
'ワンペア',
'ツーペア',
'スリーカード',
'ストレート',
'フラッシュ',
'フルハウス',
'フォーカード',
'ストレートフラッシュ',
];
function handName(score) {
if (score[0] === 8 && score[1] === 14) return 'ロイヤルフラッシュ';
return HAND_NAMES[score[0]];
}
// 5〜7枚から最も強い5枚を選ぶ
function bestHand(cards) {
let best = null;
const n = cards.length;
const pick = (start, chosen) => {
if (chosen.length === 5) {
const score = score5(chosen);
if (!best || compareScore(score, best.score) > 0) best = { score, cards: chosen.slice() };
return;
}
for (let i = start; i <= n - (5 - chosen.length); i++) {
chosen.push(cards[i]);
pick(i + 1, chosen);
chosen.pop();
}
};
pick(0, []);
return best && { score: best.score, cards: best.cards, name: handName(best.score) };
}
module.exports = { newDeck, score5, compareScore, bestHand, handName };