Skip to content

fix(client): the split is kept and the cards get smaller — the packer's objective is wrong #686

Description

@ninthworld

Part of #652. Reverses the row-count objective introduced in #676 and refined in #682. Depends on #685 (the spec correction), which must merge first.

The defect

The maintainer, on seeing a 1920×1080 board: "The creatures and lands mixing together is unacceptable. Literally just make the cards smaller, why is this concept so hard. Can't fit an image? Make the text smaller. Can't fit the cards, make them smaller."

Creatures, other permanents, and lands merge into one row at every desktop size below ultrawide. That is fieldSlots in clients/web/src/pack.ts doing exactly what it was told:

The row count is chosen to maximise card size, not to maximise rows. …every row count from one up to the one the table needs is packed, and the one whose worst row draws the biggest tile wins.

That objective always merges. One row of N cards is never smaller than three rows of the same N — the merged row can reproduce any split (same line height, count spread evenly instead of by category) and usually beats it. The "at equal card size the deeper split wins" tie-break almost never fires, because the sizes are almost never equal.

It was not disobeying the spec. §5 said the floor was hard and any row under 100px tall was a chip row, so keeping the split really did mean three rows of chips. #685 removes that.

Read first

  • docs: the floor is soft downward and hard sideways #685 must be merged. It is the spec this implements. Read docs/client-design.md §5 "The floor is soft downward and hard sideways" and "The row count is the board's, never the card's", plus §3 "Scale first. Remove last." and "The split is kept, and the cards get smaller."
  • clients/web/src/pack.tsfieldSlots, worstRow, poorer, packRow, solve.
  • clients/web/src/fit.tspresentationFor, NAME_FLOOR.

Plan

  1. fieldSlots returns the board's group count. One row per group the server's card_types produced. It stops packing candidate row counts and stops scoring them by tile size. The scene ladder's rows: 'merged' still wins where it applies — this cannot un-merge what §3's step 6 already merged.
  2. It falls below the group count only when a row cannot draw anything at all — the bottom of the ladder, not a size optimisation. State the condition in terms of the tier's own minimum, and say so in the docstring.
  3. solve scales the card below 72×100 rather than clamping there. The floor stops being a max() in the vertical direction. The horizontal direction is unchanged: fitted < FLOOR still switches the row from spacing to overlap at full size.
  4. The chip threshold is derived from §2's 9px name floor, not from the 100px row constant. fit.ts owns the answer to "can a name be set here"; pack.ts asks it rather than carrying a second number. A row that cannot set a legible name is a chip row; one that can is a card row, however small.
  5. poorer() and better() survive only where they still have a job. poorer() exists to compare candidate row counts; with the objective gone, check whether it is still needed at all rather than leaving it to rot.
  6. Report, don't hide. docs: the floor is soft downward and hard sideways #685 makes the minimum a review threshold — "the client still draws it, and the fact is reported." Decide how a card drawn below 72×100 surfaces to the maintainer (a data attribute the scale gate can assert on is enough) and say what you chose.

Guard rails

  • pack.ts reads no card. No name, no cost, no type line. It gets counts and boxes. Asking fit.ts whether a name fits at a given size must be asked in the abstract — the width a 9px name needs — never about a particular card.
  • Zero game logic. Which group a permanent is in is board.ts's answer from the server's card_types.
  • Region size is a function of the viewport alone (§5). Row count answers to the board's groups; the field's box never does. Both battlefields stay the same height whatever is on them.
  • No region of the board ever scrolls, and nothing may overflow its box by a pixel.

Acceptance criteria

  • Creatures, other permanents, and lands are in separate rows at 1280×720, 1920×1080, and 2560×1440, with a realistic board on both halves.
  • A field too short for three 100px rows draws three rows of smaller cards, not one row of large ones and not three rows of chips.
  • Rows merge only where a row cannot draw a tile at all, and a test names that viewport.
  • A card is drawn below 72×100 where the room requires it, its name still fits, and the fact is reported.
  • The monotonicity sweep from fix(client): more screen is never a worse board — the row count maximises the card #682 still passes: nothing smaller or less complete on a larger viewport. Extend it to cover row count — the number of rows must be non-decreasing in viewport height.
  • scene()'s stability property still holds.

Tests

Sweeps, not tables — non-monotonicity across viewports is this project's recurring bug class and a table of expected values has missed it three times. Prove the test bites: revert the fix, confirm the sweep reports the merges, restore it, and report the count.

Done when

A desktop board reads as creatures, other permanents, and lands — at every size — and the cards are whatever size that costs.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions