Add player stats, in-table ranking, host seating and review return
- Stats page (/stats): play style radar (TAG/LAG/rock/fish), VPIP, PFR, ATS, 3bet, WTSD, W$SD, WWSF, per-street fold rate, aggression frequency, hand win rate and net in BB, per table type, each with a bands-and-explanation sheet. Hands are counted per human player when they finish and stored as totals under data/stats/, keyed by a hash of the player ID (sent in a POST body, never in the URL). - Ranking sheet at the table: everyone seated, ordered by stack or by net (stack minus buy-ins), updating live. - The host can reorder seats by dragging (with a mini table preview and shuffle). Takes effect immediately between hands, otherwise at the start of the next hand; the dealer button stays with the same player. - The full-session review opens in the same tab and its back button returns to the table it was opened from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49203f202c
commit
dd39c043c4
18 files changed
+2699
-18
No files matched your search
+84
-2
@@ -7,6 +7,8 @@ const { Hand } = require('./hand');
|
||||
const ai = require('./ai');
|
||||
const { analyzeHand } = require('./review/analyze');
|
||||
const reviews = require('./review/store');
|
||||
const { handStats } = require('./stats/hand');
|
||||
const stats = require('./stats/store');
|
||||
|
||||
// テーブルの種類
|
||||
// chips : キャッシュオンリー。カードは実物で配り、サイトはチップだけを管理する
|
||||
@@ -37,6 +39,7 @@ class Table {
|
||||
// bot : CPU プレイヤー(style は思考の種類と性格。ブラウザには渡さない)
|
||||
this.seats = new Array(this.maxSeats).fill(null);
|
||||
this.botTurn = null; // { key, at }: CPU が行動する時刻
|
||||
this.pendingSeating = null; // ホストが決めた席順(時計回りのトークン)。ハンド中は次のハンドの始めに並べ替える
|
||||
this.withBots = this.mode === 'cpu' || this.mode === 'review';
|
||||
if (this.withBots) this.seatBots(opts.cpuCount);
|
||||
// レビュー付きCPU戦: session = 保存する対局、last = 直前のハンドのレビュー、waiting = 次のハンドへ進むのを待っている
|
||||
@@ -180,6 +183,7 @@ class Table {
|
||||
|
||||
startNewHand() {
|
||||
this.removeLeftSeats();
|
||||
this.applySeating();
|
||||
// チップがなくなった CPU は自動でリバイ
|
||||
for (const s of this.seats) {
|
||||
if (s && s.bot && s.stack === 0) {
|
||||
@@ -259,6 +263,45 @@ class Table {
|
||||
|
||||
// ---- ホスト操作 ----
|
||||
|
||||
// 席順を変える。order = 着席している全員の席番号を時計回りに並べたもの。
|
||||
// 進行中(結果の表示中を含む)のハンドは席番号で参加者を覚えているので、そのときは次のハンドの始めに並べ替える
|
||||
setSeating(token, order) {
|
||||
if (!this.isHost(token)) return 'ホストのみ操作できます';
|
||||
const current = this.seats.map((s, i) => (s && !s.left ? i : -1)).filter((i) => i >= 0);
|
||||
const valid =
|
||||
Array.isArray(order) &&
|
||||
order.length === current.length &&
|
||||
new Set(order).size === order.length &&
|
||||
order.every((i) => current.includes(i));
|
||||
if (!valid) return '着席している人が変わりました。もう一度並べてください';
|
||||
this.pendingSeating = order.map((i) => this.seats[i].token);
|
||||
if (!this.hand) this.applySeating();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 決めてある席順に並べ替える。席は詰めて 0 番から座らせる(空席は画面に出ないので、並びだけが意味を持つ)。
|
||||
// 決めた後に座った人は最後(ホストの並びの後ろ)に、元の順のまま加える。ボタンは同じ人に付いたまま
|
||||
applySeating() {
|
||||
const tokens = this.pendingSeating;
|
||||
if (!tokens) return;
|
||||
this.pendingSeating = null;
|
||||
const rank = (s) => {
|
||||
const k = tokens.indexOf(s.token);
|
||||
return k < 0 ? tokens.length : k;
|
||||
};
|
||||
const button = this.buttonSeat != null && this.seats[this.buttonSeat] ? this.seats[this.buttonSeat].token : null;
|
||||
const players = this.seats
|
||||
.map((s, k) => ({ s, k }))
|
||||
.filter((x) => x.s)
|
||||
.sort((a, b) => rank(a.s) - rank(b.s) || a.k - b.k)
|
||||
.map((x) => x.s);
|
||||
this.seats = new Array(this.maxSeats).fill(null);
|
||||
players.forEach((s, i) => {
|
||||
this.seats[i] = s;
|
||||
});
|
||||
this.buttonSeat = button ? this.seatOf(button) : null;
|
||||
}
|
||||
|
||||
start(token, now = Date.now()) {
|
||||
if (!this.isHost(token)) return 'ホストのみ開始できます';
|
||||
if (this.started) return 'すでに開始しています';
|
||||
@@ -340,6 +383,7 @@ class Table {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hand && this.hand.finished && !this.hand.statsRecorded) this.recordStats();
|
||||
if (this.hand && this.hand.finished && this.review && !this.hand.reviewed) {
|
||||
this.reviewHand();
|
||||
changed = true;
|
||||
@@ -356,6 +400,7 @@ class Table {
|
||||
this.hand = null;
|
||||
this.nextHandAt = null;
|
||||
this.removeLeftSeats();
|
||||
this.applySeating();
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
@@ -370,6 +415,25 @@ class Table {
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ---- 統計 ----
|
||||
|
||||
// 終わったハンドを、参加した人間のプレイヤーごとの統計に足す(CPU は数えない)。
|
||||
// レビュー付きCPU戦は CPU戦としてまとめる
|
||||
recordStats() {
|
||||
const h = this.hand;
|
||||
h.statsRecorded = true;
|
||||
const mode = this.mode === 'review' ? 'cpu' : this.mode;
|
||||
for (const s of h.order) {
|
||||
const seat = this.seats[s];
|
||||
if (!seat || seat.bot) continue;
|
||||
try {
|
||||
stats.record(seat.token, mode, handStats(h, s), { startedAt: h.startedAt, endedAt: h.endedAt || Date.now() });
|
||||
} catch (err) {
|
||||
console.error('[stats] 統計を数えられませんでした', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- レビュー付きCPU戦 ----
|
||||
|
||||
// 終わったハンドのレビューを作って保存し、作成者が見終わるまで次のハンドを待つ
|
||||
@@ -457,6 +521,18 @@ class Table {
|
||||
};
|
||||
}
|
||||
|
||||
inLiveHand(seat) {
|
||||
return Boolean(this.handActive && this.hand.players.has(seat) && !this.hand.players.get(seat).folded);
|
||||
}
|
||||
|
||||
// ランキング用: 持っているチップ(進行中のハンドで出した額は、降りていなければまだ本人の分として含める)と
|
||||
// 収支(それ − バイインの合計)
|
||||
standing(seat) {
|
||||
const s = this.seats[seat];
|
||||
const held = s.stack + (this.inLiveHand(seat) ? this.hand.players.get(seat).committed : 0);
|
||||
return { held, net: held - s.buyIns * this.startingStack };
|
||||
}
|
||||
|
||||
// token = 閲覧者のトークン(他人のトークンや手札は送らない)
|
||||
publicState(token, connectedTokens) {
|
||||
const now = Date.now();
|
||||
@@ -466,7 +542,7 @@ class Table {
|
||||
const h = this.hand;
|
||||
const hv = h ? h.view(mySeat, token) : null;
|
||||
const me = mySeat >= 0 ? this.seats[mySeat] : null;
|
||||
const inLiveHand = Boolean(this.handActive && h.players.has(mySeat) && !h.players.get(mySeat).folded);
|
||||
const inLiveHand = this.inLiveHand(mySeat);
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -498,6 +574,7 @@ class Table {
|
||||
seat: i,
|
||||
name: s.name,
|
||||
stack: s.stack,
|
||||
...this.standing(i),
|
||||
connected: Boolean(s.bot) || connectedTokens.has(s.token),
|
||||
bot: Boolean(s.bot),
|
||||
sittingOut: s.sittingOut,
|
||||
@@ -510,7 +587,7 @@ class Table {
|
||||
me && !me.left
|
||||
? {
|
||||
stack: me.stack,
|
||||
net: me.stack + (inLiveHand ? h.players.get(mySeat).committed : 0) - me.buyIns * this.startingStack,
|
||||
net: this.standing(mySeat).net,
|
||||
buyIns: me.buyIns,
|
||||
sittingOut: me.sittingOut,
|
||||
canRebuy: me.stack === 0 && !inLiveHand,
|
||||
@@ -519,6 +596,11 @@ class Table {
|
||||
}
|
||||
: null,
|
||||
isHost: this.isHost(token),
|
||||
// ホストだけに: 次のハンドから使う席順(席番号を時計回りに)
|
||||
pendingSeating:
|
||||
this.isHost(token) && this.pendingSeating
|
||||
? this.pendingSeating.map((t) => this.seatOf(t)).filter((i) => i >= 0 && !this.seats[i].left)
|
||||
: null,
|
||||
review: this.review && this.isHost(token) ? this.reviewView() : null,
|
||||
serverNow: now,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user