Merge fix/loop-row61: a trainer's challenge is the cartridge's until its battle is over
This commit is contained in:
commit
d0ce10e3c9
7 changed files with 375 additions and 3 deletions
|
|
@ -1608,6 +1608,48 @@ split map has the pieces its ground has, and a frame that was the cartridge's is
|
|||
fly's. The decoder, the reward catalog, the adapter version, the roles and the compatibility
|
||||
string are untouched.
|
||||
|
||||
### 12.25 A trainer's challenge is the cartridge's until its battle is over (2026-09-23, row 61)
|
||||
|
||||
Live on v0.5.5, rung 9, for twenty minutes: `GO OBJECTIVE` into Viridian Forest's south gate (map
|
||||
50, `$32`), `GO OUT` straight back onto Route 2, `GO WARP` back from the forest, `GO OBJECTIVE
|
||||
blocked` in the forest, no reward. Reproduced with the route survey from the live checkpoint
|
||||
(uniform choice per hold, xorshift seed 7), which walks the same ring for twenty brain minutes.
|
||||
|
||||
- **The ring's cause was a wall, not the gate.** The forest's only road to its north gate is a
|
||||
two-wide corridor at x = 1-2; a Bug Catcher stands on (2, 18) facing west. A walk up the
|
||||
corridor steps onto (1, 18), the trainer takes the joypad, and row 58's held push-back waits for
|
||||
the joypad to come back. `DisplayEnemyTrainerTextAndStartBattle` clears `wJoyIgnore` before the
|
||||
challenge text and `StartTrainerBattle` writes `wCurOpponent` only after that text's close-down:
|
||||
**five frames** with no box, no script bit and `wCurOpponent` zero, which the seam read as the
|
||||
fly's overworld. The push was written there; (1, 18) went into the pushed ledger, which has no
|
||||
window, and from then on every walk to the north gate had no road. `GO OBJECTIVE` walked to the
|
||||
nearest reachable tile, a dead end at (6, 1), and was blocked; `GO WARP`'s last tier took the
|
||||
south gate, whose `GO OUT` is Route 2, whose `GO OBJECTIVE` is the gate.
|
||||
- **The fact is `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE`**: set by `CheckFightingMapTrainers`
|
||||
on the "!", cleared at `.battleOccurred` after every battle (before the blackout check). It
|
||||
covers more than the five frames: the "!" bubble runs about sixty frames before `wJoyIgnore` is
|
||||
set, and they read as the fly's overworld too -- about sixty-six free-looking frames per
|
||||
engagement, measured. An overworld frame with the bit set is `Unknown` in the macros' own
|
||||
scene, with no text box, so the pad is empty, no ground is recorded and no held entry is
|
||||
decided on it. A trainer talked to by the fly never sets it; its `wCurOpponent` is written
|
||||
inside the text.
|
||||
- **On main the five frames are already row 59's** (12.24: a held push-back is written only after
|
||||
thirty frames of overworld), and that alone keeps (1, 18) clear. This row is the cartridge-fact
|
||||
layer under it: the pad is empty through the bubble as well, no ground is recorded, and it does
|
||||
not depend on the gap staying under thirty frames.
|
||||
- **The macros' reading only.** `controllable` and `scene::detect` are shared with the reward
|
||||
adapter and do not change; `PokeState`'s `scene` and `scripted` read the bit beside them. In
|
||||
macros mode the feed's `game.scene` is the palette's, so it reads `unknown` on those frames,
|
||||
which is what the contract says of a frame the cartridge is driving.
|
||||
- **The gates were modelled right.** Both forest gates are on the graph and `next_hop` answers
|
||||
the forest from the south gate and Route 2 from the north one. The south gate's `GO OUT` is the
|
||||
"a room has to be leavable" tier, a way back that is the fly's choice: with the corridor open,
|
||||
the survey seed that walked into the gate twelve times still earned the badge, on both arms.
|
||||
|
||||
Nothing is ranked or pressed for the fly: frames that were never the fly's deal nothing. 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
|
||||
## 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.")
|
||||
|
|
|
|||
|
|
@ -2967,3 +2967,87 @@ survey and the three ROM tests, which is a deviation from the ethos check's lett
|
|||
- `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.
|
||||
|
||||
## 2026-09-23, row 61: the forest's south gate, in and out
|
||||
|
||||
### What was live
|
||||
|
||||
Map 50 (`VIRIDIAN_FOREST_SOUTH_GATE`, `$32`) and Route 2's south half, rung 9, v0.5.5, about 20:45
|
||||
to 21:05 UTC: `GO OBJECTIVE` into the gate (0.3 s), `GO OUT` back onto Route 2 (0.2 s), `GO WARP`
|
||||
back out of the forest, `GO OBJECTIVE blocked` in the forest, `GO ROUTE` refused now and then; no
|
||||
reward and about eight new tiles in twenty minutes. The checkpoint is the fly at (5, 1) in the
|
||||
gate, under the forest's doorway, objective Pewter City; `next_hop` answers the forest, correctly.
|
||||
|
||||
### The survey: what walled the road
|
||||
|
||||
The route survey from the checkpoint (`FLY_PROBE_CATCH=route`, uniform choice per hold,
|
||||
`FLY_PROBE_RNG=7`, 72,000 frames) walks the live ring and never leaves rung 9. Caught at the
|
||||
refusal (`FLY_PROBE_CATCH_FRAME`, `FLY_PROBE_WHOLE=1`): the forest's only road to the north gate
|
||||
is the corridor at x = 1-2 from row 22 up to row 0; the Bug Catcher of
|
||||
`EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2` stands on (2, 18) facing west, and **(1, 18) was in the
|
||||
pushed ledger**. It was written at frame 16,389: a `GO ITEM` walk stepped onto (1, 18), the trainer
|
||||
took the joypad, and his text closed onto five frames with no box, no script bit and
|
||||
`wCurOpponent` zero (`StartTrainerBattle` runs after `DisplayTextID`'s close-down). Row 58's held
|
||||
entry was decided on the first of them. The "!" bubble before it, about sixty frames before
|
||||
`wJoyIgnore` is set, reads the same way: about sixty-six free-looking frames per engagement. The pushed ledger has no window, so the north gate had no
|
||||
road for the session; `GO OBJECTIVE` walked to the nearest reachable tile, (6, 1), a dead end, and
|
||||
was blocked, and the last tiers walked the fly back to the south gate and Route 2. Every base
|
||||
survey arm walls (1, 18); only a fly that has to come back up the corridor is trapped by it.
|
||||
|
||||
| # | trap | trigger | test | fix, or why it is left |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 61 | the "!" bubble and the frames between a sighted trainer's challenge text and `StartTrainerBattle` (about 66 per engagement) read as the fly's overworld: a pad is dealt, ground recorded, and row 58's held push-back written, walling the tile the trainer fired on for the session | any trainer whose line of sight a walk crosses; Viridian Forest's (1, 18), the one free tile of the corridor to the north gate | `a_trainers_challenge_is_the_cartridges_until_its_battle_is_over`, `the_frames_between_a_trainers_text_and_its_battle_deal_no_pad_and_record_no_ground`, `a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate` (ROM) | **fixed** in the macro seam: `state::trainer_engaged` reads `wStatusFlags7` bit 3 (`BIT_TRAINER_BATTLE`, set by `CheckFightingMapTrainers`, cleared at `.battleOccurred`); `PokeState`'s `scene` is `Unknown` on an overworld frame with it set and `scripted` is true. `controllable` and `scene::detect` unchanged; in macros mode `game.scene` reads `unknown` on those frames. `docs/design/macros.md` 12.25 |
|
||||
| 61b | the south gate deals `GO OUT` (back to Route 2) beside the forest door the objective's road takes | the "a room has to be leavable" tier in a gate whose way on is a passage | -- | **left**: a way back is the fly's choice; with the corridor open no survey stays on it |
|
||||
|
||||
### Before and after
|
||||
|
||||
The route survey, 72,000 frames (20.1 brain minutes), base `main` vs branch:
|
||||
|
||||
| driver | base | branch |
|
||||
| --- | --- | --- |
|
||||
| uniform, seed 7 (the live ring) | rung 9, 1,202 tiles, north gate never, (1, 18) walled | **BOULDER BADGE**, 1,647 tiles, north gate f20273, Pewter f23756 |
|
||||
| uniform, default seed | rung 10, 1,356 | rung 11, 1,476 |
|
||||
| `GO OBJECTIVE` preferred | rung 11, 1,529 | rung 12, 1,492 |
|
||||
| `GO OUT`, `GO OBJECTIVE` preferred | rung 11, 1,376 | rung 11, 1,446 |
|
||||
| uniform, seed 12345 | rung 11, 1,765 | identical |
|
||||
|
||||
(1, 18) is walled on every base arm and on no branch arm. The ROM-gated run (the seed-7 driver,
|
||||
40,000 frames): base walled at frame 16,389, rung 9, **fails**; branch: north gate f20272, Route 2
|
||||
f21209, Pewter f23755, passes.
|
||||
|
||||
The stub-readout trap hunt (30 brain minutes, service frame since FND-01; `main` `4d82f7d` vs
|
||||
branch): both arms rung 10, 362 distinct tiles, 147 macros all done, 11 of 113 windows flagged.
|
||||
**The stub does not walk the ring**: it passes the corridor northward and never comes back, so
|
||||
the only difference is 134 overworld frames the branch reads `Unknown`, the trainer gaps. The
|
||||
survey and the ROM test are the reproduction.
|
||||
|
||||
### Other gates
|
||||
|
||||
The north gate's `GO OUT` is toward Pewter (tier 2) and its forest door is withheld; the Route 2
|
||||
gate, Diglett's Cave's Route 2 house and the Route 22 gate are off the graph (every door `LAST_MAP`
|
||||
but Diglett's passage). The Route 22 gate is the one building with `LAST_MAP` on both sides of two
|
||||
different maps (its script sets `wLastMap` by row: under 4 is Route 23); `outdoor_of` cannot name
|
||||
both and will need a per-door answer when a rung routes through it. The trainer gap is not a gate
|
||||
fact: every sighted trainer has it.
|
||||
|
||||
### Overlap
|
||||
|
||||
Row 59 (59f, merged in v0.6.1) found the same five frames on Route 3 and holds the push until
|
||||
thirty frames of overworld; on main that alone keeps (1, 18) clear. This row is the cartridge-fact
|
||||
layer under it: the pad is empty through the bubble too, no ground is recorded, and it does not
|
||||
depend on the gap staying under thirty frames. The ROM test asserts no pad on an overworld frame
|
||||
with the bit set, which row 59 alone does not meet.
|
||||
|
||||
### Gates
|
||||
|
||||
On `main` `510727c` (v0.6.1, row 59 merged):
|
||||
|
||||
- `cargo test --release -p flybrain-gb` with `FLY_ROM`: 461 passed, 0 failed.
|
||||
- `rom_macros_mode` with the row-61 and row-59 checkpoints: row 61's test and row 59's three pass.
|
||||
Row 61's test on the branch with `trainer_engaged` neutered (main's behaviour, row 59's settle
|
||||
alone): the wall stays clear, and a pad is dealt on 67 of 67 challenge frames -- **fails**; on
|
||||
the branch 0 of 67 -- passes.
|
||||
- `cargo clippy --workspace --all-targets -- -D warnings`: clean.
|
||||
- `npm test` 663 passed; `npm run typecheck` clean; `infra/tests/lint.sh` ALL CHECKS PASSED.
|
||||
- `flysim --print-compatibility`, raw and macros: 648 bytes, sha256 `8ce67b97...a8f68`, the same
|
||||
as `main`. Decoder, reward catalog, adapter version and roles untouched.
|
||||
|
|
|
|||
|
|
@ -616,6 +616,28 @@ mod tests {
|
|||
assert_eq!(palette.observe(&mut wram, &NoLedger).scene, SceneId::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_frames_between_a_trainers_text_and_its_battle_deal_no_pad_and_record_no_ground() {
|
||||
// Row 61, Viridian Forest. A trainer who saw the fly: its "!" bubble and the five frames
|
||||
// after its challenge text read as an ordinary overworld -- no box, no script bit,
|
||||
// `wJoyIgnore` and `wCurOpponent` zero. The pad was dealt there and the push-back the
|
||||
// fly's walk had earned when the trainer took the joypad was written there, walling the
|
||||
// one free tile of the corridor to the north gate for the session.
|
||||
let mut wram = Wram::overworld();
|
||||
wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 1 << 3);
|
||||
let mut palette = PokemonPalette::new(7);
|
||||
let engaged = palette.observe(&mut wram, &NoLedger);
|
||||
assert_eq!(engaged.scene, SceneId::Unknown, "the cartridge's, inside the challenge");
|
||||
assert!(engaged.bindings.is_empty(), "nothing to press: {:?}", engaged.bindings);
|
||||
assert_eq!(palette.stood(), 0, "and no ground recorded from it");
|
||||
|
||||
// `.battleOccurred` clears the bit and the overworld is the fly's again.
|
||||
wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 0);
|
||||
let own = palette.observe(&mut wram, &NoLedger);
|
||||
assert_eq!(own.scene, SceneId::Overworld);
|
||||
assert_eq!(palette.stood(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_teleport_pad_is_not_a_tear() {
|
||||
// Saffron Gym and two Silph Co. floors warp to themselves. Standing on a pad whose
|
||||
|
|
|
|||
|
|
@ -145,6 +145,11 @@ pub mod poke {
|
|||
/// zero in the overworld, non-zero from the frame a trainer's challenge closes to the end of
|
||||
/// the battle, including the 219 frames of the battle transition in between.
|
||||
pub const CUR_OPPONENT: u16 = super::ram::wBattleType - 1;
|
||||
/// `constants/ram_constants.asm`: `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE` (row 61). Set by
|
||||
/// `CheckFightingMapTrainers` (`home/trainers.asm`) on the frame a trainer sees the player,
|
||||
/// cleared at `.battleOccurred` (`home/overworld.asm`) once the battle is over -- before the
|
||||
/// blackout check, so a lost battle clears it too. Nothing else writes it.
|
||||
pub const TRAINER_BATTLE_STATUS7: u8 = 1 << 3;
|
||||
|
||||
/// `constants/battle_constants.asm`: the non-volatile status byte.
|
||||
pub const SLP_MASK: u8 = 0b111;
|
||||
|
|
@ -1841,8 +1846,14 @@ impl<'a> PokeState<'a> {
|
|||
}
|
||||
|
||||
impl GameState for PokeState<'_> {
|
||||
/// [`super::scene::detect`], except that an overworld frame inside a trainer's challenge is
|
||||
/// the cartridge's ([`trainer_engaged`], row 61): section 12.13's `Unknown` with no text box,
|
||||
/// an empty pad the fly waits out, and no frame a held push-back is decided on.
|
||||
fn scene(&mut self) -> Scene {
|
||||
super::scene::detect(self.memory)
|
||||
match super::scene::detect(self.memory) {
|
||||
Scene::Overworld if trainer_engaged(self.memory) => Scene::Unknown,
|
||||
scene => scene,
|
||||
}
|
||||
}
|
||||
|
||||
fn player(&mut self) -> Option<Player> {
|
||||
|
|
@ -1910,6 +1921,27 @@ impl GameState for PokeState<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a trainer who saw the player is between its "!" and the end of its battle (row 61).
|
||||
///
|
||||
/// Two stretches of that window read as an overworld the fly owned, both measured in Viridian
|
||||
/// Forest: the "!" bubble, about sixty frames, drawn before `CheckFightingMapTrainers` sets
|
||||
/// `wJoyIgnore`; and five frames after the challenge text, because
|
||||
/// `DisplayEnemyTrainerTextAndStartBattle` (`home/trainers.asm`) clears `wJoyIgnore` before the
|
||||
/// text and calls `StartTrainerBattle`, which writes `wCurOpponent`, only after the text's
|
||||
/// close-down has redrawn the map. About sixty-six frames per engagement, every bit
|
||||
/// [`controllable`] reads clear. The push-back a walk earned when the trainer took the joypad (row
|
||||
/// 58's held entry) was written on the first frame after the text: the one free tile beside the
|
||||
/// trainer, in the only corridor to the forest's north gate, walled for the session.
|
||||
///
|
||||
/// **The macros' reading only.** [`controllable`] and [`super::scene::detect`] are shared with
|
||||
/// the reward adapter (the talk payout's "ready" test) and do not change; [`PokeState`]'s own
|
||||
/// `scene` and `scripted` read this beside them. In macros mode the feed's `game.scene` is the
|
||||
/// palette's, so it reads `unknown` on these frames, as the contract has it for a frame the
|
||||
/// cartridge is driving.
|
||||
pub fn trainer_engaged(memory: &mut dyn MemoryReader) -> bool {
|
||||
read(memory, ram::wStatusFlags7) & poke::TRAINER_BATTLE_STATUS7 != 0
|
||||
}
|
||||
|
||||
/// The cartridge tables on their defaults, and the exploration ledger wired through.
|
||||
///
|
||||
/// `pokemon_red/macros/cartridge.rs` defaults every [`MacroState`] method and every default
|
||||
|
|
@ -1923,7 +1955,7 @@ impl GameState for PokeState<'_> {
|
|||
/// taken rather than at the nearest door (`docs/design/macros.md` section 3).
|
||||
impl MacroState for PokeState<'_> {
|
||||
fn scripted(&mut self) -> bool {
|
||||
!controllable(self.memory)
|
||||
!controllable(self.memory) || trainer_engaged(self.memory)
|
||||
}
|
||||
|
||||
fn text_open(&mut self) -> bool {
|
||||
|
|
|
|||
|
|
@ -1222,3 +1222,34 @@ fn a_refusal_that_cannot_be_read_is_not_reported() {
|
|||
read.set(ram::wEnemyMonStatMods + 1, 1);
|
||||
assert!(PokeState::new(&mut read).move_without_effect(TAIL_WHIP.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_trainers_challenge_is_the_cartridges_until_its_battle_is_over() {
|
||||
use crate::pokemon_red::macros::state::GameState;
|
||||
// Row 61. A trainer who saw the fly: its "!" bubble (before `wJoyIgnore` is set) and the five
|
||||
// frames after its text (before `wCurOpponent` is) have every bit `controllable` reads clear.
|
||||
// The macros read them as the cartridge's; the shared readings the reward adapter uses do not
|
||||
// move.
|
||||
let mut wram = Wram::overworld();
|
||||
assert_eq!(PokeState::new(&mut wram).scene(), Scene::Overworld);
|
||||
assert!(!PokeState::new(&mut wram).scripted());
|
||||
|
||||
wram.set(ram::wStatusFlags7, poke::TRAINER_BATTLE_STATUS7);
|
||||
assert!(trainer_engaged(&mut wram));
|
||||
assert_eq!(PokeState::new(&mut wram).scene(), Scene::Unknown, "inside the challenge");
|
||||
assert!(PokeState::new(&mut wram).scripted(), "and the fly is not its own master");
|
||||
assert!(controllable(&mut wram), "the adapter's gate is unchanged");
|
||||
assert_eq!(crate::pokemon_red::scene::detect(&mut wram), Scene::Overworld, "and the shared scene");
|
||||
|
||||
// The challenge's own text is still a conversation to advance.
|
||||
wram.dialogue_box();
|
||||
assert_eq!(PokeState::new(&mut wram).scene(), Scene::Dialog);
|
||||
|
||||
// `.battleOccurred` clears the bit; the other bits of the byte are not a challenge:
|
||||
// `BIT_NO_MAP_MUSIC` after a rival, `BIT_USE_CUR_MAP_SCRIPT` from a trainer talked to.
|
||||
let mut after = Wram::overworld();
|
||||
after.set(ram::wStatusFlags7, (1 << 1) | (1 << 4) | (1 << 7));
|
||||
assert!(!trainer_engaged(&mut after));
|
||||
assert_eq!(PokeState::new(&mut after).scene(), Scene::Overworld);
|
||||
assert!(!PokeState::new(&mut after).scripted());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1389,6 +1389,10 @@ fn route_survey(
|
|||
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_CATCH_FRAME=41600` reads the first frame at or after that one on which the fly
|
||||
// has the buttons and no macro is running (row 61: the frame a walk in the forest refused).
|
||||
let catch_frame: Option<usize> =
|
||||
std::env::var("FLY_PROBE_CATCH_FRAME").ok().and_then(|value| value.parse().ok());
|
||||
|
||||
// `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.
|
||||
|
|
@ -1543,6 +1547,14 @@ fn route_survey(
|
|||
caught_at = Some(frame);
|
||||
break;
|
||||
}
|
||||
if catch_frame.is_some_and(|at| frame >= at)
|
||||
&& !running
|
||||
&& matches!(observed.scene, flybrain_gb::SceneId::Overworld)
|
||||
&& !observed.bindings.is_empty()
|
||||
{
|
||||
caught_at = Some(frame);
|
||||
break;
|
||||
}
|
||||
if let (Some(want), Some(player)) = (catch_map, player) {
|
||||
if player.map == want && last_map != Some(want) {
|
||||
entries += 1;
|
||||
|
|
@ -1581,6 +1593,8 @@ fn route_survey(
|
|||
frame as f64 * MS_PER_FRAME / 60_000.0,
|
||||
if single_refusals >= catch_after {
|
||||
"one button, refused twenty holds running".to_string()
|
||||
} else if catch_frame.is_some_and(|at| frame >= at) {
|
||||
format!("the first free frame at or after {catch_frame:?}")
|
||||
} else {
|
||||
format!("arrival {entries} on map {catch_map:?}")
|
||||
}
|
||||
|
|
@ -1634,7 +1648,9 @@ fn route_survey(
|
|||
// A room small enough to print whole is printed whole, with its people on it (row 58:
|
||||
// the gym's leader is twelve rows from the door).
|
||||
let size = state.map_size().expect("a loaded map");
|
||||
let whole = size.width <= 24 && size.height <= 24;
|
||||
// `FLY_PROBE_WHOLE=1` prints a bigger map whole too (row 61: the forest is 34 by 48).
|
||||
let whole = (size.width <= 24 && size.height <= 24)
|
||||
|| std::env::var("FLY_PROBE_WHOLE").is_ok_and(|value| value == "1");
|
||||
let people: Vec<(Tile, TalkTarget)> = path::person_targets(state)
|
||||
.into_iter()
|
||||
.chain(path::offscreen_person_targets(state))
|
||||
|
|
|
|||
|
|
@ -3661,3 +3661,148 @@ fn the_fly_goes_into_mt_moon_from_the_live_route_4_checkpoint() {
|
|||
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);
|
||||
}
|
||||
|
||||
fn row61_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||
std::env::var_os("FLY_ROW61_CHECKPOINT").map(|path| {
|
||||
flysim::store::load(std::path::Path::new(&path))
|
||||
.expect("the checkpoint should be a FLYSIM01 envelope")
|
||||
})
|
||||
}
|
||||
|
||||
/// Viridian Forest's south gate, from the checkpoint pulled during the ring.
|
||||
///
|
||||
/// **What was live** (2026-09-23, v0.5.5, rung 9 after the reset to milestone 1): for twenty
|
||||
/// minutes `GO OBJECTIVE` into the forest's south gate and `GO OUT` straight back onto Route 2,
|
||||
/// `GO WARP` back from the forest, `GO OBJECTIVE blocked` in the forest, no reward
|
||||
/// (`infra/docs/macros-traps.md` row 61). The forest's only road to its north gate is a two-wide
|
||||
/// corridor at x = 1-2 with a Bug Catcher standing on (2, 18) facing west. A walk up the corridor
|
||||
/// steps onto (1, 18) and the trainer takes the joypad. His "!" bubble (about sixty frames, before
|
||||
/// `wJoyIgnore` is set) and the five frames after his text (before `wCurOpponent` is) read as the
|
||||
/// fly's own overworld: the push-back the walk had earned was written on the first frame after the
|
||||
/// text, (1, 18) was walled for the session, and every later walk to the north gate had no road.
|
||||
/// Row 59's thirty-frame settle alone keeps the wall out; this row reads `BIT_TRAINER_BATTLE`.
|
||||
///
|
||||
/// The driver is the route survey's: the real palette, one uniform choice per hold, xorshift
|
||||
/// seeded 7 -- a harness choice, not the fly's. Before rows 59 and 61 it never reached the north
|
||||
/// gate in 72,000 frames. The claims:
|
||||
///
|
||||
/// - **no button is offered on an overworld frame inside a trainer's challenge**
|
||||
/// (`wStatusFlags7` bit 3 set), and the run does reach such frames;
|
||||
/// - **no tile of the forest is walled by a trainer's challenge**: (1, 18) never enters the pushed
|
||||
/// ledger;
|
||||
/// - **the fly goes through the north gate onto Route 2 and into Pewter City** inside the budget.
|
||||
///
|
||||
/// ```sh
|
||||
/// FLY_ROM=/path/to/pokemon-red.gb FLY_ACCEPT_ADAPTERS=pokered-unique8-v6 \
|
||||
/// FLY_ROW61_CHECKPOINT=<the rank-9 checkpoint pulled during the ring, under .local/checkpoints> \
|
||||
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture forests_north_gate
|
||||
/// ```
|
||||
#[test]
|
||||
fn a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate() {
|
||||
use flybrain_gb::pokemon_red::macros::PokemonPalette;
|
||||
use flybrain_gb::pokemon_red::macros::cartridge::{PushedLedger, Tile};
|
||||
use flybrain_gb::pokemon_red::state;
|
||||
use flybrain_gb::{MacroPalette, MemoryReader, Started};
|
||||
const FOREST: u8 = 0x33;
|
||||
const NORTH_GATE: u8 = 0x2f;
|
||||
const ROUTE_2: u8 = 0x0d;
|
||||
const PEWTER: u8 = 0x02;
|
||||
let rom = skip_without_rom!();
|
||||
let Some(checkpoint) = row61_checkpoint() else {
|
||||
eprintln!("skipped: no FLY_ROW61_CHECKPOINT");
|
||||
return;
|
||||
};
|
||||
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
|
||||
assert_eq!(run.map(), 0x32, "the checkpoint is the forest's south gate");
|
||||
|
||||
let budget = 40_000u32;
|
||||
let hold_frames = 48u32;
|
||||
let mut palette = PokemonPalette::new(SEED);
|
||||
let mut rng = 7u32;
|
||||
let mut running = false;
|
||||
let mut since_decision = hold_frames;
|
||||
let mut ms = run.ms;
|
||||
let mut arrivals: Vec<(u32, u8)> = Vec::new();
|
||||
let mut last = None;
|
||||
let mut walled_at: Option<u32> = None;
|
||||
// Overworld frames (the shared reading) inside a trainer's challenge, and those that dealt a pad.
|
||||
let mut engaged = 0u32;
|
||||
let mut engaged_dealt = 0u32;
|
||||
for frame in 0..budget {
|
||||
palette.clock(ms);
|
||||
let observed = {
|
||||
let ledger = AdapterLedger(&run.adapter);
|
||||
palette.observe(&mut run.gb, &ledger)
|
||||
};
|
||||
// `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE`, read here rather than through the seam.
|
||||
if run.gb.read8(flybrain_gb::pokemon_red::symbols::ram::wStatusFlags7) & (1 << 3) != 0
|
||||
&& flybrain_gb::pokemon_red::scene::detect(&mut run.gb)
|
||||
== flybrain_gb::pokemon_red::macros::state::Scene::Overworld
|
||||
{
|
||||
engaged += 1;
|
||||
if !observed.bindings.is_empty() {
|
||||
engaged_dealt += 1;
|
||||
}
|
||||
}
|
||||
let mut mask = 0u8;
|
||||
{
|
||||
let ledger = AdapterLedger(&run.adapter);
|
||||
if running {
|
||||
match palette.step(&mut run.gb, &ledger) {
|
||||
Some(held) => mask = held,
|
||||
None => running = false,
|
||||
}
|
||||
} else if since_decision >= hold_frames && !observed.bindings.is_empty() {
|
||||
since_decision = 0;
|
||||
rng ^= rng << 13;
|
||||
rng ^= rng >> 17;
|
||||
rng ^= rng << 5;
|
||||
let binding = &observed.bindings[rng as usize % observed.bindings.len()];
|
||||
if let Started::Running(_) = palette.start(binding.slot, &mut run.gb, &ledger) {
|
||||
running = true;
|
||||
match palette.step(&mut run.gb, &ledger) {
|
||||
Some(held) => mask = held,
|
||||
None => running = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = palette.take_finished();
|
||||
since_decision += 1;
|
||||
run.gb.set_buttons(mask);
|
||||
run.gb.run_frame().expect("a frame should complete");
|
||||
ms += MS_PER_FRAME;
|
||||
run.adapter.sample(&mut run.gb, ms);
|
||||
|
||||
if walled_at.is_none() && palette.fences().0.pushed(FOREST, Tile::new(1, 18)) {
|
||||
walled_at = Some(frame);
|
||||
}
|
||||
if let Some(player) = state::player(&mut run.gb)
|
||||
&& last != Some(player.map)
|
||||
{
|
||||
last = Some(player.map);
|
||||
arrivals.push((frame, player.map));
|
||||
}
|
||||
}
|
||||
let first = |map: u8| arrivals.iter().find(|(_, at)| *at == map).map(|(frame, _)| *frame);
|
||||
let north_gate = first(NORTH_GATE);
|
||||
let route_2_north = north_gate
|
||||
.and_then(|gate| arrivals.iter().find(|(frame, map)| *frame > gate && *map == ROUTE_2))
|
||||
.map(|(frame, _)| *frame);
|
||||
let pewter = first(PEWTER);
|
||||
eprintln!(
|
||||
"{:.1} brain minutes: challenge overworld frames {engaged}, a pad dealt on {engaged_dealt}; \
|
||||
(1, 18) walled at {walled_at:?}; north gate {north_gate:?}, Route 2 \
|
||||
after it {route_2_north:?}, Pewter City {pewter:?}; pushed {:?}; rank {}; arrivals {}",
|
||||
(ms - run.ms) / 60_000.0,
|
||||
palette.fences().0,
|
||||
run.adapter.progress().rank,
|
||||
arrivals.len(),
|
||||
);
|
||||
assert!(engaged > 0, "the run never reached a trainer's challenge");
|
||||
assert_eq!(engaged_dealt, 0, "a pad was dealt inside a trainer's challenge");
|
||||
assert_eq!(walled_at, None, "a trainer's challenge walled the forest's corridor");
|
||||
assert!(north_gate.is_some(), "the fly never reached the forest's north gate: {arrivals:?}");
|
||||
assert!(route_2_north.is_some(), "nor Route 2 through it: {arrivals:?}");
|
||||
assert!(pewter.is_some(), "nor Pewter City: {arrivals:?}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue