Switch hand ranges to Poker Skill's 6-max charts with 3bet/call frames

- Replace the Yokosawa ranges with the 6-max opening ranges (UTG, HJ, CO,
  SB, BTN). Colors keep the old order: navy (strong UTG), red UTG, yellow
  HJ, green CO, blue SB, white BTN. SB is narrower than BTN, so blue sits
  one tier above white; the purple-frame category is gone.
- Show the "BTN vs CO open" chart on the range page as frames: light blue
  for 3bet, yellow for call.
- Grade BTN facing an unlimped CO open by those frames in reviews; other
  spots keep the color-tier rule (BB defends down to green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-20 15:21:39 +09:00
1 parent 680aa20d30
commit 3ddb0996e0
8 files changed
+187 -86

No files matched your search

+5 -4
View File
@@ -64,17 +64,18 @@ http://localhost:4671 を開く。
## 管理者(ハンドレンジ学習・レビュー付きCPU戦) ## 管理者(ハンドレンジ学習・レビュー付きCPU戦)
`ADMIN_KEY` を入力して入室すると管理者になり、ロビーに「♠ ハンドレンジ学習」(`/range`:レンジ表と色・分類クイズ)と「📝 CPU戦のレビュー」(`/review`)が表示され、テーブル作成で「レビュー付きCPU戦」を選べる。ロビーの各テーブルには「削除」ボタンが出て、どのテーブルでも削除できる(着席者・観戦者には「管理者がテーブルを削除しました」と表示される)。管理者は通常のプレイもそのままできる。 `ADMIN_KEY` を入力して入室すると管理者になり、ロビーに「♠ ハンドレンジ学習」(`/range`:レンジ表と色・分類クイズ。レンジ表には CO のオープンを BTN で受けたときの対応も枠で示す。3bet は水色、コールは黄)と「📝 CPU戦のレビュー」(`/review`)が表示され、テーブル作成で「レビュー付きCPU戦」を選べる。ロビーの各テーブルには「削除」ボタンが出て、どのテーブルでも削除できる(着席者・観戦者には「管理者がテーブルを削除しました」と表示される)。管理者は通常のプレイもそのままできる。
### レビュー付きCPU戦 ### レビュー付きCPU戦
ハンドが終わるたびにレビューが開き、「次のハンドへ」を押すまで次のハンドは始まらない。 ハンドが終わるたびにレビューが開き、「次のハンドへ」を押すまで次のハンドは始まらない。
- **ハンドレンジ**: 配られた手が登録してあるハンドレンジ(ヨコサワハンドレンジ。`admin/range-data.js`。レンジ学習ページと共用)の何色か、そのとおりに行動できたかを判定する。 - **ハンドレンジ**: 配られた手が登録してあるハンドレンジ(Poker Skill の 6-max オープンレンジ。`admin/range-data.js`。レンジ学習ページと共用)の何色か、そのとおりに行動できたかを判定する。
- 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)の色以上ならレイズ、それより弱ければフォールド。8人以上は赤(紺も可)、6〜7人は黄、4〜5人は緑、3人は青、2人は白、1人(SB)は紫枠。紫枠は白の一つ下の段階として扱う。 - 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)の色以上ならレイズ、それより弱ければフォールド。6-max の席に当てはめて、5人以上は UTG の赤(紺も可)、4人は HJ の黄、3人は CO の緑、2人は BTN の白、1人は SB の青。SB のレンジは BTN より狭いので、青は白の一つ上の段階として扱う。
- 前にコール・レイズした人がいる: その人のレンジを、その人の後ろの人数に対応する色と推定し、自分の手がその一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド。紺はどんな相手にもコールまではできる。 - 前にコール・レイズした人がいる: その人のレンジを、その人の後ろの人数に対応する色と推定し、自分の手がその一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド。紺はどんな相手にもコールまではできる。
- 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ。 - 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ。
- BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。青以上でコール(CO のオープンには白、BTN には紫枠以上。相手の色の一つ下まで)、相手の三つ上ならレイズ。 - BTN で CO のオープン(ほかに参加者なし)に直面: レンジ表の枠のとおり。水色枠は 3bet、黄枠はコール、枠なしはフォールド。
- BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。緑以上でコール(CO のオープンには青、BTN・SB には白以上。相手の色の一つ下まで)、相手の三つ上ならレイズ。
- 2人目以降に参加した人のレンジも同じ決まりでコール・レイズできる一番弱い色と推定する(ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。 - 2人目以降に参加した人のレンジも同じ決まりでコール・レイズできる一番弱い色と推定する(ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。
- チェックできるときのフォールド・コールの推奨はチェックになる。判定は ◎(100点)・△(消極的 50点/攻めすぎ 40点)・✕(参加しすぎ・降りすぎ 0点)。 - チェックできるときのフォールド・コールの推奨はチェックになる。判定は ◎(100点)・△(消極的 50点/攻めすぎ 40点)・✕(参加しすぎ・降りすぎ 0点)。
- **フロップ以降の点数**: 相手のレンジ(上の色の推定から、ベット・レイズ・コールのたびに、いまの場札で強い手に絞る)に対する勝率をモンテカルロ法で求め、選択肢(フォールド・チェック・コール・標準サイズと実際のサイズのベット/レイズ)ごとの期待値を見積もる。一番良い選択肢との差(失った期待値)がポットの3%までは満点で、それを超えると1%ごとに2.5点減る(◎90〜 ○70〜 △40〜 ✕)。ソルバー(GTO)ではなく目安。 - **フロップ以降の点数**: 相手のレンジ(上の色の推定から、ベット・レイズ・コールのたびに、いまの場札で強い手に絞る)に対する勝率をモンテカルロ法で求め、選択肢(フォールド・チェック・コール・標準サイズと実際のサイズのベット/レイズ)ごとの期待値を見積もる。一番良い選択肢との差(失った期待値)がポットの3%までは満点で、それを超えると1%ごとに2.5点減る(◎90〜 ○70〜 △40〜 ✕)。ソルバー(GTO)ではなく目安。
+52 -31
View File
@@ -7,35 +7,51 @@
const RANKS = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2']; const RANKS = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2'];
// 強い順。tier が大きいほど強い(レビューでは「一つ上」「二つ上」をこの段階で数える) // 6-max のオープンレンジ(Poker Skill のプリフロップレンジ表、スタック約100BB・全員が降りて回ってきた場面)。
// 人数は「自分を含めず、まだ行動していない人の数」(自分より前に誰もコール・レイズしていないとき)。 // 強い順。tier が大きいほど強い(レビューでは「一つ上」「二つ上」をこの段階で数える)。
// 紫枠は後ろが1人(SB で全員が降りて回ってきた)ときだけオープンできる手として、白の一つ下に置く // 各ポジションのレンジは UTG ⊂ HJ ⊂ CO ⊂ SB ⊂ BTN と入れ子になっているので、手ごとに
// 「オープンできる一番狭いポジション」の色を付ける。SB は BTN より狭いので BTN の一つ上に置く。
// 紺は UTG のうち特に強い手
const CATEGORIES = [ const CATEGORIES = [
{ id: 'navy', name: '紺', label: '8人(強)', tier: 7 }, { id: 'navy', name: '紺', label: 'UTG(強)', tier: 6 },
{ id: 'red', name: '赤', label: '8人(弱)', tier: 6 }, { id: 'red', name: '赤', label: 'UTG', tier: 5 },
{ id: 'yellow', name: '黄', label: '6〜7人', tier: 5 }, { id: 'yellow', name: '黄', label: 'HJ', tier: 4 },
{ id: 'green', name: '緑', label: '4〜5人', tier: 4 }, { id: 'green', name: '緑', label: 'CO', tier: 3 },
{ id: 'blue', name: '青', label: '3人', tier: 3 }, { id: 'blue', name: '青', label: 'SB', tier: 2 },
{ id: 'white', name: '白', label: '2人', tier: 2 }, { id: 'white', name: '白', label: 'BTN', tier: 1 },
{ id: 'purple', name: '紫枠', label: '紫枠', tier: 1 },
{ id: 'gray', name: 'グレー', label: 'フォールド', tier: 0 }, { id: 'gray', name: 'グレー', label: 'フォールド', tier: 0 },
]; ];
// 全169ハンドの分類(載っていないハンドはフォールド)。紫枠はクイズ上の独立した回答 // 全169ハンドの分類(載っていないハンドはフォールド)
const RANGE = { const RANGE = {
AA: 'navy', AKs: 'navy', AQs: 'red', AJs: 'red', ATs: 'red', A9s: 'green', A8s: 'green', A7s: 'green', A6s: 'green', A5s: 'green', A4s: 'green', A3s: 'green', A2s: 'green', AA: 'navy', AKs: 'navy', AQs: 'red', AJs: 'red', ATs: 'red', A9s: 'yellow', A8s: 'yellow', A7s: 'yellow', A6s: 'yellow', A5s: 'red', A4s: 'red', A3s: 'red', A2s: 'red',
AKo: 'navy', KK: 'navy', KQs: 'red', KJs: 'yellow', KTs: 'green', K9s: 'green', K8s: 'white', K7s: 'white', K6s: 'white', K5s: 'white', K4s: 'white', K3s: 'white', K2s: 'white', AKo: 'navy', KK: 'navy', KQs: 'red', KJs: 'red', KTs: 'red', K9s: 'yellow', K8s: 'green', K7s: 'green', K6s: 'green', K5s: 'green', K4s: 'blue', K3s: 'white', K2s: 'white',
AQo: 'red', KQo: 'yellow', QQ: 'navy', QJs: 'yellow', QTs: 'green', Q9s: 'blue', Q8s: 'white', Q7s: 'white', Q6s: 'white', Q5s: 'purple', Q4s: 'purple', Q3s: 'purple', Q2s: 'purple', AQo: 'red', KQo: 'red', QQ: 'navy', QJs: 'red', QTs: 'red', Q9s: 'yellow', Q8s: 'green', Q7s: 'blue', Q6s: 'blue', Q5s: 'white', Q4s: 'white',
AJo: 'yellow', KJo: 'green', QJo: 'blue', JJ: 'red', JTs: 'yellow', J9s: 'blue', J8s: 'white', J7s: 'white', J6s: 'purple', AJo: 'red', KJo: 'yellow', QJo: 'green', JJ: 'red', JTs: 'red', J9s: 'yellow', J8s: 'green', J7s: 'blue', J6s: 'white',
ATo: 'green', KTo: 'blue', QTo: 'white', JTo: 'blue', TT: 'red', T9s: 'green', T8s: 'blue', T7s: 'purple', ATo: 'yellow', KTo: 'green', QTo: 'blue', JTo: 'blue', TT: 'red', T9s: 'red', T8s: 'yellow', T7s: 'blue', T6s: 'white',
A9o: 'blue', K9o: 'white', Q9o: 'white', J9o: 'white', T9o: 'white', 99: 'red', '98s': 'blue', '97s': 'white', '96s': 'purple', A9o: 'green', K9o: 'blue', Q9o: 'white', J9o: 'white', T9o: 'white', 99: 'red', '98s': 'red', '97s': 'green', '96s': 'blue', '95s': 'white',
A8o: 'white', '98o': 'purple', 88: 'yellow', '87s': 'white', '86s': 'purple', A8o: 'green', K8o: 'white', '98o': 'white', 88: 'red', '87s': 'yellow', '86s': 'blue', '85s': 'white',
A7o: 'white', 77: 'yellow', '76s': 'white', '75s': 'purple', A7o: 'blue', 77: 'red', '76s': 'green', '75s': 'white',
A6o: 'purple', 66: 'green', '65s': 'white', '64s': 'purple', A6o: 'blue', 66: 'red', '65s': 'green', '64s': 'white',
55: 'green', '54s': 'purple', A5o: 'blue', 55: 'red', '54s': 'green',
44: 'blue', A4o: 'white', 44: 'red',
33: 'blue', A3o: 'white', 33: 'red',
22: 'blue', A2o: 'white', 22: 'red',
};
// レイズを受けたときの対応(同じレンジ表の「CO のオープンに対する BTN の対応」)。レンジ表では枠で示す。
// 載っていないハンドはフォールド
const FACING_ACTIONS = [
{ id: '3bet', name: '水色枠', label: '3bet' },
{ id: 'call', name: '黄枠', label: 'コール' },
];
const FACING = {
AA: '3bet', KK: '3bet', QQ: '3bet', JJ: '3bet', AKs: '3bet', AKo: '3bet', A5s: '3bet', A4s: '3bet', A3s: '3bet', A2s: '3bet',
KQs: '3bet', KJs: '3bet', '76s': '3bet', '65s': '3bet', '54s': '3bet',
TT: 'call', 99: 'call', 88: 'call', 77: 'call', 66: 'call', 55: 'call', 44: 'call', 33: 'call', 22: 'call',
AQs: 'call', AJs: 'call', ATs: 'call', A9s: 'call', A8s: 'call', A7s: 'call', A6s: 'call', KTs: 'call', K9s: 'call',
QJs: 'call', QTs: 'call', Q9s: 'call', JTs: 'call', J9s: 'call', T9s: 'call', T8s: 'call', '98s': 'call', '87s': 'call',
AQo: 'call', AJo: 'call', KQo: 'call', KJo: 'call', QJo: 'call',
}; };
const BY_ID = new Map(CATEGORIES.map((c) => [c.id, c])); const BY_ID = new Map(CATEGORIES.map((c) => [c.id, c]));
@@ -47,6 +63,8 @@
const labelOf = (id) => category(id).label; const labelOf = (id) => category(id).label;
const tierOf = (hand) => category(categoryOf(hand)).tier; const tierOf = (hand) => category(categoryOf(hand)).tier;
const categoryOfTier = (tier) => CATEGORIES.find((c) => c.tier === tier) || BY_ID.get('gray'); const categoryOfTier = (tier) => CATEGORIES.find((c) => c.tier === tier) || BY_ID.get('gray');
const facingOf = (hand) => FACING[hand] || null; // '3bet' / 'call' / null(フォールド)
const facingAction = (id) => FACING_ACTIONS.find((a) => a.id === id) || null;
// ["As", "Kd"] → "AKo" // ["As", "Kd"] → "AKo"
function handKey(cards) { function handKey(cards) {
@@ -60,16 +78,19 @@
} }
// 自分より前に誰もコール・レイズしていないとき、後ろの人数(自分を含めず、まだ行動していない人)で // 自分より前に誰もコール・レイズしていないとき、後ろの人数(自分を含めず、まだ行動していない人)で
// 参加してよい一番弱い段階。8人以上は赤(紺は赤の一つ上)。0人(全員降りてBBに回った)は無し // 参加してよい一番弱い段階。6-max の席に当てはめて、5人以上は UTG の赤(紺は赤の一つ上)、4人は HJ の黄、
// 3人は CO の緑、2人は BTN の白、1人は SB の青。0人(全員降りてBBに回った)は無し
function requiredTier(behind) { function requiredTier(behind) {
if (behind >= 8) return 6; if (behind >= 5) return 5;
if (behind >= 6) return 5; if (behind === 4) return 4;
if (behind >= 4) return 4;
if (behind === 3) return 3; if (behind === 3) return 3;
if (behind === 2) return 2; if (behind === 2) return 1;
if (behind === 1) return 1; if (behind === 1) return 2;
return 0; return 0;
} }
return { RANKS, CATEGORIES, RANGE, handAt, handKey, categoryOf, category, labelOf, tierOf, categoryOfTier, requiredTier }; return {
RANKS, CATEGORIES, RANGE, FACING_ACTIONS, FACING,
handAt, handKey, categoryOf, category, labelOf, tierOf, categoryOfTier, requiredTier, facingOf, facingAction,
};
}); });
+7 -3
View File
@@ -25,7 +25,12 @@
.cat-green { --c: #4e8051; --fg: #101910; } .cat-green { --c: #4e8051; --fg: #101910; }
.cat-blue { --c: #6aa5c9; --fg: #14191f; } .cat-blue { --c: #6aa5c9; --fg: #14191f; }
.cat-white { --c: #f7f7f7; --fg: #222; } .cat-white { --c: #f7f7f7; --fg: #222; }
.cat-gray, .cat-purple { --c: #929292; --fg: #222; } .cat-gray { --c: #929292; --fg: #222; }
/* レイズを受けたときの対応の枠(3bet は水色、コールは黄)。下地の色と紛れないよう内側に暗い線を添える */
.facing-3bet { --frame: #3fd4ff; }
.facing-call { --frame: #ffe23a; }
.range-cell.facing-3bet, .range-cell.facing-call { box-shadow: inset 0 0 0 3px var(--frame), inset 0 0 0 4px rgba(0, 0, 0, 0.45); }
/* 13×13 の表 */ /* 13×13 の表 */
.range-matrix { display: grid; grid-template-columns: repeat(13, 1fr); gap: 2px; } .range-matrix { display: grid; grid-template-columns: repeat(13, 1fr); gap: 2px; }
@@ -44,14 +49,13 @@
cursor: pointer; cursor: pointer;
position: relative; position: relative;
} }
.range-cell.cat-purple { box-shadow: inset 0 0 0 2px #c12cff; }
.range-cell.selected { outline: 2px solid var(--gold); outline-offset: 1px; z-index: 1; } .range-cell.selected { outline: 2px solid var(--gold); outline-offset: 1px; z-index: 1; }
/* 凡例 */ /* 凡例 */
.range-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 12px; font-size: 12px; color: var(--muted); } .range-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 12px; font-size: 12px; color: var(--muted); }
.range-legend span { display: inline-flex; align-items: center; gap: 6px; } .range-legend span { display: inline-flex; align-items: center; gap: 6px; }
.swatch-dot { width: 14px; height: 14px; border-radius: 4px; background: var(--c); flex: none; } .swatch-dot { width: 14px; height: 14px; border-radius: 4px; background: var(--c); flex: none; }
.swatch-dot.cat-purple { box-shadow: inset 0 0 0 2px #c12cff; } .swatch-dot.swatch-frame { background: transparent; box-shadow: inset 0 0 0 3px var(--frame); }
/* 選択したハンド */ /* 選択したハンド */
.sel-hand { font-size: 28px; font-weight: 900; line-height: 1.1; } .sel-hand { font-size: 28px; font-weight: 900; line-height: 1.1; }
+25 -5
View File
@@ -3,7 +3,7 @@
'use strict'; 'use strict';
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const { RANKS, CATEGORIES, handAt, categoryOf, labelOf } = window.HandRange; const { RANKS, CATEGORIES, FACING_ACTIONS, handAt, categoryOf, labelOf, facingOf, facingAction } = window.HandRange;
function swatch(id) { function swatch(id) {
const s = document.createElement('span'); const s = document.createElement('span');
@@ -11,6 +11,13 @@
return s; return s;
} }
// レイズを受けたときの対応の枠(3bet は水色、コールは黄)
function frameSwatch(id) {
const s = document.createElement('span');
s.className = `swatch-dot swatch-frame facing-${id}`;
return s;
}
// ---------- タブ ---------- // ---------- タブ ----------
function showView(name) { function showView(name) {
$('view-range').hidden = name !== 'range'; $('view-range').hidden = name !== 'range';
@@ -28,6 +35,11 @@
item.append(swatch(c.id), c.label); item.append(swatch(c.id), c.label);
$('legend').append(item); $('legend').append(item);
} }
for (const a of FACING_ACTIONS) {
const item = document.createElement('span');
item.append(frameSwatch(a.id), `レイズを受けたら${a.label}`);
$('legend').append(item);
}
for (let r = 0; r < 13; r++) { for (let r = 0; r < 13; r++) {
for (let c = 0; c < 13; c++) { for (let c = 0; c < 13; c++) {
@@ -35,9 +47,10 @@
const cat = categoryOf(hand); const cat = categoryOf(hand);
const b = document.createElement('button'); const b = document.createElement('button');
b.type = 'button'; b.type = 'button';
b.className = `range-cell cat-${cat}`; const facing = facingOf(hand);
b.className = `range-cell cat-${cat}${facing ? ` facing-${facing}` : ''}`;
b.textContent = hand; b.textContent = hand;
b.title = `${hand}:${labelOf(cat)}`; b.title = `${hand}:${labelOf(cat)}${facing ? `/レイズを受けたら${facingAction(facing).label}` : ''}`;
b.addEventListener('click', () => select(b, hand, cat)); b.addEventListener('click', () => select(b, hand, cat));
$('matrix').append(b); $('matrix').append(b);
} }
@@ -54,10 +67,17 @@
const label = document.createElement('div'); const label = document.createElement('div');
label.className = 'sel-label'; label.className = 'sel-label';
label.append(swatch(cat), labelOf(cat)); label.append(swatch(cat), labelOf(cat));
const facing = facingOf(hand);
const vs = document.createElement('div');
vs.className = 'sel-label';
if (facing) vs.append(frameSwatch(facing), `レイズを受けたら${facingAction(facing).label}`);
else vs.append('レイズを受けたらフォールド');
const note = document.createElement('div'); const note = document.createElement('div');
note.className = 'hint'; note.className = 'hint';
note.textContent = cat === 'purple' ? '灰色+紫枠。クイズの答えは「紫枠」です。' : 'このハンドの分類です。'; note.textContent =
box.append(name, label, note); (cat === 'gray' ? 'どのポジションからもオープンしない手です。' : `${labelOf(cat)} からオープンできる一番狭い分類です。`) +
'枠は CO のオープンを BTN で受けたときの対応です。';
box.append(name, label, vs, note);
} }
$('start-from-range').addEventListener('click', () => { $('start-from-range').addEventListener('click', () => {
+1 -2
View File
@@ -833,11 +833,10 @@ label.switch { margin: 0; display: inline-block; }
.rv-foot { margin-top: 14px; font-size: 13px; color: var(--muted); } .rv-foot { margin-top: 14px; font-size: 13px; color: var(--muted); }
/* 登録してあるハンドレンジの色(レンジ表のデータとしての色なのでサイトの配色とは別) */ /* 登録してあるハンドレンジの色(レンジ表のデータとしての色なのでサイトの配色とは別) */
.rv-color { display: inline-block; font-size: 13px; font-weight: 800; padding: 2px 8px; border-radius: 6px; vertical-align: middle; background: var(--c); color: var(--fg); } .rv-color { display: inline-block; font-size: 13px; font-weight: 800; padding: 2px 8px; border-radius: 6px; vertical-align: middle; background: var(--c); color: var(--fg); }
.rv-color.cat-purple { box-shadow: inset 0 0 0 2px #9b59d0; }
.rv-color.cat-navy { --c: #20365d; --fg: #fff; box-shadow: inset 0 0 0 1px #4d6a9c; } .rv-color.cat-navy { --c: #20365d; --fg: #fff; box-shadow: inset 0 0 0 1px #4d6a9c; }
.rv-color.cat-red { --c: #9e342e; --fg: #fff; } .rv-color.cat-red { --c: #9e342e; --fg: #fff; }
.rv-color.cat-yellow { --c: #ddb356; --fg: #14191f; } .rv-color.cat-yellow { --c: #ddb356; --fg: #14191f; }
.rv-color.cat-green { --c: #4e8051; --fg: #101910; } .rv-color.cat-green { --c: #4e8051; --fg: #101910; }
.rv-color.cat-blue { --c: #6aa5c9; --fg: #14191f; } .rv-color.cat-blue { --c: #6aa5c9; --fg: #14191f; }
.rv-color.cat-white { --c: #f7f7f7; --fg: #222; } .rv-color.cat-white { --c: #f7f7f7; --fg: #222; }
.rv-color.cat-gray, .rv-color.cat-purple { --c: #929292; --fg: #222; } .rv-color.cat-gray { --c: #929292; --fg: #222; }
+1 -1
View File
@@ -11,7 +11,7 @@ const { codes, rangeOf, narrow, rangeEquity, showdownShare } = require('./equity
const STREETS = ['PREFLOP', 'FLOP', 'TURN', 'RIVER']; const STREETS = ['PREFLOP', 'FLOP', 'TURN', 'RIVER'];
// レビューで色に添える説明(クイズ用の分類名がそのままでは分かりにくいもの) // レビューで色に添える説明(クイズ用の分類名がそのままでは分かりにくいもの)
const COLOR_LABEL = { purple: '1人(SBのみ)', gray: 'レンジ外' }; const COLOR_LABEL = { gray: 'レンジ外' };
// 失った期待値がポット(コール後)のこの割合までは誤差として満点にする // 失った期待値がポット(コール後)のこの割合までは誤差として満点にする
const TOLERANCE = 0.03; const TOLERANCE = 0.03;
+21 -8
View File
@@ -1,19 +1,21 @@
'use strict'; 'use strict';
// プリフロップの判定: 配られた手が登録してあるハンドレンジ(ヨコサワハンドレンジ)の何色で、そのとおりに行動できたか。 // プリフロップの判定: 配られた手が登録してあるハンドレンジ(6-max のオープンレンジ)の何色で、そのとおりに行動できたか。
// 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)に対応する色 // 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)に対応する色
// 以上ならレイズ(オープン)、それより弱ければフォールド // 以上ならレイズ(オープン)、それより弱ければフォールド
// 自分より前にコール・レイズした人がいる: その人のレンジを色で推定し(その人の後ろの人数に対応する色)、 // 自分より前にコール・レイズした人がいる: その人のレンジを色で推定し(その人の後ろの人数に対応する色)、
// 自分の手がその色の一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド // 自分の手がその色の一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド
// 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ // 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ
// BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。青以上でコール // BTN で CO のオープン(ほかに参加者なし)に直面: レンジ表の枠のとおり。水色枠は 3bet、黄枠はコール、
// (CO のオープンなら白、BTN なら紫枠以上。相手の色の一つ下まで)、三つ上ならレイズ // 枠なしはフォールド
// BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。緑(CO)以上でコール
// (CO のオープンなら青、BTN・SB なら白以上。相手の色の一つ下まで)、三つ上ならレイズ
// 2人目以降に参加した人のレンジも同じ決まりで、コール・レイズできる一番弱い色と推定する // 2人目以降に参加した人のレンジも同じ決まりで、コール・レイズできる一番弱い色と推定する
// (ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。 // (ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。
const HandRange = require('../../admin/range-data'); const HandRange = require('../../admin/range-data');
const TOP_TIER = 7; // 紺 const TOP_TIER = 6; // 紺
const BB_CALL_TIER = 3; // 青: BB はどこからのオープンにもこの色以上でコールできる const BB_CALL_TIER = 3; // 緑: BB はどこからのオープンにもこの色以上でコールできる
const BB_DEFENSE_MAX_OPEN = 4; // BB で広く守るのはオープンがこの BB 数まで const BB_DEFENSE_MAX_OPEN = 4; // BB で広く守るのはオープンがこの BB 数まで
const ACTION_KIND = { FOLD: 'fold', CHECK: 'check', CALL: 'call', BET: 'raise', RAISE: 'raise' }; const ACTION_KIND = { FOLD: 'fold', CHECK: 'check', CALL: 'call', BET: 'raise', RAISE: 'raise' };
@@ -46,7 +48,8 @@ const tierName = (tier) => (tier > TOP_TIER ? 'なし' : HandRange.categoryOfTie
// 前に参加した人がいるときに、コール・レイズできる一番弱い色の段階。rule は // 前に参加した人がいるときに、コール・レイズできる一番弱い色の段階。rule は
// 'normal': 一つ上でコール、二つ上でレイズ(紺は紺に対してもコールできる) // 'normal': 一つ上でコール、二つ上でレイズ(紺は紺に対してもコールできる)
// 'reraised': 自分のレイズにリレイズされた。同じ色でコール、二つ上でレイズ // 'reraised': 自分のレイズにリレイズされた。同じ色でコール、二つ上でレイズ
// 'bb': BB でオープンに直面。相手の色の一つ下(青まで)でコール、三つ上でレイズ(紫枠より下は参加しない) // 'bb': BB でオープンに直面。相手の色の一つ下(緑まで)でコール、三つ上でレイズ(白より下は参加しない)
// 'vsCO': BTN で CO のオープンに直面。判定はレンジ表の枠で行い、ここでの段階は推定レンジにだけ使う('normal' と同じ)
function thresholds(rule, ref) { function thresholds(rule, ref) {
if (rule === 'reraised') return { call: Math.min(TOP_TIER, ref), raise: ref + 2 }; if (rule === 'reraised') return { call: Math.min(TOP_TIER, ref), raise: ref + 2 };
if (rule === 'bb') return { call: Math.max(1, Math.min(BB_CALL_TIER, ref - 1)), raise: ref + 3 }; if (rule === 'bb') return { call: Math.max(1, Math.min(BB_CALL_TIER, ref - 1)), raise: ref + 3 };
@@ -58,6 +61,8 @@ function recommend(tier, situation, entry) {
let rec; let rec;
if (situation.kind === 'open') { if (situation.kind === 'open') {
rec = situation.behind === 0 ? 'check' : tier >= situation.required ? 'raise' : 'fold'; rec = situation.behind === 0 ? 'check' : tier >= situation.required ? 'raise' : 'fold';
} else if (situation.rule === 'vsCO') {
rec = situation.facing === '3bet' ? 'raise' : situation.facing === 'call' ? 'call' : 'fold';
} else { } else {
rec = tier >= situation.raise ? 'raise' : tier >= situation.call ? 'call' : 'fold'; rec = tier >= situation.raise ? 'raise' : tier >= situation.call ? 'call' : 'fold';
} }
@@ -76,13 +81,15 @@ function verdictOf(rec, actual) {
// プリフロップの各行動を順に見て、hero の判断ごとに判定する。あわせて各席の推定レンジ(色の段階)を返す // プリフロップの各行動を順に見て、hero の判断ごとに判定する。あわせて各席の推定レンジ(色の段階)を返す
function judgePreflop(hand, hero) { function judgePreflop(hand, hero) {
const tier = HandRange.tierOf(HandRange.handKey(hand.players.get(hero).cards)); const key = HandRange.handKey(hand.players.get(hero).cards);
const tier = HandRange.tierOf(key);
const order = hand.preflopOrder; const order = hand.preflopOrder;
const acted = new Set(); const acted = new Set();
const estimated = new Map(); // 席 → 推定レンジの一番弱い色の段階 const estimated = new Map(); // 席 → 推定レンジの一番弱い色の段階
let maxTier = null; let maxTier = null;
let maxSeat = null; let maxSeat = null;
let raises = 0; let raises = 0;
let entrants = 0; // プリフロップでコール・レイズした回数
const raisers = new Set(); // プリフロップでレイズした席 const raisers = new Set(); // プリフロップでレイズした席
const judgments = []; const judgments = [];
@@ -92,6 +99,7 @@ function judgePreflop(hand, hero) {
// 前に参加した人がいるときの決まり(thresholds を参照) // 前に参加した人がいるときの決まり(thresholds を参照)
const ruleOf = (e) => { const ruleOf = (e) => {
if (raisers.has(e.seat)) return 'reraised'; if (raisers.has(e.seat)) return 'reraised';
if (entrants === 1 && raises === 1 && positionOf(hand, e.seat) === 'BTN' && positionOf(hand, maxSeat) === 'CO') return 'vsCO';
if (e.seat === hand.bbSeat && raises === 1 && e.currentBet <= BB_DEFENSE_MAX_OPEN * hand.bb) return 'bb'; if (e.seat === hand.bbSeat && raises === 1 && e.currentBet <= BB_DEFENSE_MAX_OPEN * hand.bb) return 'bb';
return 'normal'; return 'normal';
}; };
@@ -105,7 +113,7 @@ function judgePreflop(hand, hero) {
const situation = const situation =
maxTier == null maxTier == null
? { kind: 'open', behind: behindOf(hero), required: HandRange.requiredTier(behindOf(hero)) } ? { kind: 'open', behind: behindOf(hero), required: HandRange.requiredTier(behindOf(hero)) }
: { kind: 'facing', rule, refTier: maxTier, refSeat: maxSeat, ...limits }; : { kind: 'facing', rule, refTier: maxTier, refSeat: maxSeat, ...limits, ...(rule === 'vsCO' && { facing: HandRange.facingOf(key) }) };
const rec = recommend(tier, situation, e); const rec = recommend(tier, situation, e);
const verdict = verdictOf(rec, kind); const verdict = verdictOf(rec, kind);
judgments.push({ index, situation, recommended: rec, actual: kind, verdict, ...VERDICTS[verdict] }); judgments.push({ index, situation, recommended: rec, actual: kind, verdict, ...VERDICTS[verdict] });
@@ -122,6 +130,7 @@ function judgePreflop(hand, hero) {
maxSeat = e.seat; maxSeat = e.seat;
} }
} }
if (kind === 'call' || kind === 'raise') entrants++;
if (kind === 'raise') { if (kind === 'raise') {
raises++; raises++;
raisers.add(e.seat); raisers.add(e.seat);
@@ -138,6 +147,10 @@ function describeSituation(situation, nameOf) {
return `前に参加者なし・後ろに${situation.behind}人 → ${tierName(situation.required)}以上でオープン`; return `前に参加者なし・後ろに${situation.behind}人 → ${tierName(situation.required)}以上でオープン`;
} }
const who = nameOf(situation.refSeat); const who = nameOf(situation.refSeat);
if (situation.rule === 'vsCO') {
const action = HandRange.facingAction(situation.facing);
return `BTNで ${who}(CO)のオープンに直面 → レンジ表の枠(水色枠は3bet、黄枠はコール)。この手は${action ? `${action.name}で${action.label}` : '枠なしでフォールド'}`;
}
const ref = tierName(situation.refTier); const ref = tierName(situation.refTier);
const head = const head =
situation.rule === 'reraised' situation.rule === 'reraised'
+75 -32
View File
@@ -11,7 +11,7 @@ const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const HandRange = require('../admin/range-data'); const HandRange = require('../admin/range-data');
const { Table } = require('../server/tables'); const { Table } = require('../server/tables');
const { judgePreflop, positionOf } = require('../server/review/preflop'); const { judgePreflop, positionOf, describeSituation } = require('../server/review/preflop');
const { analyzeHand } = require('../server/review/analyze'); const { analyzeHand } = require('../server/review/analyze');
const { showdownShare } = require('../server/review/equity'); const { showdownShare } = require('../server/review/equity');
const { summarize } = require('../server/review/summary'); const { summarize } = require('../server/review/summary');
@@ -54,9 +54,20 @@ test('ハンドレンジ: 手札の表記と、後ろの人数で必要な色',
assert.equal(HandRange.handKey(['9c', '9d']), '99'); assert.equal(HandRange.handKey(['9c', '9d']), '99');
assert.equal(HandRange.categoryOf('AA'), 'navy'); assert.equal(HandRange.categoryOf('AA'), 'navy');
assert.equal(HandRange.categoryOf('72o'), 'gray'); assert.equal(HandRange.categoryOf('72o'), 'gray');
assert.equal(HandRange.tierOf('Q5s'), 1); // 紫枠は白の一つ下 assert.equal(HandRange.tierOf('Q5s'), 1); // BTN だけで開く手は白(SB の青より下)
// レイズを受けたときの対応(枠)
assert.equal(HandRange.facingOf('A5s'), '3bet');
assert.equal(HandRange.facingOf('TT'), 'call');
assert.equal(HandRange.facingOf('ATo'), null);
const share = (id) => {
const combos = (h) => (h.length === 2 ? 6 : h[2] === 's' ? 4 : 12);
const n = Object.keys(HandRange.FACING).filter((h) => HandRange.facingOf(h) === id).reduce((a, h) => a + combos(h), 0);
return Math.round((n / 1326) * 1000) / 10;
};
assert.equal(share('3bet'), 5.7);
assert.equal(share('call'), 14.0);
const req = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((b) => HandRange.categoryOfTier(HandRange.requiredTier(b)).id); const req = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((b) => HandRange.categoryOfTier(HandRange.requiredTier(b)).id);
assert.deepEqual(req, ['gray', 'purple', 'white', 'blue', 'green', 'green', 'yellow', 'yellow', 'red']); assert.deepEqual(req, ['gray', 'blue', 'white', 'green', 'yellow', 'red', 'red', 'red', 'red']);
}); });
test('ポジションの呼び方', () => { test('ポジションの呼び方', () => {
@@ -71,9 +82,9 @@ test('ポジションの呼び方', () => {
test('プリフロップ: 前に参加者がいなければ、後ろの人数の色以上でオープン', () => { test('プリフロップ: 前に参加者がいなければ、後ろの人数の色以上でオープン', () => {
// 6人: UTG〜CO が降りて BTN(後ろに2人 → 白以上) // 6人: UTG〜CO が降りて BTN(後ろに2人 → 白以上)
for (const [cards, action, verdict] of [ for (const [cards, action, verdict] of [
[['Kh', '8h'], 'raise', 'good'], // 白でレイズ [['Kh', '3h'], 'raise', 'good'], // 白でレイズ
[['Qh', '5h'], 'raise', 'loose'], // 紫枠は BTN ではオープンできない [['Qh', '3h'], 'raise', 'loose'], // レンジ外
[['Qh', '5h'], 'fold', 'good'], [['Qh', '3h'], 'fold', 'good'],
[['Ah', 'Kd'], 'call', 'passive'], // レイズできる手でコール [['Ah', 'Kd'], 'call', 'passive'], // レイズできる手でコール
[['Ah', 'Kd'], 'fold', 'tight'], [['Ah', 'Kd'], 'fold', 'tight'],
]) { ]) {
@@ -89,44 +100,47 @@ test('プリフロップ: 前に参加者がいなければ、後ろの人数の
assert.equal(j.verdict, verdict, `${cards} ${action}`); assert.equal(j.verdict, verdict, `${cards} ${action}`);
} }
// SB に全員降りて回ってきた(後ろに1人 → 紫枠以上) // SB に全員降りて回ってきた(後ろに1人 → 青以上。SB は BTN より狭い)
const t = makeTable(4); const t = makeTable(4);
act(t, 'fold'); act(t, 'fold');
act(t, 'fold'); act(t, 'fold');
const sb = t.hand.toAct; const sb = t.hand.toAct;
act(t, 'raise', 30); act(t, 'raise', 30);
give(t, sb, ['Qs', '4s']); give(t, sb, ['Ks', '4s']);
const j = judgePreflop(t.hand, sb).judgments[0]; const j = judgePreflop(t.hand, sb).judgments[0];
assert.equal(j.situation.behind, 1); assert.equal(j.situation.behind, 1);
assert.equal(j.verdict, 'good'); assert.equal(j.verdict, 'good');
give(t, sb, ['Qs', '4s']); // 白は BTN だけ
assert.equal(judgePreflop(t.hand, sb).judgments[0].verdict, 'loose');
}); });
test('プリフロップ: 前に参加者がいたら、その人の色の一つ上でコール、二つ上でレイズ', () => { test('プリフロップ: 前に参加者がいたら、その人の色の一つ上でコール、二つ上でレイズ', () => {
// 6人: UTG(後ろに5人 → 緑と推定)がレイズ。次の人(hero)の判定 // 6人: HJ(後ろに4人 → 黄と推定)がレイズ。次の人(hero、CO)の判定
for (const [cards, action, verdict] of [ for (const [cards, action, verdict] of [
[['Kh', 'Qd'], 'call', 'good'], // 黄 = 緑の一つ上 → コール [['Kh', 'Qd'], 'call', 'good'], // 赤 = 黄の一つ上 → コール
[['Kh', 'Qd'], 'raise', 'aggressive'], [['Kh', 'Qd'], 'raise', 'aggressive'],
[['Jh', 'Jd'], 'raise', 'good'], // 赤 = 二つ上 → レイズ [['Ah', 'Ad'], 'raise', 'good'], // 紺 = 二つ上 → レイズ
[['Jh', 'Jd'], 'call', 'passive'], [['Ah', 'Ad'], 'call', 'passive'],
[['Kh', 'Jd'], 'call', 'loose'], // 緑 = 同じ → フォールド [['Kh', '9h'], 'call', 'loose'], // 黄 = 同じ → フォールド
[['Kh', 'Jd'], 'fold', 'good'], [['Kh', '9h'], 'fold', 'good'],
]) { ]) {
const t = makeTable(6); const t = makeTable(6);
const utg = act(t, 'raise', 30); act(t, 'fold');
const hj = act(t, 'raise', 30);
const hero = t.hand.toAct; const hero = t.hand.toAct;
act(t, action, action === 'raise' ? 90 : 0); act(t, action, action === 'raise' ? 90 : 0);
give(t, hero, cards); give(t, hero, cards);
const { judgments, estimated } = judgePreflop(t.hand, hero); const { judgments, estimated } = judgePreflop(t.hand, hero);
assert.equal(estimated.get(utg), 4); // 緑 assert.equal(estimated.get(hj), 4); // 黄
assert.equal(judgments[0].situation.refTier, 4); assert.equal(judgments[0].situation.refTier, 4);
assert.equal(judgments[0].verdict, verdict, `${cards} ${action}`); assert.equal(judgments[0].verdict, verdict, `${cards} ${action}`);
} }
// BB: リンプ(コール)に対してはチェックでよい。二つ上ならレイズ // BB: リンプ(コール)に対してはチェックでよい。二つ上ならレイズ
const t = makeTable(4); const t = makeTable(4);
act(t, 'call'); // UTG(後ろに3人 → 青) act(t, 'call'); // UTG(後ろに3人 → 緑)
act(t, 'fold'); act(t, 'fold');
act(t, 'call'); // SB: 青の一つ上と推定 act(t, 'call'); // SB: 緑の一つ上と推定
const bb = t.hand.toAct; const bb = t.hand.toAct;
act(t, 'check'); act(t, 'check');
give(t, bb, ['7h', '2d']); give(t, bb, ['7h', '2d']);
@@ -135,14 +149,43 @@ test('プリフロップ: 前に参加者がいたら、その人の色の一つ
assert.equal(j.verdict, 'good'); assert.equal(j.verdict, 'good');
}); });
test('プリフロップ: 自分のレイズにリレイズされたら、相手と同じ色でもコールできる', () => { test('プリフロップ: BTN で CO のオープンを受けたら、レンジ表の枠(水色枠は3bet、黄枠はコール)のとおり', () => {
// 6人: UTG(hero、後ろに5人 → 緑)がオープン、HJ が3bet(緑の二つ上 → 赤と推定)、ほかは降りて hero に戻る const coOpen = (cards, action, limp = false) => {
const t = makeTable(6);
act(t, limp ? 'call' : 'fold'); // UTG
act(t, 'fold');
act(t, 'raise', 30); // CO
const hero = t.hand.toAct;
assert.equal(hero, t.hand.button);
act(t, action, action === 'raise' ? 90 : 0);
give(t, hero, cards);
return judgePreflop(t.hand, hero).judgments[0];
};
for (const [cards, action, verdict] of [ for (const [cards, action, verdict] of [
[['Jh', 'Jd'], 'call', 'good'], // 赤 = 同じ → コール [['Ah', '5h'], 'raise', 'good'], // 水色枠 → 3bet
[['Jh', 'Jd'], 'fold', 'tight'], [['Ah', '5h'], 'call', 'passive'],
[['Kh', 'Qd'], 'call', 'loose'], // 黄 = 一つ下 → フォールド [['Th', 'Td'], 'call', 'good'], // 黄枠 → コール(色の決まりなら赤でレイズ)
[['Kh', 'Qd'], 'fold', 'good'], [['Th', 'Td'], 'raise', 'aggressive'],
[['Ah', 'Ad'], 'call', 'good'], // 紺 = 一つ上 → コール(二つ上はない) [['Qh', 'Jd'], 'call', 'good'], // 黄枠(色は CO の緑)
[['Ah', 'Td'], 'call', 'loose'], // 枠なし → フォールド(色は HJ の黄)
[['Ah', 'Td'], 'fold', 'good'],
]) {
const j = coOpen(cards, action);
assert.equal(j.situation.rule, 'vsCO');
assert.equal(j.verdict, verdict, `${cards} ${action}`);
}
assert.match(describeSituation(coOpen(['Th', 'Td'], 'call').situation, () => 'CPU'), /黄枠でコール/);
// 前にリンプした人がいたら CO のオープンではないので、色の決まりで判定する
assert.equal(coOpen(['Th', 'Td'], 'call', true).situation.rule, 'normal');
});
test('プリフロップ: 自分のレイズにリレイズされたら、相手と同じ色でもコールできる', () => {
// 6人: UTG(hero、後ろに5人 → 赤)がオープン、HJ が3bet(赤の二つ上はないので紺と推定)、ほかは降りて hero に戻る
for (const [cards, action, verdict] of [
[['Ah', 'Ad'], 'call', 'good'], // 紺 = 同じ → コール(二つ上はない)
[['Ah', 'Ad'], 'fold', 'tight'],
[['Jh', 'Jd'], 'call', 'loose'], // 赤 = 一つ下 → フォールド
[['Jh', 'Jd'], 'fold', 'good'],
]) { ]) {
const t = makeTable(6); const t = makeTable(6);
const hero = act(t, 'raise', 30); const hero = act(t, 'raise', 30);
@@ -160,7 +203,7 @@ test('プリフロップ: 自分のレイズにリレイズされたら、相手
}); });
test('プリフロップ: BB は 4BB までのオープンに広くコールし、三つ上でレイズ', () => { test('プリフロップ: BB は 4BB までのオープンに広くコールし、三つ上でレイズ', () => {
// 6人: CO(後ろに3人 → 青)がオープンして BB(hero)まで降りる。白以上でコール、赤以上でレイズ // 6人: CO(後ろに3人 → 緑)がオープンして BB(hero)まで降りる。青以上でコール、紺でレイズ
const coOpen = (to, cards, action) => { const coOpen = (to, cards, action) => {
const t = makeTable(6); const t = makeTable(6);
act(t, 'fold'); act(t, 'fold');
@@ -175,22 +218,22 @@ test('プリフロップ: BB は 4BB までのオープンに広くコールし
return judgePreflop(t.hand, bb).judgments[0]; return judgePreflop(t.hand, bb).judgments[0];
}; };
for (const [cards, action, verdict] of [ for (const [cards, action, verdict] of [
[['Kh', '8h'], 'call', 'good'], // 白 = 青の一つ下 → コール [['Kh', '4h'], 'call', 'good'], // 青 = 緑の一つ下 → コール
[['Qh', '5h'], 'call', 'loose'], // 紫枠 → フォールド [['Qh', '5h'], 'call', 'loose'], // 白 → フォールド
[['Kh', 'Qd'], 'call', 'good'], // 黄 = 二つ上 → まだコール [['Kh', 'Qd'], 'call', 'good'], // 赤 = 二つ上 → まだコール
[['Kh', 'Qd'], 'raise', 'aggressive'], [['Kh', 'Qd'], 'raise', 'aggressive'],
[['Jh', 'Jd'], 'raise', 'good'], // 赤 = 三つ上 → レイズ [['Ah', 'Ad'], 'raise', 'good'], // 紺 = 三つ上 → レイズ
]) { ]) {
const j = coOpen(30, cards, action); const j = coOpen(30, cards, action);
assert.equal(j.situation.rule, 'bb'); assert.equal(j.situation.rule, 'bb');
assert.equal(j.verdict, verdict, `${cards} ${action}`); assert.equal(j.verdict, verdict, `${cards} ${action}`);
} }
// 4BB を超えるオープンには、ほかの席と同じく一つ上が必要 // 4BB を超えるオープンには、ほかの席と同じく一つ上が必要
const big = coOpen(50, ['Kh', '8h'], 'call'); const big = coOpen(50, ['Kh', '4h'], 'call');
assert.equal(big.situation.rule, 'normal'); assert.equal(big.situation.rule, 'normal');
assert.equal(big.verdict, 'loose'); assert.equal(big.verdict, 'loose');
// BTN(後ろに2人 → 白)のオープンには紫枠からコールできる。コールした BB のレンジも紫枠と推定する // BTN(後ろに2人 → 白)のオープンには白からコールできる。コールした BB のレンジも白と推定する
const t = makeTable(6); const t = makeTable(6);
for (let k = 0; k < 3; k++) act(t, 'fold'); for (let k = 0; k < 3; k++) act(t, 'fold');
act(t, 'raise', 25); act(t, 'raise', 25);