Skip to main content

Battle

Struct Battle 

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

A live battle: the player’s party (the active member fights) against one enemy, the item inventory, the type chart, and the phase machine. Drive with update + draw; outcome reports the end (the runner then resumes the suspended scene with "win"/"lose" and harvests the party state).

Implementations§

Source§

impl Battle

Source

pub fn new( player: Combatant, enemy: Combatant, chart: TypeChart, rng: Box<dyn BattleRng>, ) -> Self

A 1v1 battle between two already-built combatants (no RON hooks, no items — the unit-test shape).

Source

pub fn with_hooks( player: Combatant, enemy: Combatant, chart: TypeChart, rng: Box<dyn BattleRng>, hooks: Option<HookState>, ) -> Self

A battle between two already-built combatants, with the RON hook state when the project compiled one.

Source

pub fn full( party: Vec<Combatant>, active: usize, enemy: Combatant, items: Vec<BattleItem>, inventory: HashMap<String, u32>, chart: TypeChart, rng: Box<dyn BattleRng>, hooks: Option<HookState>, ) -> Self

The full constructor: the party + its active member, the enemy, the usable items and the inventory counts.

Source

pub fn set_levels(&mut self, levels: Option<LevelsSetup>)

Arm the levels config (v2-c; BattleSetup::start_with calls this).

Source

pub fn set_lang(&mut self, lang: &str)

Set the narration language ("en"/"zh") for the EXP/level-up lines; the runner passes its --lang here.

Source

pub fn set_currency(&mut self, currency: &str)

Set the currency label for the trainer-money narration (the runner’s shop.currency); default “G”.

Source

pub fn set_enemy_party( &mut self, rest: Vec<Combatant>, trainer: bool, money: u32, )

Arm the enemy party extras (v2-d): the enemies queued behind the active one plus the trainer flag and the win’s money reward (BattleSetup::start_with calls this; the plain constructors leave a single wild enemy).

Source

pub fn set_weather(&mut self, weather: Option<String>)

Arm the battle-local weather (v2-e): a kind: Weather RON record id, None to clear. The runner sets this from a scene’s setWeather / clearWeather before the battle begins; it is never saved and dies with the battle.

Source

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

The armed weather record id, if any (tests, introspection).

Source

pub fn begin(&mut self)

Battle-start hook pass (v2-e): narrate the armed weather’s intro, then fire both active combatants’ ability SwitchIn hooks (player first). Any produced lines queue as the battle’s opening narration; with nothing to say the battle opens on the root menu exactly as v1. The runner calls this once per battle, after [set_weather]; the plain constructors leave it to tests. No-op without hooks or once the battle is already decided.

Source

pub fn player(&self) -> &Combatant

The active player combatant.

Examples found in repository?
examples/encounter_accept.rs (line 158)
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(&self) -> &[Combatant]

The whole party (index 0 fights unless switched).

Examples found in repository?
examples/levels_accept.rs (line 129)
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/battle_accept.rs (line 142)
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}
Source

pub fn active_index(&self) -> usize

The active member’s index in party.

Source

pub fn enemy(&self) -> &Combatant

The enemy combatant.

Examples found in repository?
examples/encounter_accept.rs (line 159)
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 enemies_remaining(&self) -> usize

How many enemies still wait behind the active one (v2-d encounters).

Examples found in repository?
examples/encounter_accept.rs (line 160)
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 is_trainer(&self) -> bool

Whether this is a trainer battle (Run is blocked).

Examples found in repository?
examples/encounter_accept.rs (line 161)
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 trainer_money(&self) -> u32

The money the runner pays the player on a win (0 for wild battles).

Examples found in repository?
examples/encounter_accept.rs (line 162)
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 outcome(&self) -> Option<BattleOutcome>

The result, once the battle has ended (set when the last narration line is dismissed).

Source

pub fn log(&self) -> &[String]

The full narration history.

Examples found in repository?
examples/menu_accept.rs (line 135)
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 109)
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 153)
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 192)
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 hooks(&self) -> Option<&HookState>

The RON hook state, when this battle compiled one (tests, debug).

Source

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

The live inventory (record id → count).

Examples found in repository?
examples/battle_accept.rs (line 149)
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}
Source

pub fn party_state(&self) -> Vec<PartyMemberState>

The persistent party state (v2-b) for the runner to keep between battles: every member’s current HP/MP and status — plus level/exp (v2-c).

Source

pub fn in_menu(&self) -> bool

true while a player menu owns input.

Examples found in repository?
examples/battle_accept.rs (line 85)
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}
More examples
Hide additional examples
examples/encounter_accept.rs (line 97)
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 current_line(&self) -> Option<&str>

The narration line currently on screen.

Examples found in repository?
examples/encounter_accept.rs (line 174)
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_items(&self) -> Vec<String>

The current menu’s labels (marked × entries are unselectable).

Examples found in repository?
examples/battle_accept.rs (line 86)
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}
More examples
Hide additional examples
examples/encounter_accept.rs (line 98)
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 update(&mut self, input: &InputState)

Advance the battle one frame: menu cursor / confirm / cancel, or narration paging. Sets outcome when the battle resolves.

Source

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

Draw the battle screen: a two-tone field with placeholder combatant blobs, an enemy panel (name, element, HP bar) up top, a player panel (name, HP bar, MP) below, and the current menu or narration line at the bottom.

Auto Trait Implementations§

§

impl !RefUnwindSafe for Battle

§

impl !Send for Battle

§

impl !Sync for Battle

§

impl !UnwindSafe for Battle

§

impl Freeze for Battle

§

impl Unpin for Battle

§

impl UnsafeUnpin for Battle

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