Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ Tray generation algorithm (documented contract, tested):

Quest stages may pin the FIRST tray via the stage's `tray` field (the
"opening break" design — see CONTRIBUTING_QUESTS.md); the generator
takes over from the second tray on.
takes over from the second tray on. A score goal must exceed the
**opening ceiling** — `maxOpeningScore` (in `solvability.dart`) is the most
that opening tray can score alone, all-clear bonus included — so the
cascade can't win the stage outright; the validator enforces this.

**Quest gem stages** (`GameState.gemGoal` non-empty): gems ride on the
generated tray pieces rather than being pre-placed. After drawing a tray,
Expand Down Expand Up @@ -126,7 +129,10 @@ Schema is versioned and strictly parsed. Details: `CONTRIBUTING_QUESTS.md`.
One service (`lib/services/`); versioned JSON save model documented in
`SAVE_MODEL.md`: settings, classic high score + resumable in-progress run
(board, tray, RNG state, score, combo), quest progress, best combos, quest
cache metadata.
cache metadata. **Quest attempts are also resumable**: the in-progress
`GameState` plus its pack/stage/level ids are persisted, so leaving a stage
or killing the app resumes it exactly (the map reattaches pack/stage from
the catalog). The snapshot is cleared on win, loss, or restart.

## Key decisions

Expand Down
22 changes: 16 additions & 6 deletions CONTRIBUTING_QUESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,32 @@ toward what's reachable.

### Goals

- Score: `{ "type": "score", "target": 700 }` — reach the score.
- Gems: `{ "type": "gems", "counts": { "red": 8, "purple": 6 } }` —
collect *at least* the listed count of each color. Gems ride on the
- Score: `{ "type": "score", "target": 700 }` — reach the score. The
target **must sit above the opening ceiling**: the most the opening tray
can score on its own, *including the +300 all-clear bonus* if the opening
can empty the board. Otherwise the cascade wins the stage on move three
and it plays as a freebie. The validator computes this ceiling and
rejects any target at or below it, so a designed opening that all-clears
needs a target comfortably past ~400. Current packs ramp ~500 → ~1200.
- Gems: `{ "type": "gems", "counts": { "red": 5, "purple": 5, "blue": 5 } }`
— collect *at least* the listed count of each color. Gems ride on the
generated tray pieces (spawned only for colors you still need), so the
goal does NOT depend on gems being pre-placed on the board — you don't
put gems in the board at all for a gem goal, and counts can be large.
Collecting more than the goal is fine. Once a color is fully collected
it stops appearing on new pieces.
it stops appearing on new pieces. **Prefer multi-color goals** — aim for
**3 to 5 gem types**, even in early stages; a single-color gem goal reads
as flat. Counts can grow aggressively as the pack progresses.

### Rules the validator enforces

- Boards are exactly 8×8 with only the characters above.
- No row or column may start out already complete.
- At least 20 empty cells (otherwise the stage is likely unplayable).
- Gem goals must be satisfiable by the gems on the board.
- Score targets between 50 and 20000.
- Gem goal counts are each between 1 and 60 (gems come from the tray, so
the board doesn't need to hold them).
- Score targets between 50 and 20000, **and above the opening ceiling**
(the opening tray can't win the stage by itself — all-clear included).
- Stage ids unique within a pack; pack ids unique in the manifest.
- The manifest checksum must match the pack file.

Expand Down
22 changes: 20 additions & 2 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,24 @@ lose-screen/80%-banner visual eyeballing during play, a CI workflow
- Session ended mid slice-D device verification at user request; all
code committed on `dev`, working tree clean, 43/43 tests green.

### 2026-07-16 — Playtest: quest resume, gem tiles, difficulty tuning
- Quest attempts now resume: SaveData carries questRun (GameState) +
pack/stage/level ids; controller persists on start/each move, clears on
win/loss; quit keeps the snapshot; the map's _play resumes it if the
saved run matches the tapped node. Leaving a stage or app-kill no longer
loses progress (parity with classic).
- Gem-bearing blocks always draw on the neutral light tile — in the tray
and once placed — so the gem never fights the piece color (before, only
pre-placed gems were light).
- Difficulty (user feedback "too easy"): score targets raised but floored
ABOVE each stage's opening ceiling. Added solvability.maxOpeningScore
(best score the opening tray can make, all-clear +300 included) and a
validator rule rejecting score targets ≤ that ceiling — four stages
(s01/s02/t01/d01) were literally winnable from the opening before.
New score ramps ~500→1200. Gem goals: every gem stage now asks for 3–5
colors (was many single-color), counts grow more aggressively. Opening
cascade kept as-is per user; the higher goals carry the difficulty.

### 2026-07-16 — Gems ride on tray pieces (quest gem stages reworked)
- User: pre-placing all gems on the grid made stages swingy and capped
goals low. Now gems spawn on generated tray pieces (assignGems, ~50%
Expand All @@ -180,8 +198,8 @@ lose-screen/80%-banner visual eyeballing during play, a CI workflow
A placed gem is collected when a line through it clears.
- Model: GameState gains trayGems (per-slot cell→color) + gemGoal;
placement.stampWithGems; engine threads gems on place/refill; the
controller passes the goal; board renders placed gems in the piece
color (pre-placed stay light); tray pieces show their gems.
controller passes the goal; board + tray render gems (gem tiles later
moved to the neutral light substrate everywhere — see 07-16 playtest).
- Validator: dropped the "board must contain the goal's gems" rule
(obsolete); added a gem-count sanity range. All 20 gem stages had
their board gems stripped and goals raised (single 8 / two 6 / three 5).
Expand Down
11 changes: 11 additions & 0 deletions SAVE_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@ silently discarding progress.
"allTimeBestCombo": 0, // best combo streak ever (Combo Master screen)
"classicRun": null, // in-progress classic run, or null
"classicRunSeed": "12345", // seed that run started from (string int64)
"questRun": null, // in-progress quest attempt (GameState), or absent
"questRunPackId": "starter", // which pack/stage/level the questRun belongs to
"questRunStageId": "s07", // (the stage isn't stored in GameState, so the
"questRunLevelNumber": 7, // map reattaches it from the catalog on resume)
"questCompleted": { // packId -> sorted stage ids completed
"starter": ["s01", "s02"]
},
"lastQuestFetchEpochMs": null // throttle for the GitHub manifest fetch
}
```

The four `questRun*` keys are additive and only emitted while a quest
attempt is in progress; they are dropped together when the stage is won,
lost, or restarted from scratch. Like the classic run, `questRun` is a
`GameState` snapshot (board, tray, RNG, score, gems), so resuming needs no
replay — the map screen looks up the pack/stage by id from the loaded
catalog and hands the saved `GameState` back to the controller.

### `classicRun` (GameState snapshot)

```jsonc
Expand Down
6 changes: 3 additions & 3 deletions content/quests/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
"title": "First Steps",
"release_date": "2026-07-01",
"file": "packs/starter.json",
"checksum": "8c70a5bdbc7a9a059ef8f106e7d70e5a3dd45f7664f511cd989f5d17f4aae400"
"checksum": "8d02386d85cdf853868622b7c3ce3df82060410f260ce8869b8570f3be02bc45"
},
{
"id": "deep-dig",
"title": "Deep Dig",
"release_date": "2026-07-05",
"file": "packs/deep-dig.json",
"checksum": "eefa0748debb175ecb710141bb39e4b207fa3850162ad0b746b496c0615690b7"
"checksum": "11ba96f5ead3b4b6de075b3b766f599139cb2eb63e2b71e24a994e6ef9cf9548"
},
{
"id": "treasure-trail",
"title": "Treasure Trail",
"release_date": "2026-07-26",
"file": "packs/treasure-trail.json",
"checksum": "c4e1c2d4edd1831459d9b263a5eb91739d50a3fc9eca2daa67af5553115af2b9"
"checksum": "42d90c5ea0f4ea479cb73eef3cc2b0513862f9241a44d88ddbcfbdbbf9b84fec"
}
]
}
58 changes: 37 additions & 21 deletions content/quests/packs/deep-dig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"goal": {
"type": "score",
"target": 200
"target": 550
},
"tray": [
"rect2x3",
Expand All @@ -40,7 +40,9 @@
"goal": {
"type": "gems",
"counts": {
"blue": 8
"blue": 4,
"red": 4,
"yellow": 4
}
},
"tray": [
Expand All @@ -63,7 +65,7 @@
],
"goal": {
"type": "score",
"target": 300
"target": 650
},
"tray": [
"tRight",
Expand All @@ -86,8 +88,9 @@
"goal": {
"type": "gems",
"counts": {
"green": 6,
"red": 6
"green": 5,
"red": 5,
"purple": 5
}
},
"tray": [
Expand All @@ -110,7 +113,7 @@
],
"goal": {
"type": "score",
"target": 400
"target": 750
},
"tray": [
"line5h",
Expand All @@ -133,7 +136,9 @@
"goal": {
"type": "gems",
"counts": {
"purple": 8
"purple": 5,
"blue": 5,
"green": 5
}
},
"tray": [
Expand All @@ -156,7 +161,7 @@
],
"goal": {
"type": "score",
"target": 500
"target": 850
},
"tray": [
"line3v",
Expand All @@ -179,7 +184,10 @@
"goal": {
"type": "gems",
"counts": {
"yellow": 8
"yellow": 6,
"red": 6,
"blue": 6,
"green": 6
}
},
"hard": true,
Expand All @@ -203,7 +211,7 @@
],
"goal": {
"type": "score",
"target": 600
"target": 950
},
"tray": [
"tLeft",
Expand All @@ -226,8 +234,10 @@
"goal": {
"type": "gems",
"counts": {
"red": 6,
"blue": 6
"red": 5,
"blue": 5,
"purple": 5,
"yellow": 5
}
},
"tray": [
Expand All @@ -250,7 +260,7 @@
],
"goal": {
"type": "score",
"target": 700
"target": 1050
},
"hard": true,
"tray": [
Expand All @@ -275,7 +285,9 @@
"type": "gems",
"counts": {
"purple": 6,
"green": 6
"green": 6,
"red": 6,
"yellow": 6
}
},
"hard": true,
Expand All @@ -299,7 +311,7 @@
],
"goal": {
"type": "score",
"target": 800
"target": 1200
},
"hard": true,
"tray": [
Expand All @@ -323,7 +335,11 @@
"goal": {
"type": "gems",
"counts": {
"red": 8
"red": 6,
"blue": 6,
"purple": 6,
"yellow": 6,
"green": 6
}
},
"hard": true,
Expand All @@ -348,11 +364,11 @@
"goal": {
"type": "gems",
"counts": {
"red": 5,
"blue": 5,
"purple": 5,
"yellow": 5,
"green": 5
"red": 6,
"blue": 6,
"purple": 6,
"yellow": 6,
"green": 6
}
},
"hard": true,
Expand Down
Loading
Loading