Add CPU opponents and a table type selector (cash only / friends / CPU)

- Create screen: choose キャッシュオンリー (real cards, chips only), 友人戦
  (server deals and settles) or CPU戦 (友人戦 plus 1–8 CPU players); replaces
  the card toggle. The lobby shows each table's type
- Poker AI written from scratch (server/ai.js, no libraries): fast 7-card
  evaluator (checked against bestHand), Monte Carlo equity, and decisions from
  hand rank, pot odds, raise count, opponents and a per-CPU style (aggression,
  bluff rate); continuation bets via the last aggressor. It never sees other
  players' hole cards
- CPU seats: think time of 0.7–1.8s (faster when no human is left in the hand),
  automatic rebuy, never the host, no new hand unless a human is playing,
  re-think after pause
- Play UI: robot avatars and a "考えています…" status for CPU turns
- Tests for the evaluator, equity, legality of AI actions and CPU tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 16:55:25 +09:00
1 parent 66907345ff
commit 0d3a054f0d
12 files changed
+550 -46

No files matched your search

+16 -1
View File
@@ -29,6 +29,10 @@ class Hand {
this.pot = 0; // 前のストリートまでに集めた額
this.currentBet = 0;
this.minRaise = bb;
this.raises = 0; // このストリートのベット・レイズの回数(CPU の判断用)
this.actions = 0; // アクションの通し番号(CPU の手番の管理用)
this.aggressor = null; // このストリートで最後にベット・レイズした席(CPU の判断用)
this.prevAggressor = null; // 前のストリートで最後にベット・レイズした席
this.toAct = null;
this.finished = false;
this.showdown = false;
@@ -75,6 +79,11 @@ class Hand {
return this.list.filter((p) => !p.folded);
}
// 前のストリートまでのポット + このストリートのベット
get potTotal() {
return this.pot + this.list.reduce((sum, p) => sum + p.bet, 0);
}
put(p, amount) {
const s = this.seats[p.seat];
s.stack -= amount;
@@ -163,9 +172,11 @@ class Hand {
const raiseSize = amount - this.currentBet;
this.put(p, amount - p.bet);
p.last = { type: o.isBet ? 'BET' : 'RAISE', amount };
this.aggressor = seat;
if (raiseSize >= this.minRaise) {
// 正規のレイズ: 他の人はもう一度アクションできる
this.minRaise = raiseSize;
this.raises++;
for (const q of this.list) if (q !== p) q.acted = false;
}
// 足りないオールインのレイズはアクション済みの人に再レイズ権を与えない
@@ -175,6 +186,7 @@ class Hand {
}
if (p.allIn) p.last = { type: 'ALLIN', amount: p.bet };
p.acted = true;
this.actions++;
this.setNextActor(seat);
return null;
}
@@ -225,6 +237,9 @@ class Hand {
if (this.live().length === 1) return this.finishFoldWin();
this.currentBet = 0;
this.minRaise = this.bb;
this.raises = 0;
this.prevAggressor = this.aggressor;
this.aggressor = null;
const noMoreBetting = this.live().filter((p) => !p.allIn).length < 2;
if (!this.cards) {
@@ -365,7 +380,7 @@ class Hand {
runout: this.runout,
street: this.street,
pot: this.pot,
potTotal: this.pot + this.list.reduce((sum, p) => sum + p.bet, 0),
potTotal: this.potTotal,
currentBet: this.currentBet,
button: this.button,
sbSeat: this.sbSeat,