Skip to main content

RunnerGame

Struct RunnerGame 

Source
pub struct RunnerGame { /* private fields */ }
Expand description

A booted zero-Rust game: overworld + scene VM + dialogue/choice UI.

Owns the LoadedProject, the current RuntimeMap, the player OverworldActor, the persistent flag store (seeded into / harvested from each short-lived scene engine) and the mode state machine. Drive it with update + draw — directly (headless) or via the dotzuki_app::GameLoop impl (windowed).

Implementations§

Source§

impl RunnerGame

Source

pub fn new(project: LoadedProject, opts: RunnerOptions) -> Result<Self>

Boot the project: load the entry map (opts.map override or game.entryMap), spawn the player, and run the map’s opening dispatch. A project with no maps boots dialogue-only: the entry scene’s main storyline runs to completion, then the game idles.

§Errors

Fails when the entry map (or --map override) cannot be loaded, or when a map-less project has no entry scene.

Examples found in repository?
examples/menu_accept.rs (lines 82-91)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/levels_accept.rs (lines 76-87)
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
examples/battle_accept.rs (lines 52-61)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args.next().expect("usage: battle_accept <project-dir> [shot-dir]");
49    let shot_dir = args.next().map(PathBuf::from);
50
51    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
52    let mut game = RunnerGame::new(
53        project,
54        RunnerOptions {
55            headless: true,
56            fresh: true,
57            // Deterministic battles: always hit, 89% variance, never crit.
58            rng_script: Some(vec![50, 100, 1]),
59            ..RunnerOptions::default()
60        },
61    )
62    .expect("boot game");
63
64    // The agenda per battle (index = battles started so far − 1).
65    let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
66    let mut battles_done = 0usize;
67    let mut agenda: Vec<Pick> = Vec::new();
68    // Queued button presses; one key every other frame (a held key is not a
69    // fresh just-press, so keys alternate with idle frames).
70    let mut keys: Vec<GbButton> = Vec::new();
71    let mut last_log: Vec<String> = Vec::new();
72    let mut snapped: Vec<String> = Vec::new();
73    let mut was_in_battle_prev = false;
74
75    let mut input = InputState::new();
76    for frame in 0..6000u32 {
77        let was_in_battle = was_in_battle_prev;
78
79        // Decide this frame's input.
80        let mut mask = 0;
81        if frame % 2 == 0 {
82            if !keys.is_empty() {
83                mask = keys.remove(0).bit_mask();
84            } else if let Some(battle) = game.battle() {
85                if battle.in_menu() {
86                    let items = battle.menu_items();
87                    let label = if items.first().is_some_and(|i| i == "Fight") {
88                        "root"
89                    } else if items.iter().any(|i| i.contains('/')) {
90                        "party"
91                    } else {
92                        "sub"
93                    };
94                    if let Some(dir) = &shot_dir {
95                        let tag = format!("battle{}-{label}", battles_done + 1);
96                        if !snapped.contains(&tag) {
97                            snapped.push(tag.clone());
98                            snap(&mut game, &dir.join(format!("{tag}.png")));
99                        }
100                    }
101                    mask = match label {
102                        "root" => {
103                            let pick = agenda.first().copied().unwrap_or(Pick::Fight);
104                            if !agenda.is_empty() {
105                                agenda.remove(0);
106                            }
107                            match pick {
108                                Pick::Fight => GbButton::A.bit_mask(),
109                                Pick::Party => {
110                                    keys.push(GbButton::A);
111                                    GbButton::Down.bit_mask()
112                                }
113                                Pick::Item => {
114                                    keys.push(GbButton::Down);
115                                    keys.push(GbButton::A);
116                                    GbButton::Down.bit_mask()
117                                }
118                            }
119                        }
120                        // Submenus: confirm the entry under the cursor (the
121                        // party cursor starts on the first switchable member).
122                        _ => GbButton::A.bit_mask(),
123                    };
124                } else if frame % 4 == 0 {
125                    mask = GbButton::A.bit_mask(); // page narration
126                }
127            } else if frame % 4 == 0 {
128                mask = GbButton::A.bit_mask(); // page dialogue
129            }
130        }
131        input.set_from_bitmask(mask);
132        game.update(&input);
133        input.begin_frame();
134
135        let in_battle = game.battle().is_some();
136        // Battle-start edge: print the party it starts with (the carried-over
137        // state from battle 2 on) and load its agenda.
138        if in_battle && !was_in_battle {
139            let n = battles_done + 1;
140            let battle = game.battle().unwrap();
141            println!("battle {n} starts:");
142            for c in battle.party() {
143                let status = c.status.as_deref().unwrap_or("-");
144                println!(
145                    "  {} {}/{} MP {}/{} status {}",
146                    c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
147                );
148            }
149            println!("  inventory: {:?}", battle.inventory());
150            agenda = agendas[battles_done].to_vec();
151        }
152        if in_battle {
153            last_log = game.battle().unwrap().log().to_vec();
154        }
155        // Battle-end edge: the runner has harvested the party state.
156        if was_in_battle && !in_battle {
157            battles_done += 1;
158            println!("\nbattle {battles_done} log:");
159            for line in &last_log {
160                println!("  {line}");
161            }
162            if let Some(party) = game.party_state() {
163                println!("party state after battle {battles_done}:");
164                for m in party {
165                    println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
166                }
167            }
168            if let Some(inv) = game.inventory() {
169                println!("inventory: {inv:?}");
170            }
171            println!();
172            last_log.clear();
173            if battles_done == 2 {
174                println!("both battles done — stopping");
175                break;
176            }
177        }
178        was_in_battle_prev = in_battle;
179    }
180
181    if battles_done < 2 {
182        eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
183        std::process::exit(1);
184    }
185}
examples/encounter_accept.rs (lines 54-63)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn export_save(&self) -> Option<String>

Serialize the current state as save JSON — the persistence bridge for the WASM shell (localStorage). Returns None while the game is in a transient state that cannot round-trip (a scene engine suspended on text/choice/delay/battle/shop, or a warp transition mid-flight); stable states (overworld, menu, whiteout, idle) always export.

Source

pub fn import_save(&mut self, json: &str) -> bool

Restore a save produced by export_save (the WASM shell’s localStorage bridge). Returns false — the game keeps its current state — on unparseable JSON, a NEWER save version, or a saved map that no longer loads.

Source

pub fn poll_watch(&mut self)

Poll the file watcher and apply pending changes as one batch.

Called at the top of every update; a no-op when watching is off. Batches are debounced by [WATCH_DEBOUNCE_FRAMES] so an editor save burst applies once.

Source

pub fn reload_scenes(&mut self) -> bool

Recompile the project’s DSL and swap the compiled scenes in place. A scene mid-activation keeps running its old engine; the next activation (talk/enter) picks up the new source. On a compiler diagnostic the old scenes keep running (false).

Source

pub fn reload_current_map(&mut self) -> bool

Reload the current map from disk in place, preserving the player’s pixel position and the story flags. On a load error the old map is kept (false); false also when there is no current map.

Source

pub fn update(&mut self, input: &InputState)

Advance the game one frame. Callable without any window (headless tests, the run_headless driver); the GameLoop impl forwards here.

Examples found in repository?
examples/levels_accept.rs (line 44)
41fn frame(game: &mut RunnerGame, mask: u8) {
42    let mut input = InputState::new();
43    input.set_from_bitmask(mask);
44    game.update(&input);
45}
More examples
Hide additional examples
examples/menu_accept.rs (line 47)
44fn frame(game: &mut RunnerGame, mask: u8) {
45    let mut input = InputState::new();
46    input.set_from_bitmask(mask);
47    game.update(&input);
48}
examples/battle_accept.rs (line 132)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args.next().expect("usage: battle_accept <project-dir> [shot-dir]");
49    let shot_dir = args.next().map(PathBuf::from);
50
51    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
52    let mut game = RunnerGame::new(
53        project,
54        RunnerOptions {
55            headless: true,
56            fresh: true,
57            // Deterministic battles: always hit, 89% variance, never crit.
58            rng_script: Some(vec![50, 100, 1]),
59            ..RunnerOptions::default()
60        },
61    )
62    .expect("boot game");
63
64    // The agenda per battle (index = battles started so far − 1).
65    let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
66    let mut battles_done = 0usize;
67    let mut agenda: Vec<Pick> = Vec::new();
68    // Queued button presses; one key every other frame (a held key is not a
69    // fresh just-press, so keys alternate with idle frames).
70    let mut keys: Vec<GbButton> = Vec::new();
71    let mut last_log: Vec<String> = Vec::new();
72    let mut snapped: Vec<String> = Vec::new();
73    let mut was_in_battle_prev = false;
74
75    let mut input = InputState::new();
76    for frame in 0..6000u32 {
77        let was_in_battle = was_in_battle_prev;
78
79        // Decide this frame's input.
80        let mut mask = 0;
81        if frame % 2 == 0 {
82            if !keys.is_empty() {
83                mask = keys.remove(0).bit_mask();
84            } else if let Some(battle) = game.battle() {
85                if battle.in_menu() {
86                    let items = battle.menu_items();
87                    let label = if items.first().is_some_and(|i| i == "Fight") {
88                        "root"
89                    } else if items.iter().any(|i| i.contains('/')) {
90                        "party"
91                    } else {
92                        "sub"
93                    };
94                    if let Some(dir) = &shot_dir {
95                        let tag = format!("battle{}-{label}", battles_done + 1);
96                        if !snapped.contains(&tag) {
97                            snapped.push(tag.clone());
98                            snap(&mut game, &dir.join(format!("{tag}.png")));
99                        }
100                    }
101                    mask = match label {
102                        "root" => {
103                            let pick = agenda.first().copied().unwrap_or(Pick::Fight);
104                            if !agenda.is_empty() {
105                                agenda.remove(0);
106                            }
107                            match pick {
108                                Pick::Fight => GbButton::A.bit_mask(),
109                                Pick::Party => {
110                                    keys.push(GbButton::A);
111                                    GbButton::Down.bit_mask()
112                                }
113                                Pick::Item => {
114                                    keys.push(GbButton::Down);
115                                    keys.push(GbButton::A);
116                                    GbButton::Down.bit_mask()
117                                }
118                            }
119                        }
120                        // Submenus: confirm the entry under the cursor (the
121                        // party cursor starts on the first switchable member).
122                        _ => GbButton::A.bit_mask(),
123                    };
124                } else if frame % 4 == 0 {
125                    mask = GbButton::A.bit_mask(); // page narration
126                }
127            } else if frame % 4 == 0 {
128                mask = GbButton::A.bit_mask(); // page dialogue
129            }
130        }
131        input.set_from_bitmask(mask);
132        game.update(&input);
133        input.begin_frame();
134
135        let in_battle = game.battle().is_some();
136        // Battle-start edge: print the party it starts with (the carried-over
137        // state from battle 2 on) and load its agenda.
138        if in_battle && !was_in_battle {
139            let n = battles_done + 1;
140            let battle = game.battle().unwrap();
141            println!("battle {n} starts:");
142            for c in battle.party() {
143                let status = c.status.as_deref().unwrap_or("-");
144                println!(
145                    "  {} {}/{} MP {}/{} status {}",
146                    c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
147                );
148            }
149            println!("  inventory: {:?}", battle.inventory());
150            agenda = agendas[battles_done].to_vec();
151        }
152        if in_battle {
153            last_log = game.battle().unwrap().log().to_vec();
154        }
155        // Battle-end edge: the runner has harvested the party state.
156        if was_in_battle && !in_battle {
157            battles_done += 1;
158            println!("\nbattle {battles_done} log:");
159            for line in &last_log {
160                println!("  {line}");
161            }
162            if let Some(party) = game.party_state() {
163                println!("party state after battle {battles_done}:");
164                for m in party {
165                    println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
166                }
167            }
168            if let Some(inv) = game.inventory() {
169                println!("inventory: {inv:?}");
170            }
171            println!();
172            last_log.clear();
173            if battles_done == 2 {
174                println!("both battles done — stopping");
175                break;
176            }
177        }
178        was_in_battle_prev = in_battle;
179    }
180
181    if battles_done < 2 {
182        eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
183        std::process::exit(1);
184    }
185}
examples/encounter_accept.rs (line 148)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn draw(&mut self, fb: &mut FrameBuffer)

Render the current frame. Callable without any window; the GameLoop impl forwards here.

Examples found in repository?
examples/battle_accept.rs (line 41)
36fn snap(game: &mut RunnerGame, path: &Path) {
37    let mut fb = FrameBuffer::new(
38        RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
39        Rgba::BLACK,
40    );
41    game.draw(&mut fb);
42    save_png(&fb, path).expect("screenshot");
43    println!("  [shot] {}", path.display());
44}
More examples
Hide additional examples
examples/encounter_accept.rs (line 41)
36fn snap(game: &mut RunnerGame, path: &Path) {
37    let mut fb = FrameBuffer::new(
38        RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
39        Rgba::BLACK,
40    );
41    game.draw(&mut fb);
42    save_png(&fb, path).expect("screenshot");
43    println!("  [shot] {}", path.display());
44}
examples/levels_accept.rs (line 35)
29fn snap(game: &mut RunnerGame, dir: &Option<PathBuf>, name: &str) {
30    let Some(dir) = dir else { return };
31    let mut fb = FrameBuffer::new(
32        RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
33        Rgba::BLACK,
34    );
35    game.draw(&mut fb);
36    let path = dir.join(format!("{name}.png"));
37    save_png(&fb, &path).expect("screenshot");
38    println!("  [shot] {}", path.display());
39}
examples/menu_accept.rs (line 38)
32fn snap(game: &mut RunnerGame, dir: &Option<PathBuf>, name: &str) {
33    let Some(dir) = dir else { return };
34    let mut fb = FrameBuffer::new(
35        RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
36        Rgba::BLACK,
37    );
38    game.draw(&mut fb);
39    let path = dir.join(format!("{name}.png"));
40    save_png(&fb, &path).expect("screenshot");
41    println!("  [shot] {}", path.display());
42}
Source

pub fn current_map_id(&self) -> Option<&str>

The currently loaded map id (None in dialogue-only mode).

Examples found in repository?
examples/menu_accept.rs (line 164)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
Source

pub fn flag(&self, name: &str) -> bool

A persistent story flag’s value (defaults to false).

Examples found in repository?
examples/menu_accept.rs (line 168)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/levels_accept.rs (line 169)
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
Source

pub fn dialogue_text(&self) -> Option<&str>

The text page currently on screen, if a textbox is open (including the game-over whiteout message).

Examples found in repository?
examples/menu_accept.rs (line 68)
66fn dismiss(game: &mut RunnerGame) {
67    for _ in 0..40 {
68        if game.dialogue_text().is_none() && game.choice_options().is_none() {
69            return;
70        }
71        press_a(game);
72    }
73    panic!("dialogue did not close");
74}
75
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/levels_accept.rs (line 164)
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
Source

