Commit Graph
50 Commits
Author SHA1 Message Date
toshiocn 10962babe3 feat(airport): add inbound dispatch prototype and landing effects 2026-10-26 16:11:11 +08:00
toshiocn f40efb8ba9 fix(airport): draw apron fans as solid wedges with a gap, not a fade 2026-09-21 19:30:33 +08:00
toshiocn 79afda0085 feat(airport): draw six equal apron fans instead of strip runways
Sectors are 60-degree wedges from the board centre, tucked against the tray. Radial seams mark the ABCDEF flats. Roll back with git checkout feat/airport-edge-runway.
2026-09-21 17:58:02 +08:00
toshiocn c99716b1d5 feat(airport): bind landings to the six visual flats
Entry side is the 60-degree wedge of the port, not the cell-edge direction. Levels regenerated; hint line joins the waiting plane to the on-board path.
2026-09-21 17:55:06 +08:00
toshiocn 0e67c4c188 feat(airport): draw the entry channel to the tray edge
Board-edge runways reach the tray outline with a threshold gate, fixed apron slabs mark each side, and a connected side lights the whole plane-to-stand channel in the neutral hint colour.
2026-09-21 14:38:54 +08:00
toshiocn 1be0e9caf1 feat(airport): add the Hex Airport dispatch lab as a baseline snapshot
Six-side aprons, frozen levels, solver, rewards and verify scripts; the edge-runway readability work continues from here.
2026-09-21 14:25:09 +08:00
cabinwandClaude Opus 5 6e72b95ad8 feat(levels): c50c trades one good idea repeated for five that alternate
c50b is sealed where it stands, as a reference sample rather than a candidate.
Its packed construction was judged good — clearing a board from the rim inward
is the experience it proved out — but at 32 of 50 levels it played as one level
fifty times: 体验太单一了,因为基本都是满盘. The note on why it is kept lives at
the top of its module, because the lesson is the useful part.

It was monotonous on a second axis nobody asked about. 环绕 and 花瓣, the two
route families the brief singles out as the good-looking ones, were absent from
it entirely — not by choice but by a crossing threshold. At 1.0 crossings per
cell, 0% of orbits boards pass and 4% of spirals; at 1.2, 84% and 94% do. The
gate meant to answer "交叉点太多" had quietly deleted the families most worth
looking at, so it moves to 1.2 — still half the 2.36 that drew the complaint.

c50c alternates five kinds of level every three to five: packed boards down to
32%, long-route departures to watch, deep locks to solve, small quick boards,
and segments given over to orbits, petals and spirals by name. All six families
appear, no family exceeds 24%, and the plan is 13 segments rather than 9.

Selection learned to ask for variety outright — by family, piece count and
longest drawn route — instead of hoping it falls out of a difficulty score.

Rule bot across all three: 50/50 cleared and zero dead taps everywhere; c50c
sits between the other two on blind rotations and on how much there is to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:53:26 +08:00
cabinwandClaude Fable 5.1 e447d1f807 feat(design): add the Macaron (07) art direction and land the Signpost (06) package files
- 07 macaron, referenced from Meowdoku: cream worlds, a white tray with
  gutters, candy tokens with white arrows, two-column 关卡/待解 HUD, white
  elevated tool discs (macaron-only exception to the flat rule), full-screen
  dark result overlay, a brick ripple on board clear, an eased glide-out and
  dot-lattice dissolve on exit; Resource Han Rounded subset fonts (SIL OFL)
  in assets/fonts with the charset and regeneration note
- 06 signpost: the design.md, tokens and preview directory that earlier
  commits left untracked
- design docs, catalog, manifest, showcase and base spec updated to seven
  directions; the analysis and proposal live in docs/art-proposal-macaron.html
- the renderer, app, geometry and CSS sources for both directions were
  already carried by the level commits made from this working tree

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 15:43:25 +08:00
cabinwandClaude Opus 5 7607f48cb1 feat(levels): boards whose interior is packed and whose rim is left empty
The game has never had a board you simply clear out, and the request for one
turned out to need a construction of its own rather than a denser deep lock.
Filling cells at random can never work: a packed board has nothing that can
move — measured over forty random fills of a 37-cell board, zero pieces free in
all six orientations, every time, because a piece steps to the next cell before
it can leave and on a full board that cell is taken.

So packBoard builds backwards. Start empty, add only a piece that could leave
right now, and the placement order reversed is a removal order — solvability
becomes a property of the construction. Which cell to fill next decides
everything: at random it stalls near half, while filling the most constrained
cell first takes a 19-cell interior to 96% and fills it outright.

The rim is deliberately empty. Those pieces leave first and ask nothing, and
clearing from the rim inward gives a packed board its own difficulty curve
inside one level — the outer cells read easily, and it tightens as it opens.

