Files
Poker/admin/range.js
T
agomizi1cmandClaude Opus 5.5 3ddb0996e0 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>
2026-10-20 15:21:39 +09:00

211 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ハンドレンジ学習(管理者専用): レンジ表と色・分類クイズ
(function () {
'use strict';
const $ = (id) => document.getElementById(id);
const { RANKS, CATEGORIES, FACING_ACTIONS, handAt, categoryOf, labelOf, facingOf, facingAction } = window.HandRange;
function swatch(id) {
const s = document.createElement('span');
s.className = `swatch-dot cat-${id}`;
return s;
}
// レイズを受けたときの対応の枠(3bet は水色、コールは黄)
function frameSwatch(id) {
const s = document.createElement('span');
s.className = `swatch-dot swatch-frame facing-${id}`;
return s;
}
// ---------- タブ ----------
function showView(name) {
$('view-range').hidden = name !== 'range';
$('view-quiz').hidden = name !== 'quiz';
for (const b of $('tabs').children) b.classList.toggle('active', b.dataset.view === name);
}
$('tabs').addEventListener('click', (e) => {
const b = e.target.closest('[data-view]');
if (b) showView(b.dataset.view);
});
// ---------- レンジ表 ----------
for (const c of CATEGORIES) {
const item = document.createElement('span');
item.append(swatch(c.id), c.label);
$('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 c = 0; c < 13; c++) {
const hand = handAt(r, c);
const cat = categoryOf(hand);
const b = document.createElement('button');
b.type = 'button';
const facing = facingOf(hand);
b.className = `range-cell cat-${cat}${facing ? ` facing-${facing}` : ''}`;
b.textContent = hand;
b.title = `${hand}:${labelOf(cat)}${facing ? `/レイズを受けたら${facingAction(facing).label}` : ''}`;
b.addEventListener('click', () => select(b, hand, cat));
$('matrix').append(b);
}
}
function select(cell, hand, cat) {
document.querySelectorAll('.range-cell.selected').forEach((x) => x.classList.remove('selected'));
cell.classList.add('selected');
const box = $('selected');
box.replaceChildren();
const name = document.createElement('div');
name.className = 'sel-hand';
name.textContent = hand;
const label = document.createElement('div');
label.className = 'sel-label';
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');
note.className = 'hint';
note.textContent =
(cat === 'gray' ? 'どのポジションからもオープンしない手です。' : `${labelOf(cat)} からオープンできる一番狭い分類です。`) +
'枠は CO のオープンを BTN で受けたときの対応です。';
box.append(name, label, vs, note);
}
$('start-from-range').addEventListener('click', () => {
showView('quiz');
startQuiz();
});
// ---------- クイズ ----------
const SUITS = ['hearts', 'clubs', 'diamonds', 'spades'];
const RANK_FILE = { A: 'ace', K: 'king', Q: 'queen', J: 'jack', T: '10' };
let quizCount = 10;
let deck = [];
let index = 0;
let correct = 0;
let answered = 0;
let locked = false;
$('count').addEventListener('click', (e) => {
const b = e.target.closest('[data-count]');
if (!b) return;
quizCount = Number(b.dataset.count);
for (const x of $('count').children) x.classList.toggle('active', x === b);
});
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function startQuiz() {
const pool = [];
for (let r = 0; r < 13; r++) for (let c = 0; c < 13; c++) pool.push(handAt(r, c));
deck = shuffle(pool);
if (quizCount > 0) deck = deck.slice(0, quizCount);
index = 0;
correct = 0;
answered = 0;
$('start-quiz').textContent = '最初から';
renderQuestion();
}
// スーテッドは同じマーク、それ以外は別々のマーク
function showCards(hand) {
const s1 = SUITS[Math.floor(Math.random() * 4)];
let s2 = s1;
if (hand[2] !== 's') while (s2 === s1) s2 = SUITS[Math.floor(Math.random() * 4)];
[hand[0], hand[1]].forEach((rank, i) => {
const img = $(`card-${i + 1}`);
const suit = i === 0 ? s1 : s2;
img.hidden = true;
img.onload = () => { img.hidden = false; };
img.onerror = () => { img.hidden = true; };
img.alt = `${RANK_FILE[rank] || rank} of ${suit}`;
img.src = `/range/files/cards/${RANK_FILE[rank] || rank}_of_${suit}.png`;
});
}
function updateStats() {
$('q-score').textContent = `正解 ${correct} / ${answered}`;
$('q-accuracy').textContent = `正答率 ${answered ? Math.round((correct / answered) * 100) + '%' : '--'}`;
}
function renderQuestion() {
locked = false;
$('answers').replaceChildren();
$('feedback').hidden = true;
$('next-question').disabled = true;
if (index >= deck.length) return finishQuiz();
const hand = deck[index];
$('q-hand').textContent = hand;
$('q-prompt').textContent = 'このハンドの色・分類は?';
$('q-count').textContent = `問題 ${index + 1} / ${deck.length}`;
$('progress-bar').style.width = `${(index / deck.length) * 100}%`;
updateStats();
showCards(hand);
for (const cat of CATEGORIES) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'btn';
b.dataset.cat = cat.id;
b.append(swatch(cat.id), cat.label);
b.addEventListener('click', () => answer(cat.id, b));
$('answers').append(b);
}
}
function answer(choice, button) {
if (locked) return;
locked = true;
const hand = deck[index];
const right = categoryOf(hand);
const ok = choice === right;
if (ok) correct++;
answered++;
for (const b of $('answers').children) {
if (b.dataset.cat === right) b.classList.add('correct');
else if (b === button) b.classList.add('wrong');
b.disabled = true;
}
const fb = $('feedback');
fb.className = `quiz-feedback ${ok ? 'good' : 'bad'}`;
fb.textContent = `${ok ? '正解!' : '不正解'} ${hand} は「${labelOf(right)}」です。`;
fb.hidden = false;
updateStats();
$('next-question').disabled = false;
}
function finishQuiz() {
$('q-hand').textContent = '終了!';
$('q-prompt').textContent = `${deck.length}問中 ${correct}問正解(${deck.length ? Math.round((correct / deck.length) * 100) : 0}%)`;
$('q-count').textContent = '完了';
$('progress-bar').style.width = '100%';
$('card-1').hidden = $('card-2').hidden = true;
const fb = $('feedback');
fb.className = 'quiz-feedback';
fb.textContent = '「最初から」で再挑戦できます。';
fb.hidden = false;
}
$('start-quiz').addEventListener('click', startQuiz);
$('next-question').addEventListener('click', () => {
index++;
renderQuestion();
});
})();