pub fn choice_options(&self) -> Option<&[String]>

The choice options currently on screen, if a choice menu is open.

Examples found in repository?
examples/menu_accept.rs (line 68)
66fn dismiss(game: &mut RunnerGame) {
67    for _ in 0..40 {
68        if game.dialogue_text().is_none() && game.choice_options().is_none() {
69            return;
70        }
71        press_a(game);
72    }
73    panic!("dialogue did not close");
74}
Source

pub fn battle(&self) -> Option<&Battle>

The live battle, if one is running (test/debug introspection).

Examples found in repository?
examples/menu_accept.rs (line 134)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/levels_accept.rs (line 107)
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
examples/battle_accept.rs (line 84)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args.next().expect("usage: battle_accept <project-dir> [shot-dir]");
49    let shot_dir = args.next().map(PathBuf::from);
50
51    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
52    let mut game = RunnerGame::new(
53        project,
54        RunnerOptions {
55            headless: true,
56            fresh: true,
57            // Deterministic battles: always hit, 89% variance, never crit.
58            rng_script: Some(vec![50, 100, 1]),
59            ..RunnerOptions::default()
60        },
61    )
62    .expect("boot game");
63
64    // The agenda per battle (index = battles started so far − 1).
65    let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
66    let mut battles_done = 0usize;
67    let mut agenda: Vec<Pick> = Vec::new();
68    // Queued button presses; one key every other frame (a held key is not a
69    // fresh just-press, so keys alternate with idle frames).
70    let mut keys: Vec<GbButton> = Vec::new();
71    let mut last_log: Vec<String> = Vec::new();
72    let mut snapped: Vec<String> = Vec::new();
73    let mut was_in_battle_prev = false;
74
75    let mut input = InputState::new();
76    for frame in 0..6000u32 {
77        let was_in_battle = was_in_battle_prev;
78
79        // Decide this frame's input.
80        let mut mask = 0;
81        if frame % 2 == 0 {
82            if !keys.is_empty() {
83                mask = keys.remove(0).bit_mask();
84            } else if let Some(battle) = game.battle() {
85                if battle.in_menu() {
86                    let items = battle.menu_items();
87                    let label = if items.first().is_some_and(|i| i == "Fight") {
88                        "root"
89                    } else if items.iter().any(|i| i.contains('/')) {
90                        "party"
91                    } else {
92                        "sub"
93                    };
94                    if let Some(dir) = &shot_dir {
95                        let tag = format!("battle{}-{label}", battles_done + 1);
96                        if !snapped.contains(&tag) {
97                            snapped.push(tag.clone());
98                            snap(&mut game, &dir.join(format!("{tag}.png")));
99                        }
100                    }
101                    mask = match label {
102                        "root" => {
103                            let pick = agenda.first().copied().unwrap_or(Pick::Fight);
104                            if !agenda.is_empty() {
105                                agenda.remove(0);
106                            }
107                            match pick {
108                                Pick::Fight => GbButton::A.bit_mask(),
109                                Pick::Party => {
110                                    keys.push(GbButton::A);
111                                    GbButton::Down.bit_mask()
112                                }
113                                Pick::Item => {
114                                    keys.push(GbButton::Down);
115                                    keys.push(GbButton::A);
116                                    GbButton::Down.bit_mask()
117                                }
118                            }
119                        }
120                        // Submenus: confirm the entry under the cursor (the
121                        // party cursor starts on the first switchable member).
122                        _ => GbButton::A.bit_mask(),
123                    };
124                } else if frame % 4 == 0 {
125                    mask = GbButton::A.bit_mask(); // page narration
126                }
127            } else if frame % 4 == 0 {
128                mask = GbButton::A.bit_mask(); // page dialogue
129            }
130        }
131        input.set_from_bitmask(mask);
132        game.update(&input);
133        input.begin_frame();
134
135        let in_battle = game.battle().is_some();
136        // Battle-start edge: print the party it starts with (the carried-over
137        // state from battle 2 on) and load its agenda.
138        if in_battle && !was_in_battle {
139            let n = battles_done + 1;
140            let battle = game.battle().unwrap();
141            println!("battle {n} starts:");
142            for c in battle.party() {
143                let status = c.status.as_deref().unwrap_or("-");
144                println!(
145                    "  {} {}/{} MP {}/{} status {}",
146                    c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
147                );
148            }
149            println!("  inventory: {:?}", battle.inventory());
150            agenda = agendas[battles_done].to_vec();
151        }
152        if in_battle {
153            last_log = game.battle().unwrap().log().to_vec();
154        }
155        // Battle-end edge: the runner has harvested the party state.
156        if was_in_battle && !in_battle {
157            battles_done += 1;
158            println!("\nbattle {battles_done} log:");
159            for line in &last_log {
160                println!("  {line}");
161            }
162            if let Some(party) = game.party_state() {
163                println!("party state after battle {battles_done}:");
164                for m in party {
165                    println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
166                }
167            }
168            if let Some(inv) = game.inventory() {
169                println!("inventory: {inv:?}");
170            }
171            println!();
172            last_log.clear();
173            if battles_done == 2 {
174                println!("both battles done — stopping");
175                break;
176            }
177        }
178        was_in_battle_prev = in_battle;
179    }
180
181    if battles_done < 2 {
182        eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
183        std::process::exit(1);
184    }
185}
examples/encounter_accept.rs (line 96)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn party_state(&self) -> Option<&[PartyMemberState]>

The persistent party state, once a battle has completed or a save restored one (test/debug introspection).