Two things were tried against the ceiling and failed, and are recorded as such:
depth-first backtracking made it worse on both counts (65-78% filled against
greedy's 89%, at 2000-3000 seconds a board) because recomputing every cell's
legal directions per node spends the budget three levels deep. And the audit's
dead-turn gate could not be trusted here at all — on a packed board its breadth
search truncates, so it certified levels the rule bot then walked through three
blind turns in a row. The pool is filtered by playing it instead: a full pass
takes one second and does not lie.

The campaign is 32 packed levels to 18 deep locks, alternating, with the closing
six at a completely full interior. Same rule bot, both editions: worst blind
streak 1 either way, dead taps zero, and half again as much to do per level.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:23:18 +08:00
cabinwandClaude Opus 5 de485c0c34 fix(levels): let a packed board pay its opening as a cascade, not one long trip
The opening gate asks for a departure of at least 0.57s, which is how a sparse
board pays: one piece threading a long route while you watch. A packed board
cannot pay that way — the pieces free at the opening are the ones at the rim,
and their routes are short by construction. So the gate rejected 29 of 29
crowded candidates for lacking a payoff they deliver in a different currency:
the cascade of pieces leaving one after another, which is what the brief singles
out as 多个棋子连环出去,非常赏心悦目.

openingRunMs totals every departure available before a turn is needed, and the
gate takes either that or the single long show. Crowded boards come through at
46% density, 0.92 crossings per cell and 14% straight track.

Both thresholds scale with the motion dial the way the first one already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:14:33 +08:00
cabinwandClaude Opus 5 8d766b6f06 feat(levels): plan the second fifty as alternating crowded and deep boards
A crowded board and a deep lock cannot be the same level — at five turns with
eight pieces per orientation, 16 of 18 candidates fail the release gate, because
pieces packed in leave no room for a turn to open two of them. So the campaign
alternates the two instead of ramping one: crowded stretches for the pleasure of
clearing a full board, deep stretches as the variation between them, and the
last four are the fullest boards in the set.

24 crowded, 14 deep, 12 sparse-and-shallow to open on. Density climbs across the
crowded stretches (0.36 → 0.40 → 0.42 → 0.44) since that is the axis left once
depth is spoken for.

select-50 takes --id and --name so this edition is built beside the first rather
than over it; the fifty already played stay exactly as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:08:25 +08:00
cabinwandClaude Opus 5 83796804d0 feat(levels): gate on crossing points, and lay the campaign out as segments
trackShape moves the two drawing measurements into src so the sweep, the pool
build, the selector and the gallery share one definition. They are one property
seen twice: a chord joining opposite ports is a straight line, and two of those
in a cell always cross, which is why the filler that paired leftovers straight
across produced a board that is both all-straight and all-crossings.

Crossings per cell is now a gate at 1.0. Bending the leftover ports alone takes
the median from 2.36 to 0.93, so the gate costs a third of the candidates rather
than most of them, and it is the difference the boards were described by.

Selection gains --plan, an ordered list of segments each naming how many levels
it wants and of what kind. Depth quotas cannot express the shape this campaign
needs, because the two good things pull against each other: a crowded board
clears beautifully and caps at three turns, while a deep lock needs room and so
stays sparse — at five turns and eight pieces per orientation, 16 of 18
candidates fail the release gate. Sorting by depth would file every crowded
level first and every deep one last; segments alternate them deliberately, and a
plan is left in the order it was written rather than re-sorted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:05:54 +08:00
cabinwandClaude Opus 5 4c4c250511 fix(levels): stop rejecting crowded boards for an endgame nobody measured
No level in this project is more than a third pieces — 13% to 28% across the
shipping fifty, and the thousand-level set that preceded it peaks at 32% with
exactly one level above 30%. Clearing a full board is the satisfying part of a
clearing game, and it has never been on offer.

It was not a design choice. deadTurnAudit walks the reachable state space with a
20000-state budget, and the endgame gate read its answer. Past about eleven
pieces that budget is gone long before the walk reaches any state with three
pieces left, so the audit returned `endgameMinOpen: null` — and the gate,
written `endgameMinOpen >= 2`, compared null and got false. Every denser level
was thrown out for a bad endgame that was never looked at. `exhausted` was right
there in the audit's return value and nothing consulted it.

Endgames are now enumerated outright rather than walked to: C(26,3) is 2600
subsets, so it finishes on any board this game will ever have. The statistic is
the SHARE of endgames that are tight, not the minimum over them, because a
minimum over more subsets falls for arithmetic alone — the same confound that
made doubleDead look eight times worse for deep levels when the rate was 2%
against 5%. By share, crowded boards are the better ones: worst 0.33% against
1.73% for sparse.

With the gate measuring something, density inverts. At eight pieces per
orientation: 44% density, 18 of 23 candidates passing the whole chain, and not
one failing the dead-turn gate. At two: 19% density, 9 of 23, with eleven
failing on dead turns. perTurn becomes a swept dial, and the hard piece ceiling
that truncated the placement schedule regardless of it is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:04:46 +08:00
cabinwandClaude Opus 5 f846a342d1 feat(routes): let leftover track pair into a bend instead of straight across
Playing on a phone, the boards read as a grid of long straight lines crossing
everywhere — "一看到这个画面,我脑袋就大". The cause is one line in the filler.
Each family lays its signature routes over part of the board and leaves the rest
to completePorts, which paired every leftover port with the one straight across.
On a switchback board that made 76% of all track pairs diameters and left 43 of
61 cells containing nothing but diameters. Two diameters in one cell always
cross, so the straightness and the crossings were never two problems.

Preferring a bend halves both, in every family: switchback 76% → 40% straight
and 2.38 → 0.92 crossings per cell, crossing 86% → 10%, weave 37% → 5%.

It was also quietly suppressing 反差, the swing between a piece looking hopeless
and becoming trivial, which the design brief calls the point of the game.
Straight routes barely change between orientations. With bends, petals goes from
4.2 to 14.0 and crossing from 4.8 to 28.8 — both had been failing the ≥12 gate
and were absent from the level pool entirely. Petals is the family the user
singles out as the good-looking one. Generation yield rises with it: four seeds
now grow 24 playable levels against 3, at greater rotation depth.

createRouteBoard keeps 'straight' as its default so nothing already built moves;
the sweep opts in. labs/route-gallery draws both, with the real renderer, so the
choice is made by looking rather than by reading a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:52:37 +08:00
cabinwandClaude Opus 5 073b682234 feat(motion): raise the cruising speed, and make the show gates follow it
Pieces still read as slow. Cruising speed goes from 120ms per unit to 85: the
median departure across the shipped fifty drops 970ms → 763ms and a level's
whole animation budget 15.6s → 12.7s, about 18% off. The longest departures do
not move — the cap holds them at 3.46s — so if those still drag, the dial is the
cap, which was settled separately and is left alone.

The gates that guarantee a level opens with something worth watching are stated
in milliseconds, and they were picked while a piece crossed a unit in 120ms.
Held fixed while the speed changes, they stop meaning what they meant: this
change alone made 20 of the 50 shipped levels "fail" an opening gate every one
of them had passed, purely from the unscaled number. Scaled to the dial, none
fail — the set is unchanged, and the alarm was an artifact of the constant. So
the cruise threshold and the opening floor now derive from the speed, naming
the speed they were calibrated at.

exitTiming takes msPerUnit the way it already took knee and maxMs, and the
comparison lab gets a speed slider, reads the levels actually shipping rather
than the prototype sweep they were selected from, and totals every route in the
campaign instead of the four on screen — those four skew long, where the cap
flattens any speed change to 6%.

The test that pins constant cruising speed now reads the dial from the token
instead of hard-coding 120, so tuning motion no longer looks like a broken
guarantee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:46:32 +08:00
cabinwandClaude Opus 5 34e6e73016 docs: hand-off instructions for publishing the phone bundle
Written to be copied whole into another agent's prompt, so it names the two
failure modes that are silent rather than loud: a bundle published without
--standalone has no viewport meta and renders at 980px on a phone, and a host
serving .js as application/octet-stream leaves a blank page with no clue in the
UI. The acceptance list checks the icons and the tile sheet explicitly, because
a missing asset degrades to the fallback renderer and looks deliberate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:33:05 +08:00
cabinwandClaude Opus 5 2c01b613bd build: a phone-sized bundle carrying one campaign
The static build ships every edition, and v0.5 alone is a 9.2 MB module holding
a thousand levels — fine over localhost, not over a phone connection. This
rewrites the campaign registry to name a single edition, then follows the import
graph from src/app.js, so the editions nobody asked for are never reached. 22
modules, 645 KB of code.

Art is the part an import graph cannot find: the tile sheet and icon sheet are
loaded by code, the background, tray and fonts by stylesheets. Both sources are
scanned and anything named but missing aborts the build, because a missing tile
sheet degrades to the fallback renderer and looks like a design choice rather
than a broken build. That is how the first bundle shipped: playable, with every
icon gone and "棋子图片加载失败" in the toast.

Stylesheets under design/ reach the art with ../assets/, which climbs out of a
bundle once inlined into a page at the root, so they are rewritten before they
are scanned — what gets collected is what the page will actually request.

--standalone emits a complete HTML document for an ordinary static host;
the default emits body content only, for hosts that supply the skeleton.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:32:03 +08:00
cabinwandClaude Opus 5 6f3dddbeee fix(ui): keep the v0.5 production record link out of other editions
Ten of the c50 levels were harvested from v0.5 and still carry its `production`
block, so the difficulty dialog offered "查看这关的制作记录" while playing them —
linking to labs/campaign-review at the *c50* level number, which is a different
level in the thousand-level set. The link is guarded by campaign id the way the
level picker's equivalent already is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:31:19 +08:00
cabinwandClaude Opus 5 c0c1558464 revert(levels): ship round 2's set — contrast selection measurably made it easier
Round 3 tested one hypothesis from round 2's data: levels the agent rated 3
carried 59% more 反差 than the rest, so select for it. With both rounds now at
~100 runs, the answer is in and it is no. The back band went from 2.39 ± 0.10 to
2.19 ± 0.08, roughly two standard errors apart, and the levels averaging 2.5 or
better fell from 11 to 6. Contrast is real and it does correlate with being
rated harder; selecting on it does not produce harder levels.

The low retest reliability does not rescue the change either — r = 0.55 limits
ranking one level against another, not the mean of eighteen levels measured a
hundred times.

So round 2's levels are the ones handed over, rebuilt from its runs via
round-set --emitCampaign. select-50 cannot reproduce them: the pool it drew
from has been rebuilt since, so the same flags now return different levels. The
runs are the record.

Rule bot on the shipped set: 50/50 cleared, dead taps 0.00, blind rotations
0.12, worst blind streak 1. 198 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:26:22 +08:00
cabinwandClaude Opus 5 85af37b1f7 docs(process): three sets through one rule bot, three rounds through one agent
The rule-bot table carries the third set now, which is the point of having a
rule bot at all: a full 50-level acceptance pass takes one second, so every
selection change can be checked against the previous one before any agent time
is spent. Blind rotations 0.24 → 0.12 → 0.14, worst streak 4 → 1 → 1, dead taps
zero throughout.

The band table shows all three rounds side by side, each against the levels it
actually played. Round 3 is reported as what it is: an unconfirmed hypothesis.
It raised contrast and held rotation depth, and the back band's rating did not
follow. On a rating that retests at 0.55 that settles nothing either way, so
the page says the two are indistinguishable and names the design reasons the
third set is the one being handed over — rather than quoting whichever number
makes the last change look good.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:23:53 +08:00
cabinwandClaude Opus 5 4aa0c28cef fix(bench): rebuild each round's level set from its runs, not from a snapshot
The round comparison was about to be wrong. levels/campaign-50-r2.json was
copied from levels/campaign-50.json to preserve "the set round 2 played", but
select-50 writes that path in place and several selection experiments ran
between the round and the copy. The file kept 28 of the 50 boards round 2 had
actually played and 46 of round 3's — so the comparison was scoring one round
against another round's levels, and it read as round 3 regressing.

Runs store the whole board and its campaign index, so a round is its own record
and cannot drift. round-set.mjs rebuilds it, the report uses that, and the
snapshot file is gone rather than left lying around looking authoritative.

With the rounds correctly matched, round 3 is not a regression and not an
improvement: back band 2.17 against round 2's 2.39, on a rating that retests at
0.55, which is not enough to separate seventeen levels. The report says that
rather than picking whichever reading flatters the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:22:31 +08:00
cabinwandClaude Opus 5 dcdd4a0ada feat(levels): select for 反差, the thing round 2 says separates hard from easy
Round 2 rated 16 runs a 3 and 84 a 2, which is too little spread to rank levels
by — but enough to ask what the 3s had in common. Contrast: 32.6 against 20.5,
a 59% gap and by far the largest of any feature. Rotation depth came second at
+1.2 turns. Board size and longest route ran the *other* way: the levels rated
harder were slightly smaller with shorter routes, which is what the design brief
has said all along — 难是难在用户无法一下子判断, not in how much board there is.

So within a depth, picking evenly across the ranked slice was diluting the one
thing the brief calls the point of the game. The depth quota already supplies
the difficulty range; inside a depth there is nothing to spread for, so it takes
the highest-contrast levels instead.

Taken alone that collapses the route grammars — two families filled 36 of the
50 — so the family cap comes down to 0.26. The pair beats the shipped round on
both axes rather than trading one for the other: contrast 14.8 / 22.0 / 27.9 →
15.2 / 24.9 / 28.6, families {18,12,8,2,10} → {13,13,5,9,10}. Same rule bot,
mechanics unchanged: worst blind streak still 1, dead taps still 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:18:42 +08:00
cabinwandClaude Opus 5 f6e92b05fb docs(review): measure the sheet's own caveats instead of quoting old ones
The sheet told the reviewer that difficulty and stuck were "the only two
ratings that retest reliably (r = 0.93 / 0.85)". On this set, with 50 retest
pairs to measure against, they are 0.55 and 0.45 — so the line was telling the
reviewer to trust the one number that had just failed. It now computes both
from the round in hand and says the ratings are readable by band only.

The complaints it quoted were round 1's, written about levels this edition no
longer contains. They are pulled from the round's own reports now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:16:20 +08:00
cabinwandClaude Opus 5 6af7277179 docs(levels): report what round 2 measured, including the number it overturns
The round accidentally ran every level twice, which turned into the most useful
thing it produced: 45 retest pairs on this exact set. 84% of them replayed the
identical action sequence, so the agent is near-deterministic and a second run
of a level buys almost nothing — the mechanical metrics retest at r = 1.00 for
that reason, not because they survived a hard test.

The ratings do not. Difficulty retests at r = 0.55 here, against the 0.93 this
project had on record from a smaller sample, and every pair that disagreed had
played the same trajectory: same moves, same outcome, different number. The
report now prints the reliability it measured rather than quoting the older
figure, and says which one it replaces.

Averaged by band over ~20 runs, though, the ratings finally track the curve —
2.00 / 2.05 / 2.40 with stuck at 1.21 / 1.15 / 1.74, where round 1 was a flat
line. So the rule is not "ratings are useless", it is "ratings are a band
statistic, never a per-level rank", and that is what the rules section says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:15:26 +08:00
cabinwandClaude Opus 5 ce7f6d30cf feat(levels): gate the dead turn, and state the curve instead of hoping for it
Round 1 played all fifty levels and came back with the mechanics clean and one
complaint repeated across the reports: 短暂以为转错了方向 — a turn that changed
nothing. That one is mechanical rather than perceptual, so it can be gated.

The first attempt gated `doubleDead`, and it was wrong. That counts dead states,
which scales with the state space: three-turn levels have ~187 reachable stuck
states and five-turn ones ~739, so a raw count made deep levels look eight times
worse when the rate is 2% against 5%. It would have erased every level past
three turns — the opposite of the brief, where turning is the signature and is
not to be rationed. The gate is `maxDeadTurns`, the longest run of turns before
something opens, at 1: one beat of suspense, then a release. Eleven of the fifty
shipped at 2.

Selection could not express the curve either. Spreading picks evenly over the
ranked pool returns whatever the pool is made of, and the pool is 88%
three-turn, so no weighting moved the distribution off {3: 46}. A quota per
depth states the curve outright, deepest first so the scarce end picks before a
family cap is spent on levels the shallow end has hundreds of.

Same rule bot, both sets: blind rotations 0.24 → 0.12, worst blind streak 4 → 1,
decisions unchanged at 10.3. The same amount to do, half the wasted motion.
Rotation now runs 1.8 / 3.0 / 4.1 across the bands against 1.8 / 3.0 / 3.5.

Verdicts are keyed by the board rather than by its id, because a reselect
re-issues ids from the ordinal and `c50-01-v05` is not the same board before and
after — the review sheet was one reselect away from printing one level's verdict
under another's thumbnail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:13:02 +08:00
cabinwandClaude Opus 5 d374c208f0 feat(bench): fit difficulty from a played round, and report when it cannot
refit-weights regresses the weights select-50 scores with against what agents
actually experienced, so a round of play is a flag on the next selection rather
than an edit. Ridge, because seven collinear features on fifty levels otherwise
hand back large coefficients that cancel.

Most of the work here is in refusing to produce a number. The response averages
the difficulty rating with two continuous measures of the same thing, and a
component with no spread is dropped by name instead of being silently averaged
in as zeros — which is what the first version did, reporting R² = 0.75 for a
fit that never saw a rating. When the rating is flat the script now says so and
says what the remaining fit is really measuring, which is board size.

legibility() asks what it costs to know what you may tap: how far the eye must
follow the cheapest free piece, and how many pieces are held by somebody their
line never touches. It was built to explain why ratings came back flat. It does
not — it correlates -0.02 with them — so it stays a measurement, printed on the
review sheet beside the longest route on the board, and gates nothing.

prototype-d5 takes --seedFrom so a wide sweep shards across cores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:03:43 +08:00
cabinwandClaude Opus 5 b35ea201db fix(campaign): register c50 with the fields the app actually reads
Opening the c50 edition threw `campaign.legacyStorageKeys is not iterable`
and left the board blank — the header still drew, so the level set looked
loaded. Every other edition carries the array; the hand-written c50 entry
did not, and 197 passing tests had nothing to say about it, because nothing
tested the registration itself.

The test now walks every edition and asserts the shape app.js relies on,
including that the chapter list can actually hold the levels. It fails on the
entry as it shipped.

The task board was reading levels/prototypes/d5.json while the campaign sat in
levels/campaign-50.json, so it showed "候选关卡 18 / 50" with fifty levels
built. It reads the shipping set when there is one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:03:37 +08:00
cabinwandClaude Opus 5 96e5e4c21f feat(levels): assemble the 50-level campaign and the sheet to review it
The deep-lock construction cannot go below three turns, so it cannot supply an
opening band — every level it makes lands mid-difficulty and the curve comes
out flat. harvest-easy pulls the shallow end from the shipped v0.5 set, but
only the 3% of it that holds up: 568 of 1000 need no rotation at all and 51
more are clearable by tapping at random, which is what the monkey threshold is
for. 33 levels survive, at one and two turns.

select-50 scores candidates on mechanical features only, spreads the pick
evenly across that range, and caps any one route grammar at 36% so the campaign
cannot quietly become one template repeated — the shipped 1000 turned out to be
thirty. Weights are a flag, so refitting them from a round of agent ratings is
a re-run rather than an edit.

The result is a curve rather than a plateau: difficulty score 3.85 / 6.24 /
7.60 across the three bands, rotations 1.8 / 3.0 / 3.5, and every level keeps
three or more opening choices with a show of at least a second.

The 50 register as a playable campaign edition, so review happens in the real
game; docs/review-50.html is the handover sheet, one card per level with the
opening position drawn from the solver rather than sketched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:53:57 +08:00
cabinwandClaude Opus 5 0aa080c7c3 feat(levels): give every level an opening worth watching
The chain locks all four of its pieces at the opening by construction, and the
pieces grown around it are mostly locked too, so a level built from it hands a
player two rim pieces and a third of a second of animation before any thinking
starts. Measured across the 235-level pool: median two tappable pieces at the
opening, and the best departure available plays for 0.57s.

Growth now reserves a couple of slots for showcase pieces — tappable at the
opening, with a route long enough to read as a journey and short enough to stay
under the animation ceiling. Median opening choices go 2 to 3 and the best
opening show 0.72s to 1.12s, without shortcutting the lock: each candidate is
still put through the solver.

Rotation quota is gone from the board. Turning is free and it is what the game
is about, so depth is grown rather than capped; what is gated is whether each
turn pays out, whether pieces swing between hopeless and trivial, and whether
there is a show up front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:45:24 +08:00
cabinwandClaude Opus 5 a250baba53 fix(motion): stop a long departure from playing for seventeen seconds
Exit timing was linear in path length with no ceiling, so the longest route in
the shipped set animated for 17.4s and 19% of all departures ran past two
seconds. MOTION already carried exitMaxTravelMs and exitLongMaxMs for this;
nothing read them.

Below the knee nothing changes — every piece still cruises at the same speed,
which is the property the old test protected and the reason motion reads as
physical. Past it the piece accelerates rather than taking longer, approaching
a ceiling. Length is still legible: longer routes still play longer, just with
a compressing tail.

The test now states the rule it is protecting instead of "no duration cap":
constant speed below the knee, a bounded and monotone tail above it.

labs/exit-timing plays the same real route side by side, uncapped against a
candidate setting, with knee and ceiling on sliders — the numbers say 3.5s but
only watching it says whether that is right. Defaults are provisional at
2.4s / 3.2s pending that call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:41:06 +08:00
cabinwandClaude Opus 5 acc374e276 feat(levels): two-sided endgame release, and a board to track the 50-level run
The chain built its own worst moment: hero and tail deadlock at orientations 0
and +2 and open only at +1, with +3/+4/+5 left entirely to chance. That is why
some levels released at exactly one of six orientations, and why every model
kept writing "I don't know which way to turn" — at +2 one direction was right
and the other five were dead ends.

createDeepLock now traces candidates at all six orientations and requires the
tail to also release at +3, certified with the real swept tracer. A player
stuck at +2 turns either way and something moves. Measured by the rule bot
over the regenerated set: blind rotations, wrong-way turns and extra rotations
all go to zero, from 1.17 / 0.44 / 1.22 per level. Endgame orientation counts
shift from {1:4, 2:10, 3:4} to {2:3, 3:13, 4:1}.

It costs yield: switchback r3 and crossing r3 now produce nothing, and the set
comes out at 17 levels rather than 18. The test witnesses moved with it — a
witness is whichever seed still satisfies the construction, so they are
re-derived from the sweep rather than pinned.

docs/level-board.html is the task board for getting to 50 levels. Task state is
in bench/board.json; every number on the page — candidate count, rotation
quota, blind turns, monkey actions-per-piece, the agent batches — is measured
from the level set and the run archive at build time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:22:08 +08:00
cabinwandClaude Opus 5 8a736766fd fix(report): key levels by id, not by position in the level set
Regenerating levels/prototypes/d5.json renumbers every level, so the previous
build — which resolved runs through the current set's ordinals — silently
dropped every run played against an earlier set. The report I committed one
commit ago was built that way: 130 runs instead of 229, and the test-retest
table down to 7 paired levels from 18.

Levels are now keyed by id throughout. Runs carry their own level, so ids the
current set no longer contains are read back out of the archive and given an
ordinal after the live ones; the cross-model table intersects on ids rather
than assuming two sets number the same level alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:04:27 +08:00
cabinwandClaude Opus 5 912bec29b3 feat(levels): reject one-orientation endgames, and measure the result
Every model that played the D5 set complained about the same moment: two
pieces left, interlocked, and nothing on the board saying which way to turn.
codex-spark took it to the end — it cycled all six orientations on L18 and
gave up on a level that was solvable, with clarity rated 5. Replaying the
position shows exactly one of the six orientations releases anything.

deadTurnAudit now reports endgameMinOpen: across reachable states with three
pieces or fewer left, the fewest orientations that release anything. It is
computed inside the walk that was already happening, so it costs nothing. The
generator requires at least two.

The gate picks out precisely the four levels every model stalled on, with no
false positives — the other fourteen already had two or three. Regenerating
drops the spiral family entirely: its structure produces single-exit endgames
by construction, not by unlucky seed.

Measured on the regenerated set, rule bot first and then grok-4.6 xhigh over
all 18:

  blind rotations   1.17 -> 0.29 per level
  extra rotations   1.22 -> 0.35
  dead taps         2 -> 0
  run time          55.5s -> 34.7s
  stuck rating      1.89 -> 1.41   (this metric repeats at r = 0.85)
  "which way do I turn" complaints   22% -> 12% of levels

The gate is a floor, not a difficulty dial: difficulty also fell, 2.39 -> 2.12,
because the levels it removes are the hard ones. And it treats a symptom — the
t0/t3 interlock is what the four-piece chain in createDeepLock builds by
construction, so the complaint is halved rather than gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:03:44 +08:00
cabinwandClaude Opus 5 cefb7a5a56 docs: five-way cross-check, and correct a mislabelled batch
One batch of 18 runs was recorded as codex-spark at xhigh but had actually run
as gpt-6-astra at low effort: the CLI config was rewritten between the check
and the batch, and codex resolved the new model silently. The archives are
relabelled from the CLI's own startup header rather than from what the agent
was told to call itself, and the report now says to trust that header over the
self-reported name. The re-run verifies the resolved model after every level
and aborts the batch on a mismatch.

With the real codex-spark batch added, five independent runs of four models
from three vendors now cover the same 18 levels on the fixed board, and 16 of
17 comparable levels agree exactly on the rotation triple.

codex-spark gave up on L18 after cycling all six orientations, reporting no
exit anywhere — replaying the position shows orientation 1 releases t0, and
that it is the only one of the six that does. The level is solvable and the
routes are legible (it rated clarity 5), but nothing on the board says which
way to turn: a 1-in-6 guess with a dead end behind every other door. That is
the terminal-interlock complaint the other models kept raising, at its limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:37:38 +08:00
cabinwandClaude Opus 5 d123b17a54 docs: regenerate bench reliability report with four-way cross-check
Adds the kimi-k3 and codex-spark batches, so the rotation counters are now
compared across four independent runs of three models from three vendors on
the fixed board: 17 of 18 levels agree exactly, triple for triple.

The clean test-retest (same model, same harness, same board, run twice) also
corrects two earlier readings taken from data that had a board-format change
mixed in. Difficulty ratings reproduce at r = 0.93 rather than 0.63 — the
earlier low figure was the format change, not the metric — so an agent can be
trusted to rank 1000 levels by difficulty even though its absolute scale may
be offset. And the mechanical counters are highly reproducible rather than
deterministic: 0.82-0.92, not the 1.00 the confounded pair suggested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:10:06 +08:00
cabinwandClaude Opus 5 752b437078 docs: design review and bench reliability analysis
design-review.html is the gameplay and UI read on the shipped campaign, with
boards rendered from the repo's own geometry rather than redrawn.

bench-reliability.html asks which of the bench's numbers are worth acting on,
and answers it by running the same 18 levels twice under identical conditions.
The mechanical rotation counters reproduce (r = 0.82-0.92) and three different
models agree on them level for level, so they measure the level rather than
the player — a 12-second rule-bot sweep gets the same answer as hours of
model time. Difficulty and stuck ratings also hold up (r = 0.93 / 0.85), but
the ratings for rotation, clarity and fun do not (0.32 / 0.19 / 0.56): asking
a model whether rotation matters is noise, while counting blind rotations is
not.

Every figure is computed from bench/runs by build-bench-report.mjs; nothing in
the page is hand-written, so it can be regenerated as runs accumulate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00
cabinwandClaude Opus 5 643cba0ed9 feat(levels): D5 three-rotation prototype
The shipped 1000 levels have a hole: 568 of them need no rotation at all, and
none needs more than two — the mechanic the game is built on is mostly
decoration. This fills the empty band with levels that need at least three.

createDeepLock builds a four-piece chain where each link only releases at a
later orientation, and growDeepLevel packs the rest of the board around it
without opening a shortcut. Candidate pools are built per orientation and left
untruncated; ranking by bend count first starved the packer and capped levels
at four to six pieces instead of twelve.

deadTurnAudit enumerates every reachable (orientation, remaining-set) state to
prove no dead ends exist, and counts how long a player can be left with
nothing to do — solvable and feeling solvable are different things, which is
what "level 2 is impossible" turned out to be about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00
cabinwandClaude Opus 5 f3e91e3597 feat(bench): batch runners, aggregation and a standalone kit
Four ways in, depending on what you have: a shell agent driving bench.mjs, a
direct-API loop for models with only an endpoint (plain text in, one JSON line
out, so weak and local models can play), a rule-following bot that sweeps 1000
levels in 12 seconds, and a folder you can hand to someone else.

bench-kit.mjs builds that folder: CLI, engine, level pack, observer page and
tile atlas, about 1.5 MB, needing only Node 20 — no repo, no install, no
network. The engine files are discovered by following imports so a new module
can't be left behind, every path rewrite is asserted so a rename fails the
build loudly, and the builder plays a real game inside the result before
declaring it done.

bench-report.mjs aggregates two ways: per level (is this level any good) and
per model (how does this agent play). bench-import.mjs brings runs played
elsewhere back in under a tag.

bench-pack.mjs is the earlier, smaller export with no observer page; superseded
by bench-kit but kept in history rather than dropped silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00
cabinwandClaude Opus 5 47be0ad9c4 feat(bench): live observer page
Watch an agent play in real time, replayed with the main game's renderer and
animations. The board holds the stage with the live panel beside it: a
millisecond stopwatch, how long the current step has been thinking, steps,
decisions, pieces cleared, dead taps and rotations, then the agent's running
commentary. A banner over the board names who is playing, self-reported by the
agent via --agent.

The page polls unconditionally; the checkbox only decides whether the cursor
chases the newest step, and it follows the agent into its next run once this
one ends. Clicking any step in the feed replays the board at that moment, and
the arrow keys walk it — there is no scrub bar, since the feed already is one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00
cabinwandClaude Opus 5 c760c23fd1 feat(bench): agent bench session core and CLI
Let any agent with a shell play a level the way a person does: look at the
board, decide what to tap or which way to turn, read the feedback, decide
again. Every step carries the agent's own reason, so a human can watch and
judge the level from how it was played.

observe() returns what the eye sees — each piece's drawn route, which cells it
crosses, who is sitting on it, and who it brushes past — never "which pieces
can leave". act() goes through the real Game rules; metrics() scores the run
afterwards with the solver.

Pieces are wide, so a route with nobody standing on it can still be blocked by
a neighbour the curve hugs. That is invisible in a cell listing, so every
obstacle is reported, on-route and grazing alike: reporting only the leading
one hid the rest, and in 113 of 581 such cases clearing the blocker sprang a
collision the agent had never been shown.

AGENT.md is the single file an agent reads — rules, board reading, commands,
rating scale, worked example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00
cabinwandClaude Opus 5 bc70dbfb7b chore: ignore generated agent-bench output
Run archives, the aggregated report and the packaged standalone kit are all
regenerated from scripts; bench/runs alone is 16 MB across 1140 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:19 +08:00
cabinw 10cf93a324 feat: add Paper Routes and package five design systems
Add the paper direction with thin tracks, outlined hexagons and line arrows, without exterior color markers or piece numbers. Add a unified catalog for five designs and their 25 themes, with direct review links.

Organize each design into an authored design.md, generated tokens.json and preview. Add Agent routing, shared integration guidance, a machine-readable manifest and an executable rendering example. Preserve legacy preview and document links.

Validation: 16 focused design, theme and package tests passed; npm run design and static build passed. Confirmed all eight authored documents survive generation unchanged. Browser checks covered mobile, tablet and desktop layouts, theme links and migrated previews. Native engine integration and visual verification remain pending.
2026-09-13 13:24:34 +08:00
cabinw e32a1616be docs: add art direction and production brief 2026-09-13 12:27:56 +08:00
cabinw 4644e975b0 feat: add production campaigns, gameplay labs, and review documentation 2026-09-13 12:27:10 +08:00
cabinw d1dffa9338 Add three playable puzzle variants in an isolated lab 2026-09-13 12:19:13 +08:00
cabinw e92cccba82 feat: version campaigns and document original puzzle design
Restore v0.1 and v0.2 archives with separate progress, publish the curved v0.3 campaign, and document the original 24-level design with reproducible evidence. Refine board presentation and movement, and keep video lab tooling within its own directory.
2026-09-09 13:17:30 +08:00
cabinw 70b021d4cc feat: expand campaign and refine puzzle visuals and controls 2026-09-09 11:02:45 +08:00
cabinw 9f8990b579 chore: track Cocos Creator project for future migration 2026-09-08 21:54:30 +08:00
cabinw feb2ec5de1 docs: refresh project README and development guide 2026-09-08 21:50:53 +08:00
cabinw ef2724bb2e feat: add hex trail puzzle, level editor, and visual design guide 2026-09-08 21:45:44 +08:00