Wait to be told to deal, instead of guessing #58
@@ -3006,3 +3006,10 @@ textarea:focus-visible {
|
||||
color: #ecffef;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The quieter half of the deal prompt: things the deal is about to do that a dealer
|
||||
would otherwise only discover afterwards. */
|
||||
.dealPromptLine.dealPromptAside {
|
||||
font-size: 0.72rem;
|
||||
color: #9a9a9a;
|
||||
}
|
||||
|
||||
@@ -494,6 +494,17 @@ const ActionBar = {
|
||||
`${preview.headsUp ? " · heads-up" : ""}${preview.manual ? " · blinds set by hand" : ""}`;
|
||||
wrap.appendChild(line);
|
||||
|
||||
// Dealing relocates a button sitting on an empty chair. That has to be said
|
||||
// before it happens rather than discovered afterwards — a fresh table starts
|
||||
// the button on seat 1 whether or not anyone is sitting there.
|
||||
if (preview.buttonMoves) {
|
||||
const moved = document.createElement("p");
|
||||
moved.className = "dealPromptLine dealPromptAside";
|
||||
moved.textContent =
|
||||
`Button moves to ${name(preview.buttonIndex)} — seat ${this.state.dealerSeatIndex + 1} is empty.`;
|
||||
wrap.appendChild(moved);
|
||||
}
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "actionButtons";
|
||||
row.appendChild(this.button("Post blinds & deal", "actionDeal", () => {
|
||||
|
||||
@@ -33,10 +33,22 @@ const Betting = {
|
||||
// while the table is still being set, so a dealer can see where they'll land
|
||||
// before any money moves.
|
||||
blindPreview(state) {
|
||||
const { sbIndex, bbIndex, headsUp, manual } = HoldemState.getBlindSeats(state);
|
||||
// Dealing relocates a button parked on an empty chair before it works out who
|
||||
// posts, so the preview has to answer for the button where it *will* be. Asking
|
||||
// from where it currently sits gave a different pair of blinds than the deal
|
||||
// produced — the preview promising one thing and doing another is exactly the
|
||||
// guessing this whole step exists to remove.
|
||||
const buttonIndex = HoldemState.buttonSeatForDeal(state);
|
||||
const asDealt = buttonIndex === state.dealerSeatIndex
|
||||
? state
|
||||
: { ...state, dealerSeatIndex: buttonIndex };
|
||||
|
||||
const { sbIndex, bbIndex, headsUp, manual } = HoldemState.getBlindSeats(asDealt);
|
||||
if (sbIndex === null || bbIndex === null) return null;
|
||||
return {
|
||||
sbIndex, bbIndex, headsUp, manual: !!manual,
|
||||
buttonIndex,
|
||||
buttonMoves: buttonIndex !== state.dealerSeatIndex,
|
||||
smallBlind: this.round(state.smallBlind || 0),
|
||||
bigBlind: this.round(state.bigBlind || 0),
|
||||
};
|
||||
|
||||
@@ -96,13 +96,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
},
|
||||
|
||||
makeDealer: index => seatAction(index, `Button on ${nameOf(index)}.`, () => {
|
||||
state.dealerSeatIndex = index;
|
||||
// Before anyone has acted the blinds are still just sitting there, so
|
||||
// moving the button moves them too — a home game passes it round several
|
||||
// times before the cards come out, and the posted chips have to follow.
|
||||
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
||||
}),
|
||||
makeDealer: index => {
|
||||
// Before the deal there's nothing posted, so this is just moving a marker.
|
||||
// After it, the blinds have to follow the button or the badges and the
|
||||
// money disagree — but that's a real thing happening to real chips, and
|
||||
// the dealer gets told rather than finding out later.
|
||||
const willRepost = state.bettingOpen && Betting.onlyBlindsIn(state) &&
|
||||
HoldemState.unresolvedContributions(state) > Betting.EPSILON_CHIPS;
|
||||
|
||||
seatAction(index, willRepost
|
||||
? `Button on ${nameOf(index)} — blinds taken back and re-posted.`
|
||||
: `Button on ${nameOf(index)}.`, () => {
|
||||
state.dealerSeatIndex = index;
|
||||
if (willRepost) Betting.repostBlinds(state);
|
||||
});
|
||||
},
|
||||
|
||||
assignBlind: (index, which) => {
|
||||
const blinds = HoldemState.getBlindSeats(state);
|
||||
|
||||
@@ -273,15 +273,25 @@ const HoldemState = {
|
||||
// position off it. On the very first hand of a fresh table it starts on seat 1
|
||||
// whether or not anybody is sitting there, and a table that empties out around it
|
||||
// leaves it stranded. Move it to a real player before a hand is dealt.
|
||||
ensureButtonOnPlayer(state) {
|
||||
if (state.seats[state.dealerSeatIndex] && state.seats[state.dealerSeatIndex].inHand) return;
|
||||
// Where the button will be when the hand is dealt — without moving it.
|
||||
//
|
||||
// Kept separate from ensureButtonOnPlayer because the deal preview has to be able
|
||||
// to ask the question without answering it. It couldn't, and so the preview lied:
|
||||
// with the button parked on an empty chair it promised one pair of blinds and
|
||||
// dealing produced another, because beginHand relocates the button first and then
|
||||
// works out who posts.
|
||||
buttonSeatForDeal(state) {
|
||||
const seated = state.seats[state.dealerSeatIndex];
|
||||
if (seated && seated.inHand) return state.dealerSeatIndex;
|
||||
for (let step = 0; step < state.seatCount; step++) {
|
||||
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
||||
if (state.seats[index].inHand) {
|
||||
state.dealerSeatIndex = index;
|
||||
return;
|
||||
}
|
||||
if (state.seats[index].inHand) return index;
|
||||
}
|
||||
return state.dealerSeatIndex;
|
||||
},
|
||||
|
||||
ensureButtonOnPlayer(state) {
|
||||
state.dealerSeatIndex = this.buttonSeatForDeal(state);
|
||||
},
|
||||
|
||||
// A hand-picked blind is only usable if that seat is actually in the hand — a
|
||||
|
||||
@@ -161,3 +161,45 @@ describe("a table saved mid-hand comes back mid-hand", () => {
|
||||
assert.equal(state.bettingOpen, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the preview tells the truth about the button", () => {
|
||||
test("a button parked on an empty chair is answered for where it will be", () => {
|
||||
// The preview used to read the button where it sat, while dealing relocated it
|
||||
// first — so it promised one pair of blinds and produced another.
|
||||
const state = makeTable({ seatCount: 6, seated: [1, 2, 3], dealerSeatIndex: 0 });
|
||||
state.bettingOpen = false;
|
||||
assert.equal(state.seats[0].inHand, false, "the button is on an empty chair");
|
||||
|
||||
const preview = Betting.blindPreview(state);
|
||||
assert.equal(preview.buttonMoves, true, "and the preview knows it will move");
|
||||
|
||||
Betting.beginHand(state);
|
||||
|
||||
assert.equal(Betting.committed(state.seats[preview.sbIndex]), preview.smallBlind);
|
||||
assert.equal(Betting.committed(state.seats[preview.bbIndex]), preview.bigBlind);
|
||||
assert.equal(state.dealerSeatIndex, preview.buttonIndex, "and it moved where it said");
|
||||
});
|
||||
|
||||
test("asking where the button will be doesn't move it", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [1, 2, 3], dealerSeatIndex: 0 });
|
||||
state.bettingOpen = false;
|
||||
|
||||
Betting.blindPreview(state);
|
||||
Betting.blindPreview(state);
|
||||
|
||||
assert.equal(state.dealerSeatIndex, 0, "the preview is a question, not an action");
|
||||
});
|
||||
|
||||
test("a button already on a player is left alone", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
state.bettingOpen = false;
|
||||
const preview = Betting.blindPreview(state);
|
||||
assert.equal(preview.buttonMoves, false);
|
||||
assert.equal(preview.buttonIndex, 0);
|
||||
});
|
||||
|
||||
test("buttonSeatForDeal copes with a table nobody is sitting at", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [] });
|
||||
assert.equal(HoldemState.buttonSeatForDeal(state), state.dealerSeatIndex);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user