Compare commits

...
3 Commits
Author SHA1 Message Date
agomizi1cmandClaude Opus 5.5 b6ce648b5a Rebuild bet sizing as a bottom sheet with situation-aware presets
Bet/Raise now opens a sheet whose presets follow the spot: open sizes
in BB (adjusted for limpers), 3bet/4bet multiples, postflop pot
fractions and raise multiples. Adds -/+ steppers with hold-to-repeat,
Min/All-in at the slider ends, and a square-curve slider so small sizes
stay precise with deep stacks. The hand view now exposes the street's
raise count so the client can tell a 3bet from a 4bet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:18:26 +09:00
agomizi1cmandClaude Opus 5.5 ed5617a47a Block pinch and double-tap zoom, including on iOS Safari
iOS Safari ignores user-scalable=no, so zoom is blocked in three layers:
- viewport meta: maximum-scale=1, user-scalable=no on every page
  (covers Android, home-screen apps and iOS focus zoom on inputs)
- CSS: touch-action: pan-x pan-y on html, so scrolling still works but
  pinch and double-tap zoom do not
- JS (fluid.js): cancel Safari's gesture* events and two-finger touchmove

Double-tap is left to CSS rather than JS so rapid repeated taps on
buttons still register as clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:53:02 +09:00
agomizi1cmandClaude Opus 5.5 7941406163 Make the nav bar material nearly opaque
Content scrolled beneath the translucent nav bar showed through as
blurry gray patches behind the back button. Give the bar its own denser
material so it reads as a solid surface with only a hint of blur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:49:52 +09:00
10 changed files with 446 additions and 88 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
+16
View File
@@ -524,6 +524,22 @@
} }
} }
// ---------- 拡大させない ----------
// iOS Safari は viewport の user-scalable=no を無視するので、ピンチは gesture* イベントと
// 2本指の touchmove を止めて防ぐ。ダブルタップの拡大は CSS の touch-action で止めている
// (JS で 2回目のタップを止めると、素早い連打のクリックまで消えてしまうため)
const stopZoom = (e) => e.preventDefault();
for (const type of ['gesturestart', 'gesturechange', 'gestureend']) {
document.addEventListener(type, stopZoom, { passive: false });
}
document.addEventListener(
'touchmove',
(e) => {
if (e.touches.length > 1) e.preventDefault();
},
{ passive: false },
);
window.Fluid = { window.Fluid = {
Spring, Spring,
VelocityTracker, VelocityTracker,
+196 -35
View File
@@ -43,6 +43,8 @@
--material-thin: rgba(255, 255, 255, 0.55); --material-thin: rgba(255, 255, 255, 0.55);
--material: rgba(250, 250, 252, 0.74); --material: rgba(250, 250, 252, 0.74);
--material-thick: rgba(246, 246, 249, 0.86); --material-thick: rgba(246, 246, 249, 0.86);
/* ナビゲーションバー: 下に潜った中身がぼやけた模様として透けないよう、ほぼ不透明に */
--material-bar: rgba(242, 242, 247, 0.94);
--material-edge: rgba(255, 255, 255, 0.7); --material-edge: rgba(255, 255, 255, 0.7);
--plate: rgba(255, 255, 255, 0.84); --plate: rgba(255, 255, 255, 0.84);
--scrim: rgba(0, 0, 0, 0.3); --scrim: rgba(0, 0, 0, 0.3);
@@ -110,6 +112,7 @@
--material-thin: rgba(44, 44, 46, 0.5); --material-thin: rgba(44, 44, 46, 0.5);
--material: rgba(30, 30, 32, 0.7); --material: rgba(30, 30, 32, 0.7);
--material-thick: rgba(24, 24, 26, 0.84); --material-thick: rgba(24, 24, 26, 0.84);
--material-bar: rgba(12, 12, 13, 0.94);
--material-edge: rgba(255, 255, 255, 0.09); --material-edge: rgba(255, 255, 255, 0.09);
--plate: rgba(28, 28, 30, 0.8); --plate: rgba(28, 28, 30, 0.8);
--scrim: rgba(0, 0, 0, 0.5); --scrim: rgba(0, 0, 0, 0.5);
@@ -127,6 +130,7 @@
--material-thin: #f2f2f7; --material-thin: #f2f2f7;
--material: #f7f7f9; --material: #f7f7f9;
--material-thick: #f2f2f7; --material-thick: #f2f2f7;
--material-bar: #f2f2f7;
--plate: #ffffff; --plate: #ffffff;
} }
} }
@@ -135,6 +139,7 @@
--material-thin: #2c2c2e; --material-thin: #2c2c2e;
--material: #1c1c1e; --material: #1c1c1e;
--material-thick: #1c1c1e; --material-thick: #1c1c1e;
--material-bar: #0c0c0d;
--plate: #1c1c1e; --plate: #1c1c1e;
} }
} }
@@ -168,6 +173,8 @@ html {
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
text-size-adjust: 100%; text-size-adjust: 100%;
background: var(--bg); background: var(--bg);
/* スクロールだけ許し、ピンチとダブルタップの拡大は止める(子の touch-action もこれに絞られる) */
touch-action: pan-x pan-y;
} }
body { body {
@@ -250,9 +257,9 @@ a {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background: var(--material); background: var(--material-bar);
-webkit-backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(150%);
backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(30px) saturate(150%);
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
@@ -264,7 +271,7 @@ a {
top: 100%; top: 100%;
height: 16px; height: 16px;
pointer-events: none; pointer-events: none;
background: linear-gradient(var(--material), transparent); background: linear-gradient(var(--material-bar), transparent);
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
-webkit-mask-image: linear-gradient(#000, transparent); -webkit-mask-image: linear-gradient(#000, transparent);
@@ -1457,7 +1464,7 @@ body.page-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--room); background: var(--room);
touch-action: manipulation; /* ダブルタップでの拡大を防ぐ(ピンチ拡大は残す) */ touch-action: pan-x pan-y; /* ダブルタップ・ピンチでの拡大を防ぐ */
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
-webkit-touch-callout: none; -webkit-touch-callout: none;
@@ -2335,52 +2342,74 @@ body.page-table {
color: var(--label-3); color: var(--label-3);
} }
/* レイズ額: レイズボタンから上に開き、閉じるときはボタンへ戻る */ /* ベット/レイズ額のシート: テーブル(ポット・相手のベット)が見えるよう背景は薄く暗くするだけ */
.raise-panel { .bet-backdrop::before {
position: absolute; background: rgba(0, 0, 0, 0.16);
left: 12px; }
right: 12px; .bet-sheet {
bottom: calc(100% + 10px); max-width: 520px;
z-index: 6;
max-width: 496px;
margin: 0 auto;
padding: 14px 16px 14px;
border-radius: 24px;
background: var(--material-thick); background: var(--material-thick);
-webkit-backdrop-filter: blur(30px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(180%);
backdrop-filter: blur(30px) saturate(180%); backdrop-filter: blur(30px) saturate(180%);
box-shadow: var(--shadow-l), inset 0 0 0 0.5px var(--material-edge); box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.2), inset 0 0.5px 0 var(--material-edge);
transform-origin: 82% 100%;
} }
.raise-head { .bet-sheet::after {
background: var(--material-thick);
}
.bet-header {
padding-bottom: 4px;
}
.bet-heading {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
text-align: left;
}
.bet-context {
min-width: 0;
overflow: hidden;
color: var(--label-2);
font-size: 0.8125rem;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
white-space: nowrap;
}
.bet-body {
padding: 4px 20px 2px;
}
.bet-amount-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
} }
.raise-value { .raise-value {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
min-width: 0; min-width: 0;
font-family: var(--font-num); font-family: var(--font-num);
cursor: text;
} }
.raise-value .cur { .raise-value .cur {
color: var(--label-2); color: var(--label-2);
font-size: 1.5rem; font-size: 1.75rem;
font-weight: 700; font-weight: 700;
} }
.raise-amount { .raise-amount {
width: 8ch; width: 7.5ch;
min-width: 0; min-width: 0;
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
outline: none; outline: none;
background: transparent; background: transparent;
color: var(--label);
font-family: var(--font-num); font-family: var(--font-num);
font-size: 2.125rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.022em; line-height: 1.15;
letter-spacing: -0.025em;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield; appearance: textfield;
@@ -2394,17 +2423,131 @@ body.page-table {
box-shadow: inset 0 -2px 0 var(--orange); box-shadow: inset 0 -2px 0 var(--orange);
} }
.raise-meta { .raise-meta {
display: grid;
flex: none; flex: none;
justify-items: end;
gap: 1px;
font-variant-numeric: tabular-nums;
}
.raise-meta b {
font-size: 1.0625rem;
font-weight: 700;
}
.raise-meta span {
color: var(--label-2); color: var(--label-2);
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 600;
}
.raise-meta span:empty {
display: none;
}
/* 場面ごとの候補(3bet の 3x・ベットの 50% など)。上に倍率、下にその額 */
.bet-presets {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: 6px;
margin-top: 12px;
}
.chip-btn {
-webkit-appearance: none;
appearance: none;
display: grid;
place-content: center;
gap: 1px;
min-width: 0;
min-height: 54px;
padding: 6px 2px;
border: 0;
border-radius: 14px;
background: var(--fill-2);
color: var(--label);
font: inherit;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.45s var(--ease-spring);
}
.chip-btn b {
font-size: 0.9375rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.chip-btn span {
overflow: hidden;
color: var(--label-2);
font-size: 0.6875rem;
font-weight: 600;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s ease;
}
.chip-btn:active:not(:disabled) {
transform: scale(0.93);
transition: transform 0.1s ease-out;
}
.chip-btn.active {
background: var(--orange);
color: var(--on-orange);
}
.chip-btn.active span {
color: var(--on-orange);
opacity: 0.72;
}
.chip-btn:disabled {
color: var(--label-3);
cursor: default;
}
.chip-btn:disabled span {
color: var(--label-3);
}
/* −/+ とスライダー */
.bet-adjust {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.step-btn {
-webkit-appearance: none;
appearance: none;
display: grid;
place-items: center;
flex: none;
width: 40px;
height: 40px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--fill-2);
color: var(--label);
cursor: pointer;
touch-action: manipulation;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
transition: transform 0.45s var(--ease-spring), opacity 0.2s ease;
}
.step-btn .i {
width: 18px;
height: 18px;
stroke-width: 2.6;
}
.step-btn:active:not(:disabled) {
transform: scale(0.88);
transition: transform 0.1s ease-out;
}
.step-btn:disabled {
opacity: 0.35;
cursor: default;
} }
/* 自前のスライダー: つかんだ位置のまま指に付いてくる。端を越えるとゴムのように抵抗する */ /* 自前のスライダー: つかんだ位置のまま指に付いてくる。端を越えるとゴムのように抵抗する */
.bet-slider { .bet-slider {
position: relative; position: relative;
flex: 1;
min-width: 0;
height: 44px; height: 44px;
margin: 4px 0 8px;
outline: none; outline: none;
touch-action: none; touch-action: none;
cursor: pointer; cursor: pointer;
@@ -2453,33 +2596,51 @@ body.page-table {
.bet-slider:focus-visible .bet-thumb::before { .bet-slider:focus-visible .bet-thumb::before {
box-shadow: 0 0 0 3px var(--blue), 0 3px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 0 0 3px var(--blue), 0 3px 8px rgba(0, 0, 0, 0.2);
} }
.presets {
display: grid; /* スライダーの両端: 最小とオールイン */
grid-template-columns: repeat(5, 1fr); .bet-ends {
gap: 6px; display: flex;
justify-content: space-between;
gap: 8px;
margin-top: 2px;
} }
.chip-btn { .end-btn {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
min-height: 34px; min-height: 30px;
padding: 4px 0; padding: 4px 12px;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: var(--fill-2); background: transparent;
color: var(--label); color: var(--label-2);
font: inherit;
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 600;
font-variant-numeric: tabular-nums;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.45s var(--ease-spring); transition: background-color 0.2s ease, color 0.2s ease, transform 0.45s var(--ease-spring);
} }
.chip-btn:active { .end-btn span {
color: var(--label);
}
.end-btn:active {
transform: scale(0.93); transform: scale(0.93);
transition: transform 0.1s ease-out; transition: transform 0.1s ease-out;
} }
.chip-btn.active { .end-btn.active {
background: var(--fill-2);
color: var(--label);
}
.end-btn.allin.active {
background: var(--orange); background: var(--orange);
color: var(--on-orange); color: var(--on-orange);
} }
.end-btn.allin.active span {
color: inherit;
}
.bet-sheet .sheet-footer .btn {
font-variant-numeric: tabular-nums;
}
/* ショーダウンの勝者選択(キャッシュオンリー) */ /* ショーダウンの勝者選択(キャッシュオンリー) */
.award-panel { .award-panel {
+184 -24
View File
@@ -31,6 +31,15 @@
const reviewSheet = Fluid.Sheet($('review-modal')); const reviewSheet = Fluid.Sheet($('review-modal'));
const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' }); const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' });
// 払って閉じた・背景を押した・Esc のときも「戻る」と同じ
const betSheet = Fluid.Sheet($('bet-modal'), {
focus: '#raise-range',
onClose: () => {
if (!raising) return;
raising = false;
render();
},
});
// ---------- 名前 ---------- // ---------- 名前 ----------
function ensureName() { function ensureName() {
@@ -108,6 +117,8 @@
$('main').hidden = true; $('main').hidden = true;
$('action-bar').hidden = true; $('action-bar').hidden = true;
reviewSheet.close(); reviewSheet.close();
raising = false;
betSheet.close();
$('not-found').hidden = false; $('not-found').hidden = false;
// 自分から切断するので「接続が切れました」は出さない // 自分から切断するので「接続が切れました」は出さない
if (socket) { if (socket) {
@@ -246,6 +257,7 @@
renderSeats(s); renderSeats(s);
if (me) renderActionBar(s, me); if (me) renderActionBar(s, me);
else betSheet.close();
renderReview(s); renderReview(s);
renderTimer(); renderTimer();
@@ -473,10 +485,14 @@
$('sit-in').hidden = !m.sittingOut; $('sit-in').hidden = !m.sittingOut;
$('rebuy').hidden = !m.canRebuy; $('rebuy').hidden = !m.canRebuy;
$('rebuy').textContent = `リバイ(${chips(s.startingStack)})`; $('rebuy').textContent = `リバイ(${chips(s.startingStack)})`;
$('actions').hidden = alt || raising || awarding || reviewWaiting; $('actions').hidden = alt || awarding || reviewWaiting;
$('raise-actions').hidden = alt || !raising; if (raising && o && !alt) {
// レイズ額のパネルはレイズボタンから出て、閉じるときはボタンへ戻る renderRaisePanel(o);
Fluid.presence($('raise-panel'), !alt && raising, { scale: 0.86, y: 28 }); betSheet.open();
} else {
raising = false;
betSheet.close();
}
const fold = $('act-fold'); const fold = $('act-fold');
const call = $('act-call'); const call = $('act-call');
@@ -494,8 +510,6 @@
: o.minRaiseTo >= o.maxRaiseTo : o.minRaiseTo >= o.maxRaiseTo
? `All-in ${chips(o.maxRaiseTo)}` ? `All-in ${chips(o.maxRaiseTo)}`
: o.isBet ? 'Bet' : 'Raise'; : o.isBet ? 'Bet' : 'Raise';
if (raising) renderRaisePanel(o);
} }
// ---------- レビュー(レビュー付きCPU戦の作成者のみ) ---------- // ---------- レビュー(レビュー付きCPU戦の作成者のみ) ----------
@@ -665,18 +679,78 @@
}); });
// ---------- レイズ ---------- // ---------- レイズ ----------
function presetValue(o, kind) { // 場面ごとの額の候補。プリフロップはオープン(BB の何倍)・3bet 以降(相手の額の何倍)、
// フロップ以降はベット(ポットの何割)・レイズ(相手のベットの何倍)。
// ポットの何割は「コールしたあとのポット」に対する額を上乗せする(ポットレイズと同じ考え方)。
// def は開いたときに選んでおく候補
function betContext(o) {
const h = state.hand; const h = state.hand;
const bb = state.bigBlind;
const cur = h.currentBet;
const potAfterCall = h.potTotal + o.toCall; const potAfterCall = h.potTotal + o.toCall;
const base = h.currentBet; const pot = (f) => cur + potAfterCall * f;
const v = { const times = (x) => ({ label: `${x}x`, to: cur * x });
min: o.minRaiseTo, const potPreset = { label: 'Pot', to: pot(1) };
half: base + Math.round(potAfterCall / 2),
threeq: base + Math.round((potAfterCall * 3) / 4), if (h.street === 'PREFLOP') {
pot: base + potAfterCall, const r = h.raises || 0;
max: o.maxRaiseTo, if (r === 0) {
}[kind]; // リンプした人がいれば 1人につき 1BB 足す
return clampRaise(o, v); const limpers = Object.entries(h.players).filter(
([seat, p]) => Number(seat) !== h.bbSeat && !p.folded && p.bet >= cur
).length;
return {
title: limpers ? 'アイソレート' : 'オープン',
note: limpers ? `リンプ ${limpers}人(+${limpers}BB)` : '',
presets: [2, 2.5, 3, 4].map((x) => ({ label: `${x + limpers}BB`, to: (x + limpers) * bb })).concat(potPreset),
def: limpers ? 2 : 1,
};
}
const xs = r === 1 ? [2.5, 3, 4] : r === 2 ? [2.2, 2.5, 3] : [2, 2.5];
return {
title: `${r + 2}bet`,
note: `相手 ${chips(cur)}`,
presets: xs.map(times).concat(potPreset),
def: 1,
};
}
if (o.isBet) {
return {
title: 'ベット',
note: '',
presets: [
{ label: '33%', to: pot(1 / 3) },
{ label: '50%', to: pot(1 / 2) },
{ label: '75%', to: pot(3 / 4) },
potPreset,
{ label: '150%', to: pot(1.5) },
],
def: 1,
};
}
return {
title: (h.raises || 0) >= 2 ? 'リレイズ' : 'レイズ',
note: `相手 ${chips(cur)}`,
presets: [2.5, 3, 4].map(times).concat(potPreset),
def: 1,
};
}
// 候補の額を出せる範囲に収める(SB 単位に丸めるとブラインドが小さいうちは 50% が 57% になったりするので 1 単位)。
// 最小に届かない候補は押せない。オールインを超える候補は最初の 1つだけ All-in として残す。
// 左から額の小さい順に並べる(Pot が 4BB より小さいこともある)。def は並べ替える前の番号
function betPresets(o, ctx) {
let allInShown = false;
return ctx.presets
.map((p, i) => ({ label: p.label, raw: Math.round(p.to), def: i === ctx.def }))
.sort((a, b) => a.raw - b.raw)
.map((p) => {
const to = clampRaise(o, p.raw);
const allIn = to >= o.maxRaiseTo;
const disabled = p.raw < o.minRaiseTo || (allIn && allInShown);
if (allIn) allInShown = true;
return { label: p.label, to, allIn, disabled, def: p.def };
});
} }
function clampRaise(o, v) { function clampRaise(o, v) {
@@ -704,12 +778,15 @@
knob.onUpdate = paintKnob; knob.onUpdate = paintKnob;
if ('ResizeObserver' in window) new ResizeObserver(() => paintKnob(knob.value)).observe(slider); if ('ResizeObserver' in window) new ResizeObserver(() => paintKnob(knob.value)).observe(slider);
const fracOf = (o, v) => (o.maxRaiseTo > o.minRaiseTo ? (v - o.minRaiseTo) / (o.maxRaiseTo - o.minRaiseTo) : 1); // つまみの位置と額は 2乗の関係にする(スタックが深くても、よく使う小さい額を細かく選べるように)
const CURVE = 2;
const fracOf = (o, v) =>
o.maxRaiseTo > o.minRaiseTo ? Fluid.clamp((v - o.minRaiseTo) / (o.maxRaiseTo - o.minRaiseTo), 0, 1) ** (1 / CURVE) : 1;
// SB 単位に丸める(最大はオールイン額そのまま) // SB 単位に丸める(最大はオールイン額そのまま)
function valueAt(o, f) { function valueAt(o, f) {
if (f >= 1) return o.maxRaiseTo; if (f >= 1) return o.maxRaiseTo;
const unit = Math.max(1, state.smallBlind); const unit = Math.max(1, state.smallBlind);
const raw = o.minRaiseTo + f * (o.maxRaiseTo - o.minRaiseTo); const raw = o.minRaiseTo + f ** CURVE * (o.maxRaiseTo - o.minRaiseTo);
return clampRaise(o, o.minRaiseTo + Math.round((raw - o.minRaiseTo) / unit) * unit); return clampRaise(o, o.minRaiseTo + Math.round((raw - o.minRaiseTo) / unit) * unit);
} }
const formatBB = (x) => (Math.round(x * 10) / 10).toLocaleString('en-US'); const formatBB = (x) => (Math.round(x * 10) / 10).toLocaleString('en-US');
@@ -730,10 +807,42 @@
if (knob.target !== f) knob.to(f, { damping: 1, response: 0.3 }); if (knob.target !== f) knob.to(f, { damping: 1, response: 0.3 });
} }
paintKnob(knob.value); paintKnob(knob.value);
const h = state.hand;
const ctx = betContext(o);
$('bet-title').textContent = ctx.title;
$('bet-context').textContent = [`ポット ${chips(h.potTotal)}`, ctx.note].filter(Boolean).join(' · ');
const presets = betPresets(o, ctx);
patch(
$('presets'),
presets
.map(
(p) =>
`<button type="button" class="chip-btn${p.allIn ? ' allin' : ''}" data-to="${p.to}"${p.disabled ? ' disabled' : ''}>` +
`<b>${esc(p.label)}</b><span>${p.disabled ? '—' : p.allIn ? 'All-in' : chips(p.to)}</span></button>`
)
.join('')
);
// 同じ額の候補が 2つあっても光らせるのは 1つだけ
let lit = false;
for (const b of $('presets').children) { for (const b of $('presets').children) {
b.classList.toggle('active', presetValue(o, b.dataset.preset) === raiseValue); const on = !lit && !b.disabled && Number(b.dataset.to) === raiseValue;
if (on) lit = true;
b.classList.toggle('active', on);
} }
$('raise-meta').textContent = state.bigBlind > 0 ? `${formatBB(raiseValue / state.bigBlind)} BB` : ''; $('raise-min-amt').textContent = chips(o.minRaiseTo);
$('raise-max-amt').textContent = chips(o.maxRaiseTo);
$('raise-min').classList.toggle('active', raiseValue === o.minRaiseTo);
$('raise-max').classList.toggle('active', raiseValue === o.maxRaiseTo);
$('raise-minus').disabled = raiseValue <= o.minRaiseTo;
$('raise-plus').disabled = raiseValue >= o.maxRaiseTo;
$('raise-bb').textContent = state.bigBlind > 0 ? `${formatBB(raiseValue / state.bigBlind)} BB` : '';
// 相手の額の何倍か(オープン以外のプリフロップとレイズ)、ポットの何%か(ベット)
let ratio = '';
if (o.isBet) ratio = h.potTotal > 0 ? `ポットの ${Math.round((raiseValue / h.potTotal) * 100)}%` : '';
else if (h.street !== 'PREFLOP' || h.raises > 0) ratio = `${formatBB(raiseValue / h.currentBet)}x`;
$('raise-ratio').textContent = ratio;
const allIn = raiseValue >= o.maxRaiseTo; const allIn = raiseValue >= o.maxRaiseTo;
$('raise-confirm').textContent = allIn $('raise-confirm').textContent = allIn
? `All-in ${chips(raiseValue)}` ? `All-in ${chips(raiseValue)}`
@@ -846,9 +955,13 @@
const o = options(); const o = options();
if (!o || !o.canRaise) return; if (!o || !o.canRaise) return;
if (o.minRaiseTo >= o.maxRaiseTo) return sendAction('raise', o.maxRaiseTo); // オールインしかない if (o.minRaiseTo >= o.maxRaiseTo) return sendAction('raise', o.maxRaiseTo); // オールインしかない
// 場面のよくある額(オープン 2.5BB・3bet 3x・ベット 50% など)を選んだ状態で開く
const ctx = betContext(o);
const presets = betPresets(o, ctx);
const def = presets.find((p) => p.def && !p.disabled) || presets.find((p) => !p.disabled);
raising = true; raising = true;
raiseValue = o.minRaiseTo; raiseValue = def ? def.to : o.minRaiseTo;
knob.set(0); knob.set(fracOf(o, raiseValue));
render(); render();
}); });
$('raise-cancel').addEventListener('click', () => { $('raise-cancel').addEventListener('click', () => {
@@ -860,10 +973,57 @@
if (o) sendAction('raise', clampRaise(o, raiseValue)); if (o) sendAction('raise', clampRaise(o, raiseValue));
}); });
$('presets').addEventListener('click', (e) => { $('presets').addEventListener('click', (e) => {
const b = e.target.closest('[data-preset]'); const b = e.target.closest('[data-to]');
const o = options(); const o = options();
if (b && o) setRaise(o, presetValue(o, b.dataset.preset)); if (!b || b.disabled || !o) return;
Fluid.haptic(6);
setRaise(o, Number(b.dataset.to));
}); });
$('raise-min').addEventListener('click', () => {
const o = options();
if (o) setRaise(o, o.minRaiseTo);
});
$('raise-max').addEventListener('click', () => {
const o = options();
if (o) setRaise(o, o.maxRaiseTo);
});
// −/+: 1BB ずつ。押し続けると少し待ってから連続で、だんだん速く動く
function stepper(btn, dir) {
let timer = null;
let count = 0;
const step = () => {
const o = options();
if (!o) return stop();
const before = raiseValue;
setRaise(o, raiseValue + dir * Math.max(1, state.bigBlind));
if (raiseValue === before) return stop();
if (raiseValue === o.maxRaiseTo || raiseValue === o.minRaiseTo) Fluid.haptic(8);
};
const stop = () => {
clearTimeout(timer);
timer = null;
};
const repeat = () => {
step();
count++;
if (timer) timer = setTimeout(repeat, count < 6 ? 140 : count < 20 ? 70 : 35);
};
btn.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return;
e.preventDefault(); // 長押しで文字選択・メニューが出ないように
step();
count = 0;
timer = setTimeout(repeat, 380);
});
for (const type of ['pointerup', 'pointerleave', 'pointercancel']) btn.addEventListener(type, stop);
// キーボード(Enter / Space)から押されたとき
btn.addEventListener('click', (e) => {
if (e.detail === 0) step();
});
}
stepper($('raise-minus'), -1);
stepper($('raise-plus'), 1);
$('raise-amount').addEventListener('input', (e) => { $('raise-amount').addEventListener('input', (e) => {
const o = options(); const o = options();
const v = Number(e.target.value); const v = Number(e.target.value);
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
+44 -24
View File
@@ -2,7 +2,7 @@
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#eceef1"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#eceef1">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#070c0a"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#070c0a">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
@@ -67,25 +67,6 @@
</main> </main>
<section class="action-bar" id="action-bar" hidden> <section class="action-bar" id="action-bar" hidden>
<!-- レイズ額: レイズボタンから上に開く(すりガラスを重ねないよう .inner の外に置く) -->
<div class="raise-panel" id="raise-panel" hidden>
<div class="raise-head">
<div class="raise-value"><span class="cur" aria-hidden="true">$</span><input type="number" class="raise-amount" id="raise-amount" inputmode="numeric" aria-label="レイズ額"></div>
<span class="raise-meta" id="raise-meta"></span>
</div>
<div class="bet-slider" id="raise-range" role="slider" tabindex="0" aria-label="レイズ額">
<div class="bet-track"><div class="bet-fill"></div></div>
<div class="bet-thumb"></div>
</div>
<div class="presets" id="presets">
<button type="button" class="chip-btn" data-preset="min">Min</button>
<button type="button" class="chip-btn" data-preset="half">½ Pot</button>
<button type="button" class="chip-btn" data-preset="threeq">¾ Pot</button>
<button type="button" class="chip-btn" data-preset="pot">Pot</button>
<button type="button" class="chip-btn" data-preset="max">All-in</button>
</div>
</div>
<div class="inner"> <div class="inner">
<div class="me-row"> <div class="me-row">
<div class="my-cards" id="my-cards" hidden></div> <div class="my-cards" id="my-cards" hidden></div>
@@ -108,10 +89,6 @@
<button class="btn act-call" id="act-call" disabled>Check</button> <button class="btn act-call" id="act-call" disabled>Check</button>
<button class="btn act-raise" id="act-raise" disabled>Raise</button> <button class="btn act-raise" id="act-raise" disabled>Raise</button>
</div> </div>
<div class="actions raising" id="raise-actions" hidden>
<button class="btn" id="raise-cancel">戻る</button>
<button class="btn act-raise" id="raise-confirm">Raise</button>
</div>
<div class="actions raising" id="review-actions" hidden> <div class="actions raising" id="review-actions" hidden>
<button class="btn" id="review-show">レビュー</button> <button class="btn" id="review-show">レビュー</button>
<button class="btn btn-tint" id="review-next">次のハンドへ</button> <button class="btn btn-tint" id="review-next">次のハンドへ</button>
@@ -130,6 +107,49 @@
<a class="btn btn-tint btn-lg" href="/">ロビーへ戻る</a> <a class="btn btn-tint btn-lg" href="/">ロビーへ戻る</a>
</div> </div>
<!-- ベット/レイズ額(Bet・Raise ボタンで下から出るシート。つまみを下へ払うと閉じる)。
候補はプリフロップのオープン・3bet・4bet、フロップ以降のベット・レイズで入れ替わる -->
<div class="sheet-backdrop bet-backdrop" id="bet-modal" hidden>
<div class="sheet bet-sheet" role="dialog" aria-modal="true" aria-labelledby="bet-title" tabindex="-1">
<div class="sheet-header bet-header" data-sheet-drag>
<span class="sheet-grabber" aria-hidden="true"></span>
<div class="bet-heading">
<h2 class="sheet-title" id="bet-title">Raise</h2>
<span class="bet-context" id="bet-context"></span>
</div>
</div>
<div class="bet-body">
<div class="bet-amount-row">
<label class="raise-value"><span class="cur" aria-hidden="true">$</span><input type="number" class="raise-amount" id="raise-amount" inputmode="numeric" aria-label="ベット額"></label>
<div class="raise-meta"><b id="raise-bb"></b><span id="raise-ratio"></span></div>
</div>
<div class="bet-presets" id="presets"></div>
<div class="bet-adjust">
<button type="button" class="step-btn" id="raise-minus" aria-label="1BB 減らす">
<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12"/></svg>
</button>
<div class="bet-slider" id="raise-range" role="slider" tabindex="0" aria-label="ベット額">
<div class="bet-track"><div class="bet-fill"></div></div>
<div class="bet-thumb"></div>
</div>
<button type="button" class="step-btn" id="raise-plus" aria-label="1BB 増やす">
<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M6 12h12"/></svg>
</button>
</div>
<div class="bet-ends">
<button type="button" class="end-btn" id="raise-min">Min <span id="raise-min-amt"></span></button>
<button type="button" class="end-btn allin" id="raise-max">All-in <span id="raise-max-amt"></span></button>
</div>
</div>
<div class="sheet-footer">
<div class="btn-row">
<button type="button" class="btn btn-lg" id="raise-cancel">戻る</button>
<button type="button" class="btn btn-lg act-raise" id="raise-confirm">Raise</button>
</div>
</div>
</div>
</div>
<!-- 直前のハンドのレビュー(下から出るシート。つまみを下へ払うと閉じる) --> <!-- 直前のハンドのレビュー(下から出るシート。つまみを下へ払うと閉じる) -->
<div class="sheet-backdrop" id="review-modal" hidden> <div class="sheet-backdrop" id="review-modal" hidden>
<div class="sheet review-sheet" role="dialog" aria-modal="true" aria-labelledby="review-title" tabindex="-1"> <div class="sheet review-sheet" role="dialog" aria-modal="true" aria-labelledby="review-title" tabindex="-1">
+1
View File
@@ -444,6 +444,7 @@ class Hand {
pot: this.pot, pot: this.pot,
potTotal: this.potTotal, potTotal: this.potTotal,
currentBet: this.currentBet, currentBet: this.currentBet,
raises: this.raises, // ベット額の候補を 3bet・4bet などに合わせるため
button: this.button, button: this.button,
sbSeat: this.sbSeat, sbSeat: this.sbSeat,
bbSeat: this.bbSeat, bbSeat: this.bbSeat,