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
+4
-1
@@ -56,6 +56,8 @@ class Hand {
|
||||
this.log = [];
|
||||
this.startStacks = new Map(order.map((s) => [s, seats[s].stack])); // ブラインドを払う前
|
||||
this.allIn = null; // オールインで決着した時点の場札とポット(レビューの期待値用)
|
||||
this.startedAt = Date.now(); // 統計のプレイ時間用
|
||||
this.endedAt = null;
|
||||
|
||||
this.players = new Map();
|
||||
for (const i of order) {
|
||||
@@ -413,7 +415,8 @@ class Hand {
|
||||
}
|
||||
|
||||
end(showdown, results) {
|
||||
this.endStacks = new Map(this.order.map((s) => [s, this.seats[s] ? this.seats[s].stack : 0])); // レビュー用
|
||||
this.endStacks = new Map(this.order.map((s) => [s, this.seats[s] ? this.seats[s].stack : 0])); // レビュー・統計用
|
||||
this.endedAt = Date.now();
|
||||
this.finished = true;
|
||||
this.runout = false;
|
||||
this.showdown = showdown;
|
||||
|
||||
@@ -10,6 +10,8 @@ const { TableManager, MODES } = require('./tables');
|
||||
const reviews = require('./review/store');
|
||||
const { summarize } = require('./review/summary');
|
||||
const { buildHandPrompt } = require('./review/prompt');
|
||||
const stats = require('./stats/store');
|
||||
const { summarize: summarizeStats } = require('./stats/summary');
|
||||
|
||||
const PUBLIC = path.join(__dirname, '..', 'public');
|
||||
// 管理者専用のファイル(public の外に置き、管理者の Cookie があるときだけ返す)
|
||||
@@ -89,6 +91,7 @@ app.use(auth.requireSameOrigin);
|
||||
app.get('/', page('lobby.html'));
|
||||
app.get('/create', page('create.html'));
|
||||
app.get('/table/:id', page('table.html'));
|
||||
app.get('/stats', page('stats.html'));
|
||||
|
||||
// ---------- 管理者専用: ハンドレンジ学習 ----------
|
||||
app.get('/range', auth.requireAdmin, page('range.html', ADMIN));
|
||||
@@ -136,6 +139,17 @@ app.get('/api/reviews/:id/hands/:number/prompt', auth.requireAdmin, async (req,
|
||||
});
|
||||
|
||||
app.get('/api/me', (req, res) => res.json({ admin: auth.isAdmin(req) }));
|
||||
|
||||
// 自分の統計。プレイヤーID(席のトークン)を URL やアクセスログに残さないよう POST の本文で受け取る
|
||||
app.post('/api/stats', async (req, res, next) => {
|
||||
const token = cleanToken(req.body && req.body.token);
|
||||
if (!token) return res.status(400).json({ error: 'プレイヤーIDがありません' });
|
||||
try {
|
||||
res.json(summarizeStats(await stats.get(token)));
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
app.get('/api/tables', (req, res) => res.json(tables.list(auth.isAdmin(req))));
|
||||
app.get('/api/tables/suggest-name', (req, res) => res.json({ name: tables.suggestName() }));
|
||||
|
||||
@@ -335,6 +349,12 @@ io.on('connection', (socket) => {
|
||||
'table:sitIn': (t, token) => t.setSittingOut(token, false),
|
||||
'table:rebuy': (t, token) => t.rebuy(token),
|
||||
'table:next': (t, token) => t.reviewNext(token),
|
||||
'table:seating': (t, token, p) => {
|
||||
// order: 時計回りに並べた席番号
|
||||
const order = p && typeof p === 'object' ? p.order : null;
|
||||
const ok = Array.isArray(order) && order.length <= config.MAX_SEATS && order.every((i) => Number.isInteger(i));
|
||||
return ok ? t.setSeating(token, order) : '席順の指定が不正です';
|
||||
},
|
||||
};
|
||||
for (const [event, fn] of Object.entries(actions)) {
|
||||
socket.on(event, (p, ack) => {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
// 1ハンドから、1人のプレイヤーの統計の材料(数えるだけ。割合は summary.js で出す)を取り出す。
|
||||
// 離席・退席などで自動で行った行動は本人の判断として数えない。キャッシュオンリー(場札なし)でも同じように数える
|
||||
const { positionOf, ACTION_KIND } = require('../review/preflop');
|
||||
|
||||
const STREETS = ['PREFLOP', 'FLOP', 'TURN', 'RIVER'];
|
||||
// スチール(ATS)を試みられる位置: 全員が降りて回ってきた CO・BTN・SB
|
||||
const STEAL_POSITIONS = new Set(['CO', 'BTN', 'SB', 'BTN/SB']);
|
||||
|
||||
function handStats(hand, seat) {
|
||||
const p = hand.players.get(seat);
|
||||
const mine = hand.log.filter((e) => e.seat === seat && !e.auto);
|
||||
const pre = mine.filter((e) => e.street === 'PREFLOP');
|
||||
const kind = (e) => ACTION_KIND[e.type];
|
||||
|
||||
// フロップを見た: プリフロップで降りておらず、ハンドがフロップまで進んだ(プリフロップのオールインで
|
||||
// 残りをめくったときも含む)
|
||||
const foldedPre = hand.log.some((e) => e.seat === seat && e.street === 'PREFLOP' && e.type === 'FOLD');
|
||||
const reachedFlop = hand.showdown || hand.board.length >= 3 || hand.log.some((e) => e.street !== 'PREFLOP');
|
||||
const sawFlop = !foldedPre && reachedFlop;
|
||||
const showdown = Boolean(hand.showdown && !p.folded);
|
||||
const won = p.won > 0;
|
||||
|
||||
// スチールと 3bet の機会(プリフロップを順に見る)
|
||||
const position = positionOf(hand, seat);
|
||||
let entered = false; // 自分より前に誰かがコール・レイズした
|
||||
let raises = 0;
|
||||
let first = true;
|
||||
let atsOpp = false;
|
||||
let ats = false;
|
||||
let threeBetOpp = false;
|
||||
let threeBet = false;
|
||||
for (const e of hand.log) {
|
||||
if (e.street !== 'PREFLOP') continue;
|
||||
const k = kind(e);
|
||||
if (e.seat === seat && !e.auto) {
|
||||
if (first && !entered && STEAL_POSITIONS.has(position)) {
|
||||
atsOpp = true;
|
||||
ats = k === 'raise';
|
||||
}
|
||||
first = false;
|
||||
// 3bet の機会: プリフロップでレイズがちょうど1回あった後の判断
|
||||
if (raises === 1 && !threeBetOpp) {
|
||||
threeBetOpp = true;
|
||||
threeBet = k === 'raise';
|
||||
}
|
||||
}
|
||||
if (k === 'call' || k === 'raise') entered = true;
|
||||
if (k === 'raise') raises++;
|
||||
}
|
||||
|
||||
// ストリートごと: 判断した(そのストリートに来た)・降りた・全部の行動のうちベット・レイズ
|
||||
const seen = [0, 0, 0, 0];
|
||||
const folds = [0, 0, 0, 0];
|
||||
const actions = [0, 0, 0, 0];
|
||||
const aggressive = [0, 0, 0, 0];
|
||||
for (const e of mine) {
|
||||
const i = STREETS.indexOf(e.street);
|
||||
if (i < 0) continue;
|
||||
seen[i] = 1;
|
||||
actions[i]++;
|
||||
if (kind(e) === 'raise') aggressive[i]++;
|
||||
if (e.type === 'FOLD') folds[i] = 1;
|
||||
}
|
||||
|
||||
const start = hand.startStacks.get(seat);
|
||||
const end = hand.endStacks ? hand.endStacks.get(seat) : start;
|
||||
return {
|
||||
preflop: pre.length > 0, // VPIP・PFR の分母(BB で全員が降りてきたハンドなど、判断の無いハンドは除く)
|
||||
vpip: pre.some((e) => kind(e) === 'call' || kind(e) === 'raise'),
|
||||
pfr: pre.some((e) => kind(e) === 'raise'),
|
||||
atsOpp,
|
||||
ats,
|
||||
threeBetOpp,
|
||||
threeBet,
|
||||
sawFlop,
|
||||
showdown,
|
||||
showdownWon: showdown && won,
|
||||
wonSawFlop: sawFlop && won,
|
||||
won,
|
||||
seen,
|
||||
folds,
|
||||
actions,
|
||||
aggressive,
|
||||
netBB: hand.bb > 0 ? (end - start) / hand.bb : 0,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { handStats, STREETS };
|
||||
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
// プレイヤーごとの統計の保存先(data/stats/<プレイヤーIDのハッシュ>.json)。
|
||||
// プレイヤーID(ブラウザの localStorage にある席のトークン)はそのまま残さず、SHA-256 の先頭で名前を付ける。
|
||||
// 中身はテーブルの種類ごとの回数の合計だけで、ハンドの内容は持たない。
|
||||
// 同じプレイヤーの読み書きは順番に行う(ハンドが続けて終わっても数え漏らさない)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DIR = process.env.STATS_DIR || path.join(__dirname, '..', '..', 'data', 'stats');
|
||||
const MODES = ['chips', 'friends', 'cpu'];
|
||||
// 続けて遊んでいるとみなす間隔(前のハンドの終わりからこれ以内に始まったら、間の時間もプレイ時間に入れる)
|
||||
const CONTINUOUS_MS = 3 * 60 * 1000;
|
||||
|
||||
const queue = new Map(); // key → 直前の読み書きの Promise
|
||||
const cache = new Map(); // key → 中身(読んだもの・書いたもの)
|
||||
|
||||
const keyOf = (token) => crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 32);
|
||||
const fileOf = (key) => path.join(DIR, `${key}.json`);
|
||||
|
||||
const streets = () => [0, 0, 0, 0];
|
||||
function emptyBucket() {
|
||||
return {
|
||||
hands: 0,
|
||||
won: 0,
|
||||
preflop: 0,
|
||||
vpip: 0,
|
||||
pfr: 0,
|
||||
atsOpp: 0,
|
||||
ats: 0,
|
||||
threeBetOpp: 0,
|
||||
threeBet: 0,
|
||||
sawFlop: 0,
|
||||
showdown: 0,
|
||||
showdownWon: 0,
|
||||
wonSawFlop: 0,
|
||||
seen: streets(),
|
||||
folds: streets(),
|
||||
actions: streets(),
|
||||
aggressive: streets(),
|
||||
netBB: 0,
|
||||
ms: 0,
|
||||
};
|
||||
}
|
||||
const empty = () => ({ version: 1, firstAt: null, lastAt: null, modes: {} });
|
||||
|
||||
async function load(key) {
|
||||
if (cache.has(key)) return cache.get(key);
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(await fs.promises.readFile(fileOf(key), 'utf8'));
|
||||
if (!data || typeof data !== 'object' || !data.modes) data = empty();
|
||||
} catch {
|
||||
data = empty();
|
||||
}
|
||||
cache.set(key, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function write(key, data) {
|
||||
await fs.promises.mkdir(DIR, { recursive: true, mode: 0o700 });
|
||||
const tmp = `${fileOf(key)}.tmp`;
|
||||
await fs.promises.writeFile(tmp, JSON.stringify(data), { mode: 0o600 });
|
||||
await fs.promises.rename(tmp, fileOf(key));
|
||||
}
|
||||
|
||||
// 同じ key の処理を順番に行う
|
||||
function serial(key, fn) {
|
||||
const prev = queue.get(key) || Promise.resolve();
|
||||
const next = prev.then(fn);
|
||||
const settled = next.catch((err) => console.error('[stats] 保存に失敗しました', err.message));
|
||||
queue.set(key, settled);
|
||||
settled.then(() => {
|
||||
if (queue.get(key) === settled) queue.delete(key);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function add(bucket, entry, ms) {
|
||||
bucket.hands++;
|
||||
for (const k of ['won', 'preflop', 'vpip', 'pfr', 'atsOpp', 'ats', 'threeBetOpp', 'threeBet', 'sawFlop', 'showdown', 'showdownWon', 'wonSawFlop']) {
|
||||
if (entry[k]) bucket[k]++;
|
||||
}
|
||||
for (const k of ['seen', 'folds', 'actions', 'aggressive']) {
|
||||
for (let i = 0; i < 4; i++) bucket[k][i] += entry[k][i] || 0;
|
||||
}
|
||||
bucket.netBB = Math.round((bucket.netBB + entry.netBB) * 100) / 100;
|
||||
bucket.ms += ms;
|
||||
}
|
||||
|
||||
// 1ハンド分を足す。startedAt / endedAt はハンドの始まりと終わりの時刻
|
||||
function record(token, mode, entry, { startedAt, endedAt }) {
|
||||
if (!MODES.includes(mode)) return Promise.resolve();
|
||||
const key = keyOf(token);
|
||||
return serial(key, async () => {
|
||||
const data = await load(key);
|
||||
// プレイ時間: ハンドの長さ。前のハンドから続けて遊んでいれば、間の待ち時間も入れる
|
||||
const continuous = data.lastAt != null && startedAt - data.lastAt >= 0 && startedAt - data.lastAt <= CONTINUOUS_MS;
|
||||
const ms = Math.max(0, endedAt - (continuous ? data.lastAt : startedAt));
|
||||
if (!data.modes[mode]) data.modes[mode] = emptyBucket();
|
||||
add(data.modes[mode], entry, ms);
|
||||
if (data.firstAt == null) data.firstAt = startedAt;
|
||||
data.lastAt = endedAt;
|
||||
await write(key, data);
|
||||
});
|
||||
}
|
||||
|
||||
// 書き込み中のものを待ってから返す
|
||||
function get(token) {
|
||||
const key = keyOf(token);
|
||||
return serial(key, () => load(key));
|
||||
}
|
||||
|
||||
module.exports = { record, get, emptyBucket, MODES, DIR };
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
// 保存してある回数から、画面に出す割合を作る。テーブルの種類ごとと、すべてを合わせた合計(total)
|
||||
const { emptyBucket, MODES } = require('./store');
|
||||
|
||||
const pct = (a, b) => (b > 0 ? Math.round((a / b) * 1000) / 10 : null);
|
||||
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
||||
|
||||
function merge(buckets) {
|
||||
const out = emptyBucket();
|
||||
for (const b of buckets) {
|
||||
for (const [k, v] of Object.entries(b)) {
|
||||
if (Array.isArray(v)) v.forEach((x, i) => (out[k][i] += x));
|
||||
else out[k] += v;
|
||||
}
|
||||
}
|
||||
out.netBB = Math.round(out.netBB * 100) / 100;
|
||||
return out;
|
||||
}
|
||||
|
||||
function view(b) {
|
||||
return {
|
||||
hands: b.hands,
|
||||
won: b.won,
|
||||
winRate: pct(b.won, b.hands),
|
||||
vpip: pct(b.vpip, b.preflop),
|
||||
pfr: pct(b.pfr, b.preflop),
|
||||
ats: pct(b.ats, b.atsOpp),
|
||||
atsOpp: b.atsOpp,
|
||||
threeBet: pct(b.threeBet, b.threeBetOpp),
|
||||
threeBetOpp: b.threeBetOpp,
|
||||
sawFlop: b.sawFlop,
|
||||
showdowns: b.showdown,
|
||||
wtsd: pct(b.showdown, b.sawFlop),
|
||||
wsd: pct(b.showdownWon, b.showdown),
|
||||
wwsf: pct(b.wonSawFlop, b.sawFlop),
|
||||
// ストリートごとの降りた割合(そのストリートで判断したハンドのうち)
|
||||
fold: b.folds.map((f, i) => pct(f, b.seen[i])),
|
||||
// アグレッション頻度: 全部の行動(フォールド・チェック・コール・ベット・レイズ)のうちベット・レイズ
|
||||
afq: pct(sum(b.aggressive), sum(b.actions)),
|
||||
afqStreets: b.aggressive.map((a, i) => pct(a, b.actions[i])),
|
||||
netBB: b.netBB,
|
||||
bb100: b.hands > 0 ? Math.round((b.netBB / b.hands) * 1000) / 10 : null,
|
||||
hours: Math.round((b.ms / 3600000) * 10) / 10,
|
||||
};
|
||||
}
|
||||
|
||||
function summarize(data) {
|
||||
const modes = {};
|
||||
for (const m of MODES) modes[m] = view(data.modes[m] || emptyBucket());
|
||||
modes.total = view(merge(MODES.map((m) => data.modes[m]).filter(Boolean)));
|
||||
return { since: data.firstAt, modes };
|
||||
}
|
||||
|
||||
module.exports = { summarize };
|
||||
+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