Examples found in repository?
examples/levels_accept.rs (line 59)
58fn print_party(game: &RunnerGame, when: &str) {
59    let Some(party) = game.party_state() else { return };
60    println!("party state {when}:");
61    for m in party {
62        println!(
63            "  {} level {} exp {} hp {} mp {} status {:?}",
64            m.id, m.level, m.exp, m.hp, m.mp, m.status
65        );
66    }
67}
68
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
More examples
Hide additional examples
examples/menu_accept.rs (line 165)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
examples/battle_accept.rs (line 162)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args.next().expect("usage: battle_accept <project-dir> [shot-dir]");
49    let shot_dir = args.next().map(PathBuf::from);
50
51    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
52    let mut game = RunnerGame::new(
53        project,
54        RunnerOptions {
55            headless: true,
56            fresh: true,
57            // Deterministic battles: always hit, 89% variance, never crit.
58            rng_script: Some(vec![50, 100, 1]),
59            ..RunnerOptions::default()
60        },
61    )
62    .expect("boot game");
63
64    // The agenda per battle (index = battles started so far − 1).
65    let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
66    let mut battles_done = 0usize;
67    let mut agenda: Vec<Pick> = Vec::new();
68    // Queued button presses; one key every other frame (a held key is not a
69    // fresh just-press, so keys alternate with idle frames).
70    let mut keys: Vec<GbButton> = Vec::new();
71    let mut last_log: Vec<String> = Vec::new();
72    let mut snapped: Vec<String> = Vec::new();
73    let mut was_in_battle_prev = false;
74
75    let mut input = InputState::new();
76    for frame in 0..6000u32 {
77        let was_in_battle = was_in_battle_prev;
78
79        // Decide this frame's input.
80        let mut mask = 0;
81        if frame % 2 == 0 {
82            if !keys.is_empty() {
83                mask = keys.remove(0).bit_mask();
84            } else if let Some(battle) = game.battle() {
85                if battle.in_menu() {
86                    let items = battle.menu_items();
87                    let label = if items.first().is_some_and(|i| i == "Fight") {
88                        "root"
89                    } else if items.iter().any(|i| i.contains('/')) {
90                        "party"
91                    } else {
92                        "sub"
93                    };
94                    if let Some(dir) = &shot_dir {
95                        let tag = format!("battle{}-{label}", battles_done + 1);
96                        if !snapped.contains(&tag) {
97                            snapped.push(tag.clone());
98                            snap(&mut game, &dir.join(format!("{tag}.png")));
99                        }
100                    }
101                    mask = match label {
102                        "root" => {
103                            let pick = agenda.first().copied().unwrap_or(Pick::Fight);
104                            if !agenda.is_empty() {
105                                agenda.remove(0);
106                            }
107                            match pick {
108                                Pick::Fight => GbButton::A.bit_mask(),
109                                Pick::Party => {
110                                    keys.push(GbButton::A);
111                                    GbButton::Down.bit_mask()
112                                }
113                                Pick::Item => {
114                                    keys.push(GbButton::Down);
115                                    keys.push(GbButton::A);
116                                    GbButton::Down.bit_mask()
117                                }
118                            }
119                        }
120                        // Submenus: confirm the entry under the cursor (the
121                        // party cursor starts on the first switchable member).
122                        _ => GbButton::A.bit_mask(),
123                    };
124                } else if frame % 4 == 0 {
125                    mask = GbButton::A.bit_mask(); // page narration
126                }
127            } else if frame % 4 == 0 {
128                mask = GbButton::A.bit_mask(); // page dialogue
129            }
130        }
131        input.set_from_bitmask(mask);
132        game.update(&input);
133        input.begin_frame();
134
135        let in_battle = game.battle().is_some();
136        // Battle-start edge: print the party it starts with (the carried-over
137        // state from battle 2 on) and load its agenda.
138        if in_battle && !was_in_battle {
139            let n = battles_done + 1;
140            let battle = game.battle().unwrap();
141            println!("battle {n} starts:");
142            for c in battle.party() {
143                let status = c.status.as_deref().unwrap_or("-");
144                println!(
145                    "  {} {}/{} MP {}/{} status {}",
146                    c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
147                );
148            }
149            println!("  inventory: {:?}", battle.inventory());
150            agenda = agendas[battles_done].to_vec();
151        }
152        if in_battle {
153            last_log = game.battle().unwrap().log().to_vec();
154        }
155        // Battle-end edge: the runner has harvested the party state.
156        if was_in_battle && !in_battle {
157            battles_done += 1;
158            println!("\nbattle {battles_done} log:");
159            for line in &last_log {
160                println!("  {line}");
161            }
162            if let Some(party) = game.party_state() {
163                println!("party state after battle {battles_done}:");
164                for m in party {
165                    println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
166                }
167            }
168            if let Some(inv) = game.inventory() {
169                println!("inventory: {inv:?}");
170            }
171            println!();
172            last_log.clear();
173            if battles_done == 2 {
174                println!("both battles done — stopping");
175                break;
176            }
177        }
178        was_in_battle_prev = in_battle;
179    }
180
181    if battles_done < 2 {
182        eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
183        std::process::exit(1);
184    }
185}
examples/encounter_accept.rs (line 210)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn inventory(&self) -> Option<&HashMap<String, u32>>

The persistent battle inventory, once a battle has completed or a save restored one (test/debug introspection).

Examples found in repository?
examples/menu_accept.rs (line 112)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/battle_accept.rs (line 168)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args.next().expect("usage: battle_accept <project-dir> [shot-dir]");
49    let shot_dir = args.next().map(PathBuf::from);
50
51    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
52    let mut game = RunnerGame::new(
53        project,
54        RunnerOptions {
55            headless: true,
56            fresh: true,
57            // Deterministic battles: always hit, 89% variance, never crit.
58            rng_script: Some(vec![50, 100, 1]),
59            ..RunnerOptions::default()
60        },
61    )
62    .expect("boot game");
63
64    // The agenda per battle (index = battles started so far − 1).
65    let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
66    let mut battles_done = 0usize;
67    let mut agenda: Vec<Pick> = Vec::new();
68    // Queued button presses; one key every other frame (a held key is not a
69    // fresh just-press, so keys alternate with idle frames).
70    let mut keys: Vec<GbButton> = Vec::new();
71    let mut last_log: Vec<String> = Vec::new();
72    let mut snapped: Vec<String> = Vec::new();
73    let mut was_in_battle_prev = false;
74
75    let mut input = InputState::new();
76    for frame in 0..6000u32 {
77        let was_in_battle = was_in_battle_prev;
78
79        // Decide this frame's input.
80        let mut mask = 0;
81        if frame % 2 == 0 {
82            if !keys.is_empty() {
83                mask = keys.remove(0).bit_mask();
84            } else if let Some(battle) = game.battle() {
85                if battle.in_menu() {
86                    let items = battle.menu_items();
87                    let label = if items.first().is_some_and(|i| i == "Fight") {
88                        "root"
89                    } else if items.iter().any(|i| i.contains('/')) {
90                        "party"
91                    } else {
92                        "sub"
93                    };
94                    if let Some(dir) = &shot_dir {
95                        let tag = format!("battle{}-{label}", battles_done + 1);
96                        if !snapped.contains(&tag) {
97                            snapped.push(tag.clone());
98                            snap(&mut game, &dir.join(format!("{tag}.png")));
99                        }
100                    }
101                    mask = match label {
102                        "root" => {
103                            let pick = agenda.first().copied().unwrap_or(Pick::Fight);
104                            if !agenda.is_empty() {
105                                agenda.remove(0);
106                            }
107                            match pick {
108                                Pick::Fight => GbButton::A.bit_mask(),
109                                Pick::Party => {
110                                    keys.push(GbButton::A);
111                                    GbButton::Down.bit_mask()
112                                }
113                                Pick::Item => {
114                                    keys.push(GbButton::Down);
115                                    keys.push(GbButton::A);
116                                    GbButton::Down.bit_mask()
117                                }
118                            }
119                        }
120                        // Submenus: confirm the entry under the cursor (the
121                        // party cursor starts on the first switchable member).
122                        _ => GbButton::A.bit_mask(),
123                    };
124                } else if frame % 4 == 0 {
125                    mask = GbButton::A.bit_mask(); // page narration
126                }
127            } else if frame % 4 == 0 {
128                mask = GbButton::A.bit_mask(); // page dialogue
129            }
130        }
131        input.set_from_bitmask(mask);
132        game.update(&input);
133        input.begin_frame();
134
135        let in_battle = game.battle().is_some();
136        // Battle-start edge: print the party it starts with (the carried-over
137        // state from battle 2 on) and load its agenda.
138        if in_battle && !was_in_battle {
139            let n = battles_done + 1;
140            let battle = game.battle().unwrap();
141            println!("battle {n} starts:");
142            for c in battle.party() {
143                let status = c.status.as_deref().unwrap_or("-");
144                println!(
145                    "  {} {}/{} MP {}/{} status {}",
146                    c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
147                );
148            }
149            println!("  inventory: {:?}", battle.inventory());
150            agenda = agendas[battles_done].to_vec();
151        }
152        if in_battle {
153            last_log = game.battle().unwrap().log().to_vec();
154        }
155        // Battle-end edge: the runner has harvested the party state.
156        if was_in_battle && !in_battle {
157            battles_done += 1;
158            println!("\nbattle {battles_done} log:");
159            for line in &last_log {
160                println!("  {line}");
161            }
162            if let Some(party) = game.party_state() {
163                println!("party state after battle {battles_done}:");
164                for m in party {
165                    println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
166                }
167            }
168            if let Some(inv) = game.inventory() {
169                println!("inventory: {inv:?}");
170            }
171            println!();
172            last_log.clear();
173            if battles_done == 2 {
174                println!("both battles done — stopping");
175                break;
176            }
177        }
178        was_in_battle_prev = in_battle;
179    }
180
181    if battles_done < 2 {
182        eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
183        std::process::exit(1);
184    }
185}
examples/encounter_accept.rs (line 224)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn money(&self) -> u32

