Hold'em dealer: making the table and the bar fit the screen they are on #119
@@ -3073,6 +3073,17 @@ body.hasActionBar #holdemApp {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The layers between the main pot and the newest side pot, collapsed onto one line so
|
||||||
|
the bar's height stops depending on how many people are all-in. Set back rather than
|
||||||
|
picked out: it is the one row in the list that is not a pot to announce, and reading
|
||||||
|
as a pot is exactly the mistake it could cause. */
|
||||||
|
.potLayerMore .potLayerLabel,
|
||||||
|
.potLayerMore .potLayerAmount {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--sp-2);
|
gap: var(--sp-2);
|
||||||
|
|||||||
@@ -606,10 +606,15 @@ const ActionBar = {
|
|||||||
this.state.seats.map((player, index) => [player.id, HoldemState.displayName(player, index)])
|
this.state.seats.map((player, index) => [player.id, HoldemState.displayName(player, index)])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Four is what the bar can hold without taking the felt with it — see
|
||||||
|
// buildPotBreakdown for the measurements, and uiAudit's fixedFurniture check
|
||||||
|
// for the ceiling this is keeping the bar under.
|
||||||
return Render.buildPotBreakdown(
|
return Render.buildPotBreakdown(
|
||||||
PotCalculator.layers(contributions),
|
PotCalculator.layers(contributions),
|
||||||
PotCalculator.uncalledReturns(contributions),
|
PotCalculator.uncalledReturns(contributions),
|
||||||
id => nameById.get(id)
|
id => nameById.get(id),
|
||||||
|
null,
|
||||||
|
{ maxLayers: 4 }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2096,7 +2096,15 @@ const Render = {
|
|||||||
|
|
||||||
// Shared by the action bar (live, mid-hand) and the pot wizard (before winners are
|
// 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.
|
// picked), because both are showing the dealer the same announcement.
|
||||||
buildPotBreakdown(pots, returns, nameFor, heading) {
|
//
|
||||||
|
// `maxLayers` caps how many pots get a line of their own, and only the bar passes
|
||||||
|
// it. The bar is fixed over the felt, so nothing in it may grow with the table —
|
||||||
|
// and this list did, one row per all-in, with no ceiling. Measured on a 360x640
|
||||||
|
// phone: five players idle put the bar at 151px, six all-in at 303px, and twelve
|
||||||
|
// all-in at 411px, which is 64% of the screen and leaves 96px of felt for twelve
|
||||||
|
// seats. The wizard passes no cap: it is a dialog with room, opened at the point
|
||||||
|
// the whole list is what you are working from.
|
||||||
|
buildPotBreakdown(pots, returns, nameFor, heading, { maxLayers } = {}) {
|
||||||
if (pots.length < 2 && returns.length === 0) return null;
|
if (pots.length < 2 && returns.length === 0) return null;
|
||||||
|
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
@@ -2136,11 +2144,37 @@ const Render = {
|
|||||||
wrap.appendChild(row);
|
wrap.appendChild(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
pots.forEach(pot => addRow(
|
// The two ends of the list are the live ones, and they are live for the same
|
||||||
pot.label, pot.amount,
|
// reason: pots are paid from the newest side pot back down to the main pot, so
|
||||||
this.describeEligible(pot.eligible, pots[0].eligible, nameFor),
|
// the top layer is the next thing that happens and the main pot is the last.
|
||||||
"",
|
// Those are also the two a dealer says out loud while the chips are being
|
||||||
pot.eligible.map(nameFor).join(", ")));
|
// stacked — "main pot 7,200, side pot 4,500". The layers in between are the
|
||||||
|
// middle of that count-down, read at showdown rather than mid-hand.
|
||||||
|
//
|
||||||
|
// Elide only when it buys something: at the cap itself the middle is one row
|
||||||
|
// and replacing one row with one row is pure indirection, so the whole list
|
||||||
|
// stands until there are strictly more pots than the cap.
|
||||||
|
const middle = maxLayers && pots.length > maxLayers ? pots.slice(1, -1) : [];
|
||||||
|
const shown = new Set(middle.length ? [pots[0], pots[pots.length - 1]] : pots);
|
||||||
|
|
||||||
|
pots.forEach(pot => {
|
||||||
|
if (!shown.has(pot)) return;
|
||||||
|
addRow(
|
||||||
|
pot.label, pot.amount,
|
||||||
|
this.describeEligible(pot.eligible, pots[0].eligible, nameFor),
|
||||||
|
"",
|
||||||
|
pot.eligible.map(nameFor).join(", "));
|
||||||
|
// Kept in place rather than appended, so the amounts still read in the
|
||||||
|
// order the pots were made and the gap sits where the gap actually is.
|
||||||
|
if (middle.length && pot === pots[0]) {
|
||||||
|
addRow(
|
||||||
|
`${middle.length} more side pots`,
|
||||||
|
middle.reduce((sum, layer) => sum + layer.amount, 0),
|
||||||
|
"listed when you resolve",
|
||||||
|
"potLayerMore",
|
||||||
|
middle.map(layer => `${layer.label} ${Money.format(layer.amount)}`).join(", "));
|
||||||
|
}
|
||||||
|
});
|
||||||
returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${nameFor(ret.id)}`, "potLayerReturn"));
|
returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${nameFor(ret.id)}`, "potLayerReturn"));
|
||||||
|
|
||||||
return wrap;
|
return wrap;
|
||||||
|
|||||||
@@ -330,6 +330,56 @@ and, when the page has no room left to scroll (26px available against 94px wante
|
|||||||
well would have fixed it by making the page grow and shrink by 64px every time anything
|
well would have fixed it by making the page grow and shrink by 64px every time anything
|
||||||
was announced, which is worse than the fault.
|
was announced, which is worse than the fault.
|
||||||
|
|
||||||
|
## A sweep only measures the states it reaches
|
||||||
|
|
||||||
|
The action bar is fixed over the felt, so its height is the felt's budget — and the bar
|
||||||
|
carries the side-pot breakdown, which drew one row per all-in with no ceiling. Measured
|
||||||
|
on a 360x640 phone: five players idle put the bar at **151px**, six all-in at **303px**,
|
||||||
|
and twelve all-in at **411px** — 64% of the screen, leaving **96px of felt** to draw
|
||||||
|
twelve seats in.
|
||||||
|
|
||||||
|
Nothing here could see it, and the reason is worth writing down. Every check in
|
||||||
|
`uiAudit.js` asks whether some element is covered, clipped or unreachable; not one asked
|
||||||
|
how much room the covering element had taken. But the deeper miss was the sweep itself:
|
||||||
|
all 294 audits drove their table with `Call` and `Check`, so **no table in the history of
|
||||||
|
this file ever had a player all-in**, and the one thing on the bar whose height depends
|
||||||
|
on the table was empty in every single measurement. `runUiAudit.js` now has a fourth
|
||||||
|
shape, twelve players shoved in for twelve different amounts — there is no one-tap shove,
|
||||||
|
so it takes the raise pad's All-in preset and confirms, or `Call all-in` for anyone who
|
||||||
|
cannot cover.
|
||||||
|
|
||||||
|
- **`furnitureBudget()`** — the fixed bottom band, measured against the window. Failing
|
||||||
|
above **50%**, which is the line that needs no taste to defend: furniture larger than
|
||||||
|
the content it serves is a shape fault whatever it is showing. Warning above **30%**,
|
||||||
|
the published guidance for persistent chrome, which the shortest phone does not meet at
|
||||||
|
rest — the bar is 151px of a 640px screen before anything happens — and a sweep that is
|
||||||
|
always red stops being read.
|
||||||
|
|
||||||
|
### Cutting the middle out of a list, and which end to keep
|
||||||
|
|
||||||
|
The cap is four layers, and past it the bar draws the main pot, one line for everything
|
||||||
|
between, and the newest side pot. The two ends are not an arbitrary choice: side pots are
|
||||||
|
paid **from the newest one back down to the main pot**, so the top layer is the next thing
|
||||||
|
that happens and the main pot is the last, and those are exactly the two a live dealer
|
||||||
|
says out loud while the chips are being stacked — *"main pot 7,200, side pot 4,500"*. The
|
||||||
|
layers between are the middle of that count-down, wanted at showdown, which is the pot
|
||||||
|
wizard — the same builder, passing no cap, in a dialog that scrolls.
|
||||||
|
|
||||||
|
`potBreakdownRoom.test.js` checks the parts that could go wrong quietly:
|
||||||
|
|
||||||
|
- the row count is a function of the cap, not of the table — eight, nine, ten, eleven and
|
||||||
|
twelve all-in all draw three lines
|
||||||
|
- the drawn amounts still add up to every pot, so a dealer counting the announcement
|
||||||
|
against the chips in the middle finds no shortfall the tool caused
|
||||||
|
- the elision sits **where the gap is**, not appended after the list, so the amounts still
|
||||||
|
read in the order the pots were made
|
||||||
|
- it never fires when it would not buy anything: at exactly the cap the middle is one row,
|
||||||
|
and swapping one row for one row loses an amount and gains a sentence
|
||||||
|
|
||||||
|
Three injected faults — the cap ignored, the summary carrying only the first elided pot's
|
||||||
|
money, the elision appended rather than kept in place — fail 6, 1 and 5 of the 11 tests
|
||||||
|
respectively, and the audit check reports the original 64% when the cap is taken out.
|
||||||
|
|
||||||
## Why the hand evaluator is tested the way it is
|
## Why the hand evaluator is tested the way it is
|
||||||
|
|
||||||
`handRank.js` is the only module here that can pay the wrong person without anybody
|
`handRank.js` is the only module here that can pay the wrong person without anybody
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// The bar's pot list is not allowed to grow with the table.
|
||||||
|
//
|
||||||
|
// #actionBar is fixed over the felt, so every pixel it takes is a pixel the table
|
||||||
|
// loses — and this list took one row per all-in with no ceiling. Measured on a 360x640
|
||||||
|
// phone, five players idle: bar 151px. Six all-in: 303px. Twelve all-in: 411px, which
|
||||||
|
// is 64% of the screen, leaving 96px of felt to draw twelve seats in. A fixed overlay
|
||||||
|
// whose height depends on the state is a fault whatever it is showing, so the cap is
|
||||||
|
// checked here rather than left to whoever next adds an all-in to the audit sweep.
|
||||||
|
//
|
||||||
|
// The two ends survive because they are the two that are live. Side pots are paid from
|
||||||
|
// the newest one back down to the main pot, so the top layer is the next thing that
|
||||||
|
// happens and the main pot is the last, and those are the two a dealer says out loud
|
||||||
|
// while the chips are stacked — "main pot 7,200, side pot 4,500". The layers between
|
||||||
|
// are the middle of that count-down, wanted at showdown, which is the pot wizard, which
|
||||||
|
// passes no cap and still lists all of them.
|
||||||
|
const test = require("node:test");
|
||||||
|
const { describe } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Render, Money, PotCalculator } = require("./load");
|
||||||
|
|
||||||
|
const nameFor = id => `P${id}`;
|
||||||
|
|
||||||
|
// The same stub potBreakdown.test.js uses — enough of an element to say what the row
|
||||||
|
// is made of, and no jsdom in a suite that runs in a second.
|
||||||
|
const fakeDom = () => {
|
||||||
|
const make = () => ({
|
||||||
|
className: "", textContent: "", title: "", children: [],
|
||||||
|
classList: { add() {} },
|
||||||
|
appendChild(child) { this.children.push(child); return child; },
|
||||||
|
});
|
||||||
|
global.document = { createElement: make };
|
||||||
|
return () => { delete global.document; };
|
||||||
|
};
|
||||||
|
|
||||||
|
// A table where everyone is all-in for a different amount, which is the shape that
|
||||||
|
// makes the most pot layers — the worst case and the one that was measured. `count`
|
||||||
|
// players ladder into `count - 1` layers: two of them share a single undivided pot and
|
||||||
|
// the breakdown declines to draw at all, which is why nothing below three appears here.
|
||||||
|
const laddered = count => PotCalculator.layers(
|
||||||
|
Array.from({ length: count }, (_, i) => ({
|
||||||
|
id: `s${i}`, contribution: 100 + i * 130, folded: false,
|
||||||
|
})));
|
||||||
|
|
||||||
|
const build = (pots, options) => {
|
||||||
|
const undo = fakeDom();
|
||||||
|
try {
|
||||||
|
const wrap = Render.buildPotBreakdown(pots, [], nameFor, null, options);
|
||||||
|
return wrap.children.map(row => ({
|
||||||
|
className: row.className,
|
||||||
|
label: row.children[0].textContent,
|
||||||
|
amount: row.children[1].textContent,
|
||||||
|
who: row.children[2].textContent,
|
||||||
|
title: row.children[2].title,
|
||||||
|
}));
|
||||||
|
} finally { undo(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("the bar's copy of the pot list", () => {
|
||||||
|
test("stops growing once there are more pots than it has room for", () => {
|
||||||
|
// The property that matters: rows are a function of the cap, not of the table.
|
||||||
|
// Twelve-handed and six-handed have to render the same number of lines.
|
||||||
|
for (let players = 3; players <= 12; players++) {
|
||||||
|
const rows = build(laddered(players), { maxLayers: 4 });
|
||||||
|
assert.ok(rows.length <= 4,
|
||||||
|
`${players} all-in drew ${rows.length} rows; the bar is sized for 4`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and the number of rows never depends on how many people are all-in", () => {
|
||||||
|
const wide = [8, 9, 10, 11, 12].map(n => build(laddered(n), { maxLayers: 4 }).length);
|
||||||
|
assert.deepEqual(wide, [3, 3, 3, 3, 3],
|
||||||
|
"past the cap every table should draw main, the elision and the top pot");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the two pots a dealer actually announces", () => {
|
||||||
|
const pots = laddered(12);
|
||||||
|
const rows = build(pots, { maxLayers: 4 });
|
||||||
|
assert.equal(rows[0].label, pots[0].label, "the main pot is the last one paid");
|
||||||
|
assert.equal(rows[rows.length - 1].label, pots[pots.length - 1].label,
|
||||||
|
"the newest side pot is the first one paid");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and leaves them in the order the pots were made", () => {
|
||||||
|
const rows = build(laddered(12), { maxLayers: 4 });
|
||||||
|
assert.match(rows[1].className, /potLayerMore/,
|
||||||
|
"the elision belongs where the gap is, not appended after the list");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the money in the rows that were elided", () => {
|
||||||
|
test("is still on the screen, added up", () => {
|
||||||
|
// The one thing a summary row must not do is lose chips. Whatever is drawn has
|
||||||
|
// to account for every pot, or a dealer counting the announcement against the
|
||||||
|
// chips in the middle finds a shortfall the tool caused.
|
||||||
|
for (let players = 3; players <= 12; players++) {
|
||||||
|
const pots = laddered(players);
|
||||||
|
const total = pots.reduce((sum, pot) => sum + pot.amount, 0);
|
||||||
|
const drawn = build(pots, { maxLayers: 4 })
|
||||||
|
.reduce((sum, row) => sum + Number(row.amount.replace(/[^0-9.]/g, "")), 0);
|
||||||
|
assert.equal(drawn, total,
|
||||||
|
`${players} all-in: rows add to ${drawn}, the pots add to ${total}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and says how many it stands for", () => {
|
||||||
|
const rows = build(laddered(12), { maxLayers: 4 });
|
||||||
|
const pots = laddered(12);
|
||||||
|
assert.equal(rows[1].label, `${pots.length - 2} more side pots`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with the list itself in a title, for anyone with a pointer", () => {
|
||||||
|
const pots = laddered(12);
|
||||||
|
const rows = build(pots, { maxLayers: 4 });
|
||||||
|
pots.slice(1, -1).forEach(pot => {
|
||||||
|
assert.ok(rows[1].title.includes(pot.label),
|
||||||
|
`${pot.label} vanished without trace`);
|
||||||
|
assert.ok(rows[1].title.includes(Money.format(pot.amount)),
|
||||||
|
`${pot.label}'s amount vanished without trace`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and points at where the whole list is", () => {
|
||||||
|
// The title is a pointer affordance, so it cannot be the answer on a phone. The
|
||||||
|
// visible text has to say where the rest lives.
|
||||||
|
assert.match(build(laddered(12), { maxLayers: 4 })[1].who, /resolve/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when eliding would not buy anything", () => {
|
||||||
|
test("a list that fits is left alone", () => {
|
||||||
|
for (let players = 3; players <= 5; players++) {
|
||||||
|
const pots = laddered(players);
|
||||||
|
assert.ok(pots.length <= 4, "this loop is meant to cover the fitting sizes");
|
||||||
|
const rows = build(pots, { maxLayers: 4 });
|
||||||
|
assert.equal(rows.length, pots.length);
|
||||||
|
assert.deepEqual(rows.map(r => r.label), pots.map(p => p.label));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and a single middle layer is not replaced by a line saying there is one", () => {
|
||||||
|
// At exactly the cap the middle is one row, and swapping one row for one row is
|
||||||
|
// pure indirection: the dealer loses an amount and gains a sentence.
|
||||||
|
const pots = laddered(5);
|
||||||
|
assert.equal(pots.length, 4, "five all-in ladder into exactly the cap");
|
||||||
|
const rows = build(pots, { maxLayers: 4 });
|
||||||
|
assert.equal(rows.length, 4);
|
||||||
|
assert.ok(!rows.some(r => /potLayerMore/.test(r.className)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the wizard, which has room", () => {
|
||||||
|
test("still gets every layer", () => {
|
||||||
|
// Same builder, no cap — this is the announcement read out before anybody shows
|
||||||
|
// a hand, and it is in a dialog that scrolls. Capping it there would hide the
|
||||||
|
// pots at the moment they are being paid.
|
||||||
|
const pots = laddered(12);
|
||||||
|
const rows = build(pots, undefined);
|
||||||
|
assert.equal(rows.length, pots.length);
|
||||||
|
assert.ok(!rows.some(r => /potLayerMore/.test(r.className)));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,12 +40,21 @@ const OVERLAYS = [
|
|||||||
// sitting down more than on anything a viewport can tell you. A full table is where the
|
// sitting down more than on anything a viewport can tell you. A full table is where the
|
||||||
// seats crowd, and it was never once looked at: measured when it finally was, seventeen
|
// seats crowd, and it was never once looked at: measured when it finally was, seventeen
|
||||||
// of fifty-four tables had one player's name or stack painted under another.
|
// of fifty-four tables had one player's name or stack painted under another.
|
||||||
|
const PACKED = [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
||||||
|
["Bob", 250], ["Dee", 600], ["Sam", 900], ["Priya", 1100], ["Jonathan", 480],
|
||||||
|
["Al", 2000], ["Katarzyna", 340], ["Ted", 760], ["Wes", 1300]];
|
||||||
|
|
||||||
const TABLES = [
|
const TABLES = [
|
||||||
["half full", 9, [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
["half full", 9, [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
||||||
["Bob", 250], ["Dee", 600]], false],
|
["Bob", 250], ["Dee", 600]], false],
|
||||||
["packed", 12, [["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500],
|
["packed", 12, PACKED],
|
||||||
["Bob", 250], ["Dee", 600], ["Sam", 900], ["Priya", 1100], ["Jonathan", 480],
|
// The same twelve, every one of them all-in for a different amount. This is the
|
||||||
["Al", 2000], ["Katarzyna", 340], ["Ted", 760], ["Wes", 1300]]],
|
// shape that makes a pot layer per player, and the sweep had never reached it:
|
||||||
|
// every table here was driven with Call and Check, so the side-pot breakdown — the
|
||||||
|
// one thing on the bar whose height depends on the table — was empty at all 294
|
||||||
|
// audits. Measured when it finally was: the bar took 411px of a 640px phone and
|
||||||
|
// left 96px of felt for twelve seats.
|
||||||
|
["all in", 12, PACKED, false, 12],
|
||||||
// The half-full table again, with the board turned on and five cards on it. The
|
// The half-full table again, with the board turned on and five cards on it. The
|
||||||
// board is furniture in the middle of the felt that nothing else was ever measured
|
// board is furniture in the middle of the felt that nothing else was ever measured
|
||||||
// against — and where the middle cannot hold it, it moves to a strip above the
|
// against — and where the middle cannot hold it, it moves to a strip above the
|
||||||
@@ -116,7 +125,7 @@ async function main() {
|
|||||||
for (const [width, height, rawLabel] of VIEWPORTS) {
|
for (const [width, height, rawLabel] of VIEWPORTS) {
|
||||||
await page.setViewportSize({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
|
|
||||||
for (const [shape, seats, players, board] of TABLES) {
|
for (const [shape, seats, players, board, shoves] of TABLES) {
|
||||||
const label = `${rawLabel} · ${shape} · ${hand}`;
|
const label = `${rawLabel} · ${shape} · ${hand}`;
|
||||||
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
||||||
await page.evaluate(seedTable, [seats, players, board]);
|
await page.evaluate(seedTable, [seats, players, board]);
|
||||||
@@ -133,7 +142,36 @@ async function main() {
|
|||||||
if (deal) deal.click();
|
if (deal) deal.click();
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(220);
|
await page.waitForTimeout(220);
|
||||||
for (let i = 0; i < 3; i++) {
|
// Everyone's whole stack in, which is what stacks up the side pots.
|
||||||
|
// There is no one-tap shove: a player who can't cover the bet gets
|
||||||
|
// "Call all-in", and anyone else has to open the raise pad, take its
|
||||||
|
// All-in preset and confirm.
|
||||||
|
for (let i = 0; i < (shoves || 0); i++) {
|
||||||
|
const step = await page.evaluate(() => {
|
||||||
|
const find = re => [...document.querySelectorAll("#actionBar button")]
|
||||||
|
.find(b => !b.disabled && re.test(b.textContent.trim()));
|
||||||
|
const short = find(/^Call all-in/);
|
||||||
|
if (short) { short.click(); return "done"; }
|
||||||
|
const open = find(/^Raise|^Bet/);
|
||||||
|
if (!open) return null;
|
||||||
|
open.click();
|
||||||
|
return "pad";
|
||||||
|
});
|
||||||
|
if (!step) break;
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
if (step === "pad") {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const find = re => [...document.querySelectorAll("#actionBar button")]
|
||||||
|
.find(b => !b.disabled && re.test(b.textContent.trim()));
|
||||||
|
const max = find(/^All-in/);
|
||||||
|
if (max) max.click();
|
||||||
|
const go = find(/^Raise to|^Bet/);
|
||||||
|
if (go) go.click();
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < (shoves ? 0 : 3); i++) {
|
||||||
const acted = await page.evaluate(() => {
|
const acted = await page.evaluate(() => {
|
||||||
const btn = [...document.querySelectorAll("#actionBar button")]
|
const btn = [...document.querySelectorAll("#actionBar button")]
|
||||||
.find(b => /^(Call|Check)/.test(b.textContent.trim()) && !b.disabled);
|
.find(b => /^(Call|Check)/.test(b.textContent.trim()) && !b.disabled);
|
||||||
|
|||||||
@@ -162,6 +162,49 @@ const LayoutAudit = {
|
|||||||
return { name: "focus has room under the fixed bar", offenders };
|
return { name: "focus has room under the fixed bar", offenders };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- The furniture is not allowed to eat the table ----
|
||||||
|
|
||||||
|
// How much of the window the fixed bottom band is taking.
|
||||||
|
//
|
||||||
|
// The action bar's height is not a constant: it carries the pot breakdown, which had
|
||||||
|
// one row per all-in and no ceiling. Measured on a 360x640 phone — five players
|
||||||
|
// idle, 151px; six all-in, 303px; twelve all-in, 411px, which is 64% of the screen,
|
||||||
|
// leaving 96px of felt to draw twelve seats in. Nothing in the sweep could see it,
|
||||||
|
// because every check here asks whether a thing is covered or clipped and none of
|
||||||
|
// them asked how much room the covering thing had taken.
|
||||||
|
//
|
||||||
|
// Two lines, because there are two different claims. Half the window is the one
|
||||||
|
// worth failing on: furniture bigger than the content it serves is indefensible
|
||||||
|
// whatever it is showing, and it is a shape fault rather than a matter of taste.
|
||||||
|
// Thirty percent is the published guidance for persistent chrome and it is a warning
|
||||||
|
// here, because the shortest phone in the list does not meet it at rest — the bar is
|
||||||
|
// 151px of a 640px screen before anything happens — and a sweep that is always red
|
||||||
|
// stops being read.
|
||||||
|
furnitureBudget() {
|
||||||
|
const name = "the fixed bar leaves the table its room";
|
||||||
|
const furniture = this.bottomFurniture();
|
||||||
|
if (furniture.length === 0) {
|
||||||
|
return { name, offenders: [], skipped: "nothing is fixed to the bottom here" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const highest = Math.min(...furniture.map(node => node.getBoundingClientRect().top));
|
||||||
|
const tall = Math.round(innerHeight - highest);
|
||||||
|
const share = Math.round((100 * tall) / innerHeight);
|
||||||
|
const what = furniture.map(n => this.describe(n)).join(" + ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
offenders: share > 50
|
||||||
|
? [`${what} stands ${tall}px of a ${innerHeight}px window — ${share}%, ` +
|
||||||
|
`leaving the felt less room than the furniture`]
|
||||||
|
: [],
|
||||||
|
warnings: share > 30
|
||||||
|
? [`${name}: ${what} is ${share}% of the window (${tall}px of ${innerHeight}px); ` +
|
||||||
|
`persistent chrome is usually kept under 30%`]
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// The other half: a control that scrolling can never reach at all.
|
// The other half: a control that scrolling can never reach at all.
|
||||||
//
|
//
|
||||||
// The seat editor is a bottom sheet on a phone, and it was put away with
|
// The seat editor is a bottom sheet on a phone, and it was put away with
|
||||||
@@ -1238,6 +1281,7 @@ const LayoutAudit = {
|
|||||||
this.controlNames(),
|
this.controlNames(),
|
||||||
this.focusHasRoom(),
|
this.focusHasRoom(),
|
||||||
this.focusablesReachable(),
|
this.focusablesReachable(),
|
||||||
|
this.furnitureBudget(),
|
||||||
this.touchAction(),
|
this.touchAction(),
|
||||||
this.tapHighlight(),
|
this.tapHighlight(),
|
||||||
this.textSelectable(),
|
this.textSelectable(),
|
||||||
|
|||||||
Reference in New Issue
Block a user