Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0f75d0c14 | ||
|
|
e8d6098d39 | ||
|
|
5d1a6b7399 | ||
|
|
a816b32426 | ||
|
|
5271d5fbad | ||
|
|
552ec1a542 | ||
|
|
5c807d1581 | ||
|
|
a5b9c26902 | ||
|
|
9a5b974fda | ||
|
|
b563a56563 | ||
|
|
72ee2fb2b7 | ||
|
|
01a1728971 |
@@ -31,9 +31,13 @@
|
||||
<span id="potBadgeAmount">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="betsLayer" aria-hidden="true"></div>
|
||||
<div id="seatsLayer"></div>
|
||||
</div>
|
||||
<p id="tableHint" class="tableHint"></p>
|
||||
<p class="tableHintActions">
|
||||
<button type="button" id="openRulingsBtn" class="linkBtn">Common rulings</button>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<aside id="holdemSidebar">
|
||||
@@ -178,6 +182,7 @@
|
||||
<span>Stack</span>
|
||||
<input type="number" class="stackInput" min="0" step="1" placeholder="table default" />
|
||||
</label>
|
||||
<div class="seatBuyIns" hidden></div>
|
||||
<div class="seatContribution">
|
||||
<div class="chipCounts"></div>
|
||||
<label class="fieldRow amountOverrideRow">
|
||||
@@ -234,6 +239,73 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="addChipsOverlay" hidden>
|
||||
<div id="addChipsSheet" role="dialog" aria-labelledby="addChipsTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="addChipsHeader">
|
||||
<h2 id="addChipsTitle"></h2>
|
||||
<span id="addChipsSubtitle"></span>
|
||||
</div>
|
||||
<div id="addChipsQuick"></div>
|
||||
<div class="addChipsEntry">
|
||||
<span class="addChipsCurrency">$</span>
|
||||
<input type="text" id="addChipsInput" inputmode="decimal" autocomplete="off" aria-label="Amount to add" />
|
||||
</div>
|
||||
<div class="addChipsActions">
|
||||
<button id="addChipsConfirmBtn" class="holdemBtn holdemBtnAccent" type="button">Add to stack</button>
|
||||
<button id="addChipsCancelBtn" class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rulingsOverlay" hidden>
|
||||
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
<div class="rulingsHeader">
|
||||
<h2 id="rulingsTitle">Common rulings</h2>
|
||||
<button id="closeRulingsBtn" class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
|
||||
</div>
|
||||
|
||||
<p class="rulingsIntro">
|
||||
The conventions most rooms follow, for the calls that come up mid-hand.
|
||||
Your house rules always win — and settle a dispute before the next hand,
|
||||
not during it.
|
||||
</p>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>Redeal it — misdeal</h3>
|
||||
<ul>
|
||||
<li>The first card goes to the wrong seat.</li>
|
||||
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
|
||||
<li>Two or more boxed cards turn up on the initial deal.</li>
|
||||
<li>Either of the first two cards is exposed by dealer error.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>A board card turned too early</h3>
|
||||
<p>
|
||||
If the fourth board card gets turned before the betting round is finished,
|
||||
that card is out of play for the round. Finish the betting, then burn and
|
||||
turn what would have been the fifth card in the fourth card's place.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="ruling">
|
||||
<h3>Acting out of turn</h3>
|
||||
<p>
|
||||
A one-off is usually just backed up if no one has acted behind them.
|
||||
Doing it repeatedly is what earns a penalty.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p class="rulingsSource">
|
||||
Follows the Tournament Directors Association rules, which most rooms and
|
||||
home games take their lead from.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="seatMenuOverlay" hidden>
|
||||
<div id="seatMenuSheet" role="dialog" aria-labelledby="seatMenuTitle">
|
||||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||||
@@ -296,6 +368,7 @@
|
||||
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/cardPicker.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/seatMenu.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/addChips.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/betting.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/actionBar.js?v=@ViewData["cssVersion"]"></script>
|
||||
<script src="~/js/casino/holdem/shortcuts.js?v=@ViewData["cssVersion"]"></script>
|
||||
|
||||
@@ -146,7 +146,7 @@ body {
|
||||
}
|
||||
|
||||
.tableHint {
|
||||
color: #888;
|
||||
color: #949494;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
@@ -223,6 +223,68 @@ body {
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* ---------- Bets on the felt ---------- */
|
||||
|
||||
/* Shares the seat ring's box exactly, so a bet always lands on the spoke between its
|
||||
player and the pot. When the seat ring gets inset on a phone, this moves with it —
|
||||
measured the alternative, and a bets ring on its own geometry ends up either behind
|
||||
the pucks or on top of the pot badge. */
|
||||
#betsLayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.betChip {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
padding: 0.1rem 0.42rem 0.1rem 0.2rem;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Reads as a chip seen from above: a lighter top-left, a rim, and the notches a
|
||||
casino chip has around its edge. */
|
||||
.betChipDisc {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||
box-shadow:
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.45),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.betChipAmount {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
color: #f0f0f0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Clinging to a puck rather than sitting on its own ring — the phone layout. */
|
||||
.betChipAnchored {
|
||||
left: 50%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.betChipAbove {
|
||||
top: -11px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.betChipBelow {
|
||||
top: auto;
|
||||
bottom: -11px;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
/* ---------- Seats (compact pucks on the table) ---------- */
|
||||
|
||||
.seat {
|
||||
@@ -295,7 +357,7 @@ body {
|
||||
}
|
||||
|
||||
.seatEmptyLabel {
|
||||
color: #6d6d6d;
|
||||
color: #8a8a8a;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
@@ -311,7 +373,7 @@ body {
|
||||
top: 2px;
|
||||
left: 6px;
|
||||
font-size: 0.58rem;
|
||||
color: #777;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.seatPuckName {
|
||||
@@ -516,13 +578,267 @@ body {
|
||||
|
||||
.cardSlot.cardSlotEmpty {
|
||||
background: #191919;
|
||||
color: #5c5c5c;
|
||||
color: #8a8a8a;
|
||||
border-style: dashed;
|
||||
border-color: #3f3f3f;
|
||||
box-shadow: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ---------- Add chips ---------- */
|
||||
|
||||
.seatBuyIns {
|
||||
margin-top: -0.3rem;
|
||||
color: #9a9a9a;
|
||||
font-size: 0.75rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#addChipsOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 98;
|
||||
}
|
||||
|
||||
#addChipsOverlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#addChipsSheet {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: linear-gradient(180deg, #242424, #1a1a1a);
|
||||
border: 1px solid #3a3a3a;
|
||||
border-bottom: none;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.addChipsHeader {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.addChipsHeader h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
#addChipsSubtitle {
|
||||
color: #9a9a9a;
|
||||
font-size: 0.78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#addChipsQuick {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.addChipsQuickBtn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
min-height: 52px;
|
||||
border-radius: 11px;
|
||||
border: 1px solid #3a3a3a;
|
||||
background: linear-gradient(180deg, #2e2e2e, #232323);
|
||||
color: #ddd;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.addChipsQuickBtn.selected {
|
||||
border-color: #d8b46a;
|
||||
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
||||
color: #f0d9a8;
|
||||
}
|
||||
|
||||
.addChipsQuickLabel {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.addChipsQuickAmount {
|
||||
font-size: 0.7rem;
|
||||
color: #a8a8a8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.addChipsQuickBtn.selected .addChipsQuickAmount {
|
||||
color: #d8b46a;
|
||||
}
|
||||
|
||||
.addChipsEntry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #141414;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 11px;
|
||||
padding: 0.2rem 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.addChipsEntry:focus-within {
|
||||
border-color: #d8b46a;
|
||||
}
|
||||
|
||||
.addChipsCurrency {
|
||||
color: #8a8a8a;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
#addChipsInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #f2f2f2;
|
||||
font-family: inherit;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
.addChipsActions {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.addChipsActions .holdemBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
/* ---------- Rulings reference ---------- */
|
||||
|
||||
.tableHintActions {
|
||||
margin: 0.35rem 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.linkBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #d8b46a;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.linkBtn:hover {
|
||||
color: #d8b46a;
|
||||
}
|
||||
|
||||
#rulingsOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 97;
|
||||
}
|
||||
|
||||
#rulingsOverlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#rulingsSheet {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(180deg, #242424, #1a1a1a);
|
||||
border: 1px solid #3a3a3a;
|
||||
border-bottom: none;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 0.35rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.rulingsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rulingsHeader h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.rulingsIntro {
|
||||
margin: 0 0 1rem;
|
||||
color: #9a9a9a;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ruling {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 0.7rem;
|
||||
border-left: 2px solid rgba(216, 180, 106, 0.4);
|
||||
}
|
||||
|
||||
.ruling h3 {
|
||||
margin: 0 0 0.35rem;
|
||||
color: #d8b46a;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.ruling p,
|
||||
.ruling li {
|
||||
color: #c4c4c4;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ruling p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ruling ul {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.ruling li + li {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.rulingsSource {
|
||||
margin: 0;
|
||||
padding-top: 0.6rem;
|
||||
border-top: 1px solid #2c2c2c;
|
||||
color: #8a8a8a;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ---------- Seat long-press menu ---------- */
|
||||
|
||||
/* Held down, before the menu opens — the only feedback that a press is being
|
||||
@@ -897,7 +1213,7 @@ body {
|
||||
|
||||
.subPanel summary {
|
||||
cursor: pointer;
|
||||
color: #8f8f8f;
|
||||
color: #9a9a9a;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
@@ -933,7 +1249,7 @@ body {
|
||||
|
||||
.fieldHint {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
color: #949494;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -990,7 +1306,7 @@ body {
|
||||
}
|
||||
|
||||
.standingsHeader {
|
||||
color: #777;
|
||||
color: #8a8a8a;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
@@ -1058,7 +1374,7 @@ body {
|
||||
|
||||
.handHistoryTime {
|
||||
margin-left: auto;
|
||||
color: #777;
|
||||
color: #8a8a8a;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
@@ -1178,7 +1494,7 @@ body.hasActionBar #holdemSidebar {
|
||||
flex: none;
|
||||
min-width: 3.1rem;
|
||||
text-align: right;
|
||||
color: #7d7d7d;
|
||||
color: #949494;
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
@@ -1416,7 +1732,7 @@ body.hasActionBar #holdemSidebar {
|
||||
|
||||
.seatPuckStack {
|
||||
font-size: 0.58rem;
|
||||
color: #8f8f8f;
|
||||
color: #9a9a9a;
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
@@ -1520,7 +1836,7 @@ body.hasActionBar #toastHost {
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
color: #949494;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
@@ -1734,7 +2050,8 @@ body.hasActionBar #toastHost {
|
||||
|
||||
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
||||
so half a puck of padding is exactly enough to stop the overhang. */
|
||||
#seatsLayer {
|
||||
#seatsLayer,
|
||||
#betsLayer {
|
||||
inset: 26px 44px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// "Add chips" — a rebuy, a top-up, or the first buy-in of the night.
|
||||
//
|
||||
// Quick amounts come off the table's starting stack, because a rebuy is almost
|
||||
// always one buy-in, sometimes half or two. The keypad is there for the times it
|
||||
// isn't. Recorded as cash in as well as chips, so at the end of the night the
|
||||
// dealer can say what each player actually handed over.
|
||||
const AddChips = {
|
||||
state: null,
|
||||
onConfirm: null,
|
||||
seatIndex: null,
|
||||
draft: "",
|
||||
|
||||
open(state, seatIndex, onConfirm) {
|
||||
this.state = state;
|
||||
this.seatIndex = seatIndex;
|
||||
this.onConfirm = onConfirm;
|
||||
this.draft = String(this.baseAmount());
|
||||
this.render();
|
||||
document.getElementById("addChipsOverlay").hidden = false;
|
||||
},
|
||||
|
||||
close() {
|
||||
document.getElementById("addChipsOverlay").hidden = true;
|
||||
},
|
||||
|
||||
// The table's starting stack if there is one, otherwise something sane to nudge
|
||||
// rather than an empty box.
|
||||
baseAmount() {
|
||||
const configured = Number(this.state.defaultStack);
|
||||
if (!Number.isNaN(configured) && configured > 0) return configured;
|
||||
const bb = Number(this.state.bigBlind) || 0;
|
||||
return bb > 0 ? bb * 100 : 100;
|
||||
},
|
||||
|
||||
quickAmounts() {
|
||||
const base = this.baseAmount();
|
||||
return [
|
||||
{ label: "½ buy-in", value: Math.round(base / 2 * 100) / 100 },
|
||||
{ label: "Buy-in", value: base },
|
||||
{ label: "2 buy-ins", value: base * 2 },
|
||||
];
|
||||
},
|
||||
|
||||
render() {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
document.getElementById("addChipsTitle").textContent =
|
||||
`Add chips — ${HoldemState.displayName(player, this.seatIndex)}`;
|
||||
|
||||
const stack = Betting.stackOf(this.state, player);
|
||||
const boughtIn = Number(player.buyIns) || 0;
|
||||
document.getElementById("addChipsSubtitle").textContent = boughtIn > 0
|
||||
? `${Money.format(stack)} in front · ${Money.format(boughtIn)} in so far`
|
||||
: `${Money.format(stack)} in front`;
|
||||
|
||||
const quick = document.getElementById("addChipsQuick");
|
||||
quick.innerHTML = "";
|
||||
this.quickAmounts().forEach(option => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "addChipsQuickBtn";
|
||||
btn.classList.toggle("selected", Number(this.draft) === option.value);
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "addChipsQuickLabel";
|
||||
label.textContent = option.label;
|
||||
const amount = document.createElement("span");
|
||||
amount.className = "addChipsQuickAmount";
|
||||
amount.textContent = Money.format(option.value);
|
||||
|
||||
btn.appendChild(label);
|
||||
btn.appendChild(amount);
|
||||
btn.addEventListener("click", () => {
|
||||
this.draft = String(option.value);
|
||||
this.render();
|
||||
});
|
||||
quick.appendChild(btn);
|
||||
});
|
||||
|
||||
const input = document.getElementById("addChipsInput");
|
||||
input.value = this.draft;
|
||||
},
|
||||
|
||||
confirm() {
|
||||
const amount = Math.max(0, Number(document.getElementById("addChipsInput").value) || 0);
|
||||
this.close();
|
||||
if (amount > 0) this.onConfirm(this.seatIndex, Math.round(amount * 100) / 100);
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = AddChips;
|
||||
}
|
||||
@@ -35,9 +35,19 @@ const HandHistoryExport = {
|
||||
if (standings.length > 0) {
|
||||
rows.push([]);
|
||||
rows.push(["Session Standings"]);
|
||||
rows.push(["Player", "Paid In", "Won", "Net"]);
|
||||
// Bought in and chips now are what a cash-out is settled from; paid in and
|
||||
// won describe the hands. Both belong in the file the dealer hands over.
|
||||
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down"]);
|
||||
standings.forEach(row => {
|
||||
rows.push([row.name, Money.format(row.in), Money.format(row.won), Money.formatSigned(row.net)]);
|
||||
rows.push([
|
||||
row.name,
|
||||
Money.format(row.in),
|
||||
Money.format(row.won),
|
||||
Money.formatSigned(row.net),
|
||||
Money.format(row.boughtIn || 0),
|
||||
Money.format(row.chipsNow || 0),
|
||||
Money.formatSigned(row.cashPosition || 0),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
},
|
||||
makeDealer: index => seatAction(index, `Button on ${nameOf(index)}.`, () => { state.dealerSeatIndex = index; }),
|
||||
editSeat: index => Render.toggleSeatEditor(index),
|
||||
|
||||
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
||||
seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
|
||||
// Chips in front and cash in are different questions — the stack moves
|
||||
// every hand, what they've handed over only moves when they buy.
|
||||
player.stack = Math.round((Betting.stackOf(state, player) + amount) * 100) / 100;
|
||||
player.buyIns = Math.round(((Number(player.buyIns) || 0) + amount) * 100) / 100;
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
Render.mount(state, hooks);
|
||||
@@ -260,6 +269,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
|
||||
|
||||
document.getElementById("addChipsConfirmBtn").addEventListener("click", () => AddChips.confirm());
|
||||
document.getElementById("addChipsCancelBtn").addEventListener("click", () => AddChips.close());
|
||||
document.getElementById("addChipsOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "addChipsOverlay") AddChips.close();
|
||||
});
|
||||
document.getElementById("addChipsInput").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") AddChips.confirm();
|
||||
});
|
||||
|
||||
const rulingsOverlay = document.getElementById("rulingsOverlay");
|
||||
const closeRulings = () => { rulingsOverlay.hidden = true; };
|
||||
document.getElementById("openRulingsBtn").addEventListener("click", () => { rulingsOverlay.hidden = false; });
|
||||
document.getElementById("closeRulingsBtn").addEventListener("click", closeRulings);
|
||||
rulingsOverlay.addEventListener("click", e => {
|
||||
if (e.target === rulingsOverlay) closeRulings();
|
||||
});
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Escape" && !rulingsOverlay.hidden) closeRulings();
|
||||
});
|
||||
|
||||
document.getElementById("seatMenuCancelBtn").addEventListener("click", () => SeatMenu.close());
|
||||
document.getElementById("seatMenuOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "seatMenuOverlay") SeatMenu.close();
|
||||
|
||||
@@ -69,13 +69,85 @@ const Render = {
|
||||
// ---- Seats around the table (compact pucks) ----
|
||||
|
||||
seatPosition(index, count) {
|
||||
return this.positionOnRing(index, count, 44, 42);
|
||||
},
|
||||
|
||||
// Bets sit on the felt between their player and the pot, the way chips actually
|
||||
// do — closer in than the seats, on the same spoke. The bets layer is inset to
|
||||
// match the seat ring (see the CSS), so this one radius holds at every size.
|
||||
betPosition(index, count) {
|
||||
return this.positionOnRing(index, count, 27, 25);
|
||||
},
|
||||
|
||||
positionOnRing(index, count, rx, ry) {
|
||||
const angle = -90 + (360 / count) * index;
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
const rx = 44;
|
||||
const ry = 42;
|
||||
const left = 50 + rx * Math.cos(radians);
|
||||
const top = 50 + ry * Math.sin(radians);
|
||||
return { left, top };
|
||||
return {
|
||||
left: 50 + rx * Math.cos(radians),
|
||||
top: 50 + ry * Math.sin(radians),
|
||||
};
|
||||
},
|
||||
|
||||
// Colour a bet by the biggest chip that fits it, so a table with a chip legend
|
||||
// gets bets that look like the chips actually being pushed out.
|
||||
chipColorFor(amount) {
|
||||
const legend = [...(this.state.chipLegend || [])]
|
||||
.filter(chip => Number(chip.value) > 0)
|
||||
.sort((a, b) => Number(b.value) - Number(a.value));
|
||||
const match = legend.find(chip => amount >= Number(chip.value));
|
||||
return (match || legend[legend.length - 1] || {}).color || "#8a8a8a";
|
||||
},
|
||||
|
||||
// ---- Bets on the felt ----
|
||||
|
||||
makeBetChip(bet) {
|
||||
const chip = document.createElement("div");
|
||||
chip.className = "betChip";
|
||||
|
||||
const disc = document.createElement("span");
|
||||
disc.className = "betChipDisc";
|
||||
disc.style.background = this.chipColorFor(bet);
|
||||
|
||||
const amount = document.createElement("span");
|
||||
amount.className = "betChipAmount";
|
||||
amount.textContent = Money.format(bet);
|
||||
|
||||
chip.appendChild(disc);
|
||||
chip.appendChild(amount);
|
||||
return chip;
|
||||
},
|
||||
|
||||
// A phone's felt has no room for a third ring: measured every radius between the
|
||||
// seats and the pot badge and each one collides with one or the other. So on a
|
||||
// narrow screen the bet clings to its own puck instead — same chip, no ring —
|
||||
// sitting on whichever side faces the middle of the table.
|
||||
renderBets() {
|
||||
const layer = document.getElementById("betsLayer");
|
||||
if (!layer) return;
|
||||
layer.innerHTML = "";
|
||||
|
||||
const anchorToSeat = window.matchMedia("(max-width: 700px)").matches;
|
||||
|
||||
this.state.seats.forEach((player, index) => {
|
||||
const bet = Betting.committed(player);
|
||||
if (bet <= 0) return;
|
||||
|
||||
const chip = this.makeBetChip(bet);
|
||||
const pos = this.betPosition(index, this.state.seatCount);
|
||||
|
||||
if (anchorToSeat) {
|
||||
const seat = document.querySelector(`.seat[data-seat-index="${index}"]`);
|
||||
if (!seat) return;
|
||||
const seatPos = this.seatPosition(index, this.state.seatCount);
|
||||
chip.classList.add("betChipAnchored", seatPos.top > 50 ? "betChipAbove" : "betChipBelow");
|
||||
seat.appendChild(chip);
|
||||
return;
|
||||
}
|
||||
|
||||
chip.style.left = `${pos.left}%`;
|
||||
chip.style.top = `${pos.top}%`;
|
||||
layer.appendChild(chip);
|
||||
});
|
||||
},
|
||||
|
||||
puckLabel(player, seatIndex) {
|
||||
@@ -118,6 +190,8 @@ const Render = {
|
||||
|
||||
layer.appendChild(node);
|
||||
});
|
||||
|
||||
this.renderBets();
|
||||
},
|
||||
|
||||
isRedSuit(card) {
|
||||
@@ -168,11 +242,13 @@ const Render = {
|
||||
span.textContent = this.formatCard(card);
|
||||
cardsEl.appendChild(span);
|
||||
});
|
||||
// What's in front of them this street reads as the live number during betting;
|
||||
// between hands it falls back to their total for the hand.
|
||||
const streetBet = Betting.committed(player);
|
||||
const total = streetBet > 0
|
||||
? streetBet
|
||||
// Always this player's total for the hand. What they've just pushed out on this
|
||||
// street is shown as a chip on the felt instead — the puck used to flip between
|
||||
// the two meanings depending on the moment, which is a bad thing for a number
|
||||
// sitting next to a stack size to do.
|
||||
const handTotal = Betting.handTotal(player);
|
||||
const total = handTotal > 0
|
||||
? handTotal
|
||||
: HoldemState.computeContribution(player, this.state.chipLegend, this.state.basePotAmount);
|
||||
node.querySelector(".seatPuckTotal").textContent = total > 0 ? Money.format(total) : "";
|
||||
|
||||
@@ -415,6 +491,13 @@ const Render = {
|
||||
});
|
||||
});
|
||||
|
||||
// What they've handed over tonight, shown next to what's in front of them so a
|
||||
// cash-out is one subtraction rather than a memory test.
|
||||
const boughtIn = Number(player.buyIns) || 0;
|
||||
const boughtInEl = form.querySelector(".seatBuyIns");
|
||||
boughtInEl.textContent = boughtIn > 0 ? `Bought in ${Money.format(boughtIn)}` : "";
|
||||
boughtInEl.hidden = boughtIn <= 0;
|
||||
|
||||
const stackInput = form.querySelector(".stackInput");
|
||||
stackInput.value = player.stack ?? "";
|
||||
stackInput.addEventListener("input", () => {
|
||||
|
||||
@@ -32,6 +32,10 @@ const SeatMenu = {
|
||||
actions.push({ label: "Un-fold", run: () => this.hooks.unfold(index) });
|
||||
}
|
||||
|
||||
// The most common interruption in a cash game, and until now it meant opening
|
||||
// the editor and retyping a stack from memory.
|
||||
actions.push({ label: "Add chips…", run: () => this.hooks.addChips(index) });
|
||||
|
||||
actions.push({
|
||||
label: dealtIn ? "Sit out" : "Deal in",
|
||||
run: () => this.hooks.toggleDealtIn(index),
|
||||
|
||||
@@ -35,7 +35,7 @@ const Shortcuts = {
|
||||
// An overlay owns the keyboard while it's up: the confirm dialog already binds
|
||||
// Enter and Escape, and the card picker and seat menu are modal.
|
||||
overlayOpen() {
|
||||
return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay"]
|
||||
return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay", "rulingsOverlay", "addChipsOverlay"]
|
||||
.some(id => {
|
||||
const el = document.getElementById(id);
|
||||
return el && !el.hidden;
|
||||
|
||||
@@ -31,6 +31,8 @@ const HoldemState = {
|
||||
stack: null,
|
||||
committed: 0,
|
||||
hasActed: false,
|
||||
// Cash handed over tonight, which only moves when they buy chips.
|
||||
buyIns: 0,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -79,6 +81,7 @@ const HoldemState = {
|
||||
if (player.stack === undefined) player.stack = null;
|
||||
if (player.committed === undefined) player.committed = 0;
|
||||
if (player.hasActed === undefined) player.hasActed = false;
|
||||
if (player.buyIns === undefined) player.buyIns = 0;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
@@ -236,13 +239,32 @@ const HoldemState = {
|
||||
});
|
||||
});
|
||||
|
||||
// Cash bought in and chips currently in front don't come from the hand log at
|
||||
// all, so they're read off the seat rather than summed — a rebuy isn't a hand.
|
||||
const seatById = new Map(state.seats.map(p => [p.id, p]));
|
||||
|
||||
return Array.from(totals.values())
|
||||
.map(t => ({
|
||||
...t,
|
||||
in: Math.round(t.in * 100) / 100,
|
||||
won: Math.round(t.won * 100) / 100,
|
||||
net: Math.round((t.won - t.in) * 100) / 100,
|
||||
}))
|
||||
.map(t => {
|
||||
const seat = seatById.get(t.id);
|
||||
const boughtIn = seat ? Math.round((Number(seat.buyIns) || 0) * 100) / 100 : 0;
|
||||
const chipsNow = seat ? Math.round((Number(seat.stack) || 0) * 100) / 100 : 0;
|
||||
// Chips already pushed into the hand in progress are still this
|
||||
// player's money until it's won. Leaving them out makes the cash
|
||||
// position disagree with the hand log for as long as a hand is open —
|
||||
// and blinds are posted the instant the previous one settles, so that
|
||||
// window is almost always.
|
||||
const inFrontOfThem = seat ? (Number(seat.amountOverride) || 0) + (Number(seat.committed) || 0) : 0;
|
||||
|
||||
return {
|
||||
...t,
|
||||
in: Math.round(t.in * 100) / 100,
|
||||
won: Math.round(t.won * 100) / 100,
|
||||
net: Math.round((t.won - t.in) * 100) / 100,
|
||||
boughtIn,
|
||||
chipsNow,
|
||||
cashPosition: Math.round((chipsNow + inFrontOfThem - boughtIn) * 100) / 100,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.net - a.net);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user