The player’s current money (test/debug introspection).

Examples found in repository?
examples/menu_accept.rs (line 96)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/encounter_accept.rs (line 65)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn menu_lines(&self) -> Option<Vec<String>>

The rows the open Start menu currently displays (root labels, party detail lines, bag rows, target rows, or the note text); None when the menu is closed (test/debug introspection).

Examples found in repository?
examples/levels_accept.rs (line 176)
69fn main() {
70    let mut args = std::env::args().skip(1);
71    let dir = args.next().expect("usage: levels_accept <project-dir> [shot-dir]");
72    let shot_dir = args.next().map(PathBuf::from);
73
74    let save_file = Path::new(&dir).join(".dotzuki-save.json");
75    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
76    let mut game = RunnerGame::new(
77        project,
78        RunnerOptions {
79            headless: true,
80            fresh: true,
81            write_saves: true,
82            save_file: Some(save_file.clone()),
83            // Deterministic battles: always hit, 89% variance, never crit.
84            rng_script: Some(vec![50, 100, 1]),
85            ..RunnerOptions::default()
86        },
87    )
88    .expect("boot game");
89
90    // Auto-A drives everything: dialogue pages, battle narration, and the
91    // Fight → first-skill picks. Back-to-back `startBattle` commands swap
92    // Battle→Battle INSIDE one update, so a new battle is detected by the
93    // log resetting, not just by a not-in-battle gap.
94    let mut battle_no = 0usize;
95    let mut completed = 0usize;
96    let mut last_log: Vec<String> = Vec::new();
97    let mut was_in_battle = false;
98    let mut snapped_battle = false;
99
100    for frame_no in 0..6000u32 {
101        if frame_no % 4 == 0 {
102            frame(&mut game, GbButton::A.bit_mask());
103        } else {
104            idle(&mut game, 1);
105        }
106
107        let in_battle = game.battle().is_some();
108        if in_battle {
109            let log_len = game.battle().unwrap().log().len();
110            let new_battle = !was_in_battle || log_len < last_log.len();
111            if new_battle {
112                if was_in_battle {
113                    // Seamless Battle→Battle swap: close the previous one
114                    // out (the party state is already harvested).
115                    completed += 1;
116                    println!("\nbattle {completed} log:");
117                    for line in &last_log {
118                        println!("  {line}");
119                    }
120                    print_party(&game, &format!("after battle {completed}"));
121                    println!();
122                    if completed == 2 {
123                        break;
124                    }
125                }
126                battle_no += 1;
127                let battle = game.battle().unwrap();
128                println!("battle {battle_no} starts:");
129                for c in battle.party() {
130                    println!(
131                        "  {} level {} exp {} hp {}/{} mp {}/{} atk {}",
132                        c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
133                    );
134                }
135                if battle_no == 2 && !snapped_battle {
136                    snapped_battle = true;
137                    snap(&mut game, &shot_dir, "battle2-start");
138                }
139            }
140            last_log = game.battle().unwrap().log().to_vec();
141        } else if was_in_battle {
142            completed += 1;
143            println!("\nbattle {completed} log:");
144            for line in &last_log {
145                println!("  {line}");
146            }
147            print_party(&game, &format!("after battle {completed}"));
148            println!();
149            last_log.clear();
150            if completed == 2 {
151                break;
152            }
153        }
154        was_in_battle = in_battle;
155    }
156    if completed < 2 {
157        eprintln!("harness: only {completed} battle(s) completed within the frame cap");
158        std::process::exit(1);
159    }
160
161    // Dismiss the post-battle text so the scene finishes (the save is
162    // written at that stable point).
163    for _ in 0..40 {
164        if game.dialogue_text().is_none() {
165            break;
166        }
167        press_a(&mut game);
168    }
169    assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
170
171    // The Party view: Lv + EXP progress on the member rows.
172    frame(&mut game, GbButton::Start.bit_mask());
173    idle(&mut game, 1);
174    press_a(&mut game); // Party
175    println!("party view:");
176    for line in game.menu_lines().expect("party view open") {
177        println!("  {line}");
178    }
179    snap(&mut game, &shot_dir, "party-view");
180    frame(&mut game, GbButton::B.bit_mask());
181    idle(&mut game, 1);
182    frame(&mut game, GbButton::B.bit_mask());
183    idle(&mut game, 1);
184
185    // Save round trip: a fresh boot resumes level/exp from the save.
186    println!("\nsave round trip ({}):", save_file.display());
187    let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
188    let game = RunnerGame::new(
189        project,
190        RunnerOptions {
191            headless: true,
192            save_file: Some(save_file),
193            rng_script: Some(vec![50, 100, 1]),
194            ..RunnerOptions::default()
195        },
196    )
197    .expect("boot from save");
198    print_party(&game, "resumed from the save");
199    assert!(
200        game.party_state().is_some_and(|p| p[0].level > 1),
201        "level must survive the save round trip"
202    );
203    println!("\nacceptance OK");
204}
Source

pub fn shop_lines(&self) -> Option<Vec<String>>

The item rows the open shop displays (×-prefixed when unaffordable), plus the transient note as a final row when one is showing; None when no shop is open (test/debug introspection).

