Spec — Standard Tournament Master workbook¶
Generate a standard-format tournament workbook from a Tournament Time
Calculator CSV export, the way dual-meet-sheet-generator-spec.md already
does for dual meets — instead of duplicating last event's facility
spreadsheet and find-replacing every category key, court label and match
number by hand.
Status: implemented. The bound Apps Script is
sage-tools-api/apps-script/standard-tournament-generator.gs, running in the SAGE Standard Tournament Master workbook, whose_CATEGORY_TEMPLATEis an unchanged copy of the Pickle for Sight Annex'sHIMDtab. The generator passesapps-script/verify-standard-tournament-generator.mjs, which checks §16 against a mocked master fed both Pickle for Sight plan CSVs. §18 records where the code departs from this document; read it before trusting a cell reference.This is the "standard-tournament equivalent" the root
CLAUDE.mdhad been waiting on.
| Where the code goes | sage-tools-api/apps-script/standard-tournament-generator.gs (new file, §12.1) |
| How it ships | Bound Apps Script in the master workbook — paste, not deploy |
| New infra | none |
| New credentials | none |
sage-tools-api change |
none — no version bump, no Cloud Run deploy |
| Reuses | dual-meet-generator.gs's pure helpers verbatim where the shape matches (§12.2) |
Read the three dual-meet specs first. This one inherits their delivery
story, their Apps Script mechanics and most of their vocabulary, and only
writes down what is different. Where a section here says "same as Phase N",
it means exactly that and is not restated. The three are, all in
sage-docs/docs/specs/implemented/:
| Called here | File | Covers |
|---|---|---|
| Phase 1 spec | dual-meet-sheet-generator-spec.md |
The master-copy + bound-script approach, the plan CSV, category tabs, validation |
| Phase 2 spec | dual-meet-schedule-generator-spec.md |
SCHEDULE's geometry, match placement, category colours, tiling mechanics |
| Phase 3 spec | dual-meet-readouts-generator-spec.md |
Court Control, Timeline, CSV, STANDINGSCSV, the rebuilt-in-place guard |
0. Orientation — read this first if you have no context¶
What this is. SAGE runs pickleball tournaments. A standard tournament is the ordinary open-entry shape: pairs enter a category, get drawn into round-robin brackets, and the top finishers from each bracket advance into a single-elimination playoff. It is not a dual meet — there are no clubs, no cross-product group stage, and no fixed two-entrant playoff.
Each facility, each day runs off its own Google Sheets workbook. BKL Cup 2026 Day 2 had three: Main, Annex and Dreamcourts. Pickle for Sight 2026 has two: PCPH Main and PCPH Annex. That is the single biggest structural difference from a dual meet, which is one workbook for one venue, and §3 is about it.
Where things are.
| Thing | Path |
|---|---|
| The dual-meet generator this one copies from | sage-tools-api/apps-script/dual-meet-generator.gs |
The live-sync script that shares the Apps Script project (and owns SAGE → Fill match numbers) |
sage-tools-api/apps-script/sheets-sync.gs |
The Tournament Time Calculator, source of the plan CSV and of playoffPlan() |
sage-match-control.github.io/tools/tournament-calculator.html |
| The named-function contract §11 adopts | sage-docs/docs/technical/named-function-library.md |
| The console's Awards tab, which produces the podium (§7.7) | sage-docs/docs/specs/implemented/awards-podium-tab-spec.md |
| The event these references come from | sage-docs/docs/specs/implemented/pickle-for-sight-spec.md |
| The public site template that parses the team codes (§8) | sage-match-control.github.io/_templates/standard-tournament-template/index.html |
Vocabulary. Same as the dual-meet specs, minus clubs, plus:
| Term | Means |
|---|---|
| PFS, BKL | The two reference events (§1): Pickle for Sight 2026 and BKL Cup 2026 |
| category | A division, keyed NMD, LIXD, LI40MD, … — one tab each |
| pair | Two players who play together; the atomic competitor |
| bracket | A round-robin pool inside a category. Sizes may be uneven |
| slot | One start time across all courts — two SCHEDULE rows |
| stage | One playoff round: R32, R16, QF, SF, B, F |
| playoff slot | One numbered seat in a stage, e.g. SF_3 — gets its own team code |
| team code | <KEY>_<i> for a group pair, <KEY>_<STAGE>_<k> for a playoff slot |
| fresh seat | A playoff slot filled from the group stage by the qualifier draw, or by a bye (§7.6) |
| fed seat | A playoff slot filled by the winner of an earlier playoff match (§7.5) |
| facility | One venue running one subset of the day's categories |
MATCHES band |
One category's column of court blocks on the MATCHES tab (§10.2.2) |
| wave | A group of categories scheduled together; a later wave starts after an earlier one ends (§10.2.4) |
Section-reference convention. A bare §n means this document. A
reference into another spec always names it — "Phase 1 spec §11.1", "Phase 3
spec §5.4".
1. Sources¶
None of the workbook structure is in source control. Everything below was
read directly out of four hand-built workbooks from two events, exported as
.xlsx and read cell by cell (Phase 3 spec §1.1 has the technique):
| Workbook | File ID | What it gave |
|---|---|---|
| Pickle for Sight · PCPH Main | 1-MYwVDD8z0ENtEiy3HztRTDghcGxlAmCEwn6p6OZzRA |
4 categories, 116 matches, courts 1-4, the full-draw and tiered-bye ladders, the wave schedule (§10.2.4) |
| Pickle for Sight · PCPH Annex | 1dC0_GOnumT3_0D9MOEO8lKu_Y69oE5wIFdkK-MY33XY |
5 categories, 125 matches, courts 5-9, the round-robin-only shape (§5.2) |
| BKL Day 2 · Main (LI40) | 1resKkD3e8kjxJEcQkruzKW8N93NFjuqgtmD9_hwYHQs |
3 categories, 41 matches, 12 court blocks, the twice-to-beat case |
| BKL Day 2 · Dreamcourts (LI18) | 1H5aHgogxauIgbb3xoBSuLQjVZCCdZcI_9iBXUMp_9SI |
3 categories, 84 matches, 15 court blocks, a 4-stage playoff ladder, Standings and Timeline (Individual) |
The two Pickle for Sight workbooks are [REF] copies of the event's
workbooks. The operators built those by hand from a BKL copy, following
pickle-for-sight-spec.md §12.1, and corrected them against this spec
before the copies were taken. They are the authoritative reference:
where they and the BKL workbooks disagree, this spec follows Pickle for
Sight. BKL is used only for what Pickle for Sight does not have — the
twice-to-beat final (§5.2) and the 12- and 15-court widths. The BKL workbooks are registered in
event-data/config/events.json under bkl-cup-2026 → day3, alongside a
third (Annex) not read here.
Each Pickle for Sight workbook has its own calculator plan CSV, one per venue, not one for the whole event (§3, §4):
| File | courts |
Categories | Projected |
|---|---|---|---|
pickle-for-sight-tournament-2026-09-27-plan MAIN.csv |
4 | NWD NMD NXD LIWD |
116 matches, 9:05 PM |
pickle-for-sight-tournament-2026-09-27-plan ANNEX.csv |
5 | LIMD LIXD HIWD HIMD HIXD |
125 matches, 7:25 PM |
Read coverage. Every tab of all four workbooks was read as cell values
plus formulas, and every <definedNames> library in full. The Poster tab
holds only images and was not inspected further.
1.1 The reference events reconcile against the calculator exactly¶
Worth stating up front, because it is what licenses §5: the calculator's
calcCategory() reproduces all four reference workbooks' match counts to the
match, from nothing but pair count, bracket count, advance, fill and
solo_format.
| Workbook | Category | Pairs | Brackets | RR | Playoff | Total |
|---|---|---|---|---|---|---|
| PFS Main | NWD |
8 | 2 (4-4) | 12 | 4 | 16 |
| PFS Main | NMD |
15 | 3 (5-5-5) | 30 | 6 (tiered) | 36 |
| PFS Main | NXD |
20 | 4 (5-5-5-5) | 40 | 8 | 48 |
| PFS Main | LIWD |
8 | 2 (4-4) | 12 | 4 | 16 |
| PFS Main total | 116 | |||||
| PFS Annex | LIMD |
19 | 4 (5-5-5-4) | 36 | 8 | 44 |
| PFS Annex | LIXD |
18 | 4 (5-5-4-4) | 32 | 8 | 40 |
| PFS Annex | HIWD |
3 | 1 | 3 | 0 (solo_format: rr) |
3 |
| PFS Annex | HIMD |
13 | 3 (5-4-4) | 22 | 6 (tiered) | 28 |
| PFS Annex | HIXD |
6 | 2 (3-3) | 6 | 4 | 10 |
| PFS Annex total | 125 |
116 and 125 are each workbook's CSV row count and its last match number
less the base (1116, 2125). The bracket sizes reconcile against each
category tab's group score grids (§7.4). There is one grid per bracket, sized
5-4-4 in HIMD, 5-5-4-4 in LIXD and 3-3 in HIXD.
NMD and HIMD are both 3 brackets × top 2 under fill: bye, the same
shape as BKL's LI18MD below. Both workbooks build it as exactly the ladder
the calculator plans: one R16 match, one QF, two SF, a bronze and a
final (§7.5).
| Workbook | Category | Pairs | Brackets | RR | Playoff | Total |
|---|---|---|---|---|---|---|
| BKL Main | LI40MD |
9 | 2 (5-4) | 16 | 4 | 20 |
| BKL Main | LI40WD |
4 | 1 | 6 | 2 (twice-to-beat) | 8 |
| BKL Main | LI40XD |
7 | 2 (4-3) | 9 | 4 | 13 |
| BKL Main total | 41 | |||||
| BKL Dreamcourts | LI18MD |
15 | 3 (5-5-5) | 30 | 6 | 36 |
| BKL Dreamcourts | LI18WD |
8 | 2 (4-4) | 12 | 4 | 16 |
| BKL Dreamcourts | LI18XD |
16 | 4 (4-4-4-4) | 24 | 8 | 32 |
| BKL Dreamcourts total | 84 |
41 is Timeline!C2 and the CSV row count in BKL Main; 84 is the same in
BKL Dreamcourts. The bracket sizes reconcile against the bracket column's own
A/B/C/D tallies (13/13/9/4 across the three Dreamcourts categories).
LI18MD is the first reference that proved the tiered-bye machinery: 3 brackets × top 2
= 6 qualifiers, which tieredRounds([3,3]) reduces as 1 + 1 + 2 + 1
matches plus a bronze — and the workbook has exactly two R16 slots, two
QF slots, four SF slots, two B and two F. Even the labels match:
the calculator names rounds by walking back from the Final and doubling, so
a four-round ladder is R16 · QF · SF · Final regardless of how few pairs
are actually in the early rounds.
2. Why this is a second master, not a mode of the first¶
The category tabs run on Named Functions — workbook-level LAMBDA
definitions under Data → Named functions, in no repo, uncreatable through
the Sheets API. Phase 1 spec §1 works through why that forces
generate-from-a-copy plus bound script. All of it applies here unchanged.
The BKL workbooks carry a different, older library — 15 functions where
the Dual Meet Master has 23, and none of them built on STACKBLOCKS:
main.xlsx GETPLAYERSCOLUMN1 = LAMBDA({SCHEDULE!$E$6:$E ; …$M$6:$M ; … ; $CO$6:$CO}) 12 blocks
dream.xlsx GETPLAYERSCOLUMN1 = LAMBDA({SCHEDULE!$E$6:$E ; …$M$6:$M ; … ; $DM$6:$DM}) 15 blocks
The column list is hand-edited per workbook to match its court count. That is not a stylistic difference, it is the defect that makes a standard generator worth building at all — and it has already bitten (§2.1). §11 replaces the whole library with the dual meet's, so a generated standard workbook derives its court count from the sheet's own width like every dual meet workbook already does.
The Pickle for Sight workbooks already run §11's library. Both carry the same
24 functions: the dual meet's 23, with MATCHCOURT in §10.5's row-5 form,
plus GETWINSCORE. They run at 4 and at 5 court blocks with no column list
anywhere. That library is no longer a proposal; it is proven on a standard
workbook, and §14.1 starts from it, less the two functions nothing calls
(§11).
Two masters rather than one, for the same reason there are two site templates: the category tab is a genuinely different organism (§7), the playoff model is inverted (§8), and one workbook covers one facility rather than one event (§3). A shared master would be a mode switch on every tab.
2.1 What the hand-built workbooks get wrong¶
Recorded because each one is a requirement in disguise — the generator's job is to make these unrepresentable, not to reproduce them.
| Defect | Where | Consequence |
|---|---|---|
SCHEDULE is 15 court blocks wide but Variables!C5 says courts: 6 |
BKL Dreamcourts | Every pace estimate on Court Control divides by the wrong number |
Court labels repeat — 10 11 12 13 14 15 7 8 9 10 11 12 13 14 15 |
BKL Dreamcourts | CSV!D reports the wrong court for any match past block 9 |
CSV!C2/D2 scan a hardcoded SCHEDULE!$F$6:$BZ$78 — 10 blocks |
both BKL | Silently wrong for courts 11+; only survived because 6 were used |
| Named-function column list (12) ≠ schedule width (15) | BKL Dreamcourts | Courts 13-15 invisible to every standings formula |
Court Control!C42 = SUM(LI40MD!D:D)+SUM(LI40XD!D:D)+SUM(LI40WD!D:D) |
BKL Main | Add a category, forget the sum, "matches done" goes quietly stale |
Reference for Players!A377 = FILTER(HIMD!…) → #REF! |
BKL Main | A leftover anchor for a category this facility never ran |
Reference for Players carries an anchor for all nine of the event's categories; the four or five the other venue runs resolve to #REF! |
PFS Main, PFS Annex | Same class as the row above. Harmless while nothing reads those rows, but every spill over the tab has to step over them (§10.7) |
Timeline body is a 24-term COUNTIFS chain, one pair of terms per court |
both BKL | Unmaintainable; wrong the moment a court is added |
Variables!I38 reads Low Intermediate 18+ Women's Doubles on the HI18WD row |
BKL Main | Same class as the LIMD/LIWD typo Phase 1 spec §4.3 fixed structurally |
A playoff block's C cell claims the winner's next code is <KEY>_F_SF-1 |
all four | Not a real code — see §7.5 |
STEP 3 (AI) is prefilled <KEY>_1 … <KEY>_<n> in order on every category tab |
PFS Main, PFS Annex | The unshuffled mapping §7.6 forbids: every pair maps to its own roster slot, and the step looks done |
LIXD's playoff slot table (AT:AU) ends in two stray rows, F1x2/F1 and F2x2/F2 |
PFS Annex | Harmless — no drawn number can equal F1x2 — but a leftover of how BKL's LI40WD rigged its twice-to-beat final inside the scaffold (§7.5.1). The generator writes exactly one row per fresh seat (§7.6) |
3. The unit of generation is a facility-day, not an event¶
A dual meet is one workbook. A standard tournament is one workbook per facility per day. The plan CSV may describe the whole event (BKL) or one facility (Pickle for Sight exported one CSV per venue, §1). Three consequences, all of which the generator has to be told about because the CSV cannot carry them:
Categories are assigned to facilities by hand. BKL Day 2 put the LI40
categories on Main and the LI18 categories on Dreamcourts. Pickle for Sight
put Novice and LIWD on Main and the rest of Low and High Intermediate on
the Annex. Nothing derives that. The operator picks, either by exporting one
CSV per venue from the calculator or by selecting categories in the sidebar
(§6.3).
Court numbering is facility-local and non-contiguous. BKL Main ran courts
1-12, Dreamcourts 7-15. Pickle for Sight runs one continuous range across its
two venues, Main 1-4 and Annex 5-9, because the site places a match at a
venue by its court number (pickle-for-sight-spec.md §12.1). The label is
free text in SCHEDULE! row 5 and is what CSV!D publishes, so it has to be
an input, not "Court " & c.
Match numbers are facility-scoped and offset. BKL Main starts at 1001,
Dreamcourts at 3001, Annex (by the same rule) at 2001. Pickle for Sight
Main starts at 1001 and Annex at 2001. All the facilities' CSV tabs
merge into one day snapshot in event-data, so a collision would silently
overwrite a match. The operator types the offset when numbering the packed
schedule with SAGE → Fill match numbers (§6.2, §10.2.3).
Every category row in the CSV goes into Variables!H:P (§6.3). The workbook
documents the whole plan it was given and generates tabs for its own slice.
4. Input — the plan CSV in standard format¶
Same file as Phase 1 spec §2, same columns:
with the standard-format columns live and the dual ones inert. The mirror image of Phase 1 spec §2.1:
| Column | Standard format |
|---|---|
teams |
Live. Total pairs in the category |
brackets |
Live. Number of round-robin pools (c.group, 1-30) |
advance |
Live. Top k per bracket qualify, 1-4 |
fill |
Live. bye or wc — how a short playoff field is filled (§5.4) |
note |
Free text, straight to Variables!O |
teams_a, teams_b |
Always ignore. The calculator still writes them (c.teamsA/c.teamsB persist across a format switch), and in standard format they are meaningless leftovers |
po_format |
Always ignore. Written empty in standard format by exportCSV; older exports may carry semis |
solo_format |
Live, one-bracket categories only. ttb (twice-to-beat final) or rr (round robin only, no playoff) — §5.2. The calculator writes it only for a standard category that resolves to one bracket and is not a 2-pair best-of-3, and leaves it empty everywhere else. Empty, or a missing 12th column in an older 11-column export, means ttb — the same default the calculator's own Import CSV applies |
The file's data rows carry fewer cells than the header — setting rows stop
after value — so parse positionally and treat a missing trailing cell as
empty, as dual-meet-generator.gs's parsePlanCsv already does.
setting format must read standard. A dual file is rejected, not
approximated — the exact converse of Phase 1 spec §3.
Settings used: title, date, start, duration_min, buffer_min.
courts is read but overridden by the per-facility court list (§6.1).
In a whole-event CSV it is the event's total across all venues. In a
per-venue CSV (Pickle for Sight's 4 and 5) it is that venue's count, and
the generator warns, without failing, when it differs from the number of
court labels entered. club_a/club_b are dual-meet settings and are
ignored.
5. Every playoff shape the calculator can produce¶
This is the enumeration the generator's playoff builder has to cover. All of
it is playoffPlan(groups, adv, fill, soloFormat) and its helpers
(groupSizes, reduceToTarget, solveForTarget, tieredRounds,
calcCategory) in sage-match-control.github.io/tools/tournament-calculator.html;
the branch order below is the code's own. The generator reimplements them in
Apps Script and must give the same numbers — §1.1's tables are the check.
5.1 teams == 2 — best-of-3, no round robin¶
calcCategory short-circuits before groupSizes is ever called.
No group stage, no bracket, no bronze. Three match slots, of which the third is played only if the series splits 1-1 — the plan counts the worst case, so the workbook must carry three. No reference workbook has this shape; §7.5.1 specifies its blocks.
5.2 brackets == 1 — two shapes, picked by solo_format¶
playoffPlan(groups, adv, fill, soloFormat) reads soloFormat only when
groups == 1.
solo_format: ttb (the default) — round robin then a twice-to-beat final.
plan = { rounds: [{label:'Final · twice-to-beat', matches:2}], single:true, rrOnly:false,
qualifiers:2, bronzeText:'RR #3 automatic' }
RR #1 needs one win; RR #2 must win twice. Two match slots, and bronze is
not a played match — the third-placed pair takes it on record. The workbook
still carries it as a row, a walkover against BYE, because that is how the
console reads a bronze with no match (§7.5.1).
BKL Main's LI40WD (4 pairs, 6 RR + 2) is the only reference, and it was
rigged by hand: its final exists only on SCHEDULE and in the qualifier
scaffold, not as blocks on the tab. Its codes are the ones to copy:
LI40WD_F_1_(1), LI40WD_F_2_(1) for game 1 and LI40WD_F_1_(2),
LI40WD_F_2_(2) for game 2. The (n) suffix is already understood by the
site — matchInstanceOf() in the standard template parses it and
roundLabel() renders "Final · Match 2". The blocks are specified in §7.5.1.
solo_format: rr — round robin only.
No playoff at all. The round-robin standings decide every medal: RR #1 gold, #2
silver, #3 bronze. rounds is empty rather than a zero-match round, so
the generator builds nothing from it: no stage banner, no playoff block, no
qualifier scaffold rows and no playoff slot table.
Verified in PFS Annex's HIWD (3 pairs, advance: 1, 3 matches). The tab is
the group block and one 3×3 score grid, and nothing below them. HIWD never
appears in a playoff stage of the schedule.
With no playoff row to carry GOLD/SILVER/BRONZE, the podium comes from
the standings. The console's Awards tab already does exactly that when a
category has no final (§7.7).
5.3 brackets >= 2 && brackets * advance == 2 — a bare final¶
Two brackets, top 1 each. playoffPlan returns early:
No bronze at all, not even an automatic one. Worth calling out because every other multi-bracket shape has one.
5.4 brackets >= 2 — the tiered single-elimination ladder¶
The general case, and the one with the machinery. tieredRounds(Array(adv).fill(groups)):
advance == 1— one homogeneous tier ofgroupsentrants, reduced byreduceToTargetto 2 and then a final. Byes by lot, and they occur only in the single earliest round.advance >= 2— tiered. All group winners outrank all runners-up, which outrank 3rd places. Winners bye all the way to roundR = smallest power of 2 strictly greater than groups, skipping every earlier round entirely. The remainingR - groupsseats at that round are played for by the lower tiers, flattened into one pool, through their own preliminary round(s).
Round labels are assigned backwards from the Final, doubling: the last
round is Final, then Semifinals, Quarterfinals, Round of 16, Round
of 32. The label describes the round's structural position, not how many
pairs are in it — which is why LI18MD's ladder of 1, 1, 2, 1 matches is
labelled R16 · QF · SF · Final.
Bronze: a Bronze match round is spliced in before the Final when
qualifiers >= 4; below that bronzeText is 'losing semifinalist
automatic' and there is no match.
fill: the two options answer the same question — the field doesn't fill
a bracket cleanly, so what do we do with the empty seats? — but they measure
those seats against different things.
bye keeps the tiered ladder above untouched: the empty seats are byes in
the earliest round, and group winners keep their skip ahead.
wc abandons the ladder and fills the draw out to a complete bracket:
enough wildcard entrants to reach the next power of two, so every qualifier
starts in the same round, nobody byes, and nobody has a longer road to gold
than anyone else. The count is measured against the bracket, not against the
one play-in round the ladder happened to leave short — 3 brackets × top 2 is
6 qualifiers in a draw of 8, so 2 wildcards, not the 1 bye the tiered
ladder's first round contained. wc = 2^ceil(log2(direct)) - direct, and the
rounds are the plain halving from that bracket size down to the Final.
This changes the entrant count, so it changes the number of playoff slots the
tab must carry. Note the two fills can differ in round count as well as
size: LI18MD under bye is the four-round ladder R16 · QF · SF · Final,
and under wc is the three-round QF · SF · Final of a full draw of 8.
For advance == 1 the two rules agree — a flat bracket's first-round byes
already number 2^ceil(log2(groups)) - groups — so only tiered categories
(advance >= 2) see any change.
A tiered ladder can be deeper than its entrant count suggests. Round
labels walk back from the Final, so a ladder with six rounds labels its first
one Round of 64, however few pairs are in it. Under fill: bye, 15 of the
calculator's shapes with at most 32 qualifiers go past R32 — among them
7 brackets × top 2 (14 qualifiers, 6 rounds) and 3 brackets × top 4
(12 qualifiers, 6 rounds). §8's code grammar stops at R32, so §9 caps the
ladder's depth as well as its entrant count.
5.5 The shapes, as a table¶
teams |
brackets |
advance |
Shape | Playoff matches | Bronze |
|---|---|---|---|---|---|
| 2 | — | — | best of 3 | 3 (min 2) | none |
| any | 1 | — | RR + twice-to-beat (solo_format: ttb) |
2 (min 1) | RR #3, no match |
| any | 1 | — | RR only (solo_format: rr) |
0 | RR #3, no match |
| any | 2 | 1 | RR + Final | 1 | none |
| any | ≥2 | 1 | RR + flat single-elim, byes by lot | reduceToTarget(groups,2) + 1 |
match iff groups >= 4 |
| any | ≥2 | ≥2 | RR + tiered single-elim | tieredRounds |
match iff groups*adv + wc >= 4 |
5.6 Bracket sizes are uneven, and that is load-bearing¶
groupSizes(teams, brackets) splits as evenly as possible and enlarges the
first brackets with the remainder: 26 pairs over 6 brackets is 5-5-4-4-4-4.
Real: LI40MD is 5-4, LI40XD is 4-3.
Everything downstream has to stop assuming a square block:
- Round-robin matches are
Σ n_b(n_b - 1)/2, not one formula in onen. - A group block is
2 · n_brows, and the four blocks of a category are different heights. - The completion gate for a bracket is
n_b(n_b - 1)/2matches, i.e.SUM(J) = n_b(n_b - 1)across the block. - The
bracketcolumn cannot beceil(i / n)the way Phase 3 spec §6.3 computes it. §10.4 replaces it.
groupSizes also floors the bracket count at floor(teams/2) — every
bracket needs at least 2 pairs — and the calculator emits a note when it has
to reduce. The generator must apply the same reduction rather than trusting
brackets blindly, or a 5-pair, 4-bracket category generates four blocks and
the calculator planned three.
Likewise advance is capped at the smallest bracket's size
(adv = min(requestedAdv, min(sizes))), so a category with a bracket of 2
cannot advance 3.
6. Inputs the CSV does not carry¶
A sidebar, same pattern as the dual-meet generator's:
| Field | Default | Used by |
|---|---|---|
| Facility label | empty | Variables!C8, and through it Title!B7 — e.g. PCPH Main |
| Court labels | — | §6.1 |
| Categories in this workbook | none selected | Which tabs get built (§3) |
| Wave, per selected category | 2 for a key ending XD, else 1 |
The suggested layout and the SCHEDULE height (§10.2.4) |
| Day label | empty | Variables!P on the selected rows |
6.1 Court labels¶
A comma-separated list, in schedule order: 1,2,3,4 or 7,8,9,10,11,12.
Its length is the court count for this workbook — Variables!C5, the
SCHEDULE width and the Court Control block count all come from it, so the
Dreamcourts mismatch in §2.1 cannot recur.
Labels render as Court <label> in SCHEDULE! row 5 and are what CSV!D
publishes. Duplicates are rejected (§9).
6.2 Match numbers¶
The generator writes no match numbers. The operator packs SCHEDULE by hand
(§10.2.3) and then runs SAGE → Fill match numbers, which asks for a base
and numbers every placed match from base + 1, left to right across the
courts and down the slots. It writes the same numbers into CSV!A.
The convention the reference events use, facility index × 1000 (so Main
enters 1000 and gets 1001 onward, the Annex 2000 and gets 2001
onward), is a convention, not a rule anything enforces. Nothing in
events.json orders facilities, and no workbook can see another's numbers,
so keeping the ranges apart is the operator's check. The generator's
completion message restates the convention.
6.3 Categories, and the plan block¶
Variables!H:P gets every category row from the CSV, plus a day column
(P) and a facility column (Q, new — §10.1) that the generator fills in
for the rows it selected and leaves blank for the rest. Tabs are built only
for the selected keys, in CSV order.
That is what the reference workbooks do (BKL Main's Variables lists all
40-odd categories of the whole five-day event while carrying three tabs; both
Pickle for Sight workbooks list all nine of the event's categories), and it is
worth keeping: B1 on each category tab reads its display name back out of
that block by key, and the operator can see at a glance which slice of the
event this workbook is.
With a per-venue CSV every category in it belongs to this workbook, so the sidebar has a Select all control. The default stays "none selected": the generator cannot tell a per-venue CSV from a whole-event one, and a whole-event CSV with everything ticked would build every venue's tabs into one workbook.
7. The category tab¶
7.1 Column map¶
Verified cell by cell against BKL's LI40MD and LI18MD and all nine Pickle
for Sight category tabs, which have the same columns. Columns never move; only
row counts vary.
| Col | Holds |
|---|---|
A |
Team code — repeated on both rows of a pair |
B |
Player name — a lookup on the pair's first row, mirrored on the second |
C |
Seed/marker text (!, !!, !!!) — hand-typed, generator leaves blank |
D E |
W, L |
F G |
Points for, points against |
H |
Quotient |
I |
Free-text note (Replaced Guy Godoy) — hand-typed |
J |
Matches Done, =D+E — group rows only |
K |
Bracket number on each bracket's first group pair (§7.2); Next Round and the medal label on playoff rows (§7.5) |
L |
Bracket rank — hand-typed |
N |
Score-grid row labels |
O–… |
Score-grid body (§7.4) |
AB |
Notes column header for the roster scaffold |
AC AD AE |
Roster scaffold: pair index, STEP 1 · NAMES, code link — two rows per pair |
AG AH AI |
Roster scaffold: number, STEP 2 · CODES, STEP 3 · RANDOMIZED CODES — one row per pair |
AM AN AO AP |
Qualifier scaffold — the playoff draw (§7.6) |
AT AU AV |
Playoff slot table (§7.6) |
Header chrome: A1 is the raw key; B1 = FILTER(Variables!J:J, Variables!I:I=A1)
is the display name; AD1 = A1 is what every scaffold formula concatenates
against. Rows 2-4 carry the STEP 1/2/3 labels and the scaffold headers,
and K3 reads FOR NOTES ONLY;
row 5 carries W, L, Team Scr, Opp\nScr, Q, Matches Done,
Bracket, Br Rank. Group pairs start at row 6.
7.2 Group blocks — one continuous list, brackets marked in K¶
Unlike a dual meet, the brackets of a category are not separate blocks.
All teams pairs run continuously from row 6, two rows each, and bracket
membership is carried by column K: the bracket number on the first pair
of each bracket, and nothing on the others. HIMD (5-4-4) has K = 1 on
pair 1, 2 on pair 6 and 3 on pair 10, with no gap, no second header row
and no banner between them.
Keep that. It makes the block geometry independent of the bracket split, and
it means codes stay <KEY>_1 … <KEY>_<teams> in one unbroken run — which is
what the roster scaffold, the Reference for Players spill and the bracket
generator's own output all assume.
The generator knows the split, so it writes each number on the right pair.
The same groupSizes split drives STANDINGSCSV!G (§10.4) and the score
grids (§7.4), one grid per bracket anchored on the same first pair, so the
three always agree.
Per-pair formulas, filled down:
A6 = A$1&"_"&Variables!AB91 LI40MD_1 (the reference-number ladder, §7.3)
A7 = A6 second row of the pair
B6 = IFNA(FILTER($AD$5:$AD178,$AE$5:$AE178=A6),A6)
D6 = GETTOTALWINS(A6) E6 = GETTOTALLOSES(A6)
F6 = GETTOTALSCORE(A6) G6 = GETTOTALOPPONENTSCORE(A6)
H6 = GETSCOREQUOTIENT(A6) J6 = D6+E6
K6 = <bracket number> literal, on each bracket's first pair only
D–H and J are merged vertically across the pair's two rows. K is
merged on group rows; on playoff rows it is not, because its two rows
hold different formulas (§7.5) — the same rule Phase 1 spec §4.0.4 states for
column I there.
7.3 Variables!AB is a counter, not a draw¶
A6 = A$1&"_"&Variables!AB91, A8 = A$1&"_"&Variables!AB93, and so on. The
AB column is a ladder — AB91 = 1, AB93 = AB91+1, every second row — so
the effect is simply _1, _2, _3. It is the standard workbook's
counterpart to the dual meet's Variables!A2 ladder, spaced to the two-row
pair stride so a fill-down works.
It looks like an indirection worth removing, but keep it. The qualifier
scaffold's ordinals (AN, §7.6) read the same ladder cells. Both are
relative references that step two rows per pair, so the prototype block
(§14.1) can be copied down with copyTo and every copy numbers itself. A
literal _1, _2, … would have to be written pair by pair.
7.4 The score grid¶
One grid per bracket, anchored on that bracket's first pair row. For a
bracket of n_b pairs whose first pair row is first:
| Cell | Content |
|---|---|
O<first> … <n_b-th col><first> |
=N<first+1> … =N<first+n_b> — the column headers |
N<first+1+k> |
=A<first + 2k> — one row per pair, not two |
body (r, c) |
=GETSCOREAGAINSTPAIR($N<r>, <c>$<first>), diagonal left blank |
Verified at n_b = 5 (O:S), n_b = 4 (O:R) and n_b = 3 (O:Q), and
on the uneven splits: HIMD's grids sit at rows 6, 16 and 24 for its 5-4-4
brackets, which are pairs 1, 6 and 10. Note the offset from the
dual meet: labels are in N and the body starts at O, where the dual meet
uses M and N.
Both triangles are filled. Unlike a dual meet's cross-club grid, a
round-robin grid is genuinely two-sided — (i,j) is i's score against j and
(j,i) is j's — so a GETSCOREAGAINSTPAIR goes in every off-diagonal cell.
Several cells in the reference grids are hardcoded numbers rather than formulas, from operators typing over them mid-event. Do not read that as structure.
Playoff blocks carry a grid too, on the same mechanism, condensing each entrant's two rows into one grid row exactly as Phase 1 spec §13.2 describes:
N102 = A101 the block label, e.g. "SF-1"
O102 = N103 P102 = N104 entrant headers
N103 = A103 N104 = A105 one grid row per entrant, pointing at its SECOND row
P103 = GETSCOREAGAINSTPAIR($N103,P102)
O104 = GETSCOREAGAINSTPAIR($N104,O102)
The grid's header row is block.header + 1, one below the block's A-column
label.
7.5 The playoff ladder — forward propagation¶
This is the inversion. A dual meet's playoff entrants are pulled
backwards by feeder formulas that sort a group block (INDEX(SORTBYWINS(…),1)).
A standard tournament's are pushed forwards: each playoff block computes
where its winner goes, writes that code into column K, and the next round's
block finds its own occupants by searching the previous block's K for its
own code.
It has to work that way. A dual meet's bronze and final entrants are a
deterministic function of two group blocks. A standard ladder's are a
function of a draw plus a chain of results with byes in it — there is no
range for SORTBYWINS to sort.
Every example below is a real cell in PFS Main's NMD (15 pairs, 3
brackets × top 2, fill: bye), whose ladder is R16 → QF → SF → B + F.
Its last group pair sits on rows 34-35.
Row geometry. Everything below the group block is built from three pieces, stacked with no other rows between them:
| Piece | Rows | NMD |
|---|---|---|
| gap after the group block | 1 blank row | row 36 |
| stage banner | 1 banner row, then 1 blank row | R16 at 37; QF at 45; SF at 53; FINALS at 67 |
| match block | 1 label row, 4 entrant rows (2 per entrant), 1 blank row | R16-3 at 39-44; QF-2 at 47-52; SF-1 at 55-60; SF-2 at 61-66; B at 69-74; F at 75-80 |
A stage is its banner followed by its match blocks, in block-label order. The next stage's banner follows the last block's blank row.
Stage banner — the stage keyword in A, the stage before it in I
(- for the first stage) and the stage after it in K:
The finals banner is just A67 : FINALS.
Match block — a label row, then two entrants of two rows each:
A55 = A53&"-1" SF-1 block label (§7.6.1 numbers it)
B55 : "winner's next code ->"
C55 = <the cell of the seat the winner moves to> see the note below
D55..H55 : W / L / Team Scr / Opp Scr / Q headers
I55 : "Notes" (on early-round blocks)
K55 : "Next Round"
A56 = A$1&"_"&A53&"_1" NMD_SF_1 seat number from §7.6.1
B56 = <name lookup, see below>
D56 = GETTOTALWINS(A56) E56 = GETTOTALLOSES(A56) F56 = GETTOTALSCORE(A56)
G56 = GETTOTALOPPONENTSCORE(A56) H56 = GETSCOREQUOTIENT(A56)
K56 = <advance formula, see below>
A57 = A56 B57 : second name (spilled) K57 = K56
A58 = A$1&"_"&A53&"_2" NMD_SF_2
… same shape on rows 58-59
The B and F blocks under FINALS have the same shape, but their label
row carries only A69 : B (or A75 : F) and the D–H headers: no
"winner's next code", no C, no Next Round. Their seat codes are
A70 = A$1&"_"&A69&"_1" → NMD_B_1, and so on. Each block also has a
two-entrant score grid in N:P on the rows after its label, per §7.4.
Name lookup in B — two forms, and which one a seat gets is the whole
point:
| Seat is | B formula |
|---|---|
| fresh — drawn in from the group stage, or byeing into this round | =IFNA(FILTER(AO:AO, AP:AP=A<r>), A<r>) — the qualifier scaffold (§7.6) |
| fed — the winner of a previous-round match | =IFNA(FILTER(B<lo>:B<hi>, K<lo>:K<hi>=A<r>), A<r>), where <lo>:<hi> spans the previous stage's rows |
A single stage mixes both:
| Block | Seat | Name lookup | Winner goes to |
|---|---|---|---|
R16-3 |
R16_5, R16_6 |
both fresh | QF_3 (C39 = A48) |
QF-2 |
QF_3 |
fed — B48 = IFNA(FILTER(B38:B44, K38:K44=A48), A48) |
SF_4 (C47 = A64) |
QF_4 |
fresh | ||
SF-1 |
SF_1, SF_2 |
both fresh | F_1, loser to B_1 |
SF-2 |
SF_3 |
fresh | F_2, loser to B_2 |
SF_4 |
fed from QF-2 |
||
B, F |
_1, _2 |
fed — B70 = IFNA(FILTER(B54:B66, K54:K66=A70), A70) |
BRONZE / GOLD, SILVER |
QF_4 fresh beside QF_3 fed is precisely §5.4's tiered bye, rendered in
formulas; HIMD builds the same ladder, and BKL's LI18MD did too. In a
full draw with no byes (NXD, LIMD, LIXD: 4 brackets × top 2), every
QF seat is fresh and every SF seat is fed, QF-k's winner to SF_k.
§7.6.1 gives the one numbering rule that produces both.
A final can have a fresh seat. A flat ladder of 3 (3 brackets × top 1)
is one SF and a bye straight into the final, so F_1 is drawn in from the
qualifier scaffold and only F_2 is fed from the SF block. No reference
workbook has this shape. It follows from §7.6.1's numbering, which treats the
final like any other stage.
Advance formula in K, on each entrant's first row; the second row
mirrors it (K57 = K56):
early rounds K40 = IF(D40>0, C39, "-") winner moves to C's seat
semifinals K56 = IFNA(IFS(D56>0, A76, E56>0, A70), "-") winner -> F_1, loser -> B_1
K62 = IFNA(IFS(D62>0, A78, E62>0, A72), "-") SF-2: F_2 and B_2
bronze K70 = IF(D70>0, "BRONZE", "-")
final K76 = IFNA(IFS(D76>0, "GOLD", E76>0, "SILVER"), "-")
K is not merged on playoff rows, because the entrant's two rows hold
different things (§7.2).
The
Ccell is inconsistent in every reference and the generator must pick one form. Early-round blocks point it at the real destination cell (C39 = A48, resolving toNMD_QF_3), which is whatKthen uses. Semifinal blocks instead synthesise a label —C55 = $A$1&"_"&K53&"_"&A55→NMD_F_SF-1— which is not a team code that exists anywhere. It is harmless only because the SF's ownKformula ignoresCand names the destination cells directly. Generate the early-round form everywhere: on a semifinal block,Cpoints at theFseat its winner takes (C55 = A76).
7.5.1 Finals without a ladder — twice-to-beat and best-of-3¶
Two shapes have a final but no ladder to feed it (§5.1, §5.2). No reference
workbook builds either as blocks — BKL's LI40WD put its twice-to-beat codes
on SCHEDULE by hand, and seeded them from four hand-typed rows in its
qualifier scaffold (AM71:AP78, with F1x2/F2x2 rows in the slot table)
plus a LI40WD_B bronze seat. What follows is designed, using the same
block shape and geometry as §7.5 and the codes BKL used.
Twice-to-beat (one bracket, solo_format: ttb). After the group block,
the gap row and a FINALS banner, three blocks in this order:
Block label (A) |
Seats | Name lookup (B) |
K |
|---|---|---|---|
F (1) |
<KEY>_F_1_(1), <KEY>_F_2_(1) |
F_1: =IFNA(FILTER(AO:AO, AP:AP=$A$1&"_F_1"), A<r>); F_2: the same against "_F_2" |
the final's formula |
F (2) |
<KEY>_F_1_(2), <KEY>_F_2_(2) |
the same two lookups as game 1 | the final's formula |
B |
<KEY>_B_1, <KEY>_B_2 |
B_1: =IFNA(FILTER(AO:AO, AP:AP=A<r>), A<r>); B_2: the literal BYE on both rows |
B_1: BRONZE; B_2: - |
The qualifier scaffold (§7.6) has three rows and no draw — the round robin
decides who goes where, so AM stays -, the slot table is empty, and AP
is a literal written by the generator:
AN tier label |
AP |
AO |
|---|---|---|
RR #1 - twice to beat |
<KEY>_F_1 |
the RR #1 pair's names, operator-written |
RR #2 - challenger |
<KEY>_F_2 |
the RR #2 pair's names |
RR #3 - bronze |
<KEY>_B_1 |
the RR #3 pair's names |
The B block is a walkover: <KEY>_B_1 against a side whose name cell
reads BYE. GETPLAYERNAMESBYTEAMCODE returns BYE for <KEY>_B_2, so it
reaches CSV as that side's player, and the console's Awards tab makes the
other side bronze without scores (awards-podium-tab-spec.md §2.3). It
takes a match number and a SCHEDULE slot like any match. The decisive
final is whichever of game 1 or game 2 was played last (that spec's §2.4);
game 2 is scheduled even though it is played only if the challenger wins
game 1, because the calculator's count is the worst case.
Best-of-3 (teams == 2, §5.1). The two pairs are still listed as group
pairs <KEY>_1 and <KEY>_2, so the roster scaffold works unchanged, but
they have no score grid, no K and no qualifier scaffold. After the gap row
and a FINALS banner come three blocks, F (1), F (2) and F (3), with
seats <KEY>_F_1_(g) and <KEY>_F_2_(g). F_1's name lookup reads group
pair 1 directly, =IFNA(FILTER(B$6:B$9, A$6:A$9=$A$1&"_1"), A<r>), and F_2
the same against "_2". K carries the final's formula. No bronze.
7.6 Two scaffolds, two draws¶
The roster scaffold (AC–AI) is the same three-step shape as the dual
meet's, one column set instead of two:
| Step | Col | State on a fresh workbook |
|---|---|---|
| index | AC |
1 … teams, on each pair's first row (from AC5), generator-written |
| STEP 1 · NAMES | AD |
blank — operator pastes, 2 rows per pair |
| link | AE |
=FILTER($AI$5:$AI<end>, $AG$5:$AG<end>=AC<r>), mirrored on the second row |
| number | AG |
1 … teams, one row per pair from AG5, generator-written |
| STEP 2 · CODES | AH |
=$AD$1&"_"&AG<r>, one row per pair |
| STEP 3 · RANDOMIZED | AI |
blank — operator pastes the codes back shuffled, one row per pair |
STEP 3 stays blank for the reason dual-meet-generator.gs's writeRosterScaffold_ gives:
pre-seeding it makes an undone step look done, and an unshuffled STEP 3 maps
every pair to its own roster slot, defeating the blinding.
bracket-draw-name-import-spec.md is the one thing allowed to fill it, since
there the order comes from a seeded, published draw. Every Pickle for Sight category tab
shows the cost of getting this wrong: STEP 3 prefilled in order (§2.1).
The qualifier scaffold (AM–AP) is the second draw, and has no
dual-meet equivalent — a dual meet's playoff entrants are decided by record,
so there is nothing to draw. Here the group-stage qualifiers are drawn by lot
into the playoff slots. The reference's headers are 1st PLAYOFFS (AM3),
LETTERS HERE (AM4), PLACE NAMES HERE (AO4) and PLACE 1ST PLAYOFFS
BUNUTAN HERE (AU3, with the typo fixed). Pairs run two rows each from
row 5, one per qualifier:
| Col | Holds |
|---|---|
AM |
The drawn slot number for this qualifier — operator-written; generated as - |
AN |
First row: qualifier ordinal, =Variables!AB<row> (§7.3). Second row: the qualifier's tier label, generator-written (below) |
AO |
Qualifier player names, 2 rows per pair — operator-written |
AP |
=IFNA(FILTER(AV:AV, AT:AT=AM<r>), "-"), mirrored on the second row |
The generator writes exactly qualifiers rows (plan.qualifiers, wildcards
included), none for a round-robin-only or best-of-3 category, and the three
fixed rows of §7.5.1 for a twice-to-beat one. The tier label says which
finisher each row is waiting for, best tier first, in the order the ladder
seats them:
- tiered (
advance >= 2,fill: bye):Br 1 - Rank 1…Br <g> - Rank 1, thenRank 2 - 1st…Rank 2 - <g>th, thenRank 3 - …. The runners-up are ranked against each other across brackets. This is the formNMDandHIMDcarry, where the operator typed the winners asBr 1,Br 3,Br 2; the generator writes brackets in order. - flat or full draw (
advance == 1, orfill: wc):Br <b> - Rank <r>for every bracket and rank, thenWildcard 1…Wildcard <wc>.
The playoff slot table (AT–AV) is what AP resolves against:
| Col | Holds |
|---|---|
AT |
Slot number — a fresh seat's number, see below |
AU |
The stage the slot belongs to — F, SF, QF, R16, R32 |
AV |
=$AD$1&"_"&AU<r>&"_"&AT<r> |
The table lists fresh seats only, one row per qualifier. Fed seats are
filled by formula and are never drawn, so they have no row. HIMD's table is
the whole ladder of §7.5 in six rows: 1 SF, 2 SF, 3 SF, 4 QF,
5 R16, 6 R16. The Charts tab's PLAYOFFS OF 3 BRACKETS WITH TOP 2 tree
numbers the same seats 1 to 6.
The generator writes AU. It is the one column that encodes the whole
tiered structure. Leaving it to the operator is what left NWD's table
describing a quarterfinal it does not have (§2.1).
7.6.1 Numbering the playoff seats¶
One rule gives every playoff code its number. It reproduces all four
reference ladders — the tiered bye (NMD, HIMD, BKL's LI18MD), the full
8-draw (NXD, LIMD, LIXD) and the bare semifinal (NWD, LIWD, HIXD)
— and, run against the calculator's own playoffPlan, it gives unique codes
and unique block labels to all 105 multi-bracket combinations (2-30
brackets × advance 1-4 × bye/wc, at most 32 qualifiers) that pass §9's
depth cap. The only label clashes it produces are in ladders past R32.
Inputs, from playoffPlan: the rounds earliest to latest, bronze excluded,
each with its match count m. Round i's stage keyword counts back from the
end: F, SF, QF, R16, R32. Its fed seats number m of round
i - 1 (winners coming up), 0 for the earliest round, and its fresh seats
are the rest, 2m - fed: group qualifiers entering here, and byes.
- Walk the stages latest first —
F, thenSF,QF,R16,R32— with one counterc, starting at 0, shared by all of them. - In each stage, number its fresh seats
c + 1,c + 2, … and advancec. Order the fresh seats best tier first. - Lay the stage's seats out fresh seats first, then fed seats, and pair them in that order into matches: seats 1 and 2 are the first match, 3 and 4 the second, and so on.
- A fed seat whose match partner is fresh number
ntakesn + 1ifnis odd andn - 1ifnis even — the other half ofn's pair — unless the stage already uses that number. - Every other fed seat takes the lowest number not yet used in its stage.
- A match's block label is
<STAGE>-<j>withj = ceil(lowest number in the match / 2). - Wire each stage to the one before it (the next earlier round): the
stage's fed seats, in ascending number, take the winners of the earlier
stage's matches, in ascending
j.Bis fed by theSFlosers as §7.5 describes.
Worked for 3 brackets × top 2 (HIMD): F has no fresh seats. SF numbers
1 2 3 fresh; the fed seat partners 3, so 4; blocks SF-1 {1,2},
SF-2 {3,4}. QF numbers 4 fresh; its fed partner is 3; block QF-2.
R16 numbers 5 6; block R16-3. SF_4 takes QF-2's winner and QF_3
takes R16-3's. For a full draw of 8, SF has no fresh seats, so its fed
seats take 1 2 3 4 and QF numbers 1 … 8, which wires QF-k to SF_k.
Fresh numbers are unique across the whole ladder, so AT alone identifies a
seat and one drawn number in AM resolves to exactly one code. Fed numbers
repeat across stages (SF_3 and QF_3 are different seats), which is fine,
because the site's roundKeyword() matches on the keyword, not the number.
7.7 No awards block¶
The category tab has no awards block. BKL's tabs carried one in BA:BC
(Gold/Silver/Bronze, each FILTER(A:A, K:K=<medal>)) for a printable
Awards tab to read. Neither Pickle for Sight workbook has either, and a
generated workbook follows them — the same decision Phase 1 spec §4.2 took
for the dual meet.
The podium lives in the console instead. Control Center's Awards tab
(awards-podium-tab-spec.md) derives it from the published snapshot:
gold and silver from the decisive F match, bronze from the B match. It
already handles both shapes that have no match to read:
- Round robin only (§5.2,
solo_format: rr) — with noFmatch at all, it takes gold, silver and bronze from the standings (that spec's §2.5). - Twice-to-beat (§5.2) — the third-placed pair's bronze is a
Brow whose opponent is a bye (that spec's §2.3).
Column K on the finals still carries GOLD, SILVER and BRONZE from the
§7.5 advance formulas. Nothing in the workbook reads them; they are for the
desk to see at a glance.
8. Team code grammar¶
One category key, one underscore-delimited suffix. No club segment.
| Slot | Code | Count |
|---|---|---|
| Group pair | <KEY>_<i>, i = 1 … teams |
teams |
| Playoff slot | <KEY>_<STAGE>_<k> where STAGE ∈ {R32, R16, QF, SF} and k is the seat number from §7.6.1 |
2 per playoff match |
| Bronze | <KEY>_B_1, <KEY>_B_2 |
2 |
| Final | <KEY>_F_1, <KEY>_F_2 |
2 |
| Twice-to-beat final | <KEY>_F_1_(g), <KEY>_F_2_(g) for g = 1, 2 |
4 |
| Twice-to-beat bronze walkover | <KEY>_B_1, and <KEY>_B_2 named BYE (§7.5.1) |
2 |
| Best-of-3 final | <KEY>_F_1_(g), <KEY>_F_2_(g) for g = 1, 2, 3 |
6 |
| Round robin only | group pairs only | teams |
Total codes per category = teams + 2 × the matches its playoff blocks
hold — the calculator's playoff count, plus one for a twice-to-beat
category's bronze walkover. Verified against all fifteen reference
categories; BKL's LI40WD, which had no walkover row, carried teams + 4.
Every form above is already parsed by the standard-tournament site template —
roundKeyword() matches R16/QF/SF/F/B, matchInstanceOf() reads
the (g) suffix, and STAGE_ORDER is ['R16','QF','SF','BRONZE','FINAL'].
R32 is in the code grammar but not in STAGE_ORDER or
ROUND_KEYWORD_LABELS; a category deep enough to need it renders its R32
rows under an unsorted trailing stage. That is a site fix, tracked in §15,
not a reason to rename the codes.
9. Validation¶
All checks run before anything is written, and every failure is reported at once — Phase 1 spec §3's rule, and Phase 1 spec §9.2's abort-on-collision policy, both unchanged.
| Rule | Reason |
|---|---|
setting format == standard |
A dual file describes a different geometry |
key unique, [A-Z0-9]{2,8}, legal tab name |
Tab name is the key |
| at least one category selected | — |
teams in 2..100 |
The calculator's own bound |
brackets reduced to floor(teams/2) if larger, with a warning |
Matches groupSizes (§5.6) |
advance capped at the smallest bracket, with a warning |
Matches calcCategory (§5.6) |
solo_format empty, ttb or rr |
§4; anything else is a hand-edited file |
| total playoff entrants ≤ 32 | The Charts reference tree tops out at 32 (§10.8), and no real category has come close |
playoff ladder at most 5 rounds, bronze excluded — no stage before R32 |
§5.4: 15 bye shapes under the 32-entrant cap still reach Round of 64 or deeper, which §8 has no code for. The message names the category and suggests fill: wc or fewer brackets |
| court labels non-empty, unique, ≤ 24 | §6.1, and the Dreamcourts defect in §2.1 |
| wave a positive integer per selected category | §10.2.4 |
| no existing tab collides | Phase 1 spec §9.2 |
| every rebuilt-in-place tab is pristine | Phase 3 spec §8.2 |
A failure aborts with the offending rows named and the workbook untouched. Emitting a tab whose geometry does not match the plan it came from is the failure this section exists to prevent: it produces plausible-looking wrong medalists rather than an obvious error.
10. The other tabs¶
10.1 Variables¶
| Cells | Value |
|---|---|
A2:C8 |
setting rows: title, date, start, courts, duration_min, buffer_min, location |
C5 |
court count from the label list (§6.1), not the CSV's courts |
C6 |
slot pitch as a time — duration_min + buffer_min minutes. The Pickle for Sight workbooks label it duration_min and hold 0:25, with buffer_min (0) in C7; every slot-time formula adds Variables!$C$6 (§10.2, §10.6). Writing the sum here keeps those formulas as they are and still honours a non-zero buffer |
C8 |
Facility label |
H1:P1 |
Headers type, key, value, teams, brackets, advance, fill, note, day |
H11:P… |
The full plan block, one row per CSV category from row 11, in CSV order, and nothing below it — as in both Pickle for Sight workbooks. STANDINGSCSV!G reads I (key), K (teams) and L (brackets) to work out each pair's bracket (§10.4), so these columns must stay in place |
Q |
New — facility. Filled for the rows this workbook generated tabs for, blank otherwise |
R |
New — court labels, one per row from R2, so SCHEDULE's row 5 can be a formula rather than 12 literals |
AB90… |
The reference-number ladder (§7.3), every second row |
H:P is written from the CSV verbatim, which structurally fixes the
HI18WD-labelled-as-Low-Intermediate defect in §2.1 the same way Phase 1
spec §4.3 fixed its counterpart. solo_format is
not a column here: it changes only the playoff, which the category tab
already shows.
10.2 SCHEDULE and MATCHES¶
The generator lays every category's matches out on a new MATCHES tab,
one category beside the next, and builds SCHEDULE as an empty grid.
The operator packs the day by copying court blocks from MATCHES into
SCHEDULE (§10.2.3). Packing by hand is how both Pickle for Sight schedules
were made, and it keeps the operator in control of which categories share
the courts when. Generating the packed schedule too is written down in
§10.2.4 as a possible future implementation.
10.2.1 SCHEDULE — an empty grid¶
Geometry is the dual meet's — Phase 2 spec §2: rows 1-4 blank but for the
title band, row 5 the header band, data from row 6, a repeating 8-column
court block from column D (D names, E team 1, F match number under
Court <n>, G team 2, H names, I/J scores), a slot is two rows,
and the grid runs one row-pair past the last slot to carry the end time
(Phase 3 spec §3.2). One difference from Phase 2, taken from the Pickle for
Sight SCHEDULEs: the last court keeps its separator column, so the width
is 8 · courts + 3 (AI at 4 courts, AQ at 5). STACKBLOCKS gives the
same court count at either width.
What the generator writes:
- The title band in rows 1-3 —
D1 = Title!B6,D2 = Title!B7, andPowered by/SAGE Match Control ExpertsinD3/E3. Cosmetic, but it is what the venue screenshots. - Court headings in row 5,
="Court "&Variables!R<n>from the court labels (§6.1), withSCOREover each block'sI. - Slot times in
B:B6 = Variables!C4, thenB<r> = B<r-2>+Variables!$C$6down every slot and the end-time row. - Every court block on every slot: the name formulas
D<r> = GETPLAYERNAMESBYTEAMCODE(E<r>)andH<r> = GETPLAYERNAMESBYTEAMCODE(G<r>), which spill the pair's two names down both rows of the slot (there is novsrow), plus the merges, column widths, row heights and the category-colour rules. A blank code shows blank names.
What it leaves blank: the team codes in E/G, the match numbers in
F, and the scores.
Height. N slots, where N is the slot count the §10.2.4 packer
reaches for this workbook's categories, plus 4 spare. The packer needs
33 slots for PFS Main and 29 for PFS Annex, so N is 37 and 33. (The
operators' own hand layouts used 31 and 30.) A hand layout can be looser or
tighter than the packer's, and trailing empty slots cost nothing:
Fill match numbers skips them. Running short costs more, because
SCHEDULE and Timeline (§10.6) both have to be extended by hand. The
generator runs the packer in memory only to get N — nothing it computes is
written to SCHEDULE.
10.2.2 MATCHES — every category's matches, side by side¶
A new tab with the same geometry as SCHEDULE: 8-column court blocks
from D, two-row slots from row 6, the same name formulas in D/H and the
same merges. The shared geometry is what makes packing a copy and paste
(§10.2.3). Nothing reads MATCHES: every named function, Fill match
numbers, the sync and every readout read SCHEDULE by name, so the codes
here count for nothing until they are pasted there.
A band per category. Categories sit side by side in CSV order, each in a
band of consecutive court blocks, no gap between bands. A category's band is
c blocks wide:
c = max( Σ over brackets of floor(n_b / 2), one full round-robin round
the most matches in any playoff row, a row never needs to wrap
1 )
A row per round. Down its band, a category's rows are:
- Round-robin rounds, one row-pair per round,
max over brackets of (n_b odd ? n_b : n_b - 1)rows. Rowrholds roundrof every bracket, bracket 1's matches in the leftmost blocks. Rounds come from the circle method: fix pair 1, rotate the rest; a bracket of oddn_bgets a phantom entrant, and whoever draws it sits that round out. A bracket with fewer rounds than the category's largest leaves its blocks blank in the later rows. - One row per playoff stage, earliest first (
R32→SF), holding the stage's matches in block-label order (QF-1,QF-2, …, §7.6.1). - The finals row:
F, thenBbeside it when there is a bronze match — every Pickle for Sight category with a bronze match plays it in the same slot as the final. A twice-to-beat category takes two rows:F (1)with the bronze walkover beside game 1, thenF (2)(§7.5.1). A best-of-3 takes three,F (1)toF (3).
No pair appears twice in any MATCHES row, so any row can be pasted into a
single SCHEDULE slot as it is.
| Cell | Content |
|---|---|
| row 3, band's first block | The category's display name, =FILTER(Variables!$J:$J, Variables!$I:$I="<KEY>"), merged across the band |
row 5, each block's F |
<KEY> <k> for the band's k-th block, e.g. NMD 1 … NMD 6 — never Court … |
B on each row |
The row's label: RR 1 … RR <n>, then the stage keyword (R16, QF, SF), then F + B (or F (1), F (2), …) |
| rows 1-2 | A1 : MATCHES; A2 : Total matches |
E/G |
The match's two codes |
F |
Blank — match numbers exist only on SCHEDULE |
| an unused block | - in E/F/G, as on SCHEDULE |
B2 |
The number of matches on the tab, e.g. 116, as a plain number — what Timeline!B3 must read once packing is done (§10.2.3). It is the calculator's total plus one per twice-to-beat category, for its bronze walkover |
Column B holds labels, not times: MATCHES has no clock. The category
colour rules apply here as on SCHEDULE, so each band reads in its
category's colour.
For the Pickle for Sight categories:
| Venue | Category | Brackets | Band | RR rows | Playoff rows | Rows |
|---|---|---|---|---|---|---|
| Main | NWD |
4-4 | 4 | 3 | SF, F + B |
5 |
| Main | NMD |
5-5-5 | 6 | 5 | R16, QF, SF, F + B |
9 |
| Main | NXD |
5-5-5-5 | 8 | 5 | QF, SF, F + B |
8 |
| Main | LIWD |
4-4 | 4 | 3 | SF, F + B |
5 |
| Annex | LIMD |
5-5-5-4 | 8 | 5 | QF, SF, F + B |
8 |
| Annex | LIXD |
5-5-4-4 | 8 | 5 | QF, SF, F + B |
8 |
| Annex | HIWD |
3 | 1 | 3 | — | 3 |
| Annex | HIMD |
5-4-4 | 6 | 5 | R16, QF, SF, F + B |
9 |
| Annex | HIXD |
3-3 | 2 | 3 | SF, F + B |
5 |
Main's MATCHES is 22 court blocks wide and 9 slot rows tall; the Annex's is
25 wide and 9 tall.
10.2.3 Packing SCHEDULE by hand¶
The operator's procedure, written into the generator's completion message and the dry-run checklist:
- Copy court blocks from
MATCHESintoSCHEDULE. Select one or more whole court blocks on aMATCHESrow — both rows of the slot, starting at a block'sDcolumn — and paste them with Paste special → Formula only at the start of a court block on aSCHEDULEslot's first row. That brings the codes and the name formulas, which still point at their ownE/G, and nothing else: no formats, merges or colour rules, whichSCHEDULEalready has and which a plain paste would pile up.SCHEDULE's own colour rules colour the codes. A partial row is fine: pasting a category's round across two slots, or splitting a row between categories, only has to respect the rules below. - Keep the rules the generator used to guarantee. None of these is checked while pasting:
- no pair twice in one slot —
Timeline(§10.6) shows a2; - a playoff stage only after the stage before it has finished, and the first stage only after the category's whole round robin, ideally with one slot free for the qualifier draw (§7.6);
- a category's final and bronze in the same slot;
XDafterMD/WDat the same venue, for players entered in both. Nothing checks this one at all (§15).- Clear the rest. Leave unused courts blank or
-. - Run
SAGE → Fill match numberswith the venue's base (§6.2). It numbers every slot with a code on both sides, puts-in empty ones, and writes the same numbers intoCSV!A, copyingCSVrow 2's formulas down to add a row per match (§10.5). Re-run it after any later move. - Check
Timeline: its grand total (B3) equalsMATCHES!B2, and no body cell exceeds 1. A match pasted twice makes the total too high and shows as a code with too many matches inTimeline'sAcolumn; one never pasted makes the total short. - Set up live sync (
pickle-for-sight-spec.md§12.2).
10.2.4 Possible future: generating the packed schedule¶
Two ways the generator could fill SCHEDULE itself, recorded so the design
does not have to be rediscovered. Neither is built. The packer below already
runs, in memory, to size SCHEDULE (§10.2.1); the generation log records its
result as a suggested layout the operator can follow.
- Option 1 — generated schedule,
MATCHESas documentation. The generator packsSCHEDULEwith the algorithm below and numbers it.MATCHESstays as the readable plan behind it. - Option 3 — generated from
MATCHES, operator adjusts. The packer reads its queues straight fromMATCHESinstead of rebuilding them. An operator who reorders rows there re-runs aSAGE → Pack schedulecommand andSCHEDULEfollows. That only works whileSCHEDULEcan still be rebuilt — before the day goes live and scores exist.
Why waves. A player can enter one same-gender doubles category and one
mixed. Pickle for Sight schedules every XD category after every MD and
WD category at the same venue is finished: on Main, NXD starts in the
slot after the NMD and NWD finals; on the Annex, LIXD starts in the
slot after HIWD's last match and HIXD one slot later. Nobody is due on
two courts at once, and nobody finishes a final and walks straight into a
group match. The wave is the sidebar's per-category input (§6), defaulting
to 2 for an XD key and 1 for everything else.
The packer.
- One queue per category — its
MATCHESrows, top to bottom, each row left to right (§10.2.2). The finals row stays one unit:FandBare always placed in the same slot. A twice-to-beat category's second game and a best-of-3's later games each go one slot after the game before. - Waves in order. Wave
w + 1starts in the slot after wavew's last occupied slot. Within a wave, fill slots one at a time, courts left to right. For each slot, visit the wave's categories that still have queued matches, in CSV order, starting from categoryt mod kfor slot indextandkcategories left — so categories take turns going first. From each visited category take matches off the front of its queue while they are eligible and courts remain, and stop at that category's first ineligible match: a queue is never reordered. - Eligibility. A match is eligible for slot
twhen: - neither of its pairs is already in slot
t— the hard constraint; - it is a playoff match, and every match of the same category's previous
stage is in a slot before
t— a stage cannot overlap the one before it, because its entrants are that stage's winners; - it is the category's first playoff stage, and its last round-robin
match is at least two slots before
t— one clear slot for the qualifier draw (§7.6). This is waived when no other category in the wave has an eligible match for slott, so a category alone in its wave does not leave the courts idle. Pickle for Sight leaves that slot in six of its eight playoff categories. - Match numbers (options 1 and 3 only) are the position in placement
order, offset by the base — the same order
Fill match numbersuses.
Partial slots are centred, per Phase 2 spec §4.3. Soft preference, not
enforced: at least one idle slot between a pair's matches. Pickle for Sight
mostly gives one — taking turns in step 2 is what produces that — but
breaks it for 17 pairs across the two workbooks (NWD pairs 1-4 play slots
13 and 14). If either option is built, the §9 validation gains a per-wave
warning when courts exceeds the wave's matches in one round-robin round,
since that wave would leave courts idle in every round-robin slot (Phase 2
spec §4.4's guarantee, restated).
Category colours carry over from Phase 2 spec §6, including the
conditional-format mechanism, on both SCHEDULE and MATCHES, with one
change to the test. A dual-meet code
is <CLUB>_<KEY>_<n>, so colorFormulaFor_ compares
INDEX(SPLIT(code,"_"),2). A standard code is <KEY>_<n> with no club
segment, so the key is token 1: the standard generator's copy compares
INDEX(SPLIT(code,"_"),1). Copied unchanged, the test compares the pair
number against the key and nothing is ever coloured. The level ladder in Phase
2 spec §6.2 is {N, LI, I, HI, A}, which already covers every Pickle for
Sight key (N, LI, HI × MD/WD/XD). It needs two rungs added for the
BKL level codes (B beginner, AB advanced beginner), and the age-band keys
(LI40MD → level LI40, type MD) mean Phase 2 spec §6.4's parse has to
strip a trailing digit run from the level before looking it up. A key that
still does not resolve gets no fill, per that section.
10.3 Court Control¶
Court blocks only, as in both Pickle for Sight workbooks: B4/C4 read
Court/Match, then one 3-row block per court from row 5. In each block
B is the court's label from Variables!R (§6.1) — 1-4 on Main,
5-9 on the Annex — not its position, since CSV!B publishes it as the
live court (§10.5). C is the operator-typed match number, D/E the codes
via GETTEAM1CODEBYMATCH/GETTEAM2CODEBYMATCH, and the names spill below
through GETPLAYERNAMESBYTEAMCODE. A1 reads COURT CONTROL and B2
Matches Currently Running.
Nothing else. The dual meet's stats block (Phase 3 spec §5.1) and BKL's
standby strip (Next Stand By, TBA Stand By, Match to Record) are gone
from both Pickle for Sight workbooks and are not generated. The tab's height
is 3 · courts + 4 rows.
10.4 STANDINGSCSV¶
Phase 3 spec §5.4's seven columns, unchanged, with one column that needs a new derivation.
A2 = UNIQUE(FILTER('Reference for Players'!$A$3:$A,
REGEXMATCH('Reference for Players'!$A$3:$A, "_")))
B2 = INDEX(GETPLAYERNAMESBYTEAMCODE($A2),1) C2 = …,2)
D2 = GETTOTALWINS($A2) E2 = GETTOTALLOSES($A2) F2 = ROUND(GETSCOREQUOTIENT($A2),4)
G — the bracket. Phase 3 spec §6.3 computes ceil(i / n) from a single
n. Uneven bracket sizes (§5.6) make that wrong. Instead it recomputes
groupSizes from the category's plan row: teams in Variables!K and
brackets in Variables!L, keyed by Variables!I (§10.1):
G2 = IF($A2 = "", "",
IFERROR(
LET(
key, INDEX(SPLIT($A2, "_"), 1),
idx, VALUE(REGEXEXTRACT($A2, "^[^_]+_(\d+)$")),
pairs, VLOOKUP(key, Variables!$I:$K, 3, FALSE),
want, VLOOKUP(key, Variables!$I:$L, 4, FALSE),
nb, MAX(1, MIN(IF(want = "", 1, want), INT(pairs / 2))),
size, INT(pairs / nb),
extra, MOD(pairs, nb),
big, extra * (size + 1),
IF(idx <= big,
ROUNDUP(idx / (size + 1), 0),
extra + ROUNDUP((idx - big) / size, 0))
),
"")
)
This is the formula both Pickle for Sight workbooks already run, character
for character, on every STANDINGSCSV row.
It rests on two facts from earlier sections:
- Codes run in bracket order. Group codes are
<KEY>_1 … <KEY>_<teams>in one unbroken run, with bracket 1's pairs first (§7.2). So a pair's bracket follows from its indexialone. - The split is
groupSizes's.nbapplies the samefloor(teams/2)cap (§5.6), with a blankbracketsmeaning one bracket. The firstextrabrackets takesize + 1pairs and the rest takesize. 26 pairs over 6 brackets is5-5-4-4-4-4,LI40MD's 9 over 2 is5-4, andLI40XD's 7 over 2 is4-3, and Pickle for Sight'sHIMD13 over 3 is5-4-4. SoGagrees with the category tab'sKcolumn and its score grids.
Worked through for LIMD_11 at 26 pairs over 6 brackets: size = 4,
extra = 2, big = 10. Pairs 1–10 fill the two brackets of 5, and pair 11
is the first of the brackets of 4: 2 + ceil(1/4) = 3.
Only group codes get a bracket. The pattern ^[^_]+_(\d+)$ matches a
code that is exactly <KEY>_<number>. It is anchored at both ends because
playoff codes such as <KEY>_SF_1, <KEY>_QF_2 or <KEY>_R16_3 also end
in a number, and an unanchored _(\d+)$ would give them a bracket. They
fail to match, as do <KEY>_B_1, <KEY>_F_1 and <KEY>_F_1_(2), and
IFERROR blanks them. That is correct, since a playoff entrant belongs to no bracket.
G is the bracket number, 1, 2, 3, the same convention as the dual
meet's STANDINGSCSV and the numbers the generator writes into the category
tab's K. The site treats the column as an opaque grouping label. The LET
names are letters only, since a name like b1 parses as a cell reference and
LET rejects it.
The generator writes nothing extra for this. The formula reads the plan row the generator already writes, so there is no separate code → bracket map to keep in step with the split.
Row count is the sum of every category's codes (§8): 75 for BKL Dreamcourts, 95 for PFS Main and 111 for PFS Annex.
The A2 spill must read 'Reference for Players'!$A$3:$A open-ended, per
Phase 3 spec §7. Both Pickle for Sight workbooks read $A3:A1553, which
works today and silently truncates the day a longer category is added.
10.5 CSV¶
Phase 3 spec §5.3's tab: 12 columns, A:L, the same header names as the
dual meet's. The BKL workbooks had a thirteenth, a literal v between the two
teams; the Pickle for Sight workbooks dropped it, and the generator follows
them. GvizCsvFetcher reads by header name, so neither shape breaks the sync.
| Col | Header | Row 2 |
|---|---|---|
A |
matchNumber |
blank — Fill match numbers writes it (§10.2.3) |
B |
court |
=IFNA(FILTER('Court Control'!$B:$B,'Court Control'!$C:$C=$A2),"") |
C |
Schedule |
=MATCHTIME($A2) |
D |
CourtAssignment |
=MATCHCOURT($A2) |
E |
teamCode1 |
=GETTEAM1CODEBYMATCH($A2) |
F G |
team1Player1/2 |
=INDEX(GETPLAYERNAMESBYTEAMCODE($E2),1) / ,2) |
H |
team1Score |
=GETSCOREAGAINSTPAIR($E2,$I2) |
I |
teamCode2 |
=GETTEAM2CODEBYMATCH($A2) |
J K |
team2Player1/2 |
as F/G off $I2 |
L |
team2Score |
=GETSCOREAGAINSTPAIR($I2,$E2) |
B reads Court Control's whole columns, as both Pickle for Sight
workbooks do, so it covers every court block at any court count. Nothing
else on that tab has a number in C (§10.3).
The generator writes the header and one formula row, with A2 blank.
Fill match numbers copies row 2 down to one row per placed match and
numbers them (§10.2.3); until it runs, the tab publishes nothing.
C and D must be the MATCHTIME/MATCHCOURT named functions (§11),
as they already are in both Pickle for Sight workbooks. The BKL reference's
BYROW/BYCOL scan over a hardcoded SCHEDULE!$F$6:$BZ$78 is the third
defect in §2.1 and cannot survive a court count it was not typed for.
The dual meet's MATCHCOURT returns "Court " & <block index>, which is the
block's position, not its label. With non-contiguous court labels (§6.1)
that is wrong: Dreamcourts' block 1 is Court 10, and a PCPH Annex workbook
whose courts are 5–9 reports its block 1 as Court 1. The standard library's
MATCHCOURT reads the label the block itself carries in SCHEDULE row 5
instead. This is the definition both Pickle for Sight workbooks run, and it
reads Court 5 for the Annex's first match:
MATCHCOURT = LAMBDA(match_number, IFERROR(
LET(
h, ROWS(SCHEDULE!$B$6:$B),
pos, MATCH(match_number, GETMATCHNUMBERS(), 0),
blk, INT((pos - 1) / h),
label, INDEX(SCHEDULE!$5:$5, 1, 6 + 8 * blk),
"Court " & REGEXEXTRACT(TO_TEXT(label), "\d+")
),
"Not found"))
blkis the 0-based block index, as before.GETMATCHNUMBERS()isSTACKBLOCKS(6, 8, 6), so blockblk's match-number column is6 + 8 * blk. That is theFoffset, where row 5 carries the court heading (Phase 2 spec §2.2).- It reads row 5 rather than
Variables!Rdirectly, so the result is always the label the operator sees onSCHEDULE. Row 5 is itself="Court "&Variables!R<n>in a generated workbook, so the two agree. A hand-edited heading, or a hand-built workbook with noVariables!R, still resolves correctly. REGEXEXTRACT(..., "\d+")normalises the heading toCourt <n>whether row 5 readsCourt 5or a bare5.Court <n>is the form the site parses: it takes the trailing integer ofCourtAssignment.- The
LETnames are letters only. A name likeb1parses as a cell reference andLETrejects it.
Court Control's column B (§10.3) must carry the same labels, not
1..courts: it is what CSV!B publishes as the live court. A 5–9 venue
numbered 1–5 there shows its live matches on another venue's courts.
10.6 Timeline¶
Timeline is Phase 3 spec §5.2's pair × slot conflict grid, in the
layout both Pickle for Sight workbooks use:
| Cell | Content |
|---|---|
A1 |
TIMELINE |
B2, B3 |
TOTAL, and the match count =SUM($C$4:<lastCol><lastRow>)/2 — the /2 because every match appears twice |
C3, D3, … |
slot times: C3 = Variables!C4, then D3 = C$3+Variables!$C$6, one column per slot |
B4 |
=UNIQUE(FILTER('Reference for Players'!$A$3:$A, 'Reference for Players'!$A$3:$A <> "")) — every code, plus each anchor's key row |
A<r> |
=SUM(C<r>:<lastCol><r>) — that code's match count |
| body | =COUNTPAIRAT(C$3,$B4) |
The body replaces the BKL workbooks' 24-term COUNTIFS chain (§2.1), and the
Pickle for Sight workbooks already run it, with the grand total over the
whole body (SUM(C4:AH115)/2 on Main, SUM(C4:AF132)/2 on the Annex).
<lastCol> and <lastRow> are the last slot's column and the last code's
row: one time column per SCHEDULE slot, all N of them including the
spares (§10.2.1), and one row per code in Reference for Players. One thing the generator does differently: the B4 spill reads
$A$3:$A open-ended, where the Pickle for Sight workbooks read the bounded
A2:A115 (Annex A2:A132).
Timeline checks codes, not people. A pair's playoff entry gets a new
code (§8), and a player entered in two categories has two unrelated codes,
so a human booked on two courts in one slot reads as two rows of 1 each.
BKL Dreamcourts had a Timeline (Individual) tab keyed by player name to
catch exactly that. Neither Pickle for Sight workbook has it, and it is not
generated; §15 records the gap.
BKL Dreamcourts also carried a Standings wall-display board. Neither
Pickle for Sight workbook has it, nothing reads it, and the public event page
shows live standings. It is not generated and not in the master.
Nor is an Awards tab. BKL had one; neither Pickle for Sight workbook
does, and the podium comes from the console (§7.7).
10.7 Title, Reference for Players, Poster¶
Title — B6 = Variables!C2 and B7 = TEXT(Variables!C3,"MMM DD")&" - "&Variables!C8,
both formulas, and nothing else. The generator writes only Variables.
Reference for Players — Phase 1 spec §4.5's tab, one anchor per category
tab, in the form both Pickle for Sight workbooks use: the key in F, and a
formula in A that reads the tab through INDIRECT, so every anchor is the
same text.
The generator writes one anchor per generated category, at cumulative
offsets computed from each tab's actual height. The reference hand-places
anchors for all nine of the event's categories in both workbooks, at A3,
A106, A262, A377, …, A1009, so the anchors for the other venue's
categories resolve to #REF! (§2.1). Each generated workbook has anchors
for its own categories only.
Both downstream spills (STANDINGSCSV!A2, Timeline!B4) must read
$A$3:$A open-ended, per Phase 3 spec §7.
The BKL workbooks carried a static QA Checklist tab. Neither Pickle
for Sight workbook has it, and it is not in the master.
Poster — images only. Ships blank in the master.
10.8 Charts¶
A static parent → child bracket tree, PARENT/CHILD pairs down B/C,
for a 32-pair ladder and for the irregular ladders the tiered byes produce
(PLAYOFFS OF 23 PAIRS, with slot names like QF_R16-3 where a bye spans
two rounds). The Pickle for Sight copies, identical in both workbooks, add
PLAYOFFS OF 3 BRACKETS WITH TOP 2 at A243: seats 1 2 3 into the
semifinals and 4 into the quarterfinal that feeds SF-4, with 5 and 6
playing in below it. That is §7.6.1's numbering for the NMD/HIMD shape.
Ships in the master unchanged and is not generated. It is a reference the
operator reads while filling the playoff slot table by hand. Once the
generator writes AU itself (§7.6) the tab becomes documentation rather than
a working surface, which is the right direction — but deleting it is out of
scope here, and the irregular trees on it are the clearest existing statement
of what §5.4 produces.
11. The Named Function library¶
Replace the BKL workbooks' 15-function library with the Dual Meet Master's
23, and take the whole technical/named-function-library.md contract
as-is, with two changes: MATCHCOURT is the row-5 label form in §10.5, and
SORTBYWINS is left out. That is 22 functions. Both Pickle for Sight
workbooks run the 22 plus two dead ones (below), read out of their
<definedNames> and identical in both. The three that matter most:
| Function | Why |
|---|---|
STACKBLOCKS(first_col, step, first_row) |
Derives the court count from the sheet's own width. Removes the hand-edited column lists in §2.1 and the whole class of bug behind them |
MATCHTIME(m) / MATCHCOURT(m) |
Replace CSV!C/D's hardcoded BYROW/BYCOL window. MATCHCOURT needs the row-5 label variant in §10.5 |
COUNTPAIRAT(time, code) |
Replaces Timeline's 24-term COUNTIFS chain |
Every call site in both Pickle for Sight workbooks was counted, in cells and inside the other functions' definitions. Twenty-two functions are reached. Two are not, and neither is carried:
| Function | Why it is dead |
|---|---|
GETWINSCORE() → 11 |
Inherited from the BKL library. No cell and no function calls it |
SORTBYWINS(range) |
The dual meet's playoff feeders call it to rank a group block. A standard tournament's playoff entrants are drawn (§7.5), so nothing sorts |
Of the 22, ten are called only by other functions: STACKBLOCKS, the four
column primitives, GETMATCHNUMBERS, GETMATCHES,
GETMATCHRESULTSBYTEAMCODE, GETTEAMCODES and GETPLAYERNAMES. They look
unused from the tabs and are not. The BKL library's
GETTEAM1SCOREBYMATCH/GETTEAM2SCOREBYMATCH and GETPLAYERBYRANK are gone
from the Pickle for Sight library too.
Timeline's time headers, SCHEDULE's slot times and COUNTPAIRAT all have
to be the same chained computation off Variables!C4 and Variables!C6,
for the float-equality reason in Phase 2 spec §8.7. Do not compute slot times
in JavaScript and write literals.
12. Implementation¶
12.1 Where the code goes¶
A new file, sage-tools-api/apps-script/standard-tournament-generator.gs, bound to the
SAGE Standard Tournament Master. Not a mode inside dual-meet-generator.gs:
- The two masters are different workbooks, so neither script ever needs the other's code at runtime; a shared file would ship dual-meet code into the standard master and vice versa.
dual-meet-generator.gsis 2,800 lines and its layout engine is written around square, per-club blocks. Every one of §7's differences would be a branch.- The
onOpenblock is already duplicated verbatim acrosssheets-sync.gsanddual-meet-generator.gswith a "Change one, change both" note. This adds a third copy of that block and nothing else.
Menu: SAGE → Generate event tabs, same sidebar pattern, same
logStep_-polled progress, same self-removing menu item once
PROP_TABS_GENERATED_FOR is set.
12.2 What to copy from dual-meet-generator.gs¶
Verbatim, as pure functions — these have no dual-meet assumptions in them:
parseCsvRows_, colLetter_,
parseTimeToMinutes_, minutesToClock_, trace_, logStep_, resetLog_,
getGenerationLog, ensureRowCount_, ensureColumnCount_, mergeVertical_,
stampFormat_, assertTabsPristine_ and the REBUILT_IN_PLACE_TABS shape,
categoryColor_ (with §10.2's level-ladder extension), and the whole
buildScheduleTab_ tiling strategy — copyTo on both axes, grow down then
across, flush between stages, copy column widths and row heights explicitly
(Phase 2 spec §8.5, §8.10, §8.11).
Copied, then changed:
parsePlanCsvreads only the dual-meet fields (key,value,brackets,po_format,teams_a,teams_b). The standard copy readsteams(index 3),brackets(4),advance(5),fill(6),note(7) andsolo_format(11) instead, with the same positional, missing-cell-is- empty parsing.colorFormulaFor_testsINDEX(SPLIT(code, "_"), 1), not…, 2)— a standard code has no club segment (§10.2).
Rewritten for this shape: computeLayout, buildCategoryTab,
every write*Block*_, every feeder builder, and readoutGeometry_. New:
buildMatchesTab (§10.2.2), and packSchedule_, the §10.2.4 packer, which
is pure, returns a slot list, and today only sizes SCHEDULE and feeds the
suggested layout in the log. buildScheduleTab_'s tiling writes the empty
grid of §10.2.1.
12.3 Rebuilt in place¶
Same registry and same pristine guard as Phase 3 spec §8.2. SCHEDULE and
Court Control keep the hard constraint that makes it necessary: their tab
GIDs are what the live sync trigger watches, stored in that workbook's
SYNC_WATCHED_GIDS Script Property (sync-script-configuration-spec.md §5),
so replacing either kills the sync while the sheet still looks correct.
| Tab | Prototype |
|---|---|
SCHEDULE |
1 court block, 1 slot |
Court Control |
2 court blocks |
Timeline |
header rows 1-3 + 1 body row, stripped to A:D |
CSV |
header row + 1 match row, A:L |
STANDINGSCSV |
header row + 1 pair row, A:G |
Reference for Players |
1 anchor |
MATCHES is not in this table. Like a category tab it is a new tab,
inserted by the generator; the master has none, and a workbook that already
has one fails §9's tab-collision check.
Phase 3 spec §8.2's warning about Timeline's width applies unchanged: its
built column count scales with the event, so a prototype left wide lets an
already-built tab read as pristine and get tiled twice.
12.4 Ordering¶
Category tabs → Variables → Title → Reference for Players →
MATCHES → SCHEDULE → the readouts. Reference for Players needs each
category tab's final spill height; everything else needs SCHEDULE's
returned geometry.
SCHEDULE runs late for the blast-radius reason in Phase 2 spec §8.1.
12.5 Tracing and failure¶
Phase 2 spec §8.9 and Phase 1 spec §11.3, unchanged. Every computed extent,
every block anchor, every MATCHES band, the packer's suggested layout
slot by slot, and — new here — the resolved
playoff ladder per category as <KEY> <STAGE> slot <k> <- fresh | winner of <STAGE>-<j>.
A wrong ladder is invisible in a finished tab unless you already know which
slot was expected.
On failure mid-build: no rollback, no continuing. Report which tabs were written.
13. Build order¶
parsePlanCsv+ §9 validation, reporting every failure togetherplanCategory(cat)— pure. Bracket sizes, RR match list per bracket, the full playoff ladder from §5 and its seat numbering from §7.6.1. Verified by hand against §1.1's fifteen reference categories before any Sheets code exists- The master workbook (§14.1) — the larger half of the work
buildCategoryTab— group blocks, score grids, the playoff ladder, both scaffoldsVariables(§10.1),Title,Reference for PlayersbuildMatchesTab,packSchedule_(for the height) and the emptySCHEDULEgrid (§10.2)CSVandSTANDINGSCSV— the two the live sync needsCourt Control,Timeline
Steps 1-2 are worth doing and checking on their own: planCategory is where
every §5 branch lives, it is the only part with real logic, and it can be
verified against two played events without a workbook. Pickle for Sight's
two plan CSVs are the fixtures: feeding each through steps 1-2 must give
§1.1's match counts and §7.6's slot tables exactly.
14. What the master workbook must carry¶
A one-time manual job, and the larger half of the work — Phase 1 spec §6's counterpart.
14.1 Building it¶
- Copy the Pickle for Sight PCPH Annex reference workbook (§1). It
already runs §11's library and holds four of the five playoff shapes:
tiered (
HIMD), full draw (LIMD,LIXD), bare semifinal (HIXD) and round robin only (HIWD). - Delete the two dead named functions,
GETWINSCOREandSORTBYWINS(§11), leaving 22 withMATCHCOURTin its row-5 form. - Replace the five category tabs with a single hidden
_CATEGORY_TEMPLATE: an unchanged copy of theHIMDtab. It holds one of every block type the generator formats from (§18), so nothing is trimmed, moved or added. - Point the
Timeline!B4andSTANDINGSCSV!A2spills at'Reference for Players'!$A$3:$A, open-ended. Variables: add columnQ(facility) andR(court labels) with headers, and relabelB6to say it holds the slot pitch (§10.1).- Strip every rebuilt-in-place tab to §12.3's prototype shape, and
Reference for Playersto one anchor — last, then generate once immediately, per Phase 3 spec §12. - Name it SAGE Standard Tournament Master.
14.2 Decisions taken in the master, not in code¶
- No awards block and no
Awardstab (§7.7). The console produces the podium from the snapshot, round-robin-only and twice-to-beat included. - Column
Kkeeps its double duty — bracket number on each bracket's first group pair, next-round code and medal label on playoff rows. It is genuinely two columns' worth of meaning in one, but the whole forward-propagation chain (§7.5) keys off it, and splitting it would touch every playoff formula to fix nothing that is broken. Chartsships as-is (§10.8).Standings,Timeline (Individual)andQA Checklistare not carried (§10.6, §10.7). Neither Pickle for Sight workbook has them.
15. Open questions¶
R32 is not in the site's STAGE_ORDER. The standard template's
STAGE_ORDER is ['R16','QF','SF','BRONZE','FINAL'] and
ROUND_KEYWORD_LABELS has no R32 entry, so a category deep enough to reach
one renders those rows in an unsorted trailing stage with no label. Adding
both is a two-line site change, but it needs an event that actually reaches
R32 to test against, and none has. Validation caps both entrants (32) and
ladder depth (no stage before R32) in §9, so nothing deeper can be
generated. Still untested: neither standard event since (Pickle for Sight,
Piggleball) had a category reach R32.
Nothing checks a player booked twice. Timeline works on codes
(§10.6), and a human has a different code in each category and in each
playoff stage. Packing XD after MD/WD (§10.2.3) keeps same-gender and
mixed doubles apart, which is the common case, but that is the operator's
rule to keep, and a bad qualifier draw or a hand-moved match can still put
one player on two courts at once. Nothing in the workbook flags it.
BKL's Timeline (Individual) did, by counting player names per slot row.
Whether to bring it back is undecided; Pickle for Sight ran without it.
The published match lists show it hasn't happened yet: no player is in two
matches of the same time slot at Pickle for Sight (233 matches over two
venues, compared across both), Piggleball (68) or PickleDrive (152).
Drawing within tiers. In a tiered ladder the qualifier draw is not one
lot: group winners must land on the winners' seats (1 … g, §7.6.1) and
runners-up on the rest. The scaffold's tier labels (§7.6) tell the operator
which is which, but nothing stops a runner-up being given seat 1. A check
column beside AM that flags a seat outside its row's tier would catch it;
whether that is worth the extra formulas has not been decided.
Whether the qualifier scaffold should be drawn in a tool.
bracket-draw-name-import-spec.md fills STEP 3 of the roster scaffold from a
Bracket Generator draw. §7.6's AM column is a second draw with the same
shape and the same hand-shuffling problem, but no artifact to import: it
happens after the round robin, on the day, from the standings. Whether it
should go through the tool at all, or get its own in-sheet draw, is open —
that spec's §8.3 gives the reasoning and its §13 keeps it out of scope.
Per-facility court label ordering. Only matters if §10.2.4's packer is ever built. §6.1 takes the labels in schedule order, and the packer would fill courts in that order. Whether a partial slot should centre on the label order or on the physical court order is undecided and has never mattered.
16. Acceptance¶
Run against the two Pickle for Sight plan CSVs (§1) and confirm:
- The MAIN CSV, all four categories selected, facility
PCPH Main, courts1,2,3,4: four category tabs; aMATCHEStab 22 court blocks wide and 9 slot rows tall with bands of 4, 6, 8 and 4 blocks forNWD,NMD,NXD,LIWD, andMATCHES!B2reading 116; an emptySCHEDULEof 37 slots; and aCSVof one formula row - The ANNEX CSV, all five selected, facility
PCPH Annex, courts5,6,7,8,9: aMATCHEStab 25 blocks wide,MATCHES!B2reading 125, and an emptySCHEDULEof 33 slots - No pair appears twice in any
MATCHESrow, and each band's rows are its round-robin rounds, then its playoff stages, thenF + B(§10.2.2) - Pasting every
MATCHESrow into its ownSCHEDULEslot and runningFill match numberswith base1000gives aCSVof 116 rows,1001-1116; on the Annex with base2000, 125 rows,2001-2125, andCSV!DreadsCourt 5for match2001, notCourt 1 - The generation log's suggested layout puts no
NXDmatch in any slot before the lastNMD,NWDorLIWDmatch, and noLIXDorHIXDmatch before the lastLIMD,HIMDorHIWDmatch (§10.2.4) HIMD's group block is 13 pairs in one run withK = 1on pair 1,2on pair 6 and3on pair 10, and three score grids,5×5,4×4,4×4HIMDandNMDgenerate §7.5's tiered ladder:R16-3(R16_5,R16_6),QF-2(QF_3fed fromR16-3,QF_4fresh),SF-1(SF_1,SF_2),SF-2(SF_3,SF_4fed fromQF-2),BandF; the slot table reads1 SF,2 SF,3 SF,4 QF,5 R16,6 R16, and the qualifier scaffold's six tier labels readBr 1 - Rank 1…Rank 2 - 3rdNXD,LIMDandLIXDgenerate a full draw:QF_1…QF_8fresh,QF-k's winner toSF_k, and an eight-row slot tableNWD,LIWDandHIXDhave a four-row, all-SFslot tableHIWDgenerates a group block and one3×3grid, no playoff rows, no slot table and no qualifier rows; itsMATCHESband is 1 block wide and 3 rows tall- STEP 3 (
AI) is blank on every category tab STANDINGSCSVhas one row per pair and per playoff slot — 95 rows for Main, 111 for the Annex — withGreading1,2,3,4on group rows and blank on playoff rows- Once packed,
Timeline's grand total equalsMATCHES!B2(116, 125); no body cell exceeds 1 Court Controlis court blocks only, labelled1-4on Main and5-9on the Annex, andCSV!Breports the live court for a match typed into any of them- The workbook defines exactly §11's 22 named functions
- No
#NAME?and no#REF!anywhere
And against BKL Cup 2026 Day 2, for the shapes Pickle for Sight lacks:
- A CSV reconstructed from BKL Day 2, selecting
LI40MD LI40WD LI40XD, courts1..12, givesMATCHES!B2of 42 — the calculator's 41 plusLI40WD's bronze walkover; selectingLI18MD LI18WD LI18XD, courts7..15, gives 84, and once packed and numbered from base3000,CSV!DreadsCourt 10for match3001 LI40WDgenerates the twice-to-beat blocks of §7.5.1 —F (1),F (2)and aBwalkover whoseB_2side readsBYE— a three-row qualifier scaffold with no draw, and aMATCHESband of two rows,F (1)holding game 1 and the walkover, thenF (2)- A 2-pair category generates the best-of-3 blocks of §7.5.1, no score grid,
no qualifier scaffold, and a
MATCHESband of three rows - Every named function resolves at 4, 5, 12 and 15 court blocks with no edit to any column list
And in general:
- A category whose ladder would need a stage before
R32(7 brackets × top 2,fill: bye) is rejected by name, with the workbook untouched - Running the generator twice is refused, not half-applied
- A
format: dualCSV is rejected with a clear message and the workbook untouched - The sync publishes a generated workbook once it is packed and numbered, with no other hand-editing
17. Out of scope¶
- Any change to
dual-meet-generator.gsor the Dual Meet Master - Player name import — rosters stay a paste, both scaffolds (§7.6)
- Filling STEP 3 or the qualifier draw automatically (§15)
- Seeded or non-random draws
- Generating the packed
SCHEDULE— options 1 and 3 (§10.2.4) - Checking a hand-packed
SCHEDULEagainst the packing rules beyond whatTimelinealready shows (§10.2.3) - Cross-facility match-number allocation (§6.2)
- Categories split across two facilities in one day
R32site support (§15)
18. Divergences¶
Where standard-tournament-generator.gs departs from the sections above, or fills in
something they leave open.
Departures
MATCHESblock headers name the bracket (§10.2.2): row 5 reads<KEY> Br <b>over the blocks that hold bracketb's round-robin matches —HIMD Br 1,HIMD Br 1,HIMD Br 2, … for a 5-4-4 split — rather than<KEY> <k>. A block past the round robin's width, which only a wide playoff row needs, keeps<KEY> <k>. Brackets sit left to right,floor(n_b / 2)blocks each; every round of a bracket fills exactly that many, and brackets run largest first, so a bracket's matches stay under its own headers in every round-robin row.- Two brackets play a crossover, not a draw (§7.6). With two brackets
the playoff seating is fixed, so the qualifier scaffold's slot column
(
AM) is written in rather than left-for a draw. Top 2:Br 1 - Rank 1takes slot 1,Br 2 - Rank 2slot 2,Br 2 - Rank 1slot 3 andBr 1 - Rank 2slot 4, soSF-1is Br 1 #1 v Br 2 #2 andSF-2is Br 2 #1 v Br 1 #2. Top 1: Br 1 #1 and Br 2 #1 take the final's slots 1 and - The operator types only the names. Three or more brackets are still drawn by lot, as are two brackets with top 3 or 4 or with wildcards.
- Round-robin rounds follow SAGE's bracket guide, not the circle
method (§10.2.2). The RR tab of the BRACKET GUIDE workbook lists, for
each bracket size, which pairs meet in each round and in what order; the
generator carries it as
RR_GUIDE_and renumbers each bracket from its own first pair. The guide covers brackets of 3, 4, 5, 6, 7, 9 and 10 (2 is a single match). Any other size falls back to the circle method, and the generator warns which category did. MATCHESrow labels are not in columnB(§10.2.2). Categories sit side by side, so one row means different things for different bands — row 4 isNWD'sSFandNMD'sRR 4. Each band carries its own labels instead, on each slot's first row in the separator column just left of the band's first block (columnCfor the first band). A paste that starts at a block'sDcolumn, as §10.2.3 step 1 says, never picks them up.Bis blank below row 2.- No bracket may hold more than 13 pairs. A group score grid runs from
O, and the roster scaffold's notes column isAB, so a bracket of 14 or more would write its grid into the scaffold. §9 gains the rule; the calculator already warns above 8. fillis validated — empty,byeorwc— alongside §9'ssolo_formatcheck, for the same reason.- The twice-to-beat and best-of-3 blocks take the
Fblock's format. They are the same shape (§7.5.1), so the master needs no prototype of its own for them. CSV!B2is guarded on a blankA:=IF($A2="","",IFNA(FILTER('Court Control'!$B:$B,…))). WithA2blank, §10.5's bare form matches every blankCcell onCourt Control, spills, and shows#REF!on the one-row tab the generator leaves.SCHEDULE's andMATCHES' name formulas are guarded on a blank code:=IF(E6="","",GETPLAYERNAMESBYTEAMCODE(E6)), not §10.2.1's bare call, which shows#REF!in every slot of an emptySCHEDULE.- Conditional formats on
SCHEDULEandMATCHESare lifted off before tiling and put back afterwards with one range per block column. Tiled withcopyTo, each rule ends up listing every cell separately, and past a few hundred cellssetConditionalFormatRulesfails outright. - The generator writes
Title!B6/B7(§10.7 says it writes onlyVariables). They are the same two formulas the master carries, written so a master missing them still titlesSCHEDULE. BandABlevel colours (§10.2.4).Bis Sheets' "light 3" row;ABis halfway between "light 3" andN's "light 2", because the standard palette has no swatch between the two.
Where the spec was silent
- The master's prototypes.
_CATEGORY_TEMPLATEis an unchanged copy of the reference'sHIMDtab, not a reduced prototype tab (§14.1 step 3). It already holds one of every block, and the generator copies format only, never content, from fixed cells of it: group pair rows 6-7, the grid cells atN6:P7, theR16banner andR16-3block (rows 33-40),FINALS,BandF(rows 63-76), the roster atAB5:AE6andAG5:AI5, the draw atAM5:AP6, the slot table atAT4:AV4, and the all-black row 34 for every empty cell. So a generated tab has the reference's fills, fonts, borders and alignment, and no colour or font lives in code. The tab's own conditional formats come along, stretched to each tab's height; the two pinned toHIMD's cells (SUM($D$6:$E$13)=312) are dropped. The generator checks for the copy's banner labels before writing anything.Court Control's prototype isB:E;Reference for Players' one anchor is §10.7's formula wrapped inIFERROR(…,"")withF3blank, so it spills nothing. - The slot table starts at row 4, one row above the other scaffolds, as in the reference, with no header row of its own.
- Flat and full-draw tier labels run rank-major: every bracket's winner, then every bracket's runner-up, then the wildcards.
- Court labels with no digit are accepted with a warning:
MATCHCOURTpublishes the number in the heading, so such a court publishes asNot found. - The packer splits a finals row wider than the court count into single matches, so a one-court workbook can still place a final and its bronze.