- 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>
97 lines
2.9 KiB
JavaScript
97 lines
2.9 KiB
JavaScript
'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 };
|