Stop the last two things the table did behind the dealer's back #59
@@ -568,8 +568,19 @@ button,
|
|||||||
|
|
||||||
.dealerChip {
|
.dealerChip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
/* Inside its own seat, not on the rim.
|
||||||
right: -10px;
|
|
||||||
|
It used to sit at -10px/-10px, hanging over the seat's corner — which on an oval
|
||||||
|
of overlapping seats means hanging over the seat *next to it*. Measured on a
|
||||||
|
nine-seat table: tapping the centre of seat 1 gave the button to seat 9. That
|
||||||
|
was always slightly true and the enlarged hit area made it much worse; three of
|
||||||
|
nine seats were losing taps, one of them losing its own middle.
|
||||||
|
|
||||||
|
Tucked inside, the overlap is zero at every size. It reads as a puck in the
|
||||||
|
corner of the seat rather than on its edge, which is a small price for tapping
|
||||||
|
a player and getting that player. */
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -595,10 +606,20 @@ button,
|
|||||||
.dealerChip::after {
|
.dealerChip::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -9px;
|
/* Grows inward, over its own seat, not outward onto the neighbours.
|
||||||
right: -7px;
|
|
||||||
bottom: -15px;
|
The chip already sits outside its seat by design — top:-10px, right:-10px — so
|
||||||
left: -14px;
|
a pad that also reaches up and right lands on whatever seat is next around the
|
||||||
|
oval. It did: three of nine seats had taps taken by a neighbour's chip, seat 1
|
||||||
|
losing 10 of 49 sampled points including its own centre, which meant tapping
|
||||||
|
the middle of seat 1 gave the button to seat 9.
|
||||||
|
|
||||||
|
Down and left is its own seat, which is a large target that can spare the
|
||||||
|
corner. The two pixels of outward reach are just to cover the chip's border. */
|
||||||
|
top: -3px;
|
||||||
|
right: -3px;
|
||||||
|
bottom: -18px;
|
||||||
|
left: -18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dealerChip:focus-visible {
|
.dealerChip:focus-visible {
|
||||||
@@ -2787,9 +2808,20 @@ body.hasActionBar #toastHost {
|
|||||||
against the containing block's width, not the element's own, so narrowing the
|
against the containing block's width, not the element's own, so narrowing the
|
||||||
wrap on its own left it full height regardless. */
|
wrap on its own left it full height regardless. */
|
||||||
#pokerTableWrap {
|
#pokerTableWrap {
|
||||||
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 8.5rem);
|
/* A wide ellipse, which is what a real poker table is.
|
||||||
width: min(100%, calc(var(--feltRoom) / 0.72));
|
|
||||||
|
Capping the width by the height to hold the portrait aspect ratio was
|
||||||
|
throwing away most of the screen: at 640x360 the felt came out 135px wide
|
||||||
|
inside a 601px column — 22% of the width — leaving 366px of rim for nine
|
||||||
|
seats that need 662px. Adjacent seat centres ended up 13px apart, which is
|
||||||
|
why a seat's centre belonged to its neighbour.
|
||||||
|
|
||||||
|
Landscape has width and no height, so the table takes the width it has and
|
||||||
|
is only as tall as the room left over. */
|
||||||
|
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 7rem);
|
||||||
|
width: 100%;
|
||||||
height: var(--feltRoom);
|
height: var(--feltRoom);
|
||||||
|
min-height: 7rem;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2826,6 +2858,7 @@ body.hasActionBar #toastHost {
|
|||||||
width: 62px;
|
width: 62px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Short phones, upright ----------
|
/* ---------- Short phones, upright ----------
|
||||||
@@ -3006,3 +3039,10 @@ textarea:focus-visible {
|
|||||||
color: #ecffef;
|
color: #ecffef;
|
||||||
font-weight: 600;
|
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" : ""}`;
|
`${preview.headsUp ? " · heads-up" : ""}${preview.manual ? " · blinds set by hand" : ""}`;
|
||||||
wrap.appendChild(line);
|
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");
|
const row = document.createElement("div");
|
||||||
row.className = "actionButtons";
|
row.className = "actionButtons";
|
||||||
row.appendChild(this.button("Post blinds & deal", "actionDeal", () => {
|
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
|
// while the table is still being set, so a dealer can see where they'll land
|
||||||
// before any money moves.
|
// before any money moves.
|
||||||
blindPreview(state) {
|
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;
|
if (sbIndex === null || bbIndex === null) return null;
|
||||||
return {
|
return {
|
||||||
sbIndex, bbIndex, headsUp, manual: !!manual,
|
sbIndex, bbIndex, headsUp, manual: !!manual,
|
||||||
|
buttonIndex,
|
||||||
|
buttonMoves: buttonIndex !== state.dealerSeatIndex,
|
||||||
smallBlind: this.round(state.smallBlind || 0),
|
smallBlind: this.round(state.smallBlind || 0),
|
||||||
bigBlind: this.round(state.bigBlind || 0),
|
bigBlind: this.round(state.bigBlind || 0),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,13 +96,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
makeDealer: index => seatAction(index, `Button on ${nameOf(index)}.`, () => {
|
makeDealer: index => {
|
||||||
state.dealerSeatIndex = index;
|
// Before the deal there's nothing posted, so this is just moving a marker.
|
||||||
// Before anyone has acted the blinds are still just sitting there, so
|
// After it, the blinds have to follow the button or the badges and the
|
||||||
// moving the button moves them too — a home game passes it round several
|
// money disagree — but that's a real thing happening to real chips, and
|
||||||
// times before the cards come out, and the posted chips have to follow.
|
// the dealer gets told rather than finding out later.
|
||||||
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
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) => {
|
assignBlind: (index, which) => {
|
||||||
const blinds = HoldemState.getBlindSeats(state);
|
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
|
// 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
|
// 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.
|
// leaves it stranded. Move it to a real player before a hand is dealt.
|
||||||
ensureButtonOnPlayer(state) {
|
// Where the button will be when the hand is dealt — without moving it.
|
||||||
if (state.seats[state.dealerSeatIndex] && state.seats[state.dealerSeatIndex].inHand) return;
|
//
|
||||||
|
// 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++) {
|
for (let step = 0; step < state.seatCount; step++) {
|
||||||
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
||||||
if (state.seats[index].inHand) {
|
if (state.seats[index].inHand) return index;
|
||||||
state.dealerSeatIndex = index;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
// 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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+48
-1
@@ -133,7 +133,18 @@ const LayoutAudit = {
|
|||||||
if (el.contains(top) || top === el || top.contains(el)) return;
|
if (el.contains(top) || top === el || top.contains(el)) return;
|
||||||
// Seats ring a round table and overlap each other at the rim by
|
// Seats ring a round table and overlap each other at the rim by
|
||||||
// design; that is crowding, not an overlay hiding a control.
|
// design; that is crowding, not an overlay hiding a control.
|
||||||
if (top.closest && top.closest(".seat")) return;
|
//
|
||||||
|
// But only the seat's *body* is harmless. A neighbouring seat's own
|
||||||
|
// controls sitting on top of this one is a control being stolen, and
|
||||||
|
// exempting the whole seat subtree hid exactly that: an enlarged
|
||||||
|
// dealer-chip hit area was taking the centre of the seat three
|
||||||
|
// places round the table, and this check called it crowding and
|
||||||
|
// moved on.
|
||||||
|
const neighbourSeat = top.closest && top.closest(".seat");
|
||||||
|
if (neighbourSeat) {
|
||||||
|
const isControl = top.matches && top.matches("button, a, input, select, textarea, [role='button']");
|
||||||
|
if (!isControl) return;
|
||||||
|
}
|
||||||
blocked++;
|
blocked++;
|
||||||
if (!blockedBy) blockedBy = this.describe(top);
|
if (!blockedBy) blockedBy = this.describe(top);
|
||||||
});
|
});
|
||||||
@@ -468,6 +479,41 @@ const LayoutAudit = {
|
|||||||
return { name: `touch targets at least ${minimum}px`, offenders };
|
return { name: `touch targets at least ${minimum}px`, offenders };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tapping the middle of a seat must give you that seat.
|
||||||
|
//
|
||||||
|
// Added after a dealer-chip hit area, meant to make a 22px puck thumb-sized, grew
|
||||||
|
// outward from a chip that already hung over its seat's corner — and therefore
|
||||||
|
// over the seat next to it. On a nine-seat table, tapping the centre of seat 1
|
||||||
|
// handed the button to seat 9. The occlusion check missed it because it treated
|
||||||
|
// anything inside a `.seat` as harmless crowding.
|
||||||
|
//
|
||||||
|
// Reported separately from occlusion because the cause is different: not an
|
||||||
|
// overlay on top of the table, but two pieces of the table fighting over a tap.
|
||||||
|
seatOwnership() {
|
||||||
|
const seats = [...document.querySelectorAll(".seat")];
|
||||||
|
const offenders = [];
|
||||||
|
|
||||||
|
seats.forEach((seat, index) => {
|
||||||
|
const rect = seat.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 || rect.height === 0) return;
|
||||||
|
const x = rect.left + rect.width / 2;
|
||||||
|
const y = rect.top + rect.height / 2;
|
||||||
|
if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) return;
|
||||||
|
|
||||||
|
const hit = document.elementFromPoint(x, y);
|
||||||
|
if (!hit || seat.contains(hit) || hit === seat) return;
|
||||||
|
|
||||||
|
const other = hit.closest(".seat");
|
||||||
|
const who = other ? `seat ${seats.indexOf(other) + 1}` : this.describe(hit);
|
||||||
|
const control = hit.matches && hit.matches("button, a, input, select, [role='button']");
|
||||||
|
offenders.push(
|
||||||
|
`seat ${index + 1}'s centre goes to ${who}${control ? "'s " + this.describe(hit) : ""}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { name: "a seat's centre belongs to that seat", offenders };
|
||||||
|
},
|
||||||
|
|
||||||
// ---- Runner ----
|
// ---- Runner ----
|
||||||
|
|
||||||
runAll(options = {}) {
|
runAll(options = {}) {
|
||||||
@@ -480,6 +526,7 @@ const LayoutAudit = {
|
|||||||
this.textSpill(),
|
this.textSpill(),
|
||||||
this.horizontalOverflow(),
|
this.horizontalOverflow(),
|
||||||
this.fieldFontSize(),
|
this.fieldFontSize(),
|
||||||
|
this.seatOwnership(),
|
||||||
];
|
];
|
||||||
if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget));
|
if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user