Files
Poker/server/cards.js
T
agomizi1cmandClaude Opus 5.5 66907345ff Add optional card mode: server deals cards and settles showdowns automatically
- New "カードを配る" switch on the create-table screen (default off); the lobby
  shows "カードあり" for tables that use it
- Card mode (server/cards.js restored): crypto-shuffled deck kept server-side,
  hole cards sent only to their owner until showdown/all-in reveal, board dealt
  per street with burns, all-in runout on a timer (frozen while paused)
- Showdown is evaluated and every pot (incl. side pots, split pots) is paid out
  automatically; host winner selection is only used when card mode is off
- Play UI: board, hole cards, own cards in the action bar, hand names, winning
  cards highlighted; layout tuned for 7+ seats and short screens
- Playing-card styles use .pcard so they don't clash with the existing .card panel
- Tests for hand evaluation, card privacy, runout, side pots, ties and random play

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:44:22 +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 };