Examples found in repository?
examples/menu_accept.rs (line 101)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
More examples
Hide additional examples
examples/encounter_accept.rs (line 120)
46fn main() {
47    let mut args = std::env::args().skip(1);
48    let dir = args
49        .next()
50        .expect("usage: encounter_accept <project-dir> [shot-dir]");
51    let shot_dir = args.next().map(PathBuf::from);
52
53    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
54    let mut game = RunnerGame::new(
55        project,
56        RunnerOptions {
57            headless: true,
58            fresh: true,
59            // Deterministic battles: always hit, 89% variance, never crit.
60            rng_script: Some(vec![50, 100, 1]),
61            ..RunnerOptions::default()
62        },
63    )
64    .expect("boot game");
65    println!("money at boot: {}", game.money());
66
67    // The agenda per battle (index = battles started so far − 1): the
68    // trainer battle tries Run first (blocked), the wild battle runs.
69    // Battle 1's agenda is pre-armed: the entry scene suspends on
70    // `startBattle` during boot, so its first menu frame precedes the
71    // battle-start edge below (later battles arm on their edge — text plays
72    // between them).
73    let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
74    let mut battles_done = 0usize;
75    let mut agenda: Vec<Pick> = agendas[0].to_vec();
76    // Queued button presses; one key every other frame (a held key is not a
77    // fresh just-press, so keys alternate with idle frames).
78    let mut keys: Vec<GbButton> = Vec::new();
79    let mut last_log: Vec<String> = Vec::new();
80    let mut was_in_battle_prev = false;
81    let mut snapped: Vec<String> = Vec::new();
82    // Shop agenda: 0 = root (→ Sell), 1 = sell view (→ sell one), 2 = note
83    // (→ dismiss), 3 = back to root (→ B), 4 = exit (→ B), 5 = done.
84    let mut shop_step = 0usize;
85    let mut shop_done = false;
86
87    let mut input = InputState::new();
88    for frame in 0..6000u32 {
89        let was_in_battle = was_in_battle_prev;
90
91        // Decide this frame's input.
92        let mut mask = 0;
93        if frame % 2 == 0 {
94            if !keys.is_empty() {
95                mask = keys.remove(0).bit_mask();
96            } else if let Some(battle) = game.battle() {
97                if battle.in_menu() {
98                    let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
99                    mask = if is_root {
100                        let pick = agenda.first().copied().unwrap_or(Pick::Fight);
101                        if !agenda.is_empty() {
102                            agenda.remove(0);
103                        }
104                        match pick {
105                            Pick::Fight => GbButton::A.bit_mask(),
106                            Pick::Run => {
107                                // Fight/Party/Item/Run → Down ×3, confirm.
108                                keys.push(GbButton::Down);
109                                keys.push(GbButton::Down);
110                                keys.push(GbButton::A);
111                                GbButton::Down.bit_mask()
112                            }
113                        }
114                    } else {
115                        GbButton::A.bit_mask() // skill submenu
116                    };
117                } else if frame % 4 == 0 {
118                    mask = GbButton::A.bit_mask(); // page narration
119                }
120            } else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
121                match shop_step {
122                    0 => {
123                        println!("shop root: {:?}", game.shop_lines().unwrap());
124                        keys.push(GbButton::Down); // → Sell
125                        keys.push(GbButton::A);
126                    }
127                    1 => {
128                        println!("sell view: {:?}", game.shop_lines().unwrap());
129                        keys.push(GbButton::A); // sell one Potion
130                    }
131                    2 => {
132                        let lines = game.shop_lines().unwrap();
133                        println!("sell note: {lines:?}");
134                        if let Some(dir) = &shot_dir {
135                            snap(&mut game, &dir.join("shop-sold.png"));
136                        }
137                        keys.push(GbButton::A); // dismiss the note
138                    }
139                    3 => keys.push(GbButton::B), // back to the root
140                    _ => keys.push(GbButton::B), // exit the shop
141                }
142                shop_step += 1;
143            } else if frame % 4 == 0 {
144                mask = GbButton::A.bit_mask(); // page dialogue
145            }
146        }
147        input.set_from_bitmask(mask);
148        game.update(&input);
149        input.begin_frame();
150
151        let in_battle = game.battle().is_some();
152        // Battle-start edge: print the matchup and load its agenda.
153        if in_battle && !was_in_battle {
154            let n = battles_done + 1;
155            let battle = game.battle().unwrap();
156            println!(
157                "battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
158                battle.player().name,
159                battle.enemy().name,
160                battle.enemies_remaining(),
161                battle.is_trainer(),
162                battle.trainer_money(),
163            );
164            if battles_done > 0 {
165                agenda = agendas[battles_done].to_vec();
166            }
167        }
168        if in_battle {
169            // Screenshot the two v2-d narration lines on their first show.
170            // Screenshot the two v2-d narration lines on their first show.
171            let line = game
172                .battle()
173                .unwrap()
174                .current_line()
175                .map(str::to_string);
176            if let (Some(dir), Some(line)) = (&shot_dir, line) {
177                let tag = match line.as_str() {
178                    l if l.starts_with("Can't escape") => Some("run-blocked"),
179                    l if l.starts_with("Got away") => Some("run-safe"),
180                    l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181                        Some("trainer-money")
182                    }
183                    _ => None,
184                };
185                if let Some(tag) = tag {
186                    if !snapped.contains(&tag.to_string()) {
187                        snapped.push(tag.to_string());
188                        snap(&mut game, &dir.join(format!("{tag}.png")));
189                    }
190                }
191            }
192            last_log = game.battle().unwrap().log().to_vec();
193        }
194        // Battle-end edge: the runner has harvested the state.
195        if was_in_battle && !in_battle {
196            battles_done += 1;
197            println!("\nbattle {battles_done} log:");
198            for line in &last_log {
199                println!("  {line}");
200            }
201            if battles_done == 1 {
202                assert!(
203                    last_log
204                        .iter()
205                        .any(|l| l == "Can't escape from a trainer battle!"),
206                    "the trainer Run attempt must be blocked (and the turn not consumed)"
207                );
208            }
209            println!("money after battle {battles_done}: {}", game.money());
210            if let Some(party) = game.party_state() {
211                for m in party {
212                    println!(
213                        "  {} hp {} mp {} level {} exp {}",
214                        m.id, m.hp, m.mp, m.level, m.exp
215                    );
216                }
217            }
218            println!();
219            last_log.clear();
220        }
221        // Shop-exit edge.
222        if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223            shop_done = true;
224            println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225        }
226        // Everything done: both battles + the shop, scene finished.
227        if battles_done == 2 && shop_done && game.battle().is_none() {
228            println!("acceptance complete");
229            break;
230        }
231        was_in_battle_prev = in_battle;
232    }
233
234    // Verify the v2-d contract points and exit non-zero on any miss.
235    let money = game.money();
236    let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
237    println!("final: money {money}, potions {potions:?}");
238    assert_eq!(battles_done, 2, "both battles must complete");
239    assert!(shop_done, "the shop sell must complete");
240    assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
241    assert_eq!(potions, Some(2), "3 starting − 1 sold");
242}
Source

pub fn whiteout_active(&self) -> bool

true while the game-over whiteout owns the screen (blackout or message phase; test/debug introspection).

Examples found in repository?
examples/menu_accept.rs (line 151)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
Source

pub fn player_tile(&self) -> (i32, i32)

The player’s current tile.

Examples found in repository?
examples/menu_accept.rs (line 164)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
Source

pub fn player_elevation(&self) -> u8

The player’s current elevation level (multi-level maps; test/debug introspection).

Source

pub fn is_blocked(&self, x: i32, y: i32) -> bool

true when tile (x, y) is map-solid on the current map (solid when there is no map). NPC occupancy is not included; test/debug helper.

Source

pub fn audio(&self) -> &RunnerAudio

The audio subsystem (test/debug introspection).

Source

pub fn render_audio(&mut self, frames: usize) -> Vec<f32>

Pull frames stereo PCM frames (44100 Hz, interleaved L/R f32, length 2 * frames) from the audio engine — the render path for hosts without an audio callback thread (the WASM shell feeding WebAudio). Empty unless RunnerOptions::pcm_audio is on and a play command has arrived.

Source

pub fn debug_place(&mut self, x: i32, y: i32, facing: Direction)

Teleport the player (test/debug helper; no scene side effects).

