Hold'em dealer: five correctness fixes and four polish passes (replaces #33–#41) #42
@@ -11,6 +11,11 @@
|
|||||||
<p class="splitSubtitle">Chop a pot without setting up a table.</p>
|
<p class="splitSubtitle">Chop a pot without setting up a table.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div id="modeToggle" class="chipRow" role="group" aria-label="How the money went in">
|
||||||
|
<button type="button" id="modeEvenBtn" class="choiceChip selected">Everyone matched</button>
|
||||||
|
<button type="button" id="modeUnevenBtn" class="choiceChip">Someone's all-in</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="splitResult" aria-live="polite">
|
<div id="splitResult" aria-live="polite">
|
||||||
<div id="splitResultLabel">Each player gets</div>
|
<div id="splitResultLabel">Each player gets</div>
|
||||||
<div id="splitResultAmount">$0.00</div>
|
<div id="splitResultAmount">$0.00</div>
|
||||||
@@ -18,7 +23,18 @@
|
|||||||
<div id="splitDustNote" hidden></div>
|
<div id="splitDustNote" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="splitField">
|
<div id="unevenResult" hidden aria-live="polite">
|
||||||
|
<div id="unevenPots"></div>
|
||||||
|
<p id="unevenEmpty" class="unevenEmpty">Enter what each player put in.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="splitField unevenOnly" hidden>
|
||||||
|
<span class="splitLabel">What each player put in</span>
|
||||||
|
<div id="unevenRows"></div>
|
||||||
|
<button type="button" id="addPlayerBtn" class="splitBtn splitBtnGhost">+ Add player</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="splitField evenOnly">
|
||||||
<label for="potInput" class="splitLabel">Pot</label>
|
<label for="potInput" class="splitLabel">Pot</label>
|
||||||
<div class="potInputWrap">
|
<div class="potInputWrap">
|
||||||
<span class="potCurrency">$</span>
|
<span class="potCurrency">$</span>
|
||||||
@@ -28,12 +44,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="splitField">
|
<section class="splitField evenOnly">
|
||||||
<span class="splitLabel" id="waysLabel">Split between</span>
|
<span class="splitLabel" id="waysLabel">Split between</span>
|
||||||
<div id="waysChoices" class="chipRow" role="group" aria-labelledby="waysLabel"></div>
|
<div id="waysChoices" class="chipRow" role="group" aria-labelledby="waysLabel"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="splitField">
|
<section class="splitField evenOnly">
|
||||||
<span class="splitLabel" id="chipLabel">Smallest chip in play</span>
|
<span class="splitLabel" id="chipLabel">Smallest chip in play</span>
|
||||||
<div id="chipChoices" class="chipRow" role="group" aria-labelledby="chipLabel"></div>
|
<div id="chipChoices" class="chipRow" role="group" aria-labelledby="chipLabel"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -43,12 +59,30 @@
|
|||||||
<a href="/casino/texasholdem" class="splitBtn splitBtnGhost">Full dealer table →</a>
|
<a href="/casino/texasholdem" class="splitBtn splitBtnGhost">Full dealer table →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="splitFootnote">
|
<p class="splitFootnote evenOnly">
|
||||||
Uneven pots — someone all-in for less — need the side-pot walkthrough on the
|
If somebody was all-in for less than the others, switch to
|
||||||
<a href="/casino/texasholdem">full table</a>.
|
<strong>Someone's all-in</strong> above.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="splitFootnote unevenOnly" hidden>
|
||||||
|
Pots are listed with who can win each. Award them by hand strength, best hand
|
||||||
|
first. Nothing here is saved — the <a href="/casino/texasholdem">full table</a>
|
||||||
|
keeps a log.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template id="unevenRowTemplate">
|
||||||
|
<div class="unevenRow">
|
||||||
|
<span class="unevenRowLabel"></span>
|
||||||
|
<div class="unevenAmountWrap">
|
||||||
|
<span class="unevenCurrency">$</span>
|
||||||
|
<input type="text" class="unevenAmount" inputmode="decimal" autocomplete="off" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<button type="button" class="unevenFoldBtn" aria-pressed="false">Folded</button>
|
||||||
|
<button type="button" class="unevenRemoveBtn" aria-label="Remove player">✕</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
@section Styles {
|
@section Styles {
|
||||||
<link href="/css/casino/quicksplit.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
<link href="/css/casino/quicksplit.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||||
}
|
}
|
||||||
@@ -56,5 +90,6 @@
|
|||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
||||||
<script src="~/js/casino/holdem/splitCalculator.js?v=@ViewData["cssVersion"]"></script>
|
<script src="~/js/casino/holdem/splitCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||||
|
<script src="~/js/casino/holdem/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||||||
<script src="~/js/casino/holdem/quickSplit.js?v=@ViewData["cssVersion"]"></script>
|
<script src="~/js/casino/holdem/quickSplit.js?v=@ViewData["cssVersion"]"></script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableHint {
|
.tableHint {
|
||||||
color: #888;
|
color: #949494;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@@ -357,7 +357,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seatEmptyLabel {
|
.seatEmptyLabel {
|
||||||
color: #6d6d6d;
|
color: #8a8a8a;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -373,7 +373,7 @@ body {
|
|||||||
top: 2px;
|
top: 2px;
|
||||||
left: 6px;
|
left: 6px;
|
||||||
font-size: 0.58rem;
|
font-size: 0.58rem;
|
||||||
color: #777;
|
color: #8a8a8a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatPuckName {
|
.seatPuckName {
|
||||||
@@ -578,7 +578,7 @@ body {
|
|||||||
|
|
||||||
.cardSlot.cardSlotEmpty {
|
.cardSlot.cardSlotEmpty {
|
||||||
background: #191919;
|
background: #191919;
|
||||||
color: #5c5c5c;
|
color: #8a8a8a;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
border-color: #3f3f3f;
|
border-color: #3f3f3f;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -694,7 +694,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.addChipsCurrency {
|
.addChipsCurrency {
|
||||||
color: #777;
|
color: #8a8a8a;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,7 +735,7 @@ body {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #a9782f;
|
color: #d8b46a;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -834,7 +834,7 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 0.6rem;
|
padding-top: 0.6rem;
|
||||||
border-top: 1px solid #2c2c2c;
|
border-top: 1px solid #2c2c2c;
|
||||||
color: #7a7a7a;
|
color: #8a8a8a;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
@@ -1213,7 +1213,7 @@ body {
|
|||||||
|
|
||||||
.subPanel summary {
|
.subPanel summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #8f8f8f;
|
color: #9a9a9a;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1249,7 +1249,7 @@ body {
|
|||||||
|
|
||||||
.fieldHint {
|
.fieldHint {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: #949494;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -1306,7 +1306,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.standingsHeader {
|
.standingsHeader {
|
||||||
color: #777;
|
color: #8a8a8a;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1374,7 +1374,7 @@ body {
|
|||||||
|
|
||||||
.handHistoryTime {
|
.handHistoryTime {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #777;
|
color: #8a8a8a;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1480,6 +1480,12 @@ body.hasActionBar #holdemSidebar {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionRunout {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #d8b46a;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actionWho {
|
.actionWho {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #9c9c9c;
|
color: #9c9c9c;
|
||||||
@@ -1494,7 +1500,7 @@ body.hasActionBar #holdemSidebar {
|
|||||||
flex: none;
|
flex: none;
|
||||||
min-width: 3.1rem;
|
min-width: 3.1rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: #7d7d7d;
|
color: #949494;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -1732,7 +1738,7 @@ body.hasActionBar #holdemSidebar {
|
|||||||
|
|
||||||
.seatPuckStack {
|
.seatPuckStack {
|
||||||
font-size: 0.58rem;
|
font-size: 0.58rem;
|
||||||
color: #8f8f8f;
|
color: #9a9a9a;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
@@ -1836,7 +1842,7 @@ body.hasActionBar #toastHost {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #888;
|
color: #949494;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1962,20 +1968,75 @@ body.hasActionBar #toastHost {
|
|||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* This is the screen where the money gets decided, so it holds the same 44px+
|
||||||
|
targets as the rest of the tool rather than the smallest ones in it. Picking the
|
||||||
|
wrong winner is the one mistake here that costs somebody real money. */
|
||||||
.potWizardChoice {
|
.potWizardChoice {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.7rem;
|
||||||
|
min-height: 56px;
|
||||||
background: #141414;
|
background: #141414;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
padding: 0.5rem 0.7rem;
|
padding: 0.6rem 0.8rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.95rem;
|
||||||
|
transition: border-color 0.12s ease, background 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.potWizardChoice:hover {
|
.potWizardChoice:hover {
|
||||||
border-color: #555;
|
border-color: #555;
|
||||||
|
background: #191919;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoice input {
|
||||||
|
flex: none;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
accent-color: #1e8449;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected has to be obvious at a glance — a dealer confirms this with players
|
||||||
|
watching, and a checkbox alone is easy to misread across a table. */
|
||||||
|
.potWizardChoice.chosen {
|
||||||
|
border-color: #1e8449;
|
||||||
|
background: linear-gradient(180deg, #16301f, #12241a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoice.chosen .potWizardChoiceName {
|
||||||
|
color: #d6ffe0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoiceText {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoiceName {
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoiceUpTo {
|
||||||
|
color: #9a9a9a;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoiceCards {
|
||||||
|
flex: none;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoiceCards .cardRed {
|
||||||
|
color: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.potWizardSummary ul {
|
.potWizardSummary ul {
|
||||||
@@ -1996,6 +2057,37 @@ body.hasActionBar #toastHost {
|
|||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Every button in this modal is one a dealer presses with players waiting. */
|
||||||
|
#potWizardBody .holdemBtn {
|
||||||
|
min-height: 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The confirm is the whole point of the step, so it gets the whole width rather
|
||||||
|
than being sized to the length of its own label. */
|
||||||
|
.potWizardRound > .holdemBtn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardActions .holdemBtn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardSummary li {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
|
||||||
|
.potWizardActions {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.potWizardChoice {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
#holdemLayout {
|
#holdemLayout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -2140,11 +2232,10 @@ body.hasActionBar #toastHost {
|
|||||||
whole phone width to work with, so everything grows to thumb size — and
|
whole phone width to work with, so everything grows to thumb size — and
|
||||||
every field goes to 16px, below which iOS zooms the page on focus and the
|
every field goes to 16px, below which iOS zooms the page on focus and the
|
||||||
dealer has to pinch back out mid-hand. */
|
dealer has to pinch back out mid-hand. */
|
||||||
.seatEditorForm .seatNameInput,
|
/* Every text or number field in the form, rather than a list of the ones that
|
||||||
.seatEditorForm .amountOverrideInput,
|
existed when this was written. The stack field was added later and sat at 28px
|
||||||
.seatEditorForm .chipCountInput,
|
next to its 44px neighbours precisely because it wasn't on that list. */
|
||||||
.seatEditorForm .customFieldLabel,
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]) {
|
||||||
.seatEditorForm .customFieldValue {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 0.4rem 0.6rem;
|
padding: 0.4rem 0.6rem;
|
||||||
@@ -2208,6 +2299,77 @@ body.hasActionBar #toastHost {
|
|||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The sidebar's own controls never got the touch pass the seat form did — these
|
||||||
|
are settings a dealer changes at the table, on the same phone. Written against
|
||||||
|
the panel rather than each field, so a setting added later inherits it. */
|
||||||
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
||||||
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
||||||
|
.panelBody select {
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelBody .holdemBtnSmall,
|
||||||
|
.handHistoryActions .holdemBtn {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destructive and tiny is the worst combination: these delete a chip colour or a
|
||||||
|
custom field, and were 28px wide. */
|
||||||
|
.removeChipColorBtn,
|
||||||
|
.removeFieldBtn {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* flex-shrink would take a stated width straight back off again in a row this
|
||||||
|
narrow, which is why it stayed 32px wide with the height applied. */
|
||||||
|
.chipColorInput {
|
||||||
|
flex: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A bare checkbox can't grow into a target, but the row it sits in can — the
|
||||||
|
label is what a tap actually lands on. */
|
||||||
|
.panelBody .fieldRow {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A bare checkbox with no label box around it to grow. */
|
||||||
|
#dedicatedDealerToggle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
||||||
|
like a button. */
|
||||||
|
.linkBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The dealer button has to stay a 22px disc — it's a chip sitting on a seat, and
|
||||||
|
drawing it thumb-sized would swallow the puck. So the disc keeps its size and
|
||||||
|
an invisible pad around it takes the tap. It reaches outward, away from the
|
||||||
|
puck's own tap target, so pressing the seat still opens the seat. */
|
||||||
|
.dealerChip::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealerChip {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.toastAction {
|
.toastAction {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,171 @@ body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Mode toggle ---------- */
|
||||||
|
|
||||||
|
#modeToggle {
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Uneven amounts ---------- */
|
||||||
|
|
||||||
|
#unevenRows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenRowLabel {
|
||||||
|
flex: none;
|
||||||
|
width: 2rem;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenAmountWrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenAmountWrap:focus-within {
|
||||||
|
border-color: #d8b46a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenCurrency {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenAmount {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenFoldBtn {
|
||||||
|
flex: none;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
background: #232323;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenFoldBtn.selected {
|
||||||
|
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
||||||
|
border-color: #5b2626;
|
||||||
|
color: #ffd6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenRemoveBtn {
|
||||||
|
flex: none;
|
||||||
|
width: 30px;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #6a6a6a;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenRemoveBtn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unevenResult {
|
||||||
|
background: linear-gradient(160deg, #1f2a21, #161c17);
|
||||||
|
border: 1px solid #2c3a2f;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unevenResult[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPot {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPot + .unevenPot {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotLabel {
|
||||||
|
flex: none;
|
||||||
|
color: #d8b46a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotAmount {
|
||||||
|
flex: none;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotWho {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
color: #9a9a9a;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotReturn .unevenPotLabel,
|
||||||
|
.unevenPotReturn .unevenPotAmount {
|
||||||
|
color: #9a9a9a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotTotal {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.14) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenPotTotal .unevenPotLabel {
|
||||||
|
color: #8f9d92;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unevenEmpty {
|
||||||
|
margin: 0;
|
||||||
|
color: #8f9d92;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- The answer ---------- */
|
/* ---------- The answer ---------- */
|
||||||
|
|
||||||
#splitResult {
|
#splitResult {
|
||||||
@@ -104,6 +269,12 @@ body {
|
|||||||
margin-bottom: 1.35rem;
|
margin-bottom: 1.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A `display` declaration outranks the browser's own [hidden] rule, so anything
|
||||||
|
given one has to opt back out explicitly or toggling `hidden` does nothing. */
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.splitLabel {
|
.splitLabel {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ const ActionBar = {
|
|||||||
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
||||||
row.appendChild(players);
|
row.appendChild(players);
|
||||||
|
|
||||||
|
// Nobody is being asked to act because nobody can — say so, or the bar just
|
||||||
|
// looks like it's waiting for something.
|
||||||
|
if (Betting.isRunningOut(this.state) && !Betting.isHandOver(this.state)) {
|
||||||
|
const runout = document.createElement("span");
|
||||||
|
runout.className = "actionRunout";
|
||||||
|
const stillIn = Betting.liveSeats(this.state).length;
|
||||||
|
runout.textContent = `All-in — running out the board (${stillIn} players)`;
|
||||||
|
row.appendChild(runout);
|
||||||
|
}
|
||||||
|
|
||||||
const index = this.state.actingSeatIndex;
|
const index = this.state.actingSeatIndex;
|
||||||
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
|
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
|
||||||
const player = this.state.seats[index];
|
const player = this.state.seats[index];
|
||||||
|
|||||||
@@ -70,7 +70,19 @@ const Betting = {
|
|||||||
for (let step = 1; step <= count; step++) {
|
for (let step = 1; step <= count; step++) {
|
||||||
const index = this.seatAt(state, fromIndex, step);
|
const index = this.seatAt(state, fromIndex, step);
|
||||||
const player = state.seats[index];
|
const player = state.seats[index];
|
||||||
if (player.inHand && !player.folded && this.stackOf(state, player) >= 0) return index;
|
if (player.inHand && !player.folded) return index;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Who the dealer asks next. A player who is all-in is still in the hand and still
|
||||||
|
// wins pots, but has no decision left, so the action passes over them.
|
||||||
|
nextToAct(state, fromIndex) {
|
||||||
|
const count = state.seatCount;
|
||||||
|
for (let step = 1; step <= count; step++) {
|
||||||
|
const index = this.seatAt(state, fromIndex, step);
|
||||||
|
const player = state.seats[index];
|
||||||
|
if (player.inHand && !player.folded && this.stackOf(state, player) > this.EPSILON_CHIPS) return index;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -156,7 +168,7 @@ const Betting = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
passAction(state, index) {
|
passAction(state, index) {
|
||||||
state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextLiveIndex(state, index);
|
state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextToAct(state, index);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Street and hand boundaries ----
|
// ---- Street and hand boundaries ----
|
||||||
@@ -184,15 +196,52 @@ const Betting = {
|
|||||||
|
|
||||||
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
|
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
|
||||||
state.street = this.STREETS[nextStreetIndex] || "river";
|
state.street = this.STREETS[nextStreetIndex] || "river";
|
||||||
state.actingSeatIndex = this.liveSeats(state).length > 1
|
|
||||||
? this.nextLiveIndex(state, state.dealerSeatIndex)
|
// It takes two players with chips to have a bet. Once everyone left is all-in
|
||||||
: null;
|
// the rest of the board is just dealt out.
|
||||||
|
if (!this.canStillBet(state)) {
|
||||||
|
state.actingSeatIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the flop the first live player left of the button acts — which heads-up
|
||||||
|
// is the big blind, since the button is the small blind. Naming the big blind
|
||||||
|
// directly also covers a button parked on a seat nobody is sitting in, and it
|
||||||
|
// still has to be someone with chips left to bet.
|
||||||
|
const { bbIndex, headsUp } = HoldemState.getBlindSeats(state);
|
||||||
|
const bigBlindCanAct =
|
||||||
|
headsUp &&
|
||||||
|
bbIndex !== null &&
|
||||||
|
!state.seats[bbIndex].folded &&
|
||||||
|
this.stackOf(state, state.seats[bbIndex]) > this.EPSILON_CHIPS;
|
||||||
|
|
||||||
|
state.actingSeatIndex = bigBlindCanAct
|
||||||
|
? bbIndex
|
||||||
|
: this.nextToAct(state, state.dealerSeatIndex);
|
||||||
},
|
},
|
||||||
|
|
||||||
isHandOver(state) {
|
isHandOver(state) {
|
||||||
return this.liveSeats(state).length <= 1;
|
return this.liveSeats(state).length <= 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Players still in the hand who have chips left to put in. Fewer than two of them
|
||||||
|
// means the betting is finished even though the hand isn't.
|
||||||
|
seatsWithChips(state) {
|
||||||
|
return this.liveSeats(state).filter(s => this.stackOf(state, s.player) > this.EPSILON_CHIPS);
|
||||||
|
},
|
||||||
|
|
||||||
|
EPSILON_CHIPS: 0.004,
|
||||||
|
|
||||||
|
canStillBet(state) {
|
||||||
|
return this.liveSeats(state).length > 1 && this.seatsWithChips(state).length > 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// True when the hand is still live but nobody can bet again — the dealer is just
|
||||||
|
// running the board out.
|
||||||
|
isRunningOut(state) {
|
||||||
|
return this.liveSeats(state).length > 1 && !this.canStillBet(state);
|
||||||
|
},
|
||||||
|
|
||||||
// Deals a fresh betting round: blinds posted, action to the seat after the big
|
// Deals a fresh betting round: blinds posted, action to the seat after the big
|
||||||
// blind, which is where a dealer expects to be looking when the cards go out.
|
// blind, which is where a dealer expects to be looking when the cards go out.
|
||||||
beginHand(state) {
|
beginHand(state) {
|
||||||
@@ -207,15 +256,26 @@ const Betting = {
|
|||||||
|
|
||||||
const sb = Number(state.smallBlind) || 0;
|
const sb = Number(state.smallBlind) || 0;
|
||||||
const bb = Number(state.bigBlind) || 0;
|
const bb = Number(state.bigBlind) || 0;
|
||||||
const sbIndex = this.seatAt(state, state.dealerSeatIndex, 1);
|
const { sbIndex, bbIndex, headsUp } = HoldemState.getBlindSeats(state);
|
||||||
const bbIndex = this.seatAt(state, state.dealerSeatIndex, 2);
|
|
||||||
|
|
||||||
if (sb > 0 && state.seats[sbIndex].inHand) this.putIn(state, sbIndex, sb);
|
if (sbIndex === null || bbIndex === null) {
|
||||||
if (bb > 0 && state.seats[bbIndex].inHand) this.putIn(state, bbIndex, bb);
|
state.currentBet = 0;
|
||||||
|
state.actingSeatIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sb > 0) this.putIn(state, sbIndex, sb);
|
||||||
|
if (bb > 0) this.putIn(state, bbIndex, bb);
|
||||||
|
|
||||||
state.currentBet = this.round(Math.max(this.committed(state.seats[sbIndex]), this.committed(state.seats[bbIndex])));
|
state.currentBet = this.round(Math.max(this.committed(state.seats[sbIndex]), this.committed(state.seats[bbIndex])));
|
||||||
state.lastRaiseSize = bb || 0;
|
state.lastRaiseSize = bb || 0;
|
||||||
state.actingSeatIndex = this.nextLiveIndex(state, bbIndex);
|
|
||||||
|
// Heads-up the button is the small blind and acts first before the flop; with
|
||||||
|
// three or more it's the seat after the big blind. Either way, a blind big
|
||||||
|
// enough to put that player all-in leaves them nothing to decide.
|
||||||
|
state.actingSeatIndex = headsUp
|
||||||
|
? (this.stackOf(state, state.seats[sbIndex]) > this.EPSILON_CHIPS ? sbIndex : this.nextToAct(state, sbIndex))
|
||||||
|
: this.nextToAct(state, bbIndex);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Winners get their share put back in front of them.
|
// Winners get their share put back in front of them.
|
||||||
|
|||||||
@@ -183,10 +183,38 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById("seatCountInput").addEventListener("change", e => {
|
// A seat that has anything of a player in it — a name, chips, cash in, or a hand
|
||||||
HoldemState.resizeSeats(state, Number(e.target.value));
|
// in progress. Shrinking the table throws these away permanently, and it also
|
||||||
hooks.save(state);
|
// orphans them in the session standings, where they keep their winnings but lose
|
||||||
Render.renderAll();
|
// the stack and buy-in the settle-up is worked out from.
|
||||||
|
const seatHasSomeoneIn = player =>
|
||||||
|
!!(player.name && player.name.trim()) ||
|
||||||
|
player.inHand ||
|
||||||
|
(Number(player.stack) || 0) > 0 ||
|
||||||
|
(Number(player.buyIns) || 0) > 0 ||
|
||||||
|
(Number(player.amountOverride) || 0) > 0;
|
||||||
|
|
||||||
|
document.getElementById("seatCountInput").addEventListener("change", async e => {
|
||||||
|
const requested = Math.max(2, Math.min(24, Math.floor(Number(e.target.value)) || state.seatCount));
|
||||||
|
const losing = state.seats.slice(requested).filter(seatHasSomeoneIn);
|
||||||
|
|
||||||
|
if (losing.length > 0) {
|
||||||
|
const who = losing
|
||||||
|
.map((p, i) => HoldemState.displayName(p, requested + state.seats.slice(requested).indexOf(p)))
|
||||||
|
.join(", ");
|
||||||
|
const ok = await Dialog.confirm({
|
||||||
|
title: `Remove ${losing.length} ${losing.length === 1 ? "player" : "players"}?`,
|
||||||
|
message: `Going down to ${requested} seats takes ${who} off the table along with their chips and buy-ins, and their session totals stop adding up.`,
|
||||||
|
confirmLabel: "Remove them",
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
e.target.value = state.seatCount;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runUndoable(`Table set to ${requested} seats.`, () => HoldemState.resizeSeats(state, requested));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("directionSelect").addEventListener("change", e => {
|
document.getElementById("directionSelect").addEventListener("change", e => {
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ const PotWizard = {
|
|||||||
this.playerLabelById = new Map(
|
this.playerLabelById = new Map(
|
||||||
players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))])
|
players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))])
|
||||||
);
|
);
|
||||||
|
this.seatIndexById = new Map(players.map(p => [p.id, state.seats.indexOf(p)]));
|
||||||
|
// The label carries name and cards together for the log and the payout lines;
|
||||||
|
// the choice rows want them separately so each can be styled and read on its own.
|
||||||
|
this.nameById = new Map(
|
||||||
|
players.map(p => [p.id, HoldemState.displayName(p, state.seats.indexOf(p))])
|
||||||
|
);
|
||||||
|
this.cardsById = new Map(
|
||||||
|
players.map(p => [p.id, (p.cards || []).filter(Boolean)])
|
||||||
|
);
|
||||||
const contributions = players.map(p => ({
|
const contributions = players.map(p => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||||
@@ -49,6 +58,20 @@ const PotWizard = {
|
|||||||
this.totals.set(id, (this.totals.get(id) || 0) + amount);
|
this.totals.set(id, (this.totals.get(id) || 0) + amount);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// A chopped pot rarely divides exactly, and the odd chip goes to the first player
|
||||||
|
// left of the button — not to whoever the dealer happened to tick first. The
|
||||||
|
// calculator hands leftovers out in the order it's given the winners, so ordering
|
||||||
|
// them here is what makes the payout match the rule.
|
||||||
|
orderFromButton(ids) {
|
||||||
|
const distance = new Map();
|
||||||
|
for (let step = 1; step <= this.state.seatCount; step++) {
|
||||||
|
distance.set(HoldemState.nextSeatIndex(this.state, this.state.dealerSeatIndex, step), step);
|
||||||
|
}
|
||||||
|
return [...ids].sort((a, b) =>
|
||||||
|
(distance.get(this.seatIndexById.get(a)) ?? 0) - (distance.get(this.seatIndexById.get(b)) ?? 0)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
renderStep() {
|
renderStep() {
|
||||||
// Auto-resolve any round where only one player is left standing — nothing to ask.
|
// Auto-resolve any round where only one player is left standing — nothing to ask.
|
||||||
let contenders = PotCalculator.getContenders(this.entries);
|
let contenders = PotCalculator.getContenders(this.entries);
|
||||||
@@ -105,13 +128,46 @@ const PotWizard = {
|
|||||||
contenders.forEach(c => {
|
contenders.forEach(c => {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.className = "potWizardChoice";
|
label.className = "potWizardChoice";
|
||||||
|
|
||||||
const checkbox = document.createElement("input");
|
const checkbox = document.createElement("input");
|
||||||
checkbox.type = "checkbox";
|
checkbox.type = "checkbox";
|
||||||
checkbox.value = c.id;
|
checkbox.value = c.id;
|
||||||
const text = document.createElement("span");
|
// The row is the target, not the box — so the row has to look chosen too.
|
||||||
text.textContent = `${this.playerLabelById.get(c.id)} — up to ${Money.format(c.remaining)}`;
|
checkbox.addEventListener("change", () => {
|
||||||
|
label.classList.toggle("chosen", checkbox.checked);
|
||||||
|
});
|
||||||
label.appendChild(checkbox);
|
label.appendChild(checkbox);
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.className = "potWizardChoiceText";
|
||||||
|
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.className = "potWizardChoiceName";
|
||||||
|
name.textContent = this.nameById.get(c.id);
|
||||||
|
text.appendChild(name);
|
||||||
|
|
||||||
|
const upTo = document.createElement("span");
|
||||||
|
upTo.className = "potWizardChoiceUpTo";
|
||||||
|
upTo.textContent = `up to ${Money.format(c.remaining)}`;
|
||||||
|
text.appendChild(upTo);
|
||||||
label.appendChild(text);
|
label.appendChild(text);
|
||||||
|
|
||||||
|
// Cards sit apart from the name rather than inside it, so a dealer reading
|
||||||
|
// a row across the table sees the hand, not a sentence.
|
||||||
|
const cards = this.cardsById.get(c.id) || [];
|
||||||
|
if (cards.length > 0) {
|
||||||
|
const cardsEl = document.createElement("span");
|
||||||
|
cardsEl.className = "potWizardChoiceCards";
|
||||||
|
cards.forEach((card, i) => {
|
||||||
|
if (i > 0) cardsEl.appendChild(document.createTextNode(" "));
|
||||||
|
const span = document.createElement("span");
|
||||||
|
if (/[HD]$/i.test(card)) span.className = "cardRed";
|
||||||
|
span.textContent = Render.formatCard(card);
|
||||||
|
cardsEl.appendChild(span);
|
||||||
|
});
|
||||||
|
label.appendChild(cardsEl);
|
||||||
|
}
|
||||||
|
|
||||||
choices.appendChild(label);
|
choices.appendChild(label);
|
||||||
});
|
});
|
||||||
round.appendChild(choices);
|
round.appendChild(choices);
|
||||||
@@ -127,7 +183,7 @@ const PotWizard = {
|
|||||||
confirmBtn.addEventListener("click", () => {
|
confirmBtn.addEventListener("click", () => {
|
||||||
const selected = Array.from(choices.querySelectorAll("input:checked")).map(i => i.value);
|
const selected = Array.from(choices.querySelectorAll("input:checked")).map(i => i.value);
|
||||||
if (selected.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
const result = PotCalculator.resolveRound(this.entries, selected);
|
const result = PotCalculator.resolveRound(this.entries, this.orderFromButton(selected));
|
||||||
this.recordRound(result);
|
this.recordRound(result);
|
||||||
this.entries = result.updatedEntries;
|
this.entries = result.updatedEntries;
|
||||||
this.renderStep();
|
this.renderStep();
|
||||||
|
|||||||
@@ -15,12 +15,28 @@ const QuickSplit = {
|
|||||||
ways: 2,
|
ways: 2,
|
||||||
chip: 1,
|
chip: 1,
|
||||||
|
|
||||||
|
// "even" is the common case — one pot, everyone matched. "uneven" is the one this
|
||||||
|
// screen used to send people back to the full table for: somebody all-in for less,
|
||||||
|
// so the money splits into a main pot and side pots.
|
||||||
|
mode: "even",
|
||||||
|
entries: [],
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
this.potInput = document.getElementById("potInput");
|
this.potInput = document.getElementById("potInput");
|
||||||
this.clearPotBtn = document.getElementById("clearPotBtn");
|
this.clearPotBtn = document.getElementById("clearPotBtn");
|
||||||
|
this.entries = [{ amount: "", folded: false }, { amount: "", folded: false }, { amount: "", folded: false }];
|
||||||
|
|
||||||
this.renderWayChoices();
|
this.renderWayChoices();
|
||||||
this.renderChipChoices();
|
this.renderChipChoices();
|
||||||
|
this.renderUnevenRows();
|
||||||
|
|
||||||
|
document.getElementById("modeEvenBtn").addEventListener("click", () => this.setMode("even"));
|
||||||
|
document.getElementById("modeUnevenBtn").addEventListener("click", () => this.setMode("uneven"));
|
||||||
|
document.getElementById("addPlayerBtn").addEventListener("click", () => {
|
||||||
|
this.entries.push({ amount: "", folded: false });
|
||||||
|
this.renderUnevenRows();
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
this.potInput.addEventListener("input", () => {
|
this.potInput.addEventListener("input", () => {
|
||||||
this.sanitizePotInput();
|
this.sanitizePotInput();
|
||||||
@@ -127,7 +143,112 @@ const QuickSplit = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMode(mode) {
|
||||||
|
this.mode = mode;
|
||||||
|
document.getElementById("modeEvenBtn").classList.toggle("selected", mode === "even");
|
||||||
|
document.getElementById("modeUnevenBtn").classList.toggle("selected", mode === "uneven");
|
||||||
|
|
||||||
|
document.querySelectorAll(".evenOnly").forEach(el => { el.hidden = mode !== "even"; });
|
||||||
|
document.querySelectorAll(".unevenOnly").forEach(el => { el.hidden = mode !== "uneven"; });
|
||||||
|
document.getElementById("splitResult").hidden = mode !== "even";
|
||||||
|
document.getElementById("unevenResult").hidden = mode !== "uneven";
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUnevenRows() {
|
||||||
|
const host = document.getElementById("unevenRows");
|
||||||
|
const template = document.getElementById("unevenRowTemplate");
|
||||||
|
host.innerHTML = "";
|
||||||
|
|
||||||
|
this.entries.forEach((entry, index) => {
|
||||||
|
const row = template.content.firstElementChild.cloneNode(true);
|
||||||
|
row.querySelector(".unevenRowLabel").textContent = `P${index + 1}`;
|
||||||
|
|
||||||
|
const amount = row.querySelector(".unevenAmount");
|
||||||
|
amount.value = entry.amount;
|
||||||
|
amount.setAttribute("aria-label", `Player ${index + 1} amount`);
|
||||||
|
amount.addEventListener("input", () => {
|
||||||
|
entry.amount = amount.value.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||||
|
if (entry.amount !== amount.value) amount.value = entry.amount;
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folded money stays in the pot as dead money, but its owner can't win any
|
||||||
|
// of it — which is exactly the distinction that makes side pots tricky.
|
||||||
|
const fold = row.querySelector(".unevenFoldBtn");
|
||||||
|
fold.classList.toggle("selected", entry.folded);
|
||||||
|
fold.setAttribute("aria-pressed", String(entry.folded));
|
||||||
|
fold.addEventListener("click", () => {
|
||||||
|
entry.folded = !entry.folded;
|
||||||
|
this.renderUnevenRows();
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = row.querySelector(".unevenRemoveBtn");
|
||||||
|
remove.disabled = this.entries.length <= 2;
|
||||||
|
remove.addEventListener("click", () => {
|
||||||
|
this.entries.splice(index, 1);
|
||||||
|
this.renderUnevenRows();
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
host.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUneven() {
|
||||||
|
const players = this.entries
|
||||||
|
.map((entry, index) => ({ id: `P${index + 1}`, contribution: Number(entry.amount) || 0, folded: entry.folded }))
|
||||||
|
.filter(p => p.contribution > 0);
|
||||||
|
|
||||||
|
const pots = PotCalculator.layers(players);
|
||||||
|
const returns = PotCalculator.uncalledReturns(players);
|
||||||
|
|
||||||
|
const host = document.getElementById("unevenPots");
|
||||||
|
const empty = document.getElementById("unevenEmpty");
|
||||||
|
host.innerHTML = "";
|
||||||
|
|
||||||
|
if (pots.length === 0 && returns.length === 0) {
|
||||||
|
empty.hidden = false;
|
||||||
|
empty.textContent = players.length === 0
|
||||||
|
? "Enter what each player put in."
|
||||||
|
: "Nobody left who can win — un-fold whoever should take it.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
empty.hidden = true;
|
||||||
|
|
||||||
|
const addRow = (label, amount, who, className) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = `unevenPot ${className || ""}`;
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.className = "unevenPotLabel";
|
||||||
|
name.textContent = label;
|
||||||
|
const value = document.createElement("span");
|
||||||
|
value.className = "unevenPotAmount";
|
||||||
|
value.textContent = Money.format(amount);
|
||||||
|
const eligible = document.createElement("span");
|
||||||
|
eligible.className = "unevenPotWho";
|
||||||
|
eligible.textContent = who;
|
||||||
|
row.appendChild(name);
|
||||||
|
row.appendChild(value);
|
||||||
|
row.appendChild(eligible);
|
||||||
|
host.appendChild(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
pots.forEach(pot => addRow(pot.label, pot.amount, pot.eligible.join(", ")));
|
||||||
|
returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${ret.id}`, "unevenPotReturn"));
|
||||||
|
|
||||||
|
const total = players.reduce((sum, p) => sum + p.contribution, 0);
|
||||||
|
addRow("On the table", Math.round(total * 100) / 100, "", "unevenPotTotal");
|
||||||
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
if (this.mode === "uneven") {
|
||||||
|
this.updateUneven();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const potValue = Number(this.potInput.value);
|
const potValue = Number(this.potInput.value);
|
||||||
const pot = Number.isNaN(potValue) ? 0 : potValue;
|
const pot = Number.isNaN(potValue) ? 0 : potValue;
|
||||||
this.clearPotBtn.hidden = this.potInput.value === "";
|
this.clearPotBtn.hidden = this.potInput.value === "";
|
||||||
|
|||||||
@@ -159,14 +159,53 @@ const HoldemState = {
|
|||||||
return ((fromIndex + delta) % count + count) % count;
|
return ((fromIndex + delta) % count + count) % count;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The button passes to the next player, not the next chair. Moving it one seat at
|
||||||
|
// a time parks it on empty chairs — with four players spread around ten seats it
|
||||||
|
// sits on nobody most hands — and that isn't only untidy: position and blinds are
|
||||||
|
// supposed to come round to each player at the same rate, and they don't if the
|
||||||
|
// button is rotating past chairs nobody is sitting in.
|
||||||
advanceButton(state) {
|
advanceButton(state) {
|
||||||
|
for (let step = 1; step <= state.seatCount; step++) {
|
||||||
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
||||||
|
if (state.seats[index].inHand) {
|
||||||
|
state.dealerSeatIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nobody is dealt in; move it along anyway so it doesn't look stuck.
|
||||||
state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Seats dealt into this hand, in blind order starting from the seat after the
|
||||||
|
// button. Blinds follow the players, not the chairs — the seats immediately left
|
||||||
|
// of the button are empty often enough that counting chairs posts them to nobody.
|
||||||
|
liveOrderFromButton(state) {
|
||||||
|
const order = [];
|
||||||
|
for (let step = 1; step <= state.seatCount; step++) {
|
||||||
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
||||||
|
if (state.seats[index].inHand) order.push(index);
|
||||||
|
}
|
||||||
|
return order;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Who posts what. Heads-up is its own rule: with two players the button posts the
|
||||||
|
// small blind, which is the opposite of the usual order.
|
||||||
getBlindSeats(state) {
|
getBlindSeats(state) {
|
||||||
const sbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
const live = this.liveOrderFromButton(state);
|
||||||
const bbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 2);
|
const dealerIndex = state.dealerSeatIndex;
|
||||||
return { dealerIndex: state.dealerSeatIndex, sbIndex, bbIndex };
|
|
||||||
|
if (live.length < 2) {
|
||||||
|
return { dealerIndex, sbIndex: null, bbIndex: null, headsUp: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (live.length === 2) {
|
||||||
|
const buttonIsPlaying = state.seats[dealerIndex].inHand;
|
||||||
|
const sbIndex = buttonIsPlaying ? dealerIndex : live[0];
|
||||||
|
const bbIndex = live.find(i => i !== sbIndex);
|
||||||
|
return { dealerIndex, sbIndex, bbIndex, headsUp: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dealerIndex, sbIndex: live[0], bbIndex: live[1], headsUp: false };
|
||||||
},
|
},
|
||||||
|
|
||||||
startNextHand(state) {
|
startNextHand(state) {
|
||||||
|
|||||||
Reference in New Issue
Block a user