Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e0a687a2f |
@@ -1102,67 +1102,6 @@ body.hasActionBar #holdemSidebar {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Side-pot breakdown ---------- */
|
|
||||||
|
|
||||||
.potBreakdown {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.2rem;
|
|
||||||
margin-bottom: 0.55rem;
|
|
||||||
padding: 0.4rem 0.55rem;
|
|
||||||
background: rgba(216, 180, 106, 0.07);
|
|
||||||
border: 1px solid rgba(216, 180, 106, 0.25);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potBreakdownHeading {
|
|
||||||
margin: 0 0 0.35rem;
|
|
||||||
color: #d8b46a;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potLayer {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potLayerLabel {
|
|
||||||
color: #d8b46a;
|
|
||||||
font-weight: 600;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potLayerAmount {
|
|
||||||
color: #f2f2f2;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-weight: 600;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potLayerWho {
|
|
||||||
color: #9a9a9a;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: right;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potBreakdownWizard {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.potLayerReturn .potLayerLabel,
|
|
||||||
.potLayerReturn .potLayerAmount {
|
|
||||||
color: #9a9a9a;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ const ActionBar = {
|
|||||||
|
|
||||||
host.appendChild(this.renderStatus());
|
host.appendChild(this.renderStatus());
|
||||||
|
|
||||||
const breakdown = this.renderPotBreakdown();
|
|
||||||
if (breakdown) host.appendChild(breakdown);
|
|
||||||
|
|
||||||
if (Betting.isHandOver(this.state)) {
|
if (Betting.isHandOver(this.state)) {
|
||||||
host.appendChild(this.renderHandOver());
|
host.appendChild(this.renderHandOver());
|
||||||
} else if (this.state.actingSeatIndex === null || this.state.actingSeatIndex === undefined) {
|
} else if (this.state.actingSeatIndex === null || this.state.actingSeatIndex === undefined) {
|
||||||
@@ -74,28 +71,6 @@ const ActionBar = {
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Shown only once someone is all-in and the money has actually split — a single
|
|
||||||
// undivided pot needs no announcement, and a line that's always there stops being
|
|
||||||
// read.
|
|
||||||
renderPotBreakdown() {
|
|
||||||
const contributions = this.state.seats.map((player, index) => ({
|
|
||||||
id: player.id,
|
|
||||||
index,
|
|
||||||
contribution: Betting.handTotal(player),
|
|
||||||
folded: player.folded,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const nameById = new Map(
|
|
||||||
this.state.seats.map((player, index) => [player.id, HoldemState.displayName(player, index)])
|
|
||||||
);
|
|
||||||
|
|
||||||
return Render.buildPotBreakdown(
|
|
||||||
PotCalculator.layers(contributions),
|
|
||||||
PotCalculator.uncalledReturns(contributions),
|
|
||||||
id => nameById.get(id)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
button(label, className, onClick, sublabel) {
|
button(label, className, onClick, sublabel) {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
|
|||||||
@@ -24,79 +24,6 @@ const PotCalculator = {
|
|||||||
return entries.filter(e => !e.folded && e.remaining > this.EPSILON);
|
return entries.filter(e => !e.folded && e.remaining > this.EPSILON);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Splits the money on the table into the main pot and any side pots, with who can
|
|
||||||
// win each. Read-only — this is what a dealer says out loud before the showdown
|
|
||||||
// ("main pot four-eighty, side pot ninety between seats 3 and 5"), which is the
|
|
||||||
// announcement that goes missing far more often than the arithmetic goes wrong.
|
|
||||||
//
|
|
||||||
// Every distinct all-in amount opens a new layer: each player pays into a layer up
|
|
||||||
// to their own contribution, and only players who reached that height can win it.
|
|
||||||
// Folded money stays in — it's dead money owed to whoever wins the layer it fell in.
|
|
||||||
layers(players) {
|
|
||||||
const live = players.filter(p => p.contribution > this.EPSILON);
|
|
||||||
if (live.length === 0) return [];
|
|
||||||
|
|
||||||
const heights = [...new Set(live.map(p => Math.round(p.contribution * 100)))].sort((a, b) => a - b);
|
|
||||||
|
|
||||||
const layers = [];
|
|
||||||
let floor = 0;
|
|
||||||
heights.forEach(height => {
|
|
||||||
const band = height - floor;
|
|
||||||
const payers = live.filter(p => Math.round(p.contribution * 100) >= height);
|
|
||||||
const amount = band * payers.length;
|
|
||||||
const eligible = payers.filter(p => !p.folded).map(p => p.id);
|
|
||||||
|
|
||||||
// A band nobody can win isn't a pot — it's an overbet that never got
|
|
||||||
// called, and it goes back to whoever put it in. Announcing it as a side
|
|
||||||
// pot would overstate what's actually being played for.
|
|
||||||
if (amount > 0 && eligible.length > 0) {
|
|
||||||
layers.push({ amount: amount / 100, eligible });
|
|
||||||
}
|
|
||||||
floor = height;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Consecutive layers with the same set of eligible players are one pot to a
|
|
||||||
// dealer — nobody announces two side pots between the same two people.
|
|
||||||
const merged = [];
|
|
||||||
layers.forEach(layer => {
|
|
||||||
const previous = merged[merged.length - 1];
|
|
||||||
const sameEligibility =
|
|
||||||
previous &&
|
|
||||||
previous.eligible.length === layer.eligible.length &&
|
|
||||||
previous.eligible.every(id => layer.eligible.includes(id));
|
|
||||||
|
|
||||||
if (sameEligibility) {
|
|
||||||
previous.amount = Math.round((previous.amount + layer.amount) * 100) / 100;
|
|
||||||
} else {
|
|
||||||
merged.push({ ...layer });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return merged.map((layer, index) => ({
|
|
||||||
...layer,
|
|
||||||
label: index === 0 ? "Main pot" : `Side pot ${index}`,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Chips nobody could call, which the dealer pushes back rather than into the pot.
|
|
||||||
// Anything above the biggest live contribution was never contested — most often an
|
|
||||||
// overbet by someone who then folded.
|
|
||||||
uncalledReturns(players) {
|
|
||||||
const contributing = players.filter(p => p.contribution > this.EPSILON);
|
|
||||||
const liveCents = contributing.filter(p => !p.folded).map(p => Math.round(p.contribution * 100));
|
|
||||||
|
|
||||||
// Nobody live at all means the fold flags are wrong, not that every chip on
|
|
||||||
// the table was uncalled. That's a correction for the dealer to make, so say
|
|
||||||
// nothing here and let the wizard ask them to fix it.
|
|
||||||
if (liveCents.length === 0) return [];
|
|
||||||
|
|
||||||
const cap = Math.max(...liveCents);
|
|
||||||
|
|
||||||
return contributing
|
|
||||||
.map(p => ({ id: p.id, amount: (Math.round(p.contribution * 100) - cap) / 100 }))
|
|
||||||
.filter(r => r.amount > this.EPSILON);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Awards the current pot layer to `winnerIds` (one id, or several for a split/tie).
|
// Awards the current pot layer to `winnerIds` (one id, or several for a split/tie).
|
||||||
// The payout is capped at the smallest remaining contribution among the winners —
|
// The payout is capped at the smallest remaining contribution among the winners —
|
||||||
// that's the classic side-pot rule: an all-in player can only win what everyone at
|
// that's the classic side-pot rule: an all-in player can only win what everyone at
|
||||||
|
|||||||
@@ -18,16 +18,13 @@ 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))])
|
||||||
);
|
);
|
||||||
const contributions = players.map(p => ({
|
this.entries = PotCalculator.buildEntries(
|
||||||
id: p.id,
|
players.map(p => ({
|
||||||
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
id: p.id,
|
||||||
folded: p.folded,
|
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||||
}));
|
folded: p.folded,
|
||||||
this.entries = PotCalculator.buildEntries(contributions);
|
}))
|
||||||
// Kept so the wizard can show the whole breakdown up front, not just the layer
|
);
|
||||||
// it happens to be asking about.
|
|
||||||
this.layers = PotCalculator.layers(contributions);
|
|
||||||
this.returns = PotCalculator.uncalledReturns(contributions);
|
|
||||||
this.log = [];
|
this.log = [];
|
||||||
this.totals = new Map();
|
this.totals = new Map();
|
||||||
|
|
||||||
@@ -61,7 +58,6 @@ const PotWizard = {
|
|||||||
|
|
||||||
const body = document.getElementById("potWizardBody");
|
const body = document.getElementById("potWizardBody");
|
||||||
body.innerHTML = "";
|
body.innerHTML = "";
|
||||||
if (this.log.length === 0) body.appendChild(this.renderBreakdown());
|
|
||||||
body.appendChild(this.renderLog());
|
body.appendChild(this.renderLog());
|
||||||
|
|
||||||
const remaining = PotCalculator.totalPot(this.entries.filter(e => e.remaining > PotCalculator.EPSILON));
|
const remaining = PotCalculator.totalPot(this.entries.filter(e => e.remaining > PotCalculator.EPSILON));
|
||||||
@@ -69,17 +65,7 @@ const PotWizard = {
|
|||||||
if (contenders.length === 0 && remaining > PotCalculator.EPSILON) {
|
if (contenders.length === 0 && remaining > PotCalculator.EPSILON) {
|
||||||
const warning = document.createElement("p");
|
const warning = document.createElement("p");
|
||||||
warning.className = "potWizardPrompt";
|
warning.className = "potWizardPrompt";
|
||||||
|
warning.textContent = `${Money.format(remaining)} is still unassigned, but every remaining player is marked Folded. Uncheck Fold on whoever should win it.`;
|
||||||
// Money left over because nobody could call it isn't a pot anyone won —
|
|
||||||
// it goes back to whoever put it in. Telling the dealer to un-fold someone
|
|
||||||
// to "win" it would be wrong.
|
|
||||||
const returned = (this.returns || []).reduce((sum, r) => sum + r.amount, 0);
|
|
||||||
if (Math.abs(returned - remaining) <= PotCalculator.EPSILON && returned > 0) {
|
|
||||||
const who = this.returns.map(r => this.playerLabelById.get(r.id)).join(", ");
|
|
||||||
warning.textContent = `${Money.format(remaining)} was never called — push it back to ${who} rather than paying it out.`;
|
|
||||||
} else {
|
|
||||||
warning.textContent = `${Money.format(remaining)} is still unassigned, but every remaining player is marked Folded. Uncheck Fold on whoever should win it.`;
|
|
||||||
}
|
|
||||||
body.appendChild(warning);
|
body.appendChild(warning);
|
||||||
body.appendChild(this.renderSummary());
|
body.appendChild(this.renderSummary());
|
||||||
return;
|
return;
|
||||||
@@ -145,20 +131,6 @@ const PotWizard = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// The announcement, spelled out before anyone is asked to pick a winner: what the
|
|
||||||
// main pot is worth, what each side pot is worth, and who can win each.
|
|
||||||
renderBreakdown() {
|
|
||||||
const node = Render.buildPotBreakdown(
|
|
||||||
this.layers || [],
|
|
||||||
this.returns || [],
|
|
||||||
id => this.playerLabelById.get(id),
|
|
||||||
"Announce before the showdown"
|
|
||||||
);
|
|
||||||
if (!node) return document.createDocumentFragment();
|
|
||||||
node.classList.add("potBreakdownWizard");
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderLog() {
|
renderLog() {
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "potWizardLog";
|
wrap.className = "potWizardLog";
|
||||||
|
|||||||
@@ -520,51 +520,6 @@ const Render = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Pot breakdown ----
|
|
||||||
|
|
||||||
// Shared by the action bar (live, mid-hand) and the pot wizard (before winners are
|
|
||||||
// picked), because both are showing the dealer the same announcement.
|
|
||||||
buildPotBreakdown(pots, returns, nameFor, heading) {
|
|
||||||
if (pots.length < 2 && returns.length === 0) return null;
|
|
||||||
|
|
||||||
const wrap = document.createElement("div");
|
|
||||||
wrap.className = "potBreakdown";
|
|
||||||
|
|
||||||
if (heading) {
|
|
||||||
const title = document.createElement("p");
|
|
||||||
title.className = "potBreakdownHeading";
|
|
||||||
title.textContent = heading;
|
|
||||||
wrap.appendChild(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRow = (label, amount, who, className) => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = `potLayer ${className || ""}`;
|
|
||||||
|
|
||||||
const labelEl = document.createElement("span");
|
|
||||||
labelEl.className = "potLayerLabel";
|
|
||||||
labelEl.textContent = label;
|
|
||||||
|
|
||||||
const amountEl = document.createElement("span");
|
|
||||||
amountEl.className = "potLayerAmount";
|
|
||||||
amountEl.textContent = Money.format(amount);
|
|
||||||
|
|
||||||
const whoEl = document.createElement("span");
|
|
||||||
whoEl.className = "potLayerWho";
|
|
||||||
whoEl.textContent = who;
|
|
||||||
|
|
||||||
row.appendChild(labelEl);
|
|
||||||
row.appendChild(amountEl);
|
|
||||||
row.appendChild(whoEl);
|
|
||||||
wrap.appendChild(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
pots.forEach(pot => addRow(pot.label, pot.amount, pot.eligible.map(nameFor).join(", ")));
|
|
||||||
returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${nameFor(ret.id)}`, "potLayerReturn"));
|
|
||||||
|
|
||||||
return wrap;
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---- Session standings ----
|
// ---- Session standings ----
|
||||||
|
|
||||||
renderStandings() {
|
renderStandings() {
|
||||||
|
|||||||
Reference in New Issue
Block a user