Examples found in repository?
examples/menu_accept.rs (line 99)
76fn main() {
77    let mut args = std::env::args().skip(1);
78    let dir = args.next().expect("usage: menu_accept <project-dir> [shot-dir]");
79    let shot_dir = args.next().map(PathBuf::from);
80
81    let project = LoadedProject::load(Path::new(&dir)).expect("load project");
82    let mut game = RunnerGame::new(
83        project,
84        RunnerOptions {
85            headless: true,
86            fresh: true,
87            // Deterministic battles: always hit, 89% variance, never crit.
88            rng_script: Some(vec![50, 100, 1]),
89            ..RunnerOptions::default()
90        },
91    )
92    .expect("boot game");
93
94    dismiss(&mut game); // StartTown main
95    println!("== 1. the shop ==");
96    println!("money at boot: {} (manifest shop.startMoney)", game.money());
97
98    // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
99    game.debug_place(12, 10, Direction::Right);
100    press_a(&mut game);
101    let rows = game.shop_lines().expect("shop open");
102    println!("shop shelf: {rows:?}");
103    snap(&mut game, &shot_dir, "shop");
104
105    // Buy one Potion: 100 → 80 G, inventory 3 → 4.
106    let before = game.money();
107    press_a(&mut game);
108    let rows = game.shop_lines().expect("shop still open");
109    println!(
110        "bought a Potion: money {before} → {} G; inventory {:?}",
111        game.money(),
112        game.inventory().expect("inventory materialized")
113    );
114    println!("shop note: {:?}", rows.last());
115    snap(&mut game, &shot_dir, "shop-bought");
116    assert_eq!(game.money(), 80, "20 G charged");
117    assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
118    press_a(&mut game); // dismiss the note
119
120    // B exits; the scene resumes with its follow-up line.
121    press(&mut game, GbButton::B);
122    let page = game.dialogue_text().expect("scene resumed after the shop");
123    println!("scene resumed with: {page:?}");
124    assert!(page.contains("Come again!"), "page: {page:?}");
125    dismiss(&mut game);
126
127    println!("\n== 2. the whiteout ==");
128    // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
129    game.debug_place(12, 10, Direction::Left);
130    press_a(&mut game); // "Something stirs…"
131    press_a(&mut game); // → battle
132    let mut last_log: Vec<String> = Vec::new();
133    for _ in 0..120 {
134        let Some(battle) = game.battle() else { break };
135        last_log = battle.log().to_vec();
136        press_a(&mut game);
137    }
138    println!("lost battle log:");
139    for line in &last_log {
140        println!("  {line}");
141    }
142    assert_eq!(last_log.last().map(String::as_str), Some("You lost the battle…"));
143
144    // The scene's post-lose text still plays…
145    let page = game.dialogue_text().expect("post-lose text");
146    println!("post-lose text: {page:?}");
147    assert!(page.contains("no match"), "page: {page:?}");
148    press_a(&mut game); // scene finishes → whiteout
149
150    // …then the whiteout: blackout first, then the collapsed line.
151    assert!(game.whiteout_active(), "whiteout armed");
152    idle(&mut game, 10);
153    snap(&mut game, &shot_dir, "whiteout-blackout");
154    idle(&mut game, 25);
155    let page = game.dialogue_text().expect("whiteout message");
156    println!("whiteout message: {page:?}");
157    assert!(page.contains("collapsed"), "page: {page:?}");
158    snap(&mut game, &shot_dir, "whiteout-message");
159
160    // Landing it: party healed to full, back at the entry spawn, flags kept.
161    press_a(&mut game);
162    assert!(!game.whiteout_active());
163    println!("after the whiteout:");
164    println!("  map: {:?} tile: {:?}", game.current_map_id(), game.player_tile());
165    for m in game.party_state().expect("party state") {
166        println!("  {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
167    }
168    println!("  __played_main_StartTown flag kept: {}", game.flag("__played_main_StartTown"));
169    assert_eq!(game.current_map_id(), Some("StartTown"));
170    assert!(game
171        .party_state()
172        .unwrap()
173        .iter()
174        .all(|m| m.hp > 0 && m.status.is_none()));
175    snap(&mut game, &shot_dir, "whiteout-after");
176
177    println!("\nacceptance OK");
178}
Source

pub fn frame_count(&self) -> u64

Frames updated so far.

Trait Implementations§

Source§

impl GameLoop for RunnerGame

Available on non-WebAssembly only.
Source§

type Fb = FrameBuffer

The framebuffer type the game draws into: either the engine’s RGBA [FrameBuffer] (true-color games) or the indexed crate::RgbaIndexedFrameBuffer (fixed-palette games).
Source§

fn update(&mut self, input: &InputState)

Source§

fn draw(&mut self, frame_buffer: &mut FrameBuffer)

Source§

fn should_exit(&self) -> bool

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Conv for T

Source§

fn conv<T>(self) -> T
where Self: Into<T>,

Converts self into T using Into<T>. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> FmtForward for T

Source§

fn fmt_binary(self) -> FmtBinary<Self>
where Self: Binary,

Causes self to use its Binary implementation when Debug-formatted.
Source§

fn fmt_display(self) -> FmtDisplay<Self>
where Self: Display,

Causes self to use its Display implementation when Debug-formatted.
Source§

fn fmt_lower_exp(self) -> FmtLowerExp<Self>
where Self: LowerExp,

Causes self to use its LowerExp implementation when Debug-formatted.
Source§

fn fmt_lower_hex(self) -> FmtLowerHex<Self>
where Self: LowerHex,

Causes self to use its LowerHex implementation when Debug-formatted.
Source§

fn fmt_octal(self) -> FmtOctal<Self>
where Self: Octal,

Causes self to use its Octal implementation when Debug-formatted.
Source§

fn fmt_pointer(self) -> FmtPointer<Self>
where Self: Pointer,

Causes self to use its Pointer implementation when Debug-formatted.
Source§

fn fmt_upper_exp(self) -> FmtUpperExp<Self>
where Self: UpperExp,

Causes self to use its UpperExp implementation when Debug-formatted.
Source§

fn fmt_upper_hex(self) -> FmtUpperHex<Self>
where Self: UpperHex,

Causes self to use its UpperHex implementation when Debug-formatted.
Source§

fn fmt_list(self) -> FmtList<Self>
where &'a Self: for<'a> IntoIterator,

Formats each item in a sequence. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> MaybeSendSync for T

Source§

impl<D> OwoColorize for D

Source§

fn fg<C>(&self) -> FgColorDisplay<'_, C, Self>
where C: Color,

Set the foreground color generically Read more
Source§

fn bg<C>(&self) -> BgColorDisplay<'_, C, Self>
where C: Color,

Set the background color generically. Read more
Source§

fn black(&self) -> FgColorDisplay<'_, Black, Self>

Change the foreground color to black
Source§

fn on_black(&self) -> BgColorDisplay<'_, Black, Self>

Change the background color to black
Source§

fn red(&self) -> FgColorDisplay<'_, Red, Self>

Change the foreground color to red
Source§

fn on_red(&self) -> BgColorDisplay<'_, Red, Self>

Change the background color to red
Source§

fn green(&self) -> FgColorDisplay<'_, Green, Self>

Change the foreground color to green
Source§

fn on_green(&self) -> BgColorDisplay<'_, Green, Self>

Change the background color to green
Source§

fn yellow(&self) -> FgColorDisplay<'_, Yellow, Self>

Change the foreground color to yellow
Source§

fn on_yellow(&self) -> BgColorDisplay<'_, Yellow, Self>

Change the background color to yellow
Source§

fn blue(&self) -> FgColorDisplay<'_, Blue, Self>

Change the foreground color to blue
Source§

fn on_blue(&self) -> BgColorDisplay<'_, Blue, Self>

Change the background color to blue
Source§

fn magenta(&self) -> FgColorDisplay<'_, Magenta, Self>

Change the foreground color to magenta
Source§

fn on_magenta(&self) -> BgColorDisplay<'_, Magenta, Self>

Change the background color to magenta
Source§

fn purple(&self) -> FgColorDisplay<'_, Magenta, Self>

Change the foreground color to purple
Source§

fn on_purple(&self) -> BgColorDisplay<'_, Magenta, Self>

Change the background color to purple
Source§

fn cyan(&self) -> FgColorDisplay<'_, Cyan, Self>

Change the foreground color to cyan
Source§

fn on_cyan(&self) -> BgColorDisplay<'_, Cyan, Self>

Change the background color to cyan
Source§

fn white(&self) -> FgColorDisplay<'_, White, Self>

Change the foreground color to white
Source§

fn on_white(&self) -> BgColorDisplay<'_, White, Self>

Change the background color to white
Source§

fn default_color(&self) -> FgColorDisplay<'_, Default, Self>

Change the foreground color to the terminal default
Source§

fn on_default_color(&self) -> BgColorDisplay<'_, Default, Self>

Change the background color to the terminal default
Source§

fn bright_black(&self) -> FgColorDisplay<'_, BrightBlack, Self>

Change the foreground color to bright black
Source§

fn on_bright_black(&self) -> BgColorDisplay<'_, BrightBlack, Self>

Change the background color to bright black
Source§

fn bright_red(&self) -> FgColorDisplay<'_, BrightRed, Self>

Change the foreground color to bright red
Source§

fn on_bright_red(&self) -> BgColorDisplay<'_, BrightRed, Self>

Change the background color to bright red
Source§

fn bright_green(&self) -> FgColorDisplay<'_, BrightGreen, Self>

Change the foreground color to bright green
Source§

fn on_bright_green(&self) -> BgColorDisplay<'_, BrightGreen, Self>

Change the background color to bright green
Source§

fn bright_yellow(&self) -> FgColorDisplay<'_, BrightYellow, Self>

Change the foreground color to bright yellow
Source§

fn on_bright_yellow(&self) -> BgColorDisplay<'_, BrightYellow, Self>

Change the background color to bright yellow
Source§

fn bright_blue(&self) -> FgColorDisplay<'_, BrightBlue, Self>

Change the foreground color to bright blue
Source§

fn on_bright_blue(&self) -> BgColorDisplay<'_, BrightBlue, Self>

Change the background color to bright blue
Source§

fn bright_magenta(&self) -> FgColorDisplay<'_, BrightMagenta, Self>

Change the foreground color to bright magenta
Source§

fn on_bright_magenta(&self) -> BgColorDisplay<'_, BrightMagenta, Self>

Change the background color to bright magenta
Source§

fn bright_purple(&self) -> FgColorDisplay<'_, BrightMagenta, Self>

Change the foreground color to bright purple
Source§

fn on_bright_purple(&self) -> BgColorDisplay<'_, BrightMagenta, Self>

Change the background color to bright purple
Source§

fn bright_cyan(&self) -> FgColorDisplay<'_, BrightCyan, Self>

Change the foreground color to bright cyan
Source§

fn on_bright_cyan(&self) -> BgColorDisplay<'_, BrightCyan, Self>

Change the background color to bright cyan
Source§

fn bright_white(&self) -> FgColorDisplay<'_, BrightWhite, Self>

Change the foreground color to bright white
Source§

fn on_bright_white(&self) -> BgColorDisplay<'_, BrightWhite, Self>

Change the background color to bright white
Source§

fn bold(&self) -> BoldDisplay<'_, Self>

Make the text bold
Source§

fn dimmed(&self) -> DimDisplay<'_, Self>

Make the text dim
Source§

fn italic(&self) -> ItalicDisplay<'_, Self>

Make the text italicized
Source§

fn underline(&self) -> UnderlineDisplay<'_, Self>

Make the text underlined
Make the text blink
Make the text blink (but fast!)
Source§

fn reversed(&self) -> ReversedDisplay<'_, Self>

Swap the foreground and background colors
Source§

fn hidden(&self) -> HiddenDisplay<'_, Self>

Hide the text
Source§

fn strikethrough(&self) -> StrikeThroughDisplay<'_, Self>

Cross out the text
Source§

fn color<Color>(&self, color: Color) -> FgDynColorDisplay<'_, Color, Self>
where Color: DynColor,

Set the foreground color at runtime. Only use if you do not know which color will be used at compile-time. If the color is constant, use either OwoColorize::fg or a color-specific method, such as OwoColorize::green, Read more
Source§

fn on_color<Color>(&self, color: Color) -> BgDynColorDisplay<'_, Color, Self>
where Color: DynColor,

Set the background color at runtime. Only use if you do not know what color to use at compile-time. If the color is constant, use either OwoColorize::bg or a color-specific method, such as OwoColorize::on_yellow, Read more
Source§

fn fg_rgb<const R: u8, const G: u8, const B: u8>( &self, ) -> FgColorDisplay<'_, CustomColor<R, G, B>, Self>

Set the foreground color to a specific RGB value.
Source§

fn bg_rgb<const R: u8, const G: u8, const B: u8>( &self, ) -> BgColorDisplay<'_, CustomColor<R, G, B>, Self>

Set the background color to a specific RGB value.
Source§

fn truecolor(&self, r: u8, g: u8, b: u8) -> FgDynColorDisplay<'_, Rgb, Self>

Sets the foreground color to an RGB value.
Source§

fn on_truecolor(&self, r: u8, g: u8, b: u8) -> BgDynColorDisplay<'_, Rgb, Self>

Sets the background color to an RGB value.
Source§

fn style(&self, style: Style) -> Styled<&Self>

Apply a runtime-determined style
Source§

impl<T> Pipe for T
where T: ?Sized,

Source§

fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> R
where Self: Sized,

Pipes by value. This is generally the method you want to use. Read more
Source§

fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> R
where R: 'a,

Borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> R
where R: 'a,

Mutably borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
where Self: Borrow<B>, B: 'a + ?Sized, R: 'a,

Borrows self, then passes self.borrow() into the pipe function. Read more
Source§

fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
where Self: BorrowMut<B>, B: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.borrow_mut() into the pipe function. Read more
Source§

fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
where Self: AsRef<U>, U: 'a + ?Sized, R: 'a,

Borrows self, then passes self.as_ref() into the pipe function.
Source§

fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
where Self: AsMut<U>, U: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.as_mut() into the pipe function.
Source§

fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
where Self: Deref<Target = T>, T: 'a + ?Sized, R: 'a,

Borrows self, then passes self.deref() into the pipe function.
Source§

fn pipe_deref_mut<'a, T, R>( &'a mut self, func: impl FnOnce(&'a mut T) -> R, ) -> R
where Self: DerefMut<Target = T> + Deref, T: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.deref_mut() into the pipe function.
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Tap for T

Source§

fn tap(self, func: impl FnOnce(&Self)) -> Self

Immutable access to a value. Read more
Source§

fn tap_mut(self, func: impl FnOnce(&mut Self)) -> Self

Mutable access to a value. Read more
Source§

fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Immutable access to the Borrow<B> of a value. Read more
Source§

fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Mutable access to the BorrowMut<B> of a value. Read more
Source§

fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Immutable access to the AsRef<R> view of a value. Read more
Source§

fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Mutable access to the AsMut<R> view of a value. Read more
Source§

fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Immutable access to the Deref::Target of a value. Read more
Source§

fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Mutable access to the Deref::Target of a value. Read more
Source§

fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self

Calls .tap() only in debug builds, and is erased in release builds.
Source§

fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self

Calls .tap_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Calls .tap_borrow() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Calls .tap_borrow_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Calls .tap_ref() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Calls .tap_ref_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Calls .tap_deref() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_mut_dbg<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Calls .tap_deref_mut() only in debug builds, and is erased in release builds.
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T> TryConv for T

Source§

fn try_conv<T>(self) -> Result<T, Self::Error>
where Self: TryInto<T>,

Attempts to convert self into T using TryInto<T>. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more