'use strict'; // 1ハンドの記録(analyze.js の analyzeHand の結果)を、AI に貼り付けて「自分の判断は良かったか」を // 質問できる文章にする。そのとき Hero に見えていた情報(自分の手札・場札・全員の行動とスタック・ // ショーダウンで公開された手札)だけを書き、公開されなかった相手の手札は書かない const { STREETS } = require('./analyze'); const STREET_LABEL = { PREFLOP: 'プリフロップ', FLOP: 'フロップ', TURN: 'ターン', RIVER: 'リバー' }; const BOARD_COUNT = { PREFLOP: 0, FLOP: 3, TURN: 4, RIVER: 5 }; const SUIT = { s: '♠', h: '♥', d: '♦', c: '♣' }; const KIND_LABEL = { fold: 'フォールド', check: 'チェック', call: 'コール', raise: 'レイズ' }; // プリフロップの行動順 const POSITION_ORDER = ['UTG', 'UTG+1', 'UTG+2', 'LJ', 'HJ', 'CO', 'BTN/SB', 'BTN', 'SB', 'BB']; const money = (n) => '$' + Number(n).toLocaleString('en-US'); const round1 = (x) => Math.round(x * 10) / 10; const signedMoney = (n) => (n > 0 ? '+' : n < 0 ? '-' : '±') + money(Math.abs(n)); const signedNum = (n) => (n > 0 ? '+' : '') + n; const cardText = (c) => (typeof c === 'string' && c.length === 2 ? c[0] + (SUIT[c[1]] || c[1]) : '??'); const cardsText = (cs) => cs.map(cardText).join(' '); function buildHandPrompt(h) { const bbOf = (n) => `${round1(n / h.bb)}BB`; const amount = (n) => `${money(n)}(${bbOf(n)})`; const player = (seat) => h.players.find((p) => p.seat === seat); const who = (seat) => { const p = player(seat); if (!p) return `席${seat + 1}`; return seat === h.heroSeat ? `${p.position}(Hero・私)` : `${p.position}(${p.name})`; }; const players = h.players .slice() .sort((a, b) => POSITION_ORDER.indexOf(a.position) - POSITION_ORDER.indexOf(b.position)); const hero = player(h.heroSeat); const out = []; const line = (s = '') => out.push(s); line('以下は、ノーリミット・テキサスホールデムのキャッシュゲームで私(Hero)がプレイした1ハンドの記録です。'); line('私の各アクションが良かったか悪かったかを評価してください。'); line('評価は結果論ではなく、それぞれの判断の時点で私に見えていた情報(自分の手札・場札・相手の行動とスタック)をもとにしてください。'); line(); line('## ゲームの条件'); line(`- ノーリミット・テキサスホールデム、キャッシュゲーム(${h.players.length}人でプレイ。チップは開始スタックで何度でもリバイ可)`); line(`- ブラインド: SB ${money(h.sb)} / BB ${money(h.bb)}(アンティなし・レーキなし)`); line('- 相手はすべて CPU。CPU ごとに打ち方(タイト/ルーズ、攻撃的/受動的、ブラフの頻度)が違い、どの CPU がどんな打ち方かはわからない'); line('- 金額は $ で書き、()内に BB 換算を添える'); line(); line('## 席とスタック(ハンド開始時、プリフロップの行動順)'); for (const p of players) { line(`- ${who(p.seat)}: ${amount(p.start)}${p.seat === h.heroSeat ? ' ← 私' : ''}`); } const others = h.players.filter((p) => p.seat !== h.heroSeat).map((p) => p.start); if (others.length) line(`- 私と一番深い相手との有効スタック: ${amount(Math.min(hero.start, Math.max(...others)))}`); line(); line('## 私の手札'); line(`${cardsText(h.hero.cards)}(${h.hero.key})`); line(); // アクション。ポット・各自の残りスタックをたどりながら書く line('## アクション'); line('(「ベット/レイズ to」はそのストリートでの合計額、「コール」はその行動で出した額)'); const stack = new Map(h.players.map((p) => [p.seat, p.start])); const folded = new Set(); let pot = 0; const post = (seat, blind) => { const p = player(seat); if (!p) return 0; const paid = Math.min(blind, p.start); stack.set(seat, stack.get(seat) - paid); pot += paid; return paid; }; const sbPaid = post(h.sbSeat, h.sb); const bbPaid = post(h.bbSeat, h.bb); const actionsOf = (st) => h.actions.filter((a) => a.street === st); for (const st of STREETS) { const acts = actionsOf(st); const boardShown = h.board.length >= BOARD_COUNT[st]; if (st !== 'PREFLOP' && !boardShown) break; const alive = h.players.filter((p) => !folded.has(p.seat)).length; if (alive <= 1) break; line(); if (st === 'PREFLOP') { line(`### プリフロップ(${h.players.length}人)`); line(`- ${who(h.sbSeat)}: SB ${money(sbPaid)} を支払う`); line(`- ${who(h.bbSeat)}: BB ${money(bbPaid)} を支払う`); } else { const board = h.board.slice(0, BOARD_COUNT[st]); const head = st === 'FLOP' ? cardsText(board) : `${cardText(board[board.length - 1])}(ボード: ${cardsText(board)})`; line(`### ${STREET_LABEL[st]}: ${head}`); line(`- ポット ${amount(pot)}、残り${alive}人`); if (!acts.length) line('- (全員オールインのため行動なし)'); } for (const a of acts) { const mine = a.seat === h.heroSeat && !a.auto; let text; if (a.type === 'FOLD') text = 'フォールド'; else if (a.type === 'CHECK') text = 'チェック'; else if (a.type === 'CALL') text = `コール ${money(a.added)}`; else if (a.type === 'BET') text = `ベット ${money(a.amount)}`; else text = `レイズ to ${money(a.amount)}`; if (a.allIn) text += '(オールイン)'; if (a.auto) text += '(離席中の自動の行動)'; const before = mine ? ` ← 私の判断(行動前のポット ${amount(a.pot)}${a.toCall ? `、コールに ${money(a.toCall)} 必要` : ''}、私の残りスタック ${amount(stack.get(a.seat))})` : ''; line(`- ${who(a.seat)}: ${text}${before}`); stack.set(a.seat, stack.get(a.seat) - a.added); pot += a.added; if (a.type === 'FOLD') folded.add(a.seat); } } line(); line('## 結果'); const shown = h.showdown ? h.players.filter((p) => !p.folded && p.seat !== h.heroSeat) : []; if (shown.length) { line('- ショーダウンで公開された手札:'); for (const p of shown) line(` - ${who(p.seat)}: ${cardsText(p.cards)}`); } else if (!h.showdown) { line('- ショーダウンなし(相手の手札は公開されていない)'); } (h.results || []).forEach((r, i) => { const tag = h.results.length > 1 ? (i === 0 ? 'メインポット' : `サイドポット${i}`) : 'ポット'; const winners = r.winners.map(who).join('・'); line(`- ${tag} ${money(r.amount)} → ${winners}${r.handName ? `(${r.handName})` : ''}`); }); line(`- 私の収支: ${signedMoney(h.hero.net)}(${signedNum(round1(h.hero.net / h.bb))}BB)`); line(); // サイトの自動判定(参考) line('## このサイトの自動判定(参考)'); line('このサイトが簡易的に出した目安です。ソルバー(GTO)ではありません。鵜呑みにせず独立に評価し、おかしいと思う点があれば指摘してください。'); const color = h.hero.colorLabel ? `${h.hero.colorName}(${h.hero.colorLabel})` : h.hero.colorName; line(`- 登録してあるハンドレンジ(6-max のオープンレンジ表)での私の手の分類: ${color}`); for (const j of h.preflop) { line(`- プリフロップ ${j.mark} ${j.score}点: ${j.situation}。推奨 ${KIND_LABEL[j.recommended]}、私は ${KIND_LABEL[j.actual]}(${j.label})`); if (j.equity != null) line(` - 参加者の推定レンジに対する勝率 ${j.equity}%(コールに必要 ${j.need}%)`); } for (const d of h.decisions) { const opt = (o) => (o.kind === 'raise' ? `ベット/レイズ to ${money(o.to)}` : KIND_LABEL[o.kind]); const isActual = (o) => o.kind === d.actual.kind && o.to === d.actual.to; const evs = d.options.map((o) => `${opt(o)} ${signedNum(o.evBB)}BB${isActual(o) ? '(私)' : ''}`).join(' / '); line(`- ${STREET_LABEL[d.street]} ${d.mark} ${d.score}点: 相手の推定レンジに対する勝率 ${d.equity}%${d.need != null ? `(コールに必要 ${d.need}%)` : ''}`); line(` - 選択肢ごとの期待値の目安: ${evs}`); } const ai = h.hero.allIn; if (ai) line(`- オールイン: ${STREET_LABEL[ai.street]}で決着した時点の私の勝率 ${ai.equity}%(期待値 ${money(ai.ev)}、実際に受け取った額 ${money(ai.won)})`); line(); line('## 質問'); line('1. 私の各アクション(「← 私の判断」の行)は良かったですか? それぞれ「良い/許容範囲/悪い」で評価し、理由を説明してください。'); line('2. 悪かった判断があれば、代わりにどうすべきだったか(アクションとサイズ)と、その理由を教えてください。'); line('3. 相手の行動から考えられる相手のレンジと、このハンドから学べるポイントを教えてください。'); return out.join('\n'); } module.exports = { buildHandPrompt };