Merge fix/loop-row59: the map graph as the cartridge's headers and warps draw it, pieces and all
This commit is contained in:
commit
f32352bd5e
11 changed files with 1139 additions and 179 deletions
|
|
@ -1539,6 +1539,75 @@ Nothing is ranked, weighted or pressed for the fly: a button leaves the pad whil
|
||||||
anything and comes back when it can (a new battle resets the stages). The decoder, the reward
|
anything and comes back when it can (a new battle resets the stages). The decoder, the reward
|
||||||
catalog, the adapter version, the roles and the compatibility string are untouched.
|
catalog, the adapter version, the roles and the compatibility string are untouched.
|
||||||
|
|
||||||
|
### 12.24 The map graph is the disassembly's, piece by piece (2026-09-23, row 59)
|
||||||
|
|
||||||
|
Opened pre-emptively: the row-58 review carried the route survey past the Boulder Badge and the
|
||||||
|
fly walked Pewter City (39, 17) to Route 3 (0, 9) and back from about frame 68,000, `GO OBJECTIVE`
|
||||||
|
done on Route 3 538 times and `GO ROUTE` done on Pewter 537. Reproduced from a rank-11 checkpoint
|
||||||
|
the survey writes (`FLY_PROBE_SAVE_RANK=11`): 509 and 508, never on Route 4. The live fly got
|
||||||
|
through Route 3 anyway and met the next half on v0.6.0 at 22:20 UTC: rank 12, MT. MOON, the
|
||||||
|
objective Cerulean, on Route 4 per ten minutes `GO ROUTE` 215, `GO OBJECTIVE` 113, `GO OUT` 103,
|
||||||
|
two new tiles, in and out of the Pokécenter and the cave mouth. Route 4 was one node with Cerulean
|
||||||
|
off its east edge, which the mountain cuts off from the cave mouth's side. From the live
|
||||||
|
checkpoint the route survey on `main` walks it 930 times in 72,000 frames.
|
||||||
|
|
||||||
|
**The geography table disagreed with the headers.** Checked row by row against
|
||||||
|
`data/maps/headers/*.asm` and `data/maps/objects/*.asm` at the pinned commit:
|
||||||
|
|
||||||
|
- Route 4 is **north** of Route 3, not east (`Route3.asm`: `connection north, Route4`); Route 3's
|
||||||
|
top edge is the road to Mt. Moon's Pokécenter. Mt. Moon's doors are both **on Route 4**:
|
||||||
|
(18, 5) into the first floor and (24, 5) into B1F, whose (27, 3) is the way out. Route 3 has no
|
||||||
|
warps. So Route 3's north edge named no map, and nothing on Route 3 was the way to the rung.
|
||||||
|
- Route 14 / 15 and Route 24 / 25 had the right neighbour in the wrong column (west and east,
|
||||||
|
not south and north). Route 24's east edge is Nugget Bridge's far end, rung 16's road.
|
||||||
|
|
||||||
|
**Four maps are pieces the player cannot walk between** (12.7's rule, measured by flooding every
|
||||||
|
tile of every map on the graph from the blocks, blockset, collision list, tile-pair walls and
|
||||||
|
ledges): Route 2 as before; **Route 4**, cut by the mountain into the cave mouth's side and
|
||||||
|
Cerulean's side; **Mt. Moon B1F**, four chambers of two ladders each; **B2F**, one large piece and
|
||||||
|
two small ones. The one road through is 1F (5, 5), B1F (21, 17), B2F (5, 7), B1F (27, 3); the
|
||||||
|
other two ladders on 1F lead to dead ends.
|
||||||
|
|
||||||
|
A split row is now any number of pieces, each with its doors (the warp index and tile) and what is
|
||||||
|
one step from it, a whole map or another map's piece. Three rules keep it honest:
|
||||||
|
|
||||||
|
- **Which piece the fly is in** is what its walk can reach on the decoded grid (section 15): the
|
||||||
|
piece whose doors it reaches, when exactly one piece's are. The grid has no ledges, so where it
|
||||||
|
reaches none (Route 4 below the ledges) the nearest door answers. Flooded over every tile of
|
||||||
|
the four maps' ground in the disassembly, the rule names the right piece for all of them.
|
||||||
|
- **Which piece a door lands in** is the cartridge's own answer: a warp names the destination
|
||||||
|
warp it arrives at (`wWarpEntries` byte 2), and each piece lists its warps. An edge lands in
|
||||||
|
the piece that lists the map it is stepped off.
|
||||||
|
- **The hop is a piece**, and an exit is toward the objective only if it lands in that piece. On
|
||||||
|
1F three ladders go down to B1F and one of them is the road.
|
||||||
|
|
||||||
|
**A connection nobody can walk across is not a road.** Four header connections have no tile where
|
||||||
|
both sides are land: Pallet Town / Route 21, Cinnabar / Route 20, Route 20 / 19 (sea) and Route 22 /
|
||||||
|
23 (the League's fence). They keep their name and offer no exit and no hop. Without this the road
|
||||||
|
from Pallet Town to Cerulean was by sea, and a fly that whited out in Mt. Moon, which the survey's
|
||||||
|
did, walked into Pallet's shore every two seconds.
|
||||||
|
|
||||||
|
**One seam frame, found on the way.** Route 3's first trainer closes his challenge onto five frames
|
||||||
|
of plain overworld before `StartTrainerBattle` decides the battle (`home/trainers.asm`: it runs
|
||||||
|
after `DisplayTextID`'s close-down). Row 58's pending push-back was decided on the first of them
|
||||||
|
and walled (11, 6), the one gap between Route 3's west end and the rest of the road, for the
|
||||||
|
session. A push-back is now a refusal only once the overworld has been the fly's for thirty frames
|
||||||
|
running; a battle inside them drops it.
|
||||||
|
|
||||||
|
From the badge, the route survey reaches Route 4 at frame 70,356 and Mt. Moon at 70,707 (rung 12),
|
||||||
|
no pushed tile; after whiting out in the cave it walks the land road back from Pallet Town. The
|
||||||
|
ROM test on the stub rotation reaches Mt. Moon in 56.7 brain minutes with 4 Pewter / Route 3
|
||||||
|
crossings; the base makes 3,391 in 80.4 and never stands on Route 4. From the live 2026-09-22
|
||||||
|
rank-11 checkpoint the branch reaches Mt. Moon too, where the base ends fenced on Route 3. From
|
||||||
|
the live Route 4 checkpoint the branch is in the cave on frame 279 and stays on the road; on the
|
||||||
|
stub rotation Route 4's west doors are crossed 13 times in twenty brain minutes (whiteouts and
|
||||||
|
walks back included) against 56 on `main`.
|
||||||
|
|
||||||
|
Nothing is ranked and nothing presses for the fly: a table of maps says what the headers say, a
|
||||||
|
split map has the pieces its ground has, and a frame that was the cartridge's is not read as the
|
||||||
|
fly's. The decoder, the reward catalog, the adapter version, the roles and the compatibility
|
||||||
|
string are untouched.
|
||||||
|
|
||||||
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
||||||
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
||||||
## for the Pokécenter. heal should be a macro.")
|
## for the Pokécenter. heal should be a macro.")
|
||||||
|
|
|
||||||
|
|
@ -2851,3 +2851,119 @@ of the brain, which is the deviation.
|
||||||
648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68`
|
648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68`
|
||||||
on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version
|
on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version
|
||||||
and roles untouched.
|
and roles untouched.
|
||||||
|
|
||||||
|
- `flysim --print-compatibility`: **648 bytes, sha256 `4929f340...9ebd9`**, byte-identical to the
|
||||||
|
base. Decoder, reward catalog, adapter version and roles untouched.
|
||||||
|
|
||||||
|
## 2026-09-23, row 59: the road to Mt. Moon, and Route 4 in two
|
||||||
|
|
||||||
|
### What was coming, and what came
|
||||||
|
|
||||||
|
Opened pre-emptively. The row-58 review carried the route survey past the Boulder Badge (v0.5.5's fix):
|
||||||
|
from about frame 68,000, Pewter City (39, 17) and Route 3 (0, 9) in a ring, `GO OBJECTIVE` done
|
||||||
|
on Route 3 538 times, `GO ROUTE` done on Pewter 537. Route 3's pad was `GO ROUTE` alone, because
|
||||||
|
`GO OBJECTIVE` had nothing on Route 3 to aim at. The live fly was due there with the badge.
|
||||||
|
|
||||||
|
It got through Route 3 and met the other half on v0.6.0 at 22:20 UTC: rank 12 (MT. MOON), map 15,
|
||||||
|
per ten minutes `GO ROUTE` 215, `GO OBJECTIVE` 113, `GO OUT` 103, five distinct macros, two new
|
||||||
|
tiles; the coordinator restarted flysim. The live checkpoint from inside the ring is v7. From it the
|
||||||
|
route survey on `main` (`4d82f7d`, 72,000 frames) walks between Route 4 and the Pokécenter (map
|
||||||
|
68): 930 map changes, `GO OBJECTIVE` done on Route 4 465 and `GO ROUTE` done on the Pokécenter
|
||||||
|
462 with `GO OBJECTIVE` preferred; with a uniform choice 706 changes, `GO ROUTE` 350,
|
||||||
|
`GO OBJECTIVE` 183, `GO OUT` 170, the live shape. That is 59c.
|
||||||
|
|
||||||
|
### Reproduction
|
||||||
|
|
||||||
|
`examples/scene_probe.rs` gains `FLY_PROBE_SAVE_RANK`: the route survey writes the first safe
|
||||||
|
overworld frame at a rung as a checkpoint. From the row-58 checkpoint, `FLY_PROBE_CATCH=route
|
||||||
|
FLY_PROBE_PREFER="GO OBJECTIVE,TALK" FLY_PROBE_FRAMES=120000`, the survey is byte-identical to the
|
||||||
|
review's and writes rank 11 on frame 19,620, in the gym beside BROCK
|
||||||
|
(`.local/checkpoints/survey-rank11-row59.checkpoint`, untracked). From that checkpoint, with empty
|
||||||
|
ledgers, the base walks the ring again: `GO OBJECTIVE` done on Route 3 509, `GO ROUTE` done on
|
||||||
|
Pewter 508, never on Route 4.
|
||||||
|
|
||||||
|
### The audit: every row of the map graph against the disassembly
|
||||||
|
|
||||||
|
`geography.rs` against `data/maps/headers/*.asm` and `data/maps/objects/*.asm` at `0cd19d3`, all
|
||||||
|
outdoor rows and every `LINKS` pair; then every map on the graph flooded tile by tile (blocks,
|
||||||
|
blockset, collision list, `TilePairCollisionsLand`, `LedgeTiles`, lower-left quadrant as section
|
||||||
|
15 reads it) for pieces, and every connection for a strip that lands on land.
|
||||||
|
|
||||||
|
| # | trap | trigger | test | fix, or why it is left |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 59 | Route 3 / Route 4 in the wrong columns (`[-,-,PEWTER,ROUTE_4]` / `[-,-,ROUTE_3,CERULEAN]`; the headers say Route 4 is **north** of Route 3), and Mt. Moon 1F linked to **Route 3**, which has no warps; B1F's exit to Route 4 missing | any objective past Pewter: Route 3's north edge named no map, so it was nobody's hop | `the_rows_the_disassembly_corrected_say_what_its_headers_say`, `the_road_to_mt_moon_is_not_a_ring_at_the_pewter_end_from_the_badge_checkpoint` (ROM) | **fixed**: rows as `Route3.asm` / `Route4.asm`; `(MT_MOON_1F, ROUTE_4)`, `(MT_MOON_B1F, ROUTE_4)` per `Route4.asm`'s warps (18, 5) and (24, 5) and B1F's (27, 3) |
|
||||||
|
| 59b | Route 14 / 15 and Route 24 / 25 in the wrong columns (south/north for west/east) | Nugget Bridge's far end (rung 16) named no map; Route 14's west edge likewise | the same unit test | **fixed**: `Route14.asm`, `Route15.asm`, `Route24.asm`, `Route25.asm` |
|
||||||
|
| 59c | Route 4 is one node, though Mt. Moon cuts it in two: the cave mouth's side (Pokécenter (11, 5), 1F (18, 5), Route 3) and Cerulean's side (B1F's exit (24, 5), Cerulean) | every hop across Route 4 | `route_4s_sides_are_told_apart_by_the_doors_where_the_ground_cannot`, `route_4s_doors_and_sides_are_the_cartridges_from_the_badge_checkpoint` (ROM) | **fixed**: a `SPLIT` row |
|
||||||
|
| 59d | Mt. Moon B1F and B2F are one node each, though B1F is four chambers of two ladders and B2F three pieces; 1F has three ladders to B1F and only (5, 5) is the road, so a map-level hop sends the fly up dead ends and back | rung 13 (Cerulean) from the first floor | `the_road_from_pewter_to_cerulean_is_through_mt_moon_one_chamber_at_a_time`, `a_door_or_an_edge_lands_in_the_piece_it_opens_onto` | **fixed**: `SPLIT` rows; a piece lists its warps by index, a warp lands in the piece its destination warp is in (`wWarpEntries` byte 2), and the hop is a piece. The fly's own piece is what its walk reaches on the decoded grid, the nearest door where the grid (no ledges) reaches none; flooded over all four maps' ground, exact for every tile |
|
||||||
|
| 59e | four header connections have no tile where both sides are land -- Pallet / Route 21, Cinnabar / Route 20, Route 20 / 19 (sea), Route 22 / 23 (the League's fence) -- and the graph routed along them: from Pallet the road to Cerulean was by sea | whiting out in Mt. Moon, which the survey's fly did: `GO ROUTE` walked into Pallet's shore every 108 frames | `a_connection_nobody_can_walk_across_is_named_and_is_not_a_road` | **fixed**: `NO_CROSSING`: named, no exit, no hop. Route 22 / 23 go back into the table from the same list |
|
||||||
|
| 59f | a trainer's challenge closes onto five frames of plain overworld before `StartTrainerBattle` decides the battle; row 58's pending push-back was written on the first of them | Route 3's first trainer: (11, 6), the one gap between the road's west end and the rest of it, walled for the session | `a_challenge_closing_onto_a_few_frames_of_overworld_is_still_a_challenge` | **fixed**: a push-back is a refusal only after thirty frames running of the fly's overworld; a battle inside them drops it |
|
||||||
|
|
||||||
|
Measured and not the same kind, so left: Cerulean's south and east (Route 5, Route 9, the trashed
|
||||||
|
house's back door) are reached from the town only through the trashed house, which is off the
|
||||||
|
graph, and back over a ledge -- rung 17's road, with the Saffron gates and the Underground Path
|
||||||
|
behind it. Routes 5-8, 10-12, 15, 16, 18 and 23 are pieces joined by gate buildings off the graph,
|
||||||
|
and Rock Tunnel 1F is four pieces (rung 20). And many edges have walkable tiles past the strip
|
||||||
|
that lands on land: a walk aimed at
|
||||||
|
one is blocked and rests the whole edge for the window. On the road through rung 16 these are
|
||||||
|
Route 3's (62, 0) and (63, 0), Route 4's (6, 17) and Cerulean's (0, 13) and (25, 0), all beside the
|
||||||
|
landing tiles a walk from the road reaches first.
|
||||||
|
|
||||||
|
### Before and after
|
||||||
|
|
||||||
|
Route survey from the rank-11 checkpoint, 120,000 frames, `GO OBJECTIVE` and `TALK` preferred:
|
||||||
|
|
||||||
|
| measure | base `7784a9d` | branch |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| `GO OBJECTIVE` done on Route 3 | **509** | 9 |
|
||||||
|
| `GO ROUTE` done on Pewter | **508** | 0 |
|
||||||
|
| Route 4 / Mt. Moon first | never / never | frame 70,356 / 70,707 |
|
||||||
|
| rung at the end | 11 | **12, MT. MOON** |
|
||||||
|
| pushed tiles at the end | (11, 6), (14, 6), (14, 9) on Route 3 | none |
|
||||||
|
|
||||||
|
From the live Route 4 checkpoint, 72,000 frames, rebased on `main` `4d82f7d` (v7):
|
||||||
|
|
||||||
|
| measure | base `4d82f7d` | branch |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| map changes, `GO OBJECTIVE` preferred / uniform | **930 / 706** | 29 / 50 |
|
||||||
|
| `GO OBJECTIVE` done on Route 4, preferred / uniform | **465 / 183** | 3 / 2 |
|
||||||
|
| `GO ROUTE` done on the Pokécenter, preferred / uniform | **462 / 350** | 0 / 0 |
|
||||||
|
| into Mt. Moon, preferred / uniform | frame 541 / 826, and back out | frame 279 / 1,910 |
|
||||||
|
| rung at the end | 12 | 12 |
|
||||||
|
|
||||||
|
`the_fly_goes_into_mt_moon_from_the_live_route_4_checkpoint`, twenty brain minutes on the stub
|
||||||
|
rotation: the base crosses Route 4's west doors 56 times, 18 through the Pokécenter's and 38
|
||||||
|
through the cave's (fails); the branch 13, over three whiteouts and walks back (passes), in the
|
||||||
|
cave on frame 278.
|
||||||
|
|
||||||
|
The same survey from the live rank-11 checkpoint of 2026-09-22 (Pewter, the badge won):
|
||||||
|
the base stays on Route 3 with five pushed tiles fencing it, rank 11; the branch reaches Route 4 at
|
||||||
|
frame 118,621 and Mt. Moon at 118,972, rank 12, nothing pushed.
|
||||||
|
|
||||||
|
The ROM-gated run, `the_road_to_mt_moon_is_not_a_ring_at_the_pewter_end_from_the_badge_checkpoint`,
|
||||||
|
on the stub rotation: the base crosses between Pewter City and Route 3 **3,391** times in 80.4 brain
|
||||||
|
minutes and never stands on Route 4 (fails); the branch crosses 4 times, whites out twice on the
|
||||||
|
way, and is on Route 4 at frame 202,734 and at Mt. Moon's door at 203,232, 56.7 brain minutes
|
||||||
|
(passes). `route_4s_doors_and_sides_are_the_cartridges_from_the_badge_checkpoint` reads Route 4's
|
||||||
|
warp table off the cartridge -- (11, 5) to `$44`, (18, 5) to `$3b`, (24, 5) to `$3c`, the table's
|
||||||
|
doors in its order -- and the decoded grid puts the fly, arrived from Route 3 at (9, 17), on the cave
|
||||||
|
mouth's side.
|
||||||
|
|
||||||
|
**No trap hunt numbers.** Two 30-brain-minute stub arms were started from the badge checkpoint on
|
||||||
|
v0.5.5 with the box at load 35-40, had written nothing after two hours, and were stopped when the
|
||||||
|
live fly reached Route 4 and this row became the live priority; FND-01 has since changed the
|
||||||
|
hunt's frame, so hunts across the rebase would not compare (`9301e39`). The proof is the route
|
||||||
|
survey and the three ROM tests, which is a deviation from the ethos check's letter, recorded.
|
||||||
|
|
||||||
|
### Gates
|
||||||
|
|
||||||
|
- `cargo test --workspace --no-fail-fast` in release with `FLY_ROM`, `FLY_DATASET` and
|
||||||
|
`FLY_BADGE_CHECKPOINT`: 1,285 passed, 1 failed --
|
||||||
|
`flysim::integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`,
|
||||||
|
the known load-sensitive test (feed at 11.28 Hz with the box at load 35-40); the base fails it
|
||||||
|
too at the same load (the boot-time `total` assertion).
|
||||||
|
- `cargo clippy --workspace --all-targets`: **0 warnings**.
|
||||||
|
- `npm test` 663 passed; `npm run typecheck` clean.
|
||||||
|
- `infra/tests/lint.sh`: ALL CHECKS PASSED, the de-PII guard included.
|
||||||
|
- `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had:
|
||||||
|
648 bytes, `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68` on
|
||||||
|
`4d82f7d` (v7). Decoder, reward catalog, adapter version and roles untouched.
|
||||||
|
|
|
||||||
|
|
@ -349,10 +349,9 @@ impl MacroPalette for PokemonPalette {
|
||||||
// How far the objective is, over the same map graph `GO OBJECTIVE` walks (section
|
// How far the objective is, over the same map graph `GO OBJECTIVE` walks (section
|
||||||
// 12.15). Read from the same frame and the same state everything else is, and only
|
// 12.15). Read from the same frame and the same state everything else is, and only
|
||||||
// where the fly is its own master, for the same reason the ground is.
|
// where the fly is its own master, for the same reason the ground is.
|
||||||
let approach = standing.and_then(|player| {
|
let approach = standing.and_then(|_| {
|
||||||
let objective = palette::objective_place(&mut state)?;
|
let objective = palette::objective_place(&mut state)?;
|
||||||
let hops =
|
let hops = geography::hops(palette::region_here(&mut state)?, objective.map)?;
|
||||||
geography::hops(geography::region_at(player.map, player.y), objective.map)?;
|
|
||||||
Some((objective.map, hops))
|
Some((objective.map, hops))
|
||||||
});
|
});
|
||||||
*cached = Some(palette);
|
*cached = Some(palette);
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,12 @@ const ANSWER_REOPEN_FRAMES: u32 = 24;
|
||||||
/// animation is about three.
|
/// animation is about three.
|
||||||
const HEAL_WAIT_FRAMES: u32 = 360;
|
const HEAL_WAIT_FRAMES: u32 = 360;
|
||||||
|
|
||||||
|
/// Frames running the overworld has to be the fly's before a push-back is written as a refusal
|
||||||
|
/// ([`MacroMachine::observe_push`], row 59). A trainer's challenge text closes onto five frames of
|
||||||
|
/// overworld before the battle is decided; six times that is still half a second, and a refusal
|
||||||
|
/// the cartridge really made loses nothing by being written half a second late.
|
||||||
|
pub const PUSH_SETTLE_FRAMES: u32 = 30;
|
||||||
|
|
||||||
/// How a macro ended, i.e. the `outcome` field of the `macro` feed event (section 5: "outcome =
|
/// How a macro ended, i.e. the `outcome` field of the `macro` feed event (section 5: "outcome =
|
||||||
/// done/blocked/timeout/refused").
|
/// done/blocked/timeout/refused").
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -662,6 +668,9 @@ pub struct MacroMachine {
|
||||||
/// the overworld is a refusal and is written as one; a battle is a battle, and nothing about
|
/// the overworld is a refusal and is written as one; a battle is a battle, and nothing about
|
||||||
/// the target or the ground is learned from it.
|
/// the target or the ground is learned from it.
|
||||||
pending_push: Vec<PendingPush>,
|
pending_push: Vec<PendingPush>,
|
||||||
|
/// How many frames running the overworld has been the fly's while [`Self::pending_push`]
|
||||||
|
/// waits: the push-back is decided at [`PUSH_SETTLE_FRAMES`] (row 59).
|
||||||
|
pending_push_calm: u32,
|
||||||
/// A finished `TALK`'s target, waiting to be taken into the session's talked ledger.
|
/// A finished `TALK`'s target, waiting to be taken into the session's talked ledger.
|
||||||
///
|
///
|
||||||
/// The machine records rather than keeps: the ledger is the driver's
|
/// The machine records rather than keeps: the ledger is the driver's
|
||||||
|
|
@ -696,6 +705,7 @@ impl MacroMachine {
|
||||||
pending_talk: None,
|
pending_talk: None,
|
||||||
pending_answer: None,
|
pending_answer: None,
|
||||||
pending_push: Vec::new(),
|
pending_push: Vec::new(),
|
||||||
|
pending_push_calm: 0,
|
||||||
talked: None,
|
talked: None,
|
||||||
rng: if seed == 0 { 1 } else { seed },
|
rng: if seed == 0 { 1 } else { seed },
|
||||||
}
|
}
|
||||||
|
|
@ -979,6 +989,7 @@ impl MacroMachine {
|
||||||
self.pending_answer = None;
|
self.pending_answer = None;
|
||||||
// Nor the cartridge refusing a step: the frames it happened in are being thrown away too.
|
// Nor the cartridge refusing a step: the frames it happened in are being thrown away too.
|
||||||
self.pending_push.clear();
|
self.pending_push.clear();
|
||||||
|
self.pending_push_calm = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the fly is standing somewhere other than where the running macro began.
|
/// Whether the fly is standing somewhere other than where the running macro began.
|
||||||
|
|
@ -1036,17 +1047,32 @@ impl MacroMachine {
|
||||||
|
|
||||||
/// One frame after the cartridge took the joypad from a macro: decide what it was (row 58).
|
/// One frame after the cartridge took the joypad from a macro: decide what it was (row 58).
|
||||||
///
|
///
|
||||||
/// Back in the overworld with the buttons the fly's again: a refusal, written exactly as
|
/// Back in the overworld with the buttons the fly's again, for [`PUSH_SETTLE_FRAMES`] running:
|
||||||
/// section 12.4 and row 37 always wrote it. A battle: a trainer's challenge, and it teaches the
|
/// a refusal, written exactly as section 12.4 and row 37 always wrote it. A battle: a
|
||||||
/// ledgers nothing. Anything else -- the text, the walk, the frames between -- is still the
|
/// trainer's challenge, and it teaches the ledgers nothing. Anything else -- the text, the
|
||||||
/// cartridge's, and the decision waits.
|
/// walk, the frames between -- is still the cartridge's, and the decision waits.
|
||||||
|
///
|
||||||
|
/// The window is row 59's. A trainer's challenge text closes onto five frames of an ordinary
|
||||||
|
/// overworld -- no text, no script, no joypad bit, `wCurOpponent` still clear -- before
|
||||||
|
/// `StartTrainerBattle` runs (`home/trainers.asm`: it follows `DisplayTextID`, whose
|
||||||
|
/// close-down redraws the map first). Decided on the first of them, Route 3's first trainer
|
||||||
|
/// walled (11, 6), the one gap between the road's west end and the rest of it, for the
|
||||||
|
/// session, and the fly walked between Pewter City and that end for hours.
|
||||||
fn observe_push(&mut self, state: &mut dyn MacroState) {
|
fn observe_push(&mut self, state: &mut dyn MacroState) {
|
||||||
if self.pending_push.is_empty() {
|
if self.pending_push.is_empty() {
|
||||||
|
self.pending_push_calm = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match class(state.scene()) {
|
match class(state.scene()) {
|
||||||
Class::Battle | Class::ForcedSwitch => self.pending_push.clear(),
|
Class::Battle | Class::ForcedSwitch => {
|
||||||
|
self.pending_push.clear();
|
||||||
|
self.pending_push_calm = 0;
|
||||||
|
}
|
||||||
|
Class::Overworld if self.pending_push_calm + 1 < PUSH_SETTLE_FRAMES => {
|
||||||
|
self.pending_push_calm += 1;
|
||||||
|
}
|
||||||
Class::Overworld => {
|
Class::Overworld => {
|
||||||
|
self.pending_push_calm = 0;
|
||||||
// Every macro the script ended while it held the joypad -- the walk it interrupted
|
// Every macro the script ended while it held the joypad -- the walk it interrupted
|
||||||
// and any press made into its text -- in the order they ended.
|
// and any press made into its text -- in the order they ended.
|
||||||
for pending in std::mem::take(&mut self.pending_push) {
|
for pending in std::mem::take(&mut self.pending_push) {
|
||||||
|
|
@ -1058,7 +1084,7 @@ impl MacroMachine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => self.pending_push_calm = 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
|
||||||
use super::super::maps;
|
use super::super::maps;
|
||||||
use super::cartridge::Edge;
|
use super::cartridge::Edge;
|
||||||
|
use super::state::MapGrid;
|
||||||
|
|
||||||
/// A column with no connection on it.
|
/// A column with no connection on it.
|
||||||
const NONE: u8 = 0xff;
|
const NONE: u8 = 0xff;
|
||||||
|
|
@ -44,10 +45,13 @@ const EAST: usize = 3;
|
||||||
/// `wCurMapConnections`' four bits are loaded from. Kanto's overworld is one grid, so the table is
|
/// `wCurMapConnections`' four bits are loaded from. Kanto's overworld is one grid, so the table is
|
||||||
/// symmetric by construction and [`neighbours`] does not rely on that — it reads both directions.
|
/// symmetric by construction and [`neighbours`] does not rely on that — it reads both directions.
|
||||||
///
|
///
|
||||||
/// Two rows are worth a note. `ROUTE_3` and `ROUTE_4` are connected along the east-west axis even
|
/// Every row is the header's own, checked line by line against the disassembly at the pinned
|
||||||
/// though Mt. Moon stands between them, so the walkable path is the cave and not the edge; that
|
/// commit (row 59). Four pairs had the right neighbour in the wrong column, and a wrong column is
|
||||||
/// costs nothing, because an edge whose tiles are not walkable produces no exit at all
|
/// a wrong map on the other side of an edge: `ROUTE_3` / `ROUTE_4` (Route 4 is north of Route 3,
|
||||||
/// ([`super::path::exits`] filters on the walkable predicate) and the cave is in [`LINKS`].
|
/// not east, and Route 3's top edge is the road to Mt. Moon's Pokécenter), `ROUTE_14` /
|
||||||
|
/// `ROUTE_15` and `ROUTE_24` / `ROUTE_25` (west and east, not south and north), and `ROUTE_22` /
|
||||||
|
/// `ROUTE_23`, which the table had left out. A connection nobody can walk across is still the
|
||||||
|
/// header's, and [`NO_CROSSING`] says which.
|
||||||
const CONNECTIONS: &[(u8, [u8; 4])] = &[
|
const CONNECTIONS: &[(u8, [u8; 4])] = &[
|
||||||
(maps::PALLET_TOWN, [maps::ROUTE_1, maps::ROUTE_21, NONE, NONE]),
|
(maps::PALLET_TOWN, [maps::ROUTE_1, maps::ROUTE_21, NONE, NONE]),
|
||||||
(maps::VIRIDIAN_CITY, [maps::ROUTE_2, maps::ROUTE_1, maps::ROUTE_22, NONE]),
|
(maps::VIRIDIAN_CITY, [maps::ROUTE_2, maps::ROUTE_1, maps::ROUTE_22, NONE]),
|
||||||
|
|
@ -62,8 +66,8 @@ const CONNECTIONS: &[(u8, [u8; 4])] = &[
|
||||||
(maps::SAFFRON_CITY, [maps::ROUTE_5, maps::ROUTE_6, maps::ROUTE_7, maps::ROUTE_8]),
|
(maps::SAFFRON_CITY, [maps::ROUTE_5, maps::ROUTE_6, maps::ROUTE_7, maps::ROUTE_8]),
|
||||||
(maps::ROUTE_1, [maps::VIRIDIAN_CITY, maps::PALLET_TOWN, NONE, NONE]),
|
(maps::ROUTE_1, [maps::VIRIDIAN_CITY, maps::PALLET_TOWN, NONE, NONE]),
|
||||||
(maps::ROUTE_2, [maps::PEWTER_CITY, maps::VIRIDIAN_CITY, NONE, NONE]),
|
(maps::ROUTE_2, [maps::PEWTER_CITY, maps::VIRIDIAN_CITY, NONE, NONE]),
|
||||||
(maps::ROUTE_3, [NONE, NONE, maps::PEWTER_CITY, maps::ROUTE_4]),
|
(maps::ROUTE_3, [maps::ROUTE_4, NONE, maps::PEWTER_CITY, NONE]),
|
||||||
(maps::ROUTE_4, [NONE, NONE, maps::ROUTE_3, maps::CERULEAN_CITY]),
|
(maps::ROUTE_4, [NONE, maps::ROUTE_3, NONE, maps::CERULEAN_CITY]),
|
||||||
(maps::ROUTE_5, [maps::CERULEAN_CITY, maps::SAFFRON_CITY, NONE, NONE]),
|
(maps::ROUTE_5, [maps::CERULEAN_CITY, maps::SAFFRON_CITY, NONE, NONE]),
|
||||||
(maps::ROUTE_6, [maps::SAFFRON_CITY, maps::VERMILION_CITY, NONE, NONE]),
|
(maps::ROUTE_6, [maps::SAFFRON_CITY, maps::VERMILION_CITY, NONE, NONE]),
|
||||||
(maps::ROUTE_7, [NONE, NONE, maps::CELADON_CITY, maps::SAFFRON_CITY]),
|
(maps::ROUTE_7, [NONE, NONE, maps::CELADON_CITY, maps::SAFFRON_CITY]),
|
||||||
|
|
@ -73,28 +77,54 @@ const CONNECTIONS: &[(u8, [u8; 4])] = &[
|
||||||
(maps::ROUTE_11, [NONE, NONE, maps::VERMILION_CITY, maps::ROUTE_12]),
|
(maps::ROUTE_11, [NONE, NONE, maps::VERMILION_CITY, maps::ROUTE_12]),
|
||||||
(maps::ROUTE_12, [maps::LAVENDER_TOWN, maps::ROUTE_13, maps::ROUTE_11, NONE]),
|
(maps::ROUTE_12, [maps::LAVENDER_TOWN, maps::ROUTE_13, maps::ROUTE_11, NONE]),
|
||||||
(maps::ROUTE_13, [maps::ROUTE_12, NONE, maps::ROUTE_14, NONE]),
|
(maps::ROUTE_13, [maps::ROUTE_12, NONE, maps::ROUTE_14, NONE]),
|
||||||
(maps::ROUTE_14, [NONE, maps::ROUTE_15, NONE, maps::ROUTE_13]),
|
(maps::ROUTE_14, [NONE, NONE, maps::ROUTE_15, maps::ROUTE_13]),
|
||||||
(maps::ROUTE_15, [maps::ROUTE_14, NONE, maps::FUCHSIA_CITY, NONE]),
|
(maps::ROUTE_15, [NONE, NONE, maps::FUCHSIA_CITY, maps::ROUTE_14]),
|
||||||
(maps::ROUTE_16, [NONE, maps::ROUTE_17, NONE, maps::CELADON_CITY]),
|
(maps::ROUTE_16, [NONE, maps::ROUTE_17, NONE, maps::CELADON_CITY]),
|
||||||
(maps::ROUTE_17, [maps::ROUTE_16, maps::ROUTE_18, NONE, NONE]),
|
(maps::ROUTE_17, [maps::ROUTE_16, maps::ROUTE_18, NONE, NONE]),
|
||||||
(maps::ROUTE_18, [maps::ROUTE_17, NONE, NONE, maps::FUCHSIA_CITY]),
|
(maps::ROUTE_18, [maps::ROUTE_17, NONE, NONE, maps::FUCHSIA_CITY]),
|
||||||
(maps::ROUTE_19, [maps::FUCHSIA_CITY, NONE, maps::ROUTE_20, NONE]),
|
(maps::ROUTE_19, [maps::FUCHSIA_CITY, NONE, maps::ROUTE_20, NONE]),
|
||||||
(maps::ROUTE_20, [NONE, NONE, maps::CINNABAR_ISLAND, maps::ROUTE_19]),
|
(maps::ROUTE_20, [NONE, NONE, maps::CINNABAR_ISLAND, maps::ROUTE_19]),
|
||||||
(maps::ROUTE_21, [maps::PALLET_TOWN, maps::CINNABAR_ISLAND, NONE, NONE]),
|
(maps::ROUTE_21, [maps::PALLET_TOWN, maps::CINNABAR_ISLAND, NONE, NONE]),
|
||||||
// Route 22 ends at the League gate, which is a building rather than an edge, and this
|
(maps::ROUTE_22, [maps::ROUTE_23, NONE, NONE, maps::VIRIDIAN_CITY]),
|
||||||
// table has no id for it: Indigo Plateau is on the graph but not reachable from the south.
|
(maps::ROUTE_23, [maps::INDIGO_PLATEAU, maps::ROUTE_22, NONE, NONE]),
|
||||||
(maps::ROUTE_22, [NONE, NONE, NONE, maps::VIRIDIAN_CITY]),
|
(maps::ROUTE_24, [NONE, maps::CERULEAN_CITY, NONE, maps::ROUTE_25]),
|
||||||
(maps::ROUTE_23, [maps::INDIGO_PLATEAU, NONE, NONE, NONE]),
|
(maps::ROUTE_25, [NONE, NONE, maps::ROUTE_24, NONE]),
|
||||||
(maps::ROUTE_24, [maps::ROUTE_25, maps::CERULEAN_CITY, NONE, NONE]),
|
|
||||||
(maps::ROUTE_25, [NONE, maps::ROUTE_24, NONE, NONE]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Connections in the headers that no step on foot crosses, from both sides.
|
||||||
|
///
|
||||||
|
/// Row 59, measured from the disassembly: for every connection, the tiles of this map's edge that
|
||||||
|
/// are walkable *and* land on a walkable tile of the other map's strip (the header's offset, the
|
||||||
|
/// other map's blocks and collision list). These eight have none. Pallet Town's south edge has
|
||||||
|
/// two walkable tiles and Route 21 is water under both; Cinnabar's east edge and Route 20's two
|
||||||
|
/// ends are sea; Route 22's north edge is the League's fence, and the road is its gate, a building
|
||||||
|
/// the graph has no row for. The map on the other side is still named ([`connected`]), and a
|
||||||
|
/// surfer's road is for a later row; on foot none of them is a way out
|
||||||
|
/// ([`super::path::exits`] offers no exit on them) or a road ([`neighbours`] leaves them out).
|
||||||
|
/// Without this the road from Pallet Town to Cerulean was by sea, and a fly that whited out in
|
||||||
|
/// Mt. Moon walked into Pallet's shore once every two seconds.
|
||||||
|
const NO_CROSSING: &[(u8, Edge)] = &[
|
||||||
|
(maps::PALLET_TOWN, Edge::South),
|
||||||
|
(maps::ROUTE_21, Edge::North),
|
||||||
|
(maps::CINNABAR_ISLAND, Edge::East),
|
||||||
|
(maps::ROUTE_20, Edge::West),
|
||||||
|
(maps::ROUTE_20, Edge::East),
|
||||||
|
(maps::ROUTE_19, Edge::West),
|
||||||
|
(maps::ROUTE_22, Edge::North),
|
||||||
|
(maps::ROUTE_23, Edge::South),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Whether a step off `map`'s `edge` can land on the other map on foot ([`NO_CROSSING`]).
|
||||||
|
pub fn crossable(map: u8, edge: Edge) -> bool {
|
||||||
|
!NO_CROSSING.contains(&(map, edge))
|
||||||
|
}
|
||||||
|
|
||||||
/// Doors and floor changes, as undirected pairs of maps.
|
/// Doors and floor changes, as undirected pairs of maps.
|
||||||
///
|
///
|
||||||
/// Only the ones a rung place needs a route through, because that is all [`next_hop`] is for: an
|
/// Only the ones a rung place needs a route through, because that is all [`next_hop`] is for: an
|
||||||
/// unlisted building is simply not on the graph, which makes it a place `GO OBJECTIVE` cannot aim
|
/// unlisted building is simply not on the graph, which makes it a place `GO OBJECTIVE` cannot aim
|
||||||
/// at from another map and changes nothing else. A cave with two mouths appears twice, which is
|
/// at from another map and changes nothing else. Every pair is two warp tables that name each
|
||||||
/// what makes Mt. Moon a way from Route 3 to Route 4.
|
/// other (a `LAST_MAP` door resolved to the one outdoor map whose warps lead in).
|
||||||
const LINKS: &[(u8, u8)] = &[
|
const LINKS: &[(u8, u8)] = &[
|
||||||
(maps::REDS_HOUSE_1F, maps::PALLET_TOWN),
|
(maps::REDS_HOUSE_1F, maps::PALLET_TOWN),
|
||||||
(maps::REDS_HOUSE_2F, maps::REDS_HOUSE_1F),
|
(maps::REDS_HOUSE_2F, maps::REDS_HOUSE_1F),
|
||||||
|
|
@ -122,10 +152,14 @@ const LINKS: &[(u8, u8)] = &[
|
||||||
(maps::PEWTER_MUSEUM_2F, maps::PEWTER_MUSEUM_1F),
|
(maps::PEWTER_MUSEUM_2F, maps::PEWTER_MUSEUM_1F),
|
||||||
(maps::PEWTER_MART, maps::PEWTER_CITY),
|
(maps::PEWTER_MART, maps::PEWTER_CITY),
|
||||||
(maps::PEWTER_POKECENTER, maps::PEWTER_CITY),
|
(maps::PEWTER_POKECENTER, maps::PEWTER_CITY),
|
||||||
(maps::MT_MOON_1F, maps::ROUTE_3),
|
// Mt. Moon has two mouths, and both are on Route 4 (`data/maps/objects/Route4.asm`): (18, 5)
|
||||||
|
// into the first floor, and (24, 5) into B1F, whose (27, 3) is the way back out on the far
|
||||||
|
// side of the mountain. Route 3 has no warps at all. Which chamber of B1F and B2F each ladder
|
||||||
|
// opens onto is [`SPLIT`]'s business.
|
||||||
(maps::MT_MOON_1F, maps::ROUTE_4),
|
(maps::MT_MOON_1F, maps::ROUTE_4),
|
||||||
(maps::MT_MOON_1F, maps::MT_MOON_B1F),
|
(maps::MT_MOON_1F, maps::MT_MOON_B1F),
|
||||||
(maps::MT_MOON_B1F, maps::MT_MOON_B2F),
|
(maps::MT_MOON_B1F, maps::MT_MOON_B2F),
|
||||||
|
(maps::MT_MOON_B1F, maps::ROUTE_4),
|
||||||
(maps::CERULEAN_GYM, maps::CERULEAN_CITY),
|
(maps::CERULEAN_GYM, maps::CERULEAN_CITY),
|
||||||
(maps::CERULEAN_MART, maps::CERULEAN_CITY),
|
(maps::CERULEAN_MART, maps::CERULEAN_CITY),
|
||||||
(maps::CERULEAN_POKECENTER, maps::CERULEAN_CITY),
|
(maps::CERULEAN_POKECENTER, maps::CERULEAN_CITY),
|
||||||
|
|
@ -141,11 +175,12 @@ const LINKS: &[(u8, u8)] = &[
|
||||||
/// the forest's south gate; the north half touches Pewter City and the forest's north gate; the
|
/// the forest's south gate; the north half touches Pewter City and the forest's north gate; the
|
||||||
/// belt of trees between them needs CUT. A graph with one node for it answered "Pewter is two
|
/// belt of trees between them needs CUT. A graph with one node for it answered "Pewter is two
|
||||||
/// hops from the south gate, south" — which is a road that does not exist — and sent the fly back
|
/// hops from the south gate, south" — which is a road that does not exist — and sent the fly back
|
||||||
/// out of the gate it had just walked into, once per hold, for four hours.
|
/// out of the gate it had just walked into, once per hold, for four hours. Row 59 found three more
|
||||||
|
/// on the road to Cerulean: Route 4, and Mt. Moon's two lower floors ([`SPLIT`]).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Region {
|
pub struct Region {
|
||||||
pub map: u8,
|
pub map: u8,
|
||||||
/// Which piece, for a map [`SPLIT`] has a row for; 0 everywhere else.
|
/// Which piece, for a map [`SPLIT`] has a row for: its index in that row. 0 everywhere else.
|
||||||
pub part: u8,
|
pub part: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,108 +189,268 @@ impl Region {
|
||||||
pub const fn whole(map: u8) -> Self {
|
pub const fn whole(map: u8) -> Self {
|
||||||
Self { map, part: 0 }
|
Self { map, part: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Piece `part` of a map [`SPLIT`] has a row for.
|
||||||
|
pub const fn piece(map: u8, part: u8) -> Self {
|
||||||
|
Self { map, part }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`SPLIT`]'s two piece numbers.
|
/// One piece of a split map.
|
||||||
const NORTH_PIECE: u8 = 0;
|
struct Piece {
|
||||||
const SOUTH_PIECE: u8 = 1;
|
/// The map's own warps that stand on this piece's ground: the index into its warp table (as
|
||||||
|
/// `wWarpEntries` holds it, and as a warp elsewhere names it for its destination, 0-based) and
|
||||||
|
/// the tile. A warp that lands on one of these lands in this piece; the tiles are what the
|
||||||
|
/// fly's own piece is told apart by ([`region_on`]).
|
||||||
|
doors: &'static [(u8, u8, u8)],
|
||||||
|
/// Everything one step from this piece: a whole map, or a piece of another split map. Their
|
||||||
|
/// maps together are exactly [`neighbours`]'s answer for the map, and every step is listed
|
||||||
|
/// back from the other side; [`tests::a_split_maps_pieces_add_up_and_answer_each_other`] pins
|
||||||
|
/// both.
|
||||||
|
next: &'static [Region],
|
||||||
|
}
|
||||||
|
|
||||||
/// A map whose walkable ground is in two pieces, and which of its neighbours each piece touches.
|
/// A map whose walkable ground is in pieces the player cannot walk between.
|
||||||
struct Split {
|
struct Split {
|
||||||
map: u8,
|
map: u8,
|
||||||
/// The tile rows each piece's own doorway is on, measured from the cartridge: a tile belongs
|
pieces: &'static [Piece],
|
||||||
/// to the piece whose row it is nearer to. Anchoring on the doorways rather than on a row in
|
|
||||||
/// the middle means the number comes from the warp table rather than from a claim about where
|
|
||||||
/// the trees are, and a tile in the impassable belt between them — ground the fly cannot
|
|
||||||
/// stand on — is the only place the answer could be wrong.
|
|
||||||
north_door: u8,
|
|
||||||
south_door: u8,
|
|
||||||
/// The neighbours reachable from each piece. Together they are exactly [`neighbours`]'s answer
|
|
||||||
/// for the map, which [`tests::a_split_maps_pieces_divide_its_neighbours_between_them`] pins.
|
|
||||||
north: &'static [u8],
|
|
||||||
south: &'static [u8],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every map whose ground is in two pieces. One row, and it took four hours of stream to find.
|
/// Route 2's two halves, in [`SPLIT`]'s order.
|
||||||
|
#[cfg(test)]
|
||||||
|
const NORTH_PIECE: u8 = 0;
|
||||||
|
#[cfg(test)]
|
||||||
|
const SOUTH_PIECE: u8 = 1;
|
||||||
|
|
||||||
|
/// Route 4's two sides of the mountain.
|
||||||
|
#[cfg(test)]
|
||||||
|
const WEST_SIDE: u8 = 0;
|
||||||
|
const EAST_SIDE: u8 = 1;
|
||||||
|
|
||||||
|
/// Mt. Moon B1F's four chambers, named by what they hold.
|
||||||
|
const B1F_EXIT: u8 = 0;
|
||||||
|
const B1F_WEST: u8 = 1;
|
||||||
|
const B1F_MIDDLE: u8 = 2;
|
||||||
|
const B1F_SOUTH: u8 = 3;
|
||||||
|
|
||||||
|
/// Mt. Moon B2F's three pieces.
|
||||||
|
const B2F_MAIN: u8 = 0;
|
||||||
|
const B2F_NORTH: u8 = 1;
|
||||||
|
const B2F_SOUTH: u8 = 2;
|
||||||
|
|
||||||
|
/// Every map whose ground is in pieces, with each piece's doors and neighbours.
|
||||||
///
|
///
|
||||||
/// `ROUTE_2`, surveyed from the cartridge on 2026-09-17 (`docs/design/macros-wram.md`'s method,
|
/// Every row is measured from the disassembly at the pinned commit: the map's blocks, its
|
||||||
/// the run recorded in `infra/docs/macros-traps.md` row 33). The map is 20 by 72 and its warp
|
/// tileset's blockset and collision list, the tile-pair walls and the ledges, flooded tile by tile
|
||||||
/// table reads:
|
/// (`infra/docs/macros-traps.md` row 59 has the method). A map is listed here only when two of its
|
||||||
|
/// ways out are on different pieces, and the audit ran over every map on the graph.
|
||||||
///
|
///
|
||||||
/// | warp | tile | into |
|
/// - **`ROUTE_2`** (row 33): the forest's north gate at (3, 11) and Pewter's edge; the south gate
|
||||||
/// | ---: | --- | --- |
|
/// at (3, 43) and Viridian's. Maps 46, 48 and 49 stay off the graph, the module's standing rule
|
||||||
/// | 0 | (12, 9) | `DIGLETTS_CAVE_ROUTE_2` (46) |
|
/// for a building no rung place needs a route through: 49's two doors are both Route 2's own.
|
||||||
/// | 1 | (3, 11) | `VIRIDIAN_FOREST_NORTH_GATE` (47) |
|
/// - **`ROUTE_4`** (row 59): Mt. Moon stands across it. The west side holds the Pokécenter at
|
||||||
/// | 2 | (15, 19) | `ROUTE_2_TRADE_HOUSE` (48) |
|
/// (11, 5), the cave mouth at (18, 5) and the road down to Route 3; the east side holds B1F's
|
||||||
/// | 3 | (16, 35) | `ROUTE_2_GATE` (49) |
|
/// exit at (24, 5) and the ledges down to Cerulean. From the Pewter side the only way east is
|
||||||
/// | 4 | (15, 39) | `ROUTE_2_GATE` (49) |
|
/// through the mountain.
|
||||||
/// | 5 | (3, 43) | `VIRIDIAN_FOREST_SOUTH_GATE` (50) |
|
/// - **`MT_MOON_B1F`** (row 59): four chambers, each two ladders and nothing between them. The
|
||||||
///
|
/// one road through is 1F (5, 5) to the west chamber, (21, 17) down to B2F, B2F (5, 7) up to the
|
||||||
/// with `north: true` and `south: true` in `wCurMapConnections` — Pewter off the top row, Viridian
|
/// exit chamber, (27, 3) out onto Route 4's east side. The middle and south chambers are ladders
|
||||||
/// off the bottom one. The two forest gates at rows 11 and 43 are the doorways this splits on.
|
/// to dead ends on B2F.
|
||||||
///
|
/// - **`MT_MOON_B2F`** (row 59): the fossil floor, one large piece with the two ladders the road
|
||||||
/// Maps 46, 48 and 49 are deliberately *not* on the graph, which is this module's standing rule
|
/// uses, and two small pieces under the dead-end ladders.
|
||||||
/// for a building no rung place needs a route through: 49's two doors are both warps of `ROUTE_2`
|
const SPLIT: &[Split] = &[
|
||||||
/// itself, so it is a shortcut within one map rather than a way between two, and 46 and 48 are
|
Split {
|
||||||
/// ends of the line. A route the table does not carry is simply not offered; nothing is guessed.
|
map: maps::ROUTE_2,
|
||||||
const SPLIT: &[Split] = &[Split {
|
pieces: &[
|
||||||
map: maps::ROUTE_2,
|
Piece {
|
||||||
north_door: 11,
|
doors: &[(1, 3, 11)],
|
||||||
south_door: 43,
|
next: &[
|
||||||
north: &[maps::PEWTER_CITY, maps::VIRIDIAN_FOREST_NORTH_GATE],
|
Region::whole(maps::PEWTER_CITY),
|
||||||
south: &[maps::VIRIDIAN_CITY, maps::VIRIDIAN_FOREST_SOUTH_GATE],
|
Region::whole(maps::VIRIDIAN_FOREST_NORTH_GATE),
|
||||||
}];
|
],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(5, 3, 43)],
|
||||||
|
next: &[
|
||||||
|
Region::whole(maps::VIRIDIAN_CITY),
|
||||||
|
Region::whole(maps::VIRIDIAN_FOREST_SOUTH_GATE),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Split {
|
||||||
|
map: maps::ROUTE_4,
|
||||||
|
pieces: &[
|
||||||
|
Piece {
|
||||||
|
doors: &[(0, 11, 5), (1, 18, 5)],
|
||||||
|
next: &[Region::whole(maps::ROUTE_3), Region::whole(maps::MT_MOON_1F)],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(2, 24, 5)],
|
||||||
|
next: &[
|
||||||
|
Region::piece(maps::MT_MOON_B1F, B1F_EXIT),
|
||||||
|
Region::whole(maps::CERULEAN_CITY),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Split {
|
||||||
|
map: maps::MT_MOON_B1F,
|
||||||
|
pieces: &[
|
||||||
|
Piece {
|
||||||
|
doors: &[(6, 23, 3), (7, 27, 3)],
|
||||||
|
next: &[
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_MAIN),
|
||||||
|
Region::piece(maps::ROUTE_4, EAST_SIDE),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(0, 5, 5), (4, 21, 17)],
|
||||||
|
next: &[
|
||||||
|
Region::whole(maps::MT_MOON_1F),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_MAIN),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(1, 17, 11), (2, 25, 9)],
|
||||||
|
next: &[
|
||||||
|
Region::whole(maps::MT_MOON_1F),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_NORTH),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(3, 25, 15), (5, 13, 27)],
|
||||||
|
next: &[
|
||||||
|
Region::whole(maps::MT_MOON_1F),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_SOUTH),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Split {
|
||||||
|
map: maps::MT_MOON_B2F,
|
||||||
|
pieces: &[
|
||||||
|
Piece {
|
||||||
|
doors: &[(1, 21, 17), (3, 5, 7)],
|
||||||
|
next: &[
|
||||||
|
Region::piece(maps::MT_MOON_B1F, B1F_EXIT),
|
||||||
|
Region::piece(maps::MT_MOON_B1F, B1F_WEST),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(0, 25, 9)],
|
||||||
|
next: &[Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE)],
|
||||||
|
},
|
||||||
|
Piece {
|
||||||
|
doors: &[(2, 15, 27)],
|
||||||
|
next: &[Region::piece(maps::MT_MOON_B1F, B1F_SOUTH)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
fn split_of(map: u8) -> Option<&'static Split> {
|
fn split_of(map: u8) -> Option<&'static Split> {
|
||||||
SPLIT.iter().find(|split| split.map == map)
|
SPLIT.iter().find(|split| split.map == map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The piece of `map` a tile on row `y` is in.
|
/// The pieces of a split map with their numbers, which are their [`Region::part`]s.
|
||||||
///
|
fn pieces(split: &'static Split) -> impl Iterator<Item = (u8, &'static Piece)> {
|
||||||
/// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. Callers pass the player's own
|
split.pieces.iter().enumerate().filter_map(|(part, piece)| Some((u8::try_from(part).ok()?, piece)))
|
||||||
/// row, which is the only thing that can tell the two halves of `ROUTE_2` apart.
|
|
||||||
pub fn region_at(map: u8, y: u8) -> Region {
|
|
||||||
match split_of(map) {
|
|
||||||
None => Region::whole(map),
|
|
||||||
Some(split) => {
|
|
||||||
let north = y.abs_diff(split.north_door);
|
|
||||||
let south = y.abs_diff(split.south_door);
|
|
||||||
Region { map, part: if north <= south { NORTH_PIECE } else { SOUTH_PIECE } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The piece of `map` that `from` opens onto, or `None` when no piece of it touches `from`.
|
/// The piece of `map` the tile `(x, y)` is in, by the doors alone.
|
||||||
///
|
///
|
||||||
/// This is the reverse of [`region_at`] and it needs no tile: a door or an edge is listed under
|
/// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. On a split map it is the piece
|
||||||
/// exactly one piece, so "which half of Route 2 does the north gate open onto" is a table lookup.
|
/// with the nearest door, counting tiles across and down, the first piece on a tie. That is exact
|
||||||
/// `None` is an edge the graph does not have -- the south half of Route 2 is not reachable from
|
/// for every tile of Route 2's and Route 4's ground, which is where the grid cannot answer
|
||||||
/// Pewter City, whatever the map ids alone would suggest.
|
/// ([`region_on`]: a ledge is a one-way step the grid does not model), and it is only the fallback
|
||||||
fn region_toward(map: u8, from: u8) -> Option<Region> {
|
/// on Mt. Moon's floors, whose chambers wrap round each other.
|
||||||
match split_of(map) {
|
pub fn region_at(map: u8, x: u8, y: u8) -> Region {
|
||||||
None => Some(Region::whole(map)),
|
let Some(split) = split_of(map) else { return Region::whole(map) };
|
||||||
Some(split) => {
|
let distance = |piece: &Piece| {
|
||||||
if split.north.contains(&from) {
|
piece
|
||||||
Some(Region { map, part: NORTH_PIECE })
|
.doors
|
||||||
} else if split.south.contains(&from) {
|
.iter()
|
||||||
Some(Region { map, part: SOUTH_PIECE })
|
.map(|(_, dx, dy)| u16::from(x.abs_diff(*dx)) + u16::from(y.abs_diff(*dy)))
|
||||||
} else {
|
.min()
|
||||||
None
|
.unwrap_or(u16::MAX)
|
||||||
}
|
};
|
||||||
|
let part = pieces(split).min_by_key(|(part, piece)| (distance(piece), *part)).map_or(0, |(part, _)| part);
|
||||||
|
Region { map, part }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The piece of `map` the fly standing on `(x, y)` is in.
|
||||||
|
///
|
||||||
|
/// The ground decides: with the decoded map grid (section 15) the piece is the one whose doors a
|
||||||
|
/// walk from here can reach, when exactly one piece's can. The grid has no ledges -- a ledge is a
|
||||||
|
/// step one way only, and the grid reads it as a wall -- so on the part of Route 4 below the
|
||||||
|
/// ledges no door is reachable and [`region_at`] answers from the doors. Row 59 flooded every tile
|
||||||
|
/// of every piece's ground in the disassembly under this rule and it names the right piece for
|
||||||
|
/// all of them.
|
||||||
|
pub fn region_on(map: u8, x: u8, y: u8, grid: Option<&MapGrid>) -> Region {
|
||||||
|
let Some(split) = split_of(map) else { return Region::whole(map) };
|
||||||
|
if let Some(grid) = grid.filter(|grid| grid.map() == map) {
|
||||||
|
let walk = grid.reachable(x, y);
|
||||||
|
// A door tile the collision list refuses is still stepped onto from beside it.
|
||||||
|
let reached = |dx: u8, dy: u8| {
|
||||||
|
walk.contains(dx, dy)
|
||||||
|
|| [(0i16, 1i16), (0, -1), (1, 0), (-1, 0)].iter().any(|(ox, oy)| {
|
||||||
|
match (u8::try_from(i16::from(dx) + ox), u8::try_from(i16::from(dy) + oy)) {
|
||||||
|
(Ok(nx), Ok(ny)) => walk.contains(nx, ny),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut hit =
|
||||||
|
pieces(split).filter(|(_, piece)| piece.doors.iter().any(|(_, dx, dy)| reached(*dx, *dy)));
|
||||||
|
if let (Some((part, _)), None) = (hit.next(), hit.next()) {
|
||||||
|
return Region { map, part };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
region_at(map, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The piece of `map` a warp lands in when it names `map`'s warp `index` as its destination.
|
||||||
|
///
|
||||||
|
/// `None` only for a split map whose table does not list that warp, which is not guessed at.
|
||||||
|
pub fn arrival_by_warp(map: u8, index: u8) -> Option<Region> {
|
||||||
|
let Some(split) = split_of(map) else { return Some(Region::whole(map)) };
|
||||||
|
pieces(split)
|
||||||
|
.find(|(_, piece)| piece.doors.iter().any(|(door, _, _)| *door == index))
|
||||||
|
.map(|(part, _)| Region { map, part })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The piece of `map` that stepping off an edge of `from` lands in.
|
||||||
|
///
|
||||||
|
/// An edge is listed under exactly one piece of a split map: Pewter's south edge opens onto Route
|
||||||
|
/// 2's north half, Route 3's north edge onto Route 4's west side and Cerulean's west edge onto its
|
||||||
|
/// east side. `None` is an edge the graph does not have.
|
||||||
|
pub fn arrival_by_edge(map: u8, from: u8) -> Option<Region> {
|
||||||
|
let Some(split) = split_of(map) else { return Some(Region::whole(map)) };
|
||||||
|
pieces(split)
|
||||||
|
.find(|(_, piece)| piece.next.iter().any(|next| next.map == from))
|
||||||
|
.map(|(part, _)| Region { map, part })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every piece of ground one step from `region`.
|
/// Every piece of ground one step from `region`.
|
||||||
fn region_neighbours(region: Region) -> Vec<Region> {
|
fn region_neighbours(region: Region) -> Vec<Region> {
|
||||||
let of = |map: u8| region_toward(map, region.map);
|
if let Some(split) = split_of(region.map) {
|
||||||
match split_of(region.map) {
|
return split.pieces.get(usize::from(region.part)).map_or_else(Vec::new, |piece| piece.next.to_vec());
|
||||||
None => neighbours(region.map).into_iter().filter_map(of).collect(),
|
}
|
||||||
Some(split) => {
|
// A whole map steps onto every piece of a split neighbour that lists it back: Mt. Moon's
|
||||||
let own = if region.part == NORTH_PIECE { split.north } else { split.south };
|
// first floor has a ladder into three of B1F's four chambers.
|
||||||
own.iter().copied().filter_map(of).collect()
|
let mut out = Vec::new();
|
||||||
|
for map in neighbours(region.map) {
|
||||||
|
match split_of(map) {
|
||||||
|
None => out.push(Region::whole(map)),
|
||||||
|
Some(split) => out.extend(
|
||||||
|
pieces(split)
|
||||||
|
.filter(|(_, piece)| piece.next.contains(®ion))
|
||||||
|
.map(|(part, _)| Region { map, part }),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The map on the other side of `map`'s `edge`, or `None` where the table does not know.
|
/// The map on the other side of `map`'s `edge`, or `None` where the table does not know.
|
||||||
|
|
@ -280,11 +475,20 @@ pub fn outdoor_of(interior: u8) -> Option<u8> {
|
||||||
neighbours(interior).into_iter().find(|map| super::cartridge::outdoors(*map))
|
neighbours(interior).into_iter().find(|map| super::cartridge::outdoors(*map))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every map one step from `map`, doors and edges together, deduplicated and in id order.
|
/// Every map one step on foot from `map`, doors and edges together, deduplicated and in id order.
|
||||||
|
///
|
||||||
|
/// A connection with no crossing ([`NO_CROSSING`]) is not a step on foot and is left out.
|
||||||
pub fn neighbours(map: u8) -> Vec<u8> {
|
pub fn neighbours(map: u8) -> Vec<u8> {
|
||||||
|
const EDGES: [Edge; 4] = [Edge::North, Edge::South, Edge::West, Edge::East];
|
||||||
let mut out: Vec<u8> = Vec::new();
|
let mut out: Vec<u8> = Vec::new();
|
||||||
if let Some(row) = CONNECTIONS.iter().find(|(id, _)| *id == map) {
|
if let Some(row) = CONNECTIONS.iter().find(|(id, _)| *id == map) {
|
||||||
out.extend(row.1.iter().copied().filter(|id| *id != NONE));
|
out.extend(
|
||||||
|
row.1
|
||||||
|
.iter()
|
||||||
|
.zip(EDGES)
|
||||||
|
.filter(|(id, edge)| **id != NONE && crossable(map, *edge))
|
||||||
|
.map(|(id, _)| *id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (a, b) in LINKS {
|
for (a, b) in LINKS {
|
||||||
if *a == map {
|
if *a == map {
|
||||||
|
|
@ -296,7 +500,7 @@ pub fn neighbours(map: u8) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
// Symmetric closure: a row that names a neighbour is a connection whichever side lists it.
|
// Symmetric closure: a row that names a neighbour is a connection whichever side lists it.
|
||||||
for (id, row) in CONNECTIONS {
|
for (id, row) in CONNECTIONS {
|
||||||
if row.contains(&map) {
|
if row.iter().zip(EDGES).any(|(other, edge)| *other == map && crossable(*id, edge)) {
|
||||||
out.push(*id);
|
out.push(*id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -313,16 +517,23 @@ pub fn neighbours(map: u8) -> Vec<u8> {
|
||||||
/// an unreachable one, and for `from == to` -- there is no hop to take when the fly is already
|
/// an unreachable one, and for `from == to` -- there is no hop to take when the fly is already
|
||||||
/// there, and `GO OBJECTIVE` has its own answer for that case.
|
/// there, and `GO OBJECTIVE` has its own answer for that case.
|
||||||
pub fn next_hop(from: Region, to: u8) -> Option<u8> {
|
pub fn next_hop(from: Region, to: u8) -> Option<u8> {
|
||||||
|
next_step(from, to).map(|hop| hop.map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`next_hop`] with the piece it lands in, which is what an exit has to match on a map with two
|
||||||
|
/// doors into one split map: three of Mt. Moon's first-floor ladders go down to B1F, and only
|
||||||
|
/// one of them reaches the way out ([`arrival_by_warp`] names where each one lands).
|
||||||
|
pub fn next_step(from: Region, to: u8) -> Option<Region> {
|
||||||
if from.map == to {
|
if from.map == to {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut seen: HashSet<Region> = HashSet::from([from]);
|
let mut seen: HashSet<Region> = HashSet::from([from]);
|
||||||
// piece -> the first hop out of `from` that reaches it
|
// piece -> the first hop out of `from` that reaches it
|
||||||
let mut first: HashMap<Region, u8> = HashMap::new();
|
let mut first: HashMap<Region, Region> = HashMap::new();
|
||||||
let mut queue: VecDeque<Region> = VecDeque::new();
|
let mut queue: VecDeque<Region> = VecDeque::new();
|
||||||
for hop in region_neighbours(from) {
|
for hop in region_neighbours(from) {
|
||||||
if seen.insert(hop) {
|
if seen.insert(hop) {
|
||||||
first.insert(hop, hop.map);
|
first.insert(hop, hop);
|
||||||
queue.push_back(hop);
|
queue.push_back(hop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -495,6 +706,62 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_rows_the_disassembly_corrected_say_what_its_headers_say() {
|
||||||
|
// Row 59, `data/maps/headers/*.asm` at the pinned commit. Route 4 is north of Route 3:
|
||||||
|
// Route 3's top edge is the road to Mt. Moon's Pokécenter, and Route 3 has no east exit.
|
||||||
|
assert_eq!(connected(maps::ROUTE_3, Edge::North), Some(maps::ROUTE_4));
|
||||||
|
assert_eq!(connected(maps::ROUTE_3, Edge::East), None);
|
||||||
|
assert_eq!(connected(maps::ROUTE_4, Edge::South), Some(maps::ROUTE_3));
|
||||||
|
assert_eq!(connected(maps::ROUTE_4, Edge::West), None);
|
||||||
|
assert_eq!(connected(maps::ROUTE_4, Edge::East), Some(maps::CERULEAN_CITY));
|
||||||
|
// Nugget Bridge's far end is Route 24's east edge, not its north one.
|
||||||
|
assert_eq!(connected(maps::ROUTE_24, Edge::East), Some(maps::ROUTE_25));
|
||||||
|
assert_eq!(connected(maps::ROUTE_24, Edge::North), None);
|
||||||
|
assert_eq!(connected(maps::ROUTE_25, Edge::West), Some(maps::ROUTE_24));
|
||||||
|
assert_eq!(connected(maps::ROUTE_25, Edge::South), None);
|
||||||
|
assert_eq!(connected(maps::ROUTE_14, Edge::West), Some(maps::ROUTE_15));
|
||||||
|
assert_eq!(connected(maps::ROUTE_15, Edge::East), Some(maps::ROUTE_14));
|
||||||
|
// Mt. Moon's two mouths are both on Route 4, and Route 3 has no warps: a cave door that
|
||||||
|
// is not there is a road the fly walks up and down for ever (row 59's Pewter ring).
|
||||||
|
assert!(!neighbours(maps::ROUTE_3).contains(&maps::MT_MOON_1F));
|
||||||
|
assert_eq!(outdoor_of(maps::MT_MOON_1F), Some(maps::ROUTE_4));
|
||||||
|
assert_eq!(outdoor_of(maps::MT_MOON_B1F), Some(maps::ROUTE_4));
|
||||||
|
assert_eq!(
|
||||||
|
neighbours(maps::ROUTE_3),
|
||||||
|
vec![maps::PEWTER_CITY, maps::ROUTE_4],
|
||||||
|
"Route 3 is a road between two maps and nothing else"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_connection_nobody_can_walk_across_is_named_and_is_not_a_road() {
|
||||||
|
// Row 59: Pallet Town's shore. The header connects it to Route 21, which is water.
|
||||||
|
assert_eq!(connected(maps::PALLET_TOWN, Edge::South), Some(maps::ROUTE_21));
|
||||||
|
assert!(!crossable(maps::PALLET_TOWN, Edge::South));
|
||||||
|
assert!(!neighbours(maps::PALLET_TOWN).contains(&maps::ROUTE_21));
|
||||||
|
assert!(!neighbours(maps::ROUTE_21).contains(&maps::PALLET_TOWN));
|
||||||
|
// So the road from Pallet Town to Cerulean is the long one on land: north, through the
|
||||||
|
// forest, Pewter and Mt. Moon, and not by sea through Cinnabar and Fuchsia.
|
||||||
|
assert_eq!(next_hop(Region::whole(maps::PALLET_TOWN), maps::CERULEAN_CITY), Some(maps::ROUTE_1));
|
||||||
|
assert_eq!(hops(Region::whole(maps::PALLET_TOWN), maps::CERULEAN_CITY), Some(16));
|
||||||
|
// Every entry is one of the header's own connections, and both sides are listed.
|
||||||
|
for (map, edge) in NO_CROSSING {
|
||||||
|
let other = connected(*map, *edge).expect("a no-crossing entry is a header connection");
|
||||||
|
let back = match edge {
|
||||||
|
Edge::North => Edge::South,
|
||||||
|
Edge::South => Edge::North,
|
||||||
|
Edge::West => Edge::East,
|
||||||
|
Edge::East => Edge::West,
|
||||||
|
};
|
||||||
|
assert!(!crossable(other, back), "{other:#04x} lists {map:#04x} as crossable");
|
||||||
|
}
|
||||||
|
// Indigo Plateau is on the graph and, until a row gives the League gate, not on foot
|
||||||
|
// from the south.
|
||||||
|
assert_eq!(connected(maps::ROUTE_22, Edge::North), Some(maps::ROUTE_23));
|
||||||
|
assert_eq!(next_hop(Region::whole(maps::VIRIDIAN_CITY), maps::INDIGO_PLATEAU), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_front_door_resolves_to_the_town_outside_it() {
|
fn a_front_door_resolves_to_the_town_outside_it() {
|
||||||
assert_eq!(outdoor_of(maps::OAKS_LAB), Some(maps::PALLET_TOWN));
|
assert_eq!(outdoor_of(maps::OAKS_LAB), Some(maps::PALLET_TOWN));
|
||||||
|
|
@ -520,7 +787,7 @@ mod tests {
|
||||||
assert_eq!(next_hop(at(maps::OAKS_LAB), maps::VIRIDIAN_MART), Some(maps::PALLET_TOWN));
|
assert_eq!(next_hop(at(maps::OAKS_LAB), maps::VIRIDIAN_MART), Some(maps::PALLET_TOWN));
|
||||||
// Upstairs is two hops from the town, through the ground floor.
|
// Upstairs is two hops from the town, through the ground floor.
|
||||||
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::REDS_HOUSE_2F), Some(maps::REDS_HOUSE_1F));
|
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::REDS_HOUSE_2F), Some(maps::REDS_HOUSE_1F));
|
||||||
// Through the cave, because Route 3 and Route 4 are the same two maps either way round.
|
// Out along Route 3, whose only other end is the road up to Mt. Moon.
|
||||||
assert_eq!(next_hop(at(maps::PEWTER_CITY), maps::CERULEAN_GYM), Some(maps::ROUTE_3));
|
assert_eq!(next_hop(at(maps::PEWTER_CITY), maps::CERULEAN_GYM), Some(maps::ROUTE_3));
|
||||||
// Nowhere to go, and nowhere known.
|
// Nowhere to go, and nowhere known.
|
||||||
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::PALLET_TOWN), None);
|
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::PALLET_TOWN), None);
|
||||||
|
|
@ -528,37 +795,169 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_split_maps_pieces_divide_its_neighbours_between_them() {
|
fn a_split_maps_pieces_add_up_and_answer_each_other() {
|
||||||
// The invariant that keeps [`SPLIT`] honest: a piece's own list is a real subset of the
|
// The invariants that keep [`SPLIT`] honest. A typo in any of them is a road that does
|
||||||
// map's neighbours, the two pieces together are all of them, and neither claims the same
|
// not exist, or a door that leads nowhere.
|
||||||
// neighbour twice. A typo here is a road that does not exist.
|
|
||||||
for split in SPLIT {
|
for split in SPLIT {
|
||||||
let mut both: Vec<u8> =
|
// The pieces' neighbours together are exactly the map's.
|
||||||
split.north.iter().chain(split.south.iter()).copied().collect();
|
let mut maps_of: Vec<u8> =
|
||||||
both.sort_unstable();
|
split.pieces.iter().flat_map(|piece| piece.next.iter().map(|r| r.map)).collect();
|
||||||
let mut once = both.clone();
|
maps_of.sort_unstable();
|
||||||
once.dedup();
|
maps_of.dedup();
|
||||||
assert_eq!(both, once, "{:#04x} lists a neighbour under both pieces", split.map);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
both,
|
maps_of,
|
||||||
neighbours(split.map),
|
neighbours(split.map),
|
||||||
"{:#04x}'s pieces do not add up to its neighbours",
|
"{:#04x}'s pieces do not add up to its neighbours",
|
||||||
split.map
|
split.map
|
||||||
);
|
);
|
||||||
assert_ne!(split.north_door, split.south_door);
|
let mut doors: Vec<u8> =
|
||||||
|
split.pieces.iter().flat_map(|piece| piece.doors.iter().map(|d| d.0)).collect();
|
||||||
|
doors.sort_unstable();
|
||||||
|
let count = doors.len();
|
||||||
|
doors.dedup();
|
||||||
|
assert_eq!(doors.len(), count, "{:#04x} lists one warp under two pieces", split.map);
|
||||||
|
for (part, piece) in pieces(split) {
|
||||||
|
let here = Region { map: split.map, part };
|
||||||
|
assert!(!piece.doors.is_empty(), "{here:?} has no door to be told apart by");
|
||||||
|
// A door's own tile is in its own piece.
|
||||||
|
for (_, x, y) in piece.doors {
|
||||||
|
assert_eq!(region_at(split.map, *x, *y), here, "door ({x}, {y})");
|
||||||
|
}
|
||||||
|
// Every step is listed back from the other side, so a route is reversible.
|
||||||
|
for next in piece.next {
|
||||||
|
assert!(
|
||||||
|
region_neighbours(*next).contains(&here),
|
||||||
|
"{next:?} does not step back onto {here:?}"
|
||||||
|
);
|
||||||
|
if let Some(other) = split_of(next.map) {
|
||||||
|
assert!(usize::from(next.part) < other.pieces.len(), "{next:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_road_from_pewter_to_cerulean_is_through_mt_moon_one_chamber_at_a_time() {
|
||||||
|
// Row 59. Every step of the road, as the pieces the fly stands in, measured from the
|
||||||
|
// disassembly: Route 3's top edge, Route 4's west side, the cave mouth at (18, 5), 1F's
|
||||||
|
// ladder at (5, 5), B1F's west chamber, its ladder at (21, 17), B2F, its ladder at (5, 7),
|
||||||
|
// B1F's exit chamber, (27, 3), Route 4's east side, Cerulean.
|
||||||
|
let road = [
|
||||||
|
Region::whole(maps::PEWTER_CITY),
|
||||||
|
Region::whole(maps::ROUTE_3),
|
||||||
|
Region::piece(maps::ROUTE_4, WEST_SIDE),
|
||||||
|
Region::whole(maps::MT_MOON_1F),
|
||||||
|
Region::piece(maps::MT_MOON_B1F, B1F_WEST),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_MAIN),
|
||||||
|
Region::piece(maps::MT_MOON_B1F, B1F_EXIT),
|
||||||
|
Region::piece(maps::ROUTE_4, EAST_SIDE),
|
||||||
|
Region::whole(maps::CERULEAN_CITY),
|
||||||
|
];
|
||||||
|
for pair in road.windows(2) {
|
||||||
|
assert_eq!(next_step(pair[0], maps::CERULEAN_CITY), Some(pair[1]), "from {:?}", pair[0]);
|
||||||
|
}
|
||||||
|
assert_eq!(hops(road[0], maps::CERULEAN_CITY), Some(8));
|
||||||
|
// Mt. Moon's rung is the first floor, one hop from the cave mouth's side of Route 4 --
|
||||||
|
// which is where the Pewter ring said the fly could never get to.
|
||||||
|
assert_eq!(next_hop(Region::whole(maps::PEWTER_CITY), maps::MT_MOON_1F), Some(maps::ROUTE_3));
|
||||||
|
assert_eq!(next_hop(Region::whole(maps::ROUTE_3), maps::MT_MOON_1F), Some(maps::ROUTE_4));
|
||||||
|
assert_eq!(
|
||||||
|
next_hop(Region::piece(maps::ROUTE_4, WEST_SIDE), maps::MT_MOON_1F),
|
||||||
|
Some(maps::MT_MOON_1F)
|
||||||
|
);
|
||||||
|
// The dead ends lead back the way they came.
|
||||||
|
assert_eq!(
|
||||||
|
next_step(Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE), maps::CERULEAN_CITY),
|
||||||
|
Some(Region::whole(maps::MT_MOON_1F))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
next_step(Region::piece(maps::MT_MOON_B2F, B2F_SOUTH), maps::CERULEAN_CITY),
|
||||||
|
Some(Region::piece(maps::MT_MOON_B1F, B1F_SOUTH))
|
||||||
|
);
|
||||||
|
// From Cerulean's side of the mountain the way back to Pewter is the cave, not Route 4's
|
||||||
|
// south edge, which is on the other side.
|
||||||
|
assert_eq!(
|
||||||
|
next_step(Region::piece(maps::ROUTE_4, EAST_SIDE), maps::PEWTER_CITY),
|
||||||
|
Some(Region::piece(maps::MT_MOON_B1F, B1F_EXIT))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
next_step(Region::whole(maps::CERULEAN_CITY), maps::ROUTE_24),
|
||||||
|
Some(Region::whole(maps::ROUTE_24))
|
||||||
|
);
|
||||||
|
assert_eq!(next_hop(Region::whole(maps::ROUTE_24), maps::ROUTE_25), Some(maps::ROUTE_25));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_door_or_an_edge_lands_in_the_piece_it_opens_onto() {
|
||||||
|
// Which warp of the destination a warp names is the cartridge's own answer (`wWarpEntries`
|
||||||
|
// byte 2, 0-based): 1F's ladders are B1F's warps 0, 2 and 3.
|
||||||
|
assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 0), Some(Region::piece(maps::MT_MOON_B1F, B1F_WEST)));
|
||||||
|
assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 2), Some(Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE)));
|
||||||
|
assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 3), Some(Region::piece(maps::MT_MOON_B1F, B1F_SOUTH)));
|
||||||
|
// B1F's exit is `LAST_MAP` warp 2: Route 4's (24, 5), the far side of the mountain.
|
||||||
|
assert_eq!(arrival_by_warp(maps::ROUTE_4, 2), Some(Region::piece(maps::ROUTE_4, EAST_SIDE)));
|
||||||
|
// 1F's doormat is `LAST_MAP` warp 1: the cave mouth, the Pewter side.
|
||||||
|
assert_eq!(arrival_by_warp(maps::ROUTE_4, 1), Some(Region::piece(maps::ROUTE_4, WEST_SIDE)));
|
||||||
|
// The forest gates' doormats onto Route 2 are its warps 1 and 5.
|
||||||
|
assert_eq!(arrival_by_warp(maps::ROUTE_2, 1), Some(Region::piece(maps::ROUTE_2, NORTH_PIECE)));
|
||||||
|
assert_eq!(arrival_by_warp(maps::ROUTE_2, 5), Some(Region::piece(maps::ROUTE_2, SOUTH_PIECE)));
|
||||||
|
// A warp the table does not list is not guessed at; a whole map is always whole.
|
||||||
|
assert_eq!(arrival_by_warp(maps::ROUTE_2, 0), None);
|
||||||
|
assert_eq!(arrival_by_warp(maps::PEWTER_GYM, 0), Some(Region::whole(maps::PEWTER_GYM)));
|
||||||
|
// Edges.
|
||||||
|
assert_eq!(arrival_by_edge(maps::ROUTE_4, maps::ROUTE_3), Some(Region::piece(maps::ROUTE_4, WEST_SIDE)));
|
||||||
|
assert_eq!(
|
||||||
|
arrival_by_edge(maps::ROUTE_4, maps::CERULEAN_CITY),
|
||||||
|
Some(Region::piece(maps::ROUTE_4, EAST_SIDE))
|
||||||
|
);
|
||||||
|
assert_eq!(arrival_by_edge(maps::ROUTE_2, maps::PEWTER_CITY), Some(Region::piece(maps::ROUTE_2, NORTH_PIECE)));
|
||||||
|
assert_eq!(arrival_by_edge(maps::ROUTE_3, maps::ROUTE_4), Some(Region::whole(maps::ROUTE_3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn route_4s_sides_are_told_apart_by_the_doors_where_the_ground_cannot() {
|
||||||
|
// With no grid, the nearest door: the cave mouth's side reaches down to Route 3's road at
|
||||||
|
// (7..11, 17), and Cerulean's side is everything east of the mountain.
|
||||||
|
assert_eq!(region_at(maps::ROUTE_4, 9, 17), Region::piece(maps::ROUTE_4, WEST_SIDE));
|
||||||
|
assert_eq!(region_at(maps::ROUTE_4, 18, 6), Region::piece(maps::ROUTE_4, WEST_SIDE));
|
||||||
|
assert_eq!(region_at(maps::ROUTE_4, 24, 6), Region::piece(maps::ROUTE_4, EAST_SIDE));
|
||||||
|
assert_eq!(region_at(maps::ROUTE_4, 89, 10), Region::piece(maps::ROUTE_4, EAST_SIDE));
|
||||||
|
assert_eq!(region_at(maps::ROUTE_3, 60, 0), Region::whole(maps::ROUTE_3));
|
||||||
|
// B2F's chambers wrap round each other, so there the grid decides.
|
||||||
|
let mut grid = MapGrid::new(maps::MT_MOON_B2F, 40, 36);
|
||||||
|
// A corridor from B2F's (5, 7) ladder east along row 7 and down column 33 to (33, 31):
|
||||||
|
// nearer the (15, 27) ladder than either of its own, and on the main piece.
|
||||||
|
for x in 4..=33 {
|
||||||
|
grid.set(x, 7, 0, super::super::state::Walkable::Yes);
|
||||||
|
}
|
||||||
|
for y in 7..=31 {
|
||||||
|
grid.set(33, y, 0, super::super::state::Walkable::Yes);
|
||||||
|
}
|
||||||
|
assert_eq!(region_at(maps::MT_MOON_B2F, 33, 31), Region::piece(maps::MT_MOON_B2F, B2F_SOUTH));
|
||||||
|
assert_eq!(
|
||||||
|
region_on(maps::MT_MOON_B2F, 33, 31, Some(&grid)),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_MAIN)
|
||||||
|
);
|
||||||
|
// A grid of another map says nothing, and neither does none.
|
||||||
|
let other = MapGrid::new(maps::ROUTE_4, 90, 18);
|
||||||
|
assert_eq!(
|
||||||
|
region_on(maps::MT_MOON_B2F, 33, 31, Some(&other)),
|
||||||
|
Region::piece(maps::MT_MOON_B2F, B2F_SOUTH)
|
||||||
|
);
|
||||||
|
assert_eq!(region_on(maps::MT_MOON_B2F, 33, 31, None), Region::piece(maps::MT_MOON_B2F, B2F_SOUTH));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn route_2s_halves_are_told_apart_by_the_row_the_fly_is_standing_on() {
|
fn route_2s_halves_are_told_apart_by_the_row_the_fly_is_standing_on() {
|
||||||
// The two doorways are rows 11 and 43, measured from the cartridge's own warp table.
|
// The two doorways are rows 11 and 43, measured from the cartridge's own warp table.
|
||||||
assert_eq!(region_at(maps::ROUTE_2, 11).part, NORTH_PIECE);
|
assert_eq!(region_at(maps::ROUTE_2, 3, 11).part, NORTH_PIECE);
|
||||||
assert_eq!(region_at(maps::ROUTE_2, 43).part, SOUTH_PIECE);
|
assert_eq!(region_at(maps::ROUTE_2, 3, 43).part, SOUTH_PIECE);
|
||||||
assert_eq!(region_at(maps::ROUTE_2, 0).part, NORTH_PIECE, "Pewter's end");
|
assert_eq!(region_at(maps::ROUTE_2, 8, 0).part, NORTH_PIECE, "Pewter's end");
|
||||||
assert_eq!(region_at(maps::ROUTE_2, 71).part, SOUTH_PIECE, "Viridian's end");
|
assert_eq!(region_at(maps::ROUTE_2, 8, 71).part, SOUTH_PIECE, "Viridian's end");
|
||||||
// Every other map is one piece, whatever row is asked about.
|
// Every other map is one piece, whatever row is asked about.
|
||||||
assert_eq!(region_at(maps::ROUTE_1, 30), Region::whole(maps::ROUTE_1));
|
assert_eq!(region_at(maps::ROUTE_1, 10, 30), Region::whole(maps::ROUTE_1));
|
||||||
assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 7).part, 0);
|
assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 4, 7).part, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -578,16 +977,16 @@ mod tests {
|
||||||
Some(maps::ROUTE_2)
|
Some(maps::ROUTE_2)
|
||||||
);
|
);
|
||||||
// Route 2's north half steps off its own top edge; its south half walks to the gate.
|
// Route 2's north half steps off its own top edge; its south half walks to the gate.
|
||||||
assert_eq!(next_hop(region_at(maps::ROUTE_2, 11), maps::PEWTER_CITY), Some(maps::PEWTER_CITY));
|
assert_eq!(next_hop(region_at(maps::ROUTE_2, 3, 11), maps::PEWTER_CITY), Some(maps::PEWTER_CITY));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_hop(region_at(maps::ROUTE_2, 43), maps::PEWTER_CITY),
|
next_hop(region_at(maps::ROUTE_2, 3, 43), maps::PEWTER_CITY),
|
||||||
Some(maps::VIRIDIAN_FOREST_SOUTH_GATE)
|
Some(maps::VIRIDIAN_FOREST_SOUTH_GATE)
|
||||||
);
|
);
|
||||||
// From Viridian City the first hop is still Route 2, which is the road the fly takes north.
|
// From Viridian City the first hop is still Route 2, which is the road the fly takes north.
|
||||||
assert_eq!(next_hop(Region::whole(maps::VIRIDIAN_CITY), maps::PEWTER_CITY), Some(maps::ROUTE_2));
|
assert_eq!(next_hop(Region::whole(maps::VIRIDIAN_CITY), maps::PEWTER_CITY), Some(maps::ROUTE_2));
|
||||||
// And the way back south from the north half is the forest, not Route 2's own bottom edge.
|
// And the way back south from the north half is the forest, not Route 2's own bottom edge.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_hop(region_at(maps::ROUTE_2, 11), maps::VIRIDIAN_CITY),
|
next_hop(region_at(maps::ROUTE_2, 3, 11), maps::VIRIDIAN_CITY),
|
||||||
Some(maps::VIRIDIAN_FOREST_NORTH_GATE)
|
Some(maps::VIRIDIAN_FOREST_NORTH_GATE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -645,11 +1044,11 @@ mod tests {
|
||||||
assert_eq!(here.map, maps::PEWTER_GYM);
|
assert_eq!(here.map, maps::PEWTER_GYM);
|
||||||
assert_eq!(steps, 3);
|
assert_eq!(steps, 3);
|
||||||
// A split map is measured from the piece the fly is standing in, exactly as `next_hop` is.
|
// A split map is measured from the piece the fly is standing in, exactly as `next_hop` is.
|
||||||
assert_eq!(hops(region_at(maps::ROUTE_2, 11), maps::PEWTER_CITY), Some(1));
|
assert_eq!(hops(region_at(maps::ROUTE_2, 3, 11), maps::PEWTER_CITY), Some(1));
|
||||||
// Five from the south half, because the belt of trees between the halves needs CUT and
|
// Five from the south half, because the belt of trees between the halves needs CUT and
|
||||||
// the road is the forest: the south gate, the forest, the north gate, Route 2's north
|
// the road is the forest: the south gate, the forest, the north gate, Route 2's north
|
||||||
// half, Pewter.
|
// half, Pewter.
|
||||||
assert_eq!(hops(region_at(maps::ROUTE_2, 43), maps::PEWTER_CITY), Some(5));
|
assert_eq!(hops(region_at(maps::ROUTE_2, 3, 43), maps::PEWTER_CITY), Some(5));
|
||||||
// And nothing is guessed.
|
// And nothing is guessed.
|
||||||
assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None);
|
assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1422,14 +1422,47 @@ fn exit_tiers(state: &mut dyn MacroState, way: Way) -> Vec<Exit> {
|
||||||
/// was in. Empty when there is no objective and when it is on this map.
|
/// was in. Empty when there is no objective and when it is on this map.
|
||||||
pub fn toward_objective(state: &mut dyn MacroState, candidates: &[Exit]) -> Vec<Exit> {
|
pub fn toward_objective(state: &mut dyn MacroState, candidates: &[Exit]) -> Vec<Exit> {
|
||||||
let Some(objective) = objective_place(state) else { return Vec::new() };
|
let Some(objective) = objective_place(state) else { return Vec::new() };
|
||||||
let Some(player) = state.player() else { return Vec::new() };
|
let Some(from) = region_here(state) else { return Vec::new() };
|
||||||
let here = player.map;
|
let here = from.map;
|
||||||
if here == objective.map {
|
if here == objective.map {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let hop = geography::next_hop(geography::region_at(here, player.y), objective.map);
|
match geography::next_step(from, objective.map) {
|
||||||
let aim = hop.unwrap_or(objective.map);
|
Some(hop) => {
|
||||||
candidates.iter().copied().filter(|exit| exit.destination(here) == Some(aim)).collect()
|
candidates.iter().copied().filter(|exit| leads_to(state, exit, here, hop)).collect()
|
||||||
|
}
|
||||||
|
None => candidates
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|exit| exit.destination(here) == Some(objective.map))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The piece of ground the fly is standing in: its map, and on a map whose ground is in pieces
|
||||||
|
/// the piece its walk can reach the doors of (`docs/design/macros.md` sections 12.7 and 12.24).
|
||||||
|
pub fn region_here(state: &mut dyn MacroState) -> Option<geography::Region> {
|
||||||
|
let player = state.player()?;
|
||||||
|
let grid = state.map_grid();
|
||||||
|
Some(geography::region_on(player.map, player.x, player.y, grid.as_deref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `exit` takes the fly onto `hop`: the map on the other side, and on a map whose ground
|
||||||
|
/// is in pieces, the piece it lands in. A warp names the destination's warp it arrives at, which
|
||||||
|
/// is what tells Mt. Moon's three ladders down to B1F apart (section 12.24); an edge lands in the
|
||||||
|
/// piece that lists the map it is stepped off. A landing the table cannot name is not a match.
|
||||||
|
fn leads_to(state: &mut dyn MacroState, exit: &Exit, here: u8, hop: geography::Region) -> bool {
|
||||||
|
if exit.destination(here) != Some(hop.map) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let landing = match exit.id {
|
||||||
|
ExitId::Warp(index) => state
|
||||||
|
.warps()
|
||||||
|
.get(usize::from(index))
|
||||||
|
.and_then(|warp| geography::arrival_by_warp(hop.map, warp.destination_warp)),
|
||||||
|
ExitId::Edge(_) => geography::arrival_by_edge(hop.map, here),
|
||||||
|
};
|
||||||
|
landing == Some(hop)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The people on this map still worth walking to, each with the key the ledgers name it by.
|
/// The people on this map still worth walking to, each with the key the ledgers name it by.
|
||||||
|
|
@ -1768,9 +1801,10 @@ pub fn goals_toward(state: &mut dyn MacroState, target: u8) -> Vec<Aim> {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
if let Some(hop) = geography::next_hop(geography::region_at(here, player.y), target) {
|
let hop = region_here(state).and_then(|from| geography::next_step(from, target));
|
||||||
|
if let Some(hop) = hop {
|
||||||
let toward: Vec<Exit> =
|
let toward: Vec<Exit> =
|
||||||
exits.iter().copied().filter(|exit| exit.destination(here) == Some(hop)).collect();
|
exits.iter().copied().filter(|exit| leads_to(state, exit, here, hop)).collect();
|
||||||
if !toward.is_empty() {
|
if !toward.is_empty() {
|
||||||
return of(toward);
|
return of(toward);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,9 @@ pub fn exits(state: &mut dyn MacroState) -> Vec<Exit> {
|
||||||
Edge::East => connections.east,
|
Edge::East => connections.east,
|
||||||
Edge::West => connections.west,
|
Edge::West => connections.west,
|
||||||
};
|
};
|
||||||
if !connected {
|
// A connection the headers name and no step on foot crosses -- Pallet Town's shore --
|
||||||
|
// is not a way out (`geography::NO_CROSSING`, row 59).
|
||||||
|
if !connected || !geography::crossable(player.map, edge) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A step off the edge of an outdoor map is the next area; off an interior one -- which
|
// A step off the edge of an outdoor map is the next area; off an interior one -- which
|
||||||
|
|
|
||||||
|
|
@ -536,18 +536,22 @@ impl MapGrid {
|
||||||
/// reachable ones is fenced in, and no amount of re-planning is going to help it
|
/// reachable ones is fenced in, and no amount of re-planning is going to help it
|
||||||
/// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`).
|
/// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`).
|
||||||
pub fn reachable_from(&self, x: u8, y: u8) -> usize {
|
pub fn reachable_from(&self, x: u8, y: u8) -> usize {
|
||||||
if self.index(x, y).is_none() {
|
self.reachable(x, y).iter().filter(|seen| **seen).count()
|
||||||
return 0;
|
}
|
||||||
}
|
|
||||||
|
/// Whether `(tx, ty)` is among the tiles [`MapGrid::reachable_from`] counts from `(x, y)`.
|
||||||
|
///
|
||||||
|
/// The whole flood at once, row-major like the grid itself, so a caller asking about several
|
||||||
|
/// tiles pays for one walk. Off the map is never reachable.
|
||||||
|
pub fn reachable(&self, x: u8, y: u8) -> Reachable {
|
||||||
let mut seen = vec![false; self.tiles.len()];
|
let mut seen = vec![false; self.tiles.len()];
|
||||||
|
let Some(start) = self.index(x, y) else {
|
||||||
|
return Reachable { width: self.width, seen };
|
||||||
|
};
|
||||||
|
seen[start] = true;
|
||||||
let mut queue = std::collections::VecDeque::new();
|
let mut queue = std::collections::VecDeque::new();
|
||||||
if let Some(index) = self.index(x, y) {
|
|
||||||
seen[index] = true;
|
|
||||||
}
|
|
||||||
queue.push_back((x, y));
|
queue.push_back((x, y));
|
||||||
let mut count = 0;
|
|
||||||
while let Some((tx, ty)) = queue.pop_front() {
|
while let Some((tx, ty)) = queue.pop_front() {
|
||||||
count += 1;
|
|
||||||
for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] {
|
for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] {
|
||||||
if self.walled(tx, ty, facing) {
|
if self.walled(tx, ty, facing) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -563,7 +567,30 @@ impl MapGrid {
|
||||||
queue.push_back((nx, ny));
|
queue.push_back((nx, ny));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count
|
Reachable { width: self.width, seen }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tiles a walk from one tile of a [`MapGrid`] could reach ([`MapGrid::reachable`]).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Reachable {
|
||||||
|
width: u8,
|
||||||
|
seen: Vec<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reachable {
|
||||||
|
/// Whether the walk reaches `(x, y)`.
|
||||||
|
pub fn contains(&self, x: u8, y: u8) -> bool {
|
||||||
|
x < self.width
|
||||||
|
&& self
|
||||||
|
.seen
|
||||||
|
.get(usize::from(y) * usize::from(self.width) + usize::from(x))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self) -> impl Iterator<Item = &bool> {
|
||||||
|
self.seen.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -854,14 +854,16 @@ fn settle(machine: &mut MacroMachine, world: &mut World) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The cartridge gives the joypad back in the overworld: one frame of it, observed, and whatever
|
/// The cartridge gives the joypad back in the overworld: the settle window of it, observed, and
|
||||||
/// it decided taken into the ledgers (row 58).
|
/// whatever it decided taken into the ledgers (rows 58 and 59).
|
||||||
fn hand_back(machine: &mut MacroMachine, world: &mut World) {
|
fn hand_back(machine: &mut MacroMachine, world: &mut World) {
|
||||||
world.scene = Scene::Overworld;
|
world.scene = Scene::Overworld;
|
||||||
world.scripted = false;
|
world.scripted = false;
|
||||||
world.scripted_at = None;
|
world.scripted_at = None;
|
||||||
world.switch = None;
|
world.switch = None;
|
||||||
machine.observe_frame(world);
|
for _ in 0..super::executor::PUSH_SETTLE_FRAMES {
|
||||||
|
machine.observe_frame(world);
|
||||||
|
}
|
||||||
settle(machine, world);
|
settle(machine, world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4160,18 +4162,20 @@ fn go_shop_and_go_heal_are_on_the_pad_while_their_errand_stands() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_edge_the_table_cannot_name_stops_being_somewhere_new_once_it_is_stood_on() {
|
fn an_edge_the_table_cannot_name_stops_being_somewhere_new_once_it_is_stood_on() {
|
||||||
// The rung-11 reading of row 54 (`infra/docs/macros-traps.md`). The cartridge reports Route
|
// The rung-11 reading of row 54 (`infra/docs/macros-traps.md`). The cartridge reported Route
|
||||||
// 3's connections as north and west; `geography`'s row carries west and east, so the north
|
// 3's connections as north and west while `geography`'s row carried west and east, so the
|
||||||
// edge's destination is unnameable -- and an unnameable destination counted as *unvisited*,
|
// north edge's destination was unnameable -- and an unnameable destination counted as
|
||||||
// which made those tiles first-tier for `GO ROUTE` on every hold for ever, with
|
// *unvisited*, which made those tiles first-tier for `GO ROUTE` on every hold for ever. Row 59
|
||||||
// `GO OBJECTIVE` off the pad beside them because nothing on this map leads to the objective.
|
// corrected the row itself, and since then every header connection has a row; the rule is
|
||||||
|
// pinned on `$0B`, the one outdoor id with no header of its own (`UNUSED_MAP_0B`), whose
|
||||||
|
// edges the table cannot name.
|
||||||
let mut world = World::room();
|
let mut world = World::room();
|
||||||
world.map = maps::ROUTE_3;
|
world.map = 0x0b;
|
||||||
world.size = MapSize { width: 8, height: 8 };
|
world.size = MapSize { width: 8, height: 8 };
|
||||||
world.player = Tile::new(4, 4);
|
world.player = Tile::new(4, 4);
|
||||||
world.connections = Connections { north: true, south: false, east: false, west: true };
|
world.connections = Connections { north: true, south: false, east: false, west: true };
|
||||||
// West is Pewter City, which the table does name and the run has stood on.
|
// West the run has already stood on.
|
||||||
world.seen_maps.insert(maps::PEWTER_CITY);
|
world.visited.insert(ExitId::Edge(Edge::West));
|
||||||
|
|
||||||
let north: Vec<ExitId> = ways(&mut world, Way::Route).iter().map(|exit| exit.id).collect();
|
let north: Vec<ExitId> = ways(&mut world, Way::Route).iter().map(|exit| exit.id).collect();
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -5427,6 +5431,37 @@ fn facing_one_of_the_rungs_people_is_the_arrival() {
|
||||||
assert!(on_the_pad(&mut world, MacroKind::GoObjective), "turned away, the walk is back");
|
assert!(on_the_pad(&mut world, MacroKind::GoObjective), "turned away, the walk is back");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_challenge_closing_onto_a_few_frames_of_overworld_is_still_a_challenge() {
|
||||||
|
// Row 59, Route 3's first trainer. The challenge text closes, and for five frames the screen
|
||||||
|
// is an ordinary overworld with nothing set -- no text, no script, no joypad bit, the battle
|
||||||
|
// not yet decided -- before `StartTrainerBattle` runs. Decided on the first of them, the walk
|
||||||
|
// the trainer interrupted walled (11, 6) for the session: the one gap east on Route 3.
|
||||||
|
let mut world = World::room().at(3, 3);
|
||||||
|
world.map = 0x00;
|
||||||
|
world.connections = Connections { north: true, south: false, east: false, west: false };
|
||||||
|
world.switch = Some((4, Scene::Dialog));
|
||||||
|
world.scripted_at = Some(4);
|
||||||
|
let north = TargetKey::Exit(ExitId::Edge(Edge::North));
|
||||||
|
let mut machine = MacroMachine::new(0x1234_5678);
|
||||||
|
assert_eq!(run_with(&mut machine, &mut world, MacroKind::GoRoute), Ok(MacroAbort::Done));
|
||||||
|
|
||||||
|
// The text closes onto five frames of overworld, and then the battle is decided.
|
||||||
|
world.scene = Scene::Overworld;
|
||||||
|
world.scripted = false;
|
||||||
|
world.scripted_at = None;
|
||||||
|
world.switch = None;
|
||||||
|
for _ in 0..5 {
|
||||||
|
machine.observe_frame(&mut world);
|
||||||
|
}
|
||||||
|
world.scene = Scene::Battle { own_turn: false, forced_switch: false };
|
||||||
|
machine.observe_frame(&mut world);
|
||||||
|
settle(&mut machine, &mut world);
|
||||||
|
hand_back(&mut machine, &mut world);
|
||||||
|
assert!(!world.targets.blocked(world.map, north), "a challenge is not the road refusing");
|
||||||
|
assert!(world.pushes.is_empty(), "and the gap it was walking through is still ground");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_trainer_walking_up_teaches_the_ledgers_nothing() {
|
fn a_trainer_walking_up_teaches_the_ledgers_nothing() {
|
||||||
// The other half of the gym. A walk toward the leader crossed the Jr. Trainer's line of sight;
|
// The other half of the gym. A walk toward the leader crossed the Jr. Trainer's line of sight;
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,12 @@ fn pad(gb: &mut Emulator, adapter: &PokemonRedReward, label: &str) {
|
||||||
println!("- scene `{scene:?}`, player {player:?}, map {}x{}", size.width, size.height);
|
println!("- scene `{scene:?}`, player {player:?}, map {}x{}", size.width, size.height);
|
||||||
println!("- objective: {:?}", state.objective());
|
println!("- objective: {:?}", state.objective());
|
||||||
if let Some(objective) = state.objective() {
|
if let Some(objective) = state.objective() {
|
||||||
|
let from = palette::region_here(state)
|
||||||
|
.unwrap_or(geography::Region::whole(player.map));
|
||||||
println!(
|
println!(
|
||||||
"- `next_hop({:?}, {:#04x})` = {:?}, neighbours {:?}",
|
"- `next_step({from:?}, {:#04x})` = {:?}, neighbours {:?}",
|
||||||
geography::region_at(player.map, player.y),
|
|
||||||
objective.map,
|
objective.map,
|
||||||
geography::next_hop(geography::region_at(player.map, player.y), objective.map),
|
geography::next_step(from, objective.map),
|
||||||
geography::neighbours(player.map)
|
geography::neighbours(player.map)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1346,7 +1347,12 @@ fn dialog_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64
|
||||||
/// (comma-separated names) presses those buttons whenever they are dealt.
|
/// (comma-separated names) presses those buttons whenever they are dealt.
|
||||||
///
|
///
|
||||||
/// [`PokemonPalette`]: flybrain_gb::pokemon_red::macros::PokemonPalette
|
/// [`PokemonPalette`]: flybrain_gb::pokemon_red::macros::PokemonPalette
|
||||||
fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) {
|
fn route_survey(
|
||||||
|
gb: &mut Emulator,
|
||||||
|
adapter: &mut PokemonRedReward,
|
||||||
|
ms: &mut f64,
|
||||||
|
checkpoint: &flysim::store::Checkpoint,
|
||||||
|
) {
|
||||||
use flybrain_gb::MacroPalette;
|
use flybrain_gb::MacroPalette;
|
||||||
use flybrain_gb::pokemon_red::macros::cartridge::{FACINGS, MacroState, TalkTarget, TargetKey};
|
use flybrain_gb::pokemon_red::macros::cartridge::{FACINGS, MacroState, TalkTarget, TargetKey};
|
||||||
use flybrain_gb::pokemon_red::macros::path::Way;
|
use flybrain_gb::pokemon_red::macros::path::Way;
|
||||||
|
|
@ -1380,6 +1386,9 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64)
|
||||||
let mut entries = 0usize;
|
let mut entries = 0usize;
|
||||||
let mut arrived_at = 0usize;
|
let mut arrived_at = 0usize;
|
||||||
let mut last_map: Option<u8> = None;
|
let mut last_map: Option<u8> = None;
|
||||||
|
let save_rank: Option<u32> =
|
||||||
|
std::env::var("FLY_PROBE_SAVE_RANK").ok().and_then(|value| value.parse().ok());
|
||||||
|
let mut saved = false;
|
||||||
|
|
||||||
// `FLY_PROBE_HOLD=right:96,up:32` holds raw directions first and prints where the fly is
|
// `FLY_PROBE_HOLD=right:96,up:32` holds raw directions first and prints where the fly is
|
||||||
// every eight frames: what the cartridge does with a press, before any macro is asked.
|
// every eight frames: what the cartridge does with a press, before any macro is asked.
|
||||||
|
|
@ -1508,6 +1517,28 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64)
|
||||||
gb.run_frame().expect("a frame should complete");
|
gb.run_frame().expect("a frame should complete");
|
||||||
*ms += MS_PER_FRAME;
|
*ms += MS_PER_FRAME;
|
||||||
adapter.sample(gb, *ms);
|
adapter.sample(gb, *ms);
|
||||||
|
// `FLY_PROBE_SAVE_RANK=11` with `FLY_PROBE_SAVE` writes the first frame at or past that
|
||||||
|
// rung where the fly has the buttons in the overworld and no macro is running: a state the
|
||||||
|
// stream could be restored into next (row 59, after the badge), carried forward by the
|
||||||
|
// survey from the source checkpoint. The session ledgers are not in it, as they are in no
|
||||||
|
// checkpoint.
|
||||||
|
if let Some(want) = save_rank
|
||||||
|
&& !saved
|
||||||
|
&& adapter.progress().rank >= want
|
||||||
|
&& !running
|
||||||
|
&& matches!(observed.scene, flybrain_gb::SceneId::Overworld)
|
||||||
|
&& state::controllable(gb)
|
||||||
|
&& let Some(path) = std::env::var_os("FLY_PROBE_SAVE")
|
||||||
|
{
|
||||||
|
saved = true;
|
||||||
|
let bytes = save_state(checkpoint, gb, adapter, frame, &path);
|
||||||
|
println!(
|
||||||
|
"f{frame:<6} {:?} saved rank {} to `{}` ({bytes} bytes)",
|
||||||
|
state::player(gb).map(|p| (p.map, p.x, p.y)),
|
||||||
|
adapter.progress().rank,
|
||||||
|
std::path::Path::new(&path).display()
|
||||||
|
);
|
||||||
|
}
|
||||||
if single_refusals >= catch_after {
|
if single_refusals >= catch_after {
|
||||||
caught_at = Some(frame);
|
caught_at = Some(frame);
|
||||||
break;
|
break;
|
||||||
|
|
@ -1673,6 +1704,28 @@ fn separator_table(classes: &BTreeMap<(bool, bool, bool), u64>) -> String {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This state as a `FLYSIM01` checkpoint: the agent half is the source checkpoint's, unchanged --
|
||||||
|
/// the release box's own run carried forward by the stub, not a synthesised save -- and `.local/`
|
||||||
|
/// is not tracked, exactly as every other checkpoint in this workspace. Returns the size written.
|
||||||
|
fn save_state(
|
||||||
|
checkpoint: &flysim::store::Checkpoint,
|
||||||
|
gb: &mut Emulator,
|
||||||
|
adapter: &PokemonRedReward,
|
||||||
|
frame: usize,
|
||||||
|
path: &std::ffi::OsStr,
|
||||||
|
) -> usize {
|
||||||
|
let mut runtime = checkpoint.runtime.clone();
|
||||||
|
runtime.emulator = gb.export_state().expect("the emulator should export");
|
||||||
|
runtime.reward = adapter.export_state();
|
||||||
|
runtime.framebuffer = gb.framebuffer().to_vec();
|
||||||
|
runtime.emulator_frame = frame as u64;
|
||||||
|
let bytes =
|
||||||
|
flysim::store::encode(&checkpoint.agent, &runtime).expect("the envelope should encode");
|
||||||
|
flysim::store::write_atomic(std::path::Path::new(path), &bytes)
|
||||||
|
.expect("the checkpoint should be writable");
|
||||||
|
bytes.len()
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let Some(path) = std::env::var_os("FLY_ROM") else {
|
let Some(path) = std::env::var_os("FLY_ROM") else {
|
||||||
println!("FLY_ROM is not set, so there is nothing to probe.");
|
println!("FLY_ROM is not set, so there is nothing to probe.");
|
||||||
|
|
@ -1764,7 +1817,7 @@ fn main() {
|
||||||
// Row 57's pad survey: earn the session's ledgers from the checkpoint with the real palette,
|
// Row 57's pad survey: earn the session's ledgers from the checkpoint with the real palette,
|
||||||
// and read the frame the pad comes down to one refusing button on.
|
// and read the frame the pad comes down to one refusing button on.
|
||||||
if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "route") {
|
if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "route") {
|
||||||
route_survey(&mut gb, &mut adapter, &mut ms);
|
route_survey(&mut gb, &mut adapter, &mut ms, &checkpoint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1877,19 +1930,10 @@ fn main() {
|
||||||
// release box's own run carried forward by the stub, not a synthesised save -- and
|
// release box's own run carried forward by the stub, not a synthesised save -- and
|
||||||
// `.local/` is not tracked, exactly as every other checkpoint in this workspace.
|
// `.local/` is not tracked, exactly as every other checkpoint in this workspace.
|
||||||
if let Some(save) = std::env::var_os("FLY_PROBE_SAVE") {
|
if let Some(save) = std::env::var_os("FLY_PROBE_SAVE") {
|
||||||
let mut runtime = checkpoint.runtime.clone();
|
let bytes = save_state(&checkpoint, &mut gb, &adapter, frame, &save);
|
||||||
runtime.emulator = gb.export_state().expect("the emulator should export");
|
|
||||||
runtime.reward = adapter.export_state();
|
|
||||||
runtime.framebuffer = gb.framebuffer().to_vec();
|
|
||||||
runtime.emulator_frame = frame as u64;
|
|
||||||
let bytes = flysim::store::encode(&checkpoint.agent, &runtime)
|
|
||||||
.expect("the envelope should encode");
|
|
||||||
flysim::store::write_atomic(std::path::Path::new(&save), &bytes)
|
|
||||||
.expect("the checkpoint should be writable");
|
|
||||||
println!(
|
println!(
|
||||||
"\nWrote this state to `{}` ({} bytes).",
|
"\nWrote this state to `{}` ({bytes} bytes).",
|
||||||
std::path::Path::new(&save).display(),
|
std::path::Path::new(&save).display()
|
||||||
bytes.len()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The survey method on the one question the fix turns on: **what does the
|
// The survey method on the one question the fix turns on: **what does the
|
||||||
|
|
|
||||||
|
|
@ -3452,3 +3452,212 @@ fn tail_whip_at_its_limit_is_not_dealt_and_a_route_one_battle_is_won() {
|
||||||
);
|
);
|
||||||
assert!(won > 0, "no wild battle was won in {budget} frames: {battles:?}");
|
assert!(won > 0, "no wild battle was won in {budget} frames: {battles:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROUTE_4: u32 = 0x0f;
|
||||||
|
const MT_MOON_1F: u32 = 0x3b;
|
||||||
|
|
||||||
|
/// A rung-11 checkpoint just after the Boulder Badge, or `None` to skip.
|
||||||
|
///
|
||||||
|
/// Row 59's is the route survey's own: `examples/scene_probe.rs` driven from the row-58 checkpoint
|
||||||
|
/// with `FLY_PROBE_CATCH=route FLY_PROBE_PREFER="GO OBJECTIVE,TALK"` until rung 11, written by
|
||||||
|
/// `FLY_PROBE_SAVE_RANK=11` on the first safe overworld frame -- in the gym, beside BROCK.
|
||||||
|
fn badge_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||||
|
std::env::var_os("FLY_BADGE_CHECKPOINT").map(|path| {
|
||||||
|
flysim::store::load(std::path::Path::new(&path))
|
||||||
|
.expect("the checkpoint should be a FLYSIM01 envelope")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From the badge: the fly takes the road to Mt. Moon instead of walking the Pewter end of Route 3.
|
||||||
|
///
|
||||||
|
/// **What the row-58 review measured** (the route survey carried past the badge, 2026-09-23):
|
||||||
|
/// from about frame 68,000, Pewter City (39, 17) and Route 3 (0, 9) in a ring, `GO OBJECTIVE`
|
||||||
|
/// done on Route 3 538 times and `GO ROUTE` done on Pewter 537. The live fly was due to reach
|
||||||
|
/// the same state as soon as it earned the badge.
|
||||||
|
///
|
||||||
|
/// **What was wrong** (`infra/docs/macros-traps.md` row 59), two things stacked:
|
||||||
|
///
|
||||||
|
/// - the map graph had Route 4 east of Route 3 and a Mt. Moon door on Route 3. The headers put
|
||||||
|
/// Route 4 north of Route 3 and both of Mt. Moon's doors on Route 4, so Route 3's north edge,
|
||||||
|
/// the one road to the mountain, named no map and was nobody's first hop; and Route 4's two
|
||||||
|
/// sides, which the mountain cuts apart, were one node;
|
||||||
|
/// - Route 3's first trainer closed his challenge onto five frames of plain overworld before the
|
||||||
|
/// battle was decided, and the walk he interrupted was written as a refusal on the first of
|
||||||
|
/// them: (11, 6), the one gap between the road's west end and the rest of it, walled for the
|
||||||
|
/// session.
|
||||||
|
///
|
||||||
|
/// The claims, none of them about which button the fly presses:
|
||||||
|
///
|
||||||
|
/// - the fly is on **Route 4** and at **Mt. Moon's door** (map `0x3b`) inside the budget;
|
||||||
|
/// - Pewter City and Route 3 are **not a ring**: under forty crossings between them, against
|
||||||
|
/// over a thousand on the base.
|
||||||
|
///
|
||||||
|
/// ```sh
|
||||||
|
/// FLY_ROM=/path/to/pokemon-red.gb \
|
||||||
|
/// FLY_BADGE_CHECKPOINT=.local/checkpoints/survey-rank11-row59.checkpoint \
|
||||||
|
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture the_road_to_mt_moon
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn the_road_to_mt_moon_is_not_a_ring_at_the_pewter_end_from_the_badge_checkpoint() {
|
||||||
|
let rom = skip_without_rom!();
|
||||||
|
let Some(checkpoint) = badge_checkpoint() else {
|
||||||
|
eprintln!("skipped: no FLY_BADGE_CHECKPOINT");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
|
||||||
|
assert!(run.adapter.progress().rank >= 11, "the checkpoint is past the badge");
|
||||||
|
|
||||||
|
let mut crossings = 0u32;
|
||||||
|
let mut on_route_4: Option<u32> = None;
|
||||||
|
let mut at_mt_moon: Option<u32> = None;
|
||||||
|
let mut previous = run.map();
|
||||||
|
for frame in 0..288_000u32 {
|
||||||
|
run.frame();
|
||||||
|
let map = run.map();
|
||||||
|
if map != previous {
|
||||||
|
if (previous == PEWTER_CITY && map == ROUTE_3) || (previous == ROUTE_3 && map == PEWTER_CITY) {
|
||||||
|
crossings += 1;
|
||||||
|
}
|
||||||
|
previous = map;
|
||||||
|
}
|
||||||
|
if on_route_4.is_none() && map == ROUTE_4 {
|
||||||
|
on_route_4 = Some(frame);
|
||||||
|
}
|
||||||
|
if at_mt_moon.is_none() && map == MT_MOON_1F {
|
||||||
|
at_mt_moon = Some(frame);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let progress = run.adapter.progress();
|
||||||
|
eprintln!(
|
||||||
|
"{:.1} brain minutes: Route 4 at {on_route_4:?}, Mt. Moon at {at_mt_moon:?}, Pewter / Route 3 \
|
||||||
|
crossings {crossings}, rank {} ({}), route {:?}, macros {:?}",
|
||||||
|
run.ms / 60_000.0,
|
||||||
|
progress.rank,
|
||||||
|
progress.rank_label,
|
||||||
|
run.route,
|
||||||
|
run.started
|
||||||
|
);
|
||||||
|
assert!(crossings < 40, "{crossings} crossings between Pewter City and Route 3: {:?}", run.started);
|
||||||
|
assert!(on_route_4.is_some(), "the fly never reached Route 4: {:?}", run.route);
|
||||||
|
assert!(at_mt_moon.is_some(), "the fly never reached Mt. Moon's door: {:?}", run.route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route 4's two sides, read off the cartridge: the warp table the geography's pieces are keyed
|
||||||
|
/// by, and the piece the fly is standing in on arrival from Route 3.
|
||||||
|
///
|
||||||
|
/// The table's door indices and tiles are the disassembly's (`data/maps/objects/Route4.asm`); this
|
||||||
|
/// is the same three warps read from `wWarpEntries` on the loaded map, and the piece named by the
|
||||||
|
/// decoded grid ([`geography::region_on`]) rather than by the doors.
|
||||||
|
#[test]
|
||||||
|
fn route_4s_doors_and_sides_are_the_cartridges_from_the_badge_checkpoint() {
|
||||||
|
use flybrain_gb::pokemon_red::macros::geography::{self, Region};
|
||||||
|
let rom = skip_without_rom!();
|
||||||
|
let Some(checkpoint) = badge_checkpoint() else {
|
||||||
|
eprintln!("skipped: no FLY_BADGE_CHECKPOINT");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
|
||||||
|
let mut read = None;
|
||||||
|
for _ in 0..216_000u32 {
|
||||||
|
run.frame();
|
||||||
|
if run.map() != ROUTE_4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(player) = flybrain_gb::pokemon_red::state::player(&mut run.gb) else { continue };
|
||||||
|
let Ok(grid) = flybrain_gb::pokemon_red::state::map_grid(&mut run.gb) else { continue };
|
||||||
|
if u32::from(player.map) != ROUTE_4 || grid.map() != player.map {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let warps = flybrain_gb::pokemon_red::state::warps(&mut run.gb);
|
||||||
|
read = Some((player, warps, geography::region_on(player.map, player.x, player.y, Some(&grid))));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Some((player, warps, region)) = read else {
|
||||||
|
panic!("the fly never stood on Route 4 with its grid decoded: {:?}", run.route);
|
||||||
|
};
|
||||||
|
eprintln!("on Route 4 at ({}, {}), piece {region:?}, warps {warps:?}", player.x, player.y);
|
||||||
|
let doors: Vec<(u8, u8, u8)> =
|
||||||
|
warps.iter().map(|warp| (warp.x, warp.y, warp.destination_map)).collect();
|
||||||
|
assert_eq!(
|
||||||
|
doors,
|
||||||
|
vec![(11, 5, 0x44), (18, 5, 0x3b), (24, 5, 0x3c)],
|
||||||
|
"the Pokécenter, the cave mouth and B1F's exit, in the table's order"
|
||||||
|
);
|
||||||
|
assert_eq!(region, Region::piece(0x0f, 0), "arrived from Route 3, on the cave mouth's side");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MT_MOON_POKECENTER: u32 = 0x44;
|
||||||
|
|
||||||
|
/// The live checkpoint from inside row 59's ring (Route 4, rank 12 MT. MOON), or `None` to skip.
|
||||||
|
fn mt_moon_live_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||||
|
std::env::var_os("FLY_MT_MOON_CHECKPOINT").map(|path| {
|
||||||
|
flysim::store::load(std::path::Path::new(&path))
|
||||||
|
.expect("the checkpoint should be a FLYSIM01 envelope")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From the live checkpoint taken inside the ring: the fly goes into Mt. Moon instead of in and
|
||||||
|
/// out of the Pokécenter beside it.
|
||||||
|
///
|
||||||
|
/// **What was live** (2026-09-23 22:20 UTC, v0.6.0, rank 12 MT. MOON, the objective Cerulean City):
|
||||||
|
/// on Route 4, per ten minutes `GO ROUTE` 215, `GO OBJECTIVE` 113, `GO OUT` 103, five distinct
|
||||||
|
/// macros and two new tiles. Route 4 was one node on the map graph with Cerulean off its east
|
||||||
|
/// edge, which Mt. Moon cuts off from the cave mouth's side, so the objective aimed at ground no
|
||||||
|
/// walk could reach and the Pokécenter door was the way out that was left. The route survey from
|
||||||
|
/// this checkpoint on `main` walks Route 4 and the Pokécenter 930 times in 72,000 frames.
|
||||||
|
///
|
||||||
|
/// The claims, none of them about which button the fly presses, over twenty brain minutes on the
|
||||||
|
/// stub rotation: the fly is **inside Mt. Moon** (map `0x3b`), and Route 4's west side and its two
|
||||||
|
/// doors, the Pokécenter and the cave mouth, are **not a ring**: under twenty-five crossings in all.
|
||||||
|
/// The base makes 56 (18 through the Pokécenter's door, 38 through the cave's); the rotation walks
|
||||||
|
/// into the cave on the base too, and back out, and in, because from 1F the graph's Cerulean was
|
||||||
|
/// Route 4's east edge beside it.
|
||||||
|
///
|
||||||
|
/// ```sh
|
||||||
|
/// FLY_ROM=/path/to/pokemon-red.gb \
|
||||||
|
/// FLY_MT_MOON_CHECKPOINT=.local/checkpoints/release-rank12-route4.checkpoint \
|
||||||
|
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture the_fly_goes_into_mt_moon
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn the_fly_goes_into_mt_moon_from_the_live_route_4_checkpoint() {
|
||||||
|
let rom = skip_without_rom!();
|
||||||
|
let Some(checkpoint) = mt_moon_live_checkpoint() else {
|
||||||
|
eprintln!("skipped: no FLY_MT_MOON_CHECKPOINT");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
|
||||||
|
assert!(run.adapter.progress().rank >= 12, "the checkpoint is the rung the ring was on");
|
||||||
|
|
||||||
|
let mut crossings = 0u32;
|
||||||
|
let mut in_mt_moon: Option<u32> = None;
|
||||||
|
let mut on_route_4 = 0u32;
|
||||||
|
let mut previous = run.map();
|
||||||
|
for frame in 0..72_000u32 {
|
||||||
|
run.frame();
|
||||||
|
let map = run.map();
|
||||||
|
if map != previous {
|
||||||
|
let door = |other: u32| other == MT_MOON_POKECENTER || other == MT_MOON_1F;
|
||||||
|
if (previous == ROUTE_4 && door(map)) || (door(previous) && map == ROUTE_4) {
|
||||||
|
crossings += 1;
|
||||||
|
}
|
||||||
|
previous = map;
|
||||||
|
}
|
||||||
|
if in_mt_moon.is_none() && map == MT_MOON_1F {
|
||||||
|
in_mt_moon = Some(frame);
|
||||||
|
}
|
||||||
|
if map == ROUTE_4 {
|
||||||
|
on_route_4 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"{:.1} brain minutes: Mt. Moon at {in_mt_moon:?}, crossings of Route 4's west doors {crossings}, \
|
||||||
|
frames on Route 4 {on_route_4}, rank {}, route {:?}, macros {:?}",
|
||||||
|
run.ms / 60_000.0,
|
||||||
|
run.adapter.progress().rank,
|
||||||
|
run.route,
|
||||||
|
run.started
|
||||||
|
);
|
||||||
|
assert!(crossings < 25, "{crossings} crossings of Route 4's west doors: {:?}", run.started);
|
||||||
|
assert!(in_mt_moon.is_some(), "the fly never went into Mt. Moon: {:?}", run.route);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue