pub struct RunnerGame { /* private fields */ }Expand description
A booted zero-Rust game: overworld + scene VM + dialogue/choice UI.
Owns the LoadedProject, the current RuntimeMap, the player
OverworldActor, the persistent flag store (seeded into / harvested
from each short-lived scene engine) and the mode state machine. Drive it
with update + draw — directly (headless)
or via the dotzuki_app::GameLoop impl (windowed).
Implementations§
Source§impl RunnerGame
impl RunnerGame
Sourcepub fn new(project: LoadedProject, opts: RunnerOptions) -> Result<Self>
pub fn new(project: LoadedProject, opts: RunnerOptions) -> Result<Self>
Boot the project: load the entry map (opts.map override or
game.entryMap), spawn the player, and run the map’s opening
dispatch. A project with no maps boots dialogue-only: the entry
scene’s main storyline runs to completion, then the game idles.
§Errors
Fails when the entry map (or --map override) cannot be loaded, or
when a map-less project has no entry scene.
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}46fn main() {
47 let mut args = std::env::args().skip(1);
48 let dir = args
49 .next()
50 .expect("usage: battle_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
66 // The agenda per battle (index = battles started so far − 1).
67 let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
68 let mut battles_done = 0usize;
69 let mut agenda: Vec<Pick> = Vec::new();
70 // Queued button presses; one key every other frame (a held key is not a
71 // fresh just-press, so keys alternate with idle frames).
72 let mut keys: Vec<GbButton> = Vec::new();
73 let mut last_log: Vec<String> = Vec::new();
74 let mut snapped: Vec<String> = Vec::new();
75 let mut was_in_battle_prev = false;
76
77 let mut input = InputState::new();
78 for frame in 0..6000u32 {
79 let was_in_battle = was_in_battle_prev;
80
81 // Decide this frame's input.
82 let mut mask = 0;
83 if frame % 2 == 0 {
84 if !keys.is_empty() {
85 mask = keys.remove(0).bit_mask();
86 } else if let Some(battle) = game.battle() {
87 if battle.in_menu() {
88 let items = battle.menu_items();
89 let label = if items.first().is_some_and(|i| i == "Fight") {
90 "root"
91 } else if items.iter().any(|i| i.contains('/')) {
92 "party"
93 } else {
94 "sub"
95 };
96 if let Some(dir) = &shot_dir {
97 let tag = format!("battle{}-{label}", battles_done + 1);
98 if !snapped.contains(&tag) {
99 snapped.push(tag.clone());
100 snap(&mut game, &dir.join(format!("{tag}.png")));
101 }
102 }
103 mask = match label {
104 "root" => {
105 let pick = agenda.first().copied().unwrap_or(Pick::Fight);
106 if !agenda.is_empty() {
107 agenda.remove(0);
108 }
109 match pick {
110 Pick::Fight => GbButton::A.bit_mask(),
111 Pick::Party => {
112 keys.push(GbButton::A);
113 GbButton::Down.bit_mask()
114 }
115 Pick::Item => {
116 keys.push(GbButton::Down);
117 keys.push(GbButton::A);
118 GbButton::Down.bit_mask()
119 }
120 }
121 }
122 // Submenus: confirm the entry under the cursor (the
123 // party cursor starts on the first switchable member).
124 _ => GbButton::A.bit_mask(),
125 };
126 } else if frame % 4 == 0 {
127 mask = GbButton::A.bit_mask(); // page narration
128 }
129 } else if frame % 4 == 0 {
130 mask = GbButton::A.bit_mask(); // page dialogue
131 }
132 }
133 input.set_from_bitmask(mask);
134 game.update(&input);
135 input.begin_frame();
136
137 let in_battle = game.battle().is_some();
138 // Battle-start edge: print the party it starts with (the carried-over
139 // state from battle 2 on) and load its agenda.
140 if in_battle && !was_in_battle {
141 let n = battles_done + 1;
142 let battle = game.battle().unwrap();
143 println!("battle {n} starts:");
144 for c in battle.party() {
145 let status = c.status.as_deref().unwrap_or("-");
146 println!(
147 " {} {}/{} MP {}/{} status {}",
148 c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
149 );
150 }
151 println!(" inventory: {:?}", battle.inventory());
152 agenda = agendas[battles_done].to_vec();
153 }
154 if in_battle {
155 last_log = game.battle().unwrap().log().to_vec();
156 }
157 // Battle-end edge: the runner has harvested the party state.
158 if was_in_battle && !in_battle {
159 battles_done += 1;
160 println!("\nbattle {battles_done} log:");
161 for line in &last_log {
162 println!(" {line}");
163 }
164 if let Some(party) = game.party_state() {
165 println!("party state after battle {battles_done}:");
166 for m in party {
167 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
168 }
169 }
170 if let Some(inv) = game.inventory() {
171 println!("inventory: {inv:?}");
172 }
173 println!();
174 last_log.clear();
175 if battles_done == 2 {
176 println!("both battles done — stopping");
177 break;
178 }
179 }
180 was_in_battle_prev = in_battle;
181 }
182
183 if battles_done < 2 {
184 eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
185 std::process::exit(1);
186 }
187}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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn export_save(&self) -> Option<String>
pub fn export_save(&self) -> Option<String>
Serialize the current state as save JSON — the persistence bridge for
the WASM shell (localStorage). Returns None while the game is in a
transient state that cannot round-trip (a scene engine suspended on
text/choice/delay/battle/shop, or a warp transition mid-flight);
stable states (overworld, menu, whiteout, idle) always export.
Sourcepub fn import_save(&mut self, json: &str) -> bool
pub fn import_save(&mut self, json: &str) -> bool
Restore a save produced by export_save (the
WASM shell’s localStorage bridge). Returns false — the game keeps
its current state — on unparseable JSON, a NEWER save version, or a
saved map that no longer loads.
Sourcepub fn poll_watch(&mut self)
pub fn poll_watch(&mut self)
Poll the file watcher and apply pending changes as one batch.
Called at the top of every update; a no-op when
watching is off. Batches are debounced by [WATCH_DEBOUNCE_FRAMES]
so an editor save burst applies once.
Sourcepub fn reload_scenes(&mut self) -> bool
pub fn reload_scenes(&mut self) -> bool
Recompile the project’s DSL and swap the compiled scenes in place.
A scene mid-activation keeps running its old engine; the next
activation (talk/enter) picks up the new source. On a compiler
diagnostic the old scenes keep running (false).
Sourcepub fn reload_current_map(&mut self) -> bool
pub fn reload_current_map(&mut self) -> bool
Reload the current map from disk in place, preserving the player’s
pixel position and the story flags. On a load error the old map is
kept (false); false also when there is no current map.
Sourcepub fn update(&mut self, input: &InputState)
pub fn update(&mut self, input: &InputState)
Advance the game one frame. Callable without any window (headless
tests, the run_headless driver); the GameLoop impl forwards here.
Examples found in repository?
More examples
46fn main() {
47 let mut args = std::env::args().skip(1);
48 let dir = args
49 .next()
50 .expect("usage: battle_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
66 // The agenda per battle (index = battles started so far − 1).
67 let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
68 let mut battles_done = 0usize;
69 let mut agenda: Vec<Pick> = Vec::new();
70 // Queued button presses; one key every other frame (a held key is not a
71 // fresh just-press, so keys alternate with idle frames).
72 let mut keys: Vec<GbButton> = Vec::new();
73 let mut last_log: Vec<String> = Vec::new();
74 let mut snapped: Vec<String> = Vec::new();
75 let mut was_in_battle_prev = false;
76
77 let mut input = InputState::new();
78 for frame in 0..6000u32 {
79 let was_in_battle = was_in_battle_prev;
80
81 // Decide this frame's input.
82 let mut mask = 0;
83 if frame % 2 == 0 {
84 if !keys.is_empty() {
85 mask = keys.remove(0).bit_mask();
86 } else if let Some(battle) = game.battle() {
87 if battle.in_menu() {
88 let items = battle.menu_items();
89 let label = if items.first().is_some_and(|i| i == "Fight") {
90 "root"
91 } else if items.iter().any(|i| i.contains('/')) {
92 "party"
93 } else {
94 "sub"
95 };
96 if let Some(dir) = &shot_dir {
97 let tag = format!("battle{}-{label}", battles_done + 1);
98 if !snapped.contains(&tag) {
99 snapped.push(tag.clone());
100 snap(&mut game, &dir.join(format!("{tag}.png")));
101 }
102 }
103 mask = match label {
104 "root" => {
105 let pick = agenda.first().copied().unwrap_or(Pick::Fight);
106 if !agenda.is_empty() {
107 agenda.remove(0);
108 }
109 match pick {
110 Pick::Fight => GbButton::A.bit_mask(),
111 Pick::Party => {
112 keys.push(GbButton::A);
113 GbButton::Down.bit_mask()
114 }
115 Pick::Item => {
116 keys.push(GbButton::Down);
117 keys.push(GbButton::A);
118 GbButton::Down.bit_mask()
119 }
120 }
121 }
122 // Submenus: confirm the entry under the cursor (the
123 // party cursor starts on the first switchable member).
124 _ => GbButton::A.bit_mask(),
125 };
126 } else if frame % 4 == 0 {
127 mask = GbButton::A.bit_mask(); // page narration
128 }
129 } else if frame % 4 == 0 {
130 mask = GbButton::A.bit_mask(); // page dialogue
131 }
132 }
133 input.set_from_bitmask(mask);
134 game.update(&input);
135 input.begin_frame();
136
137 let in_battle = game.battle().is_some();
138 // Battle-start edge: print the party it starts with (the carried-over
139 // state from battle 2 on) and load its agenda.
140 if in_battle && !was_in_battle {
141 let n = battles_done + 1;
142 let battle = game.battle().unwrap();
143 println!("battle {n} starts:");
144 for c in battle.party() {
145 let status = c.status.as_deref().unwrap_or("-");
146 println!(
147 " {} {}/{} MP {}/{} status {}",
148 c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
149 );
150 }
151 println!(" inventory: {:?}", battle.inventory());
152 agenda = agendas[battles_done].to_vec();
153 }
154 if in_battle {
155 last_log = game.battle().unwrap().log().to_vec();
156 }
157 // Battle-end edge: the runner has harvested the party state.
158 if was_in_battle && !in_battle {
159 battles_done += 1;
160 println!("\nbattle {battles_done} log:");
161 for line in &last_log {
162 println!(" {line}");
163 }
164 if let Some(party) = game.party_state() {
165 println!("party state after battle {battles_done}:");
166 for m in party {
167 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
168 }
169 }
170 if let Some(inv) = game.inventory() {
171 println!("inventory: {inv:?}");
172 }
173 println!();
174 last_log.clear();
175 if battles_done == 2 {
176 println!("both battles done — stopping");
177 break;
178 }
179 }
180 was_in_battle_prev = in_battle;
181 }
182
183 if battles_done < 2 {
184 eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
185 std::process::exit(1);
186 }
187}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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn draw(&mut self, fb: &mut FrameBuffer)
pub fn draw(&mut self, fb: &mut FrameBuffer)
Render the current frame. Callable without any window; the GameLoop
impl forwards here.
Examples found in repository?
More examples
Sourcepub fn current_map_id(&self) -> Option<&str>
pub fn current_map_id(&self) -> Option<&str>
The currently loaded map id (None in dialogue-only mode).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}Sourcepub fn flag(&self, name: &str) -> bool
pub fn flag(&self, name: &str) -> bool
A persistent story flag’s value (defaults to false).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}Sourcepub fn dialogue_text(&self) -> Option<&str>
pub fn dialogue_text(&self) -> Option<&str>
The text page currently on screen, if a textbox is open (including the game-over whiteout message).
Examples found in repository?
66fn dismiss(game: &mut RunnerGame) {
67 for _ in 0..40 {
68 if game.dialogue_text().is_none() && game.choice_options().is_none() {
69 return;
70 }
71 press_a(game);
72 }
73 panic!("dialogue did not close");
74}
75
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}Sourcepub fn choice_options(&self) -> Option<&[String]>
pub fn choice_options(&self) -> Option<&[String]>
The choice options currently on screen, if a choice menu is open.
Sourcepub fn battle(&self) -> Option<&Battle>
pub fn battle(&self) -> Option<&Battle>
The live battle, if one is running (test/debug introspection).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}46fn main() {
47 let mut args = std::env::args().skip(1);
48 let dir = args
49 .next()
50 .expect("usage: battle_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
66 // The agenda per battle (index = battles started so far − 1).
67 let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
68 let mut battles_done = 0usize;
69 let mut agenda: Vec<Pick> = Vec::new();
70 // Queued button presses; one key every other frame (a held key is not a
71 // fresh just-press, so keys alternate with idle frames).
72 let mut keys: Vec<GbButton> = Vec::new();
73 let mut last_log: Vec<String> = Vec::new();
74 let mut snapped: Vec<String> = Vec::new();
75 let mut was_in_battle_prev = false;
76
77 let mut input = InputState::new();
78 for frame in 0..6000u32 {
79 let was_in_battle = was_in_battle_prev;
80
81 // Decide this frame's input.
82 let mut mask = 0;
83 if frame % 2 == 0 {
84 if !keys.is_empty() {
85 mask = keys.remove(0).bit_mask();
86 } else if let Some(battle) = game.battle() {
87 if battle.in_menu() {
88 let items = battle.menu_items();
89 let label = if items.first().is_some_and(|i| i == "Fight") {
90 "root"
91 } else if items.iter().any(|i| i.contains('/')) {
92 "party"
93 } else {
94 "sub"
95 };
96 if let Some(dir) = &shot_dir {
97 let tag = format!("battle{}-{label}", battles_done + 1);
98 if !snapped.contains(&tag) {
99 snapped.push(tag.clone());
100 snap(&mut game, &dir.join(format!("{tag}.png")));
101 }
102 }
103 mask = match label {
104 "root" => {
105 let pick = agenda.first().copied().unwrap_or(Pick::Fight);
106 if !agenda.is_empty() {
107 agenda.remove(0);
108 }
109 match pick {
110 Pick::Fight => GbButton::A.bit_mask(),
111 Pick::Party => {
112 keys.push(GbButton::A);
113 GbButton::Down.bit_mask()
114 }
115 Pick::Item => {
116 keys.push(GbButton::Down);
117 keys.push(GbButton::A);
118 GbButton::Down.bit_mask()
119 }
120 }
121 }
122 // Submenus: confirm the entry under the cursor (the
123 // party cursor starts on the first switchable member).
124 _ => GbButton::A.bit_mask(),
125 };
126 } else if frame % 4 == 0 {
127 mask = GbButton::A.bit_mask(); // page narration
128 }
129 } else if frame % 4 == 0 {
130 mask = GbButton::A.bit_mask(); // page dialogue
131 }
132 }
133 input.set_from_bitmask(mask);
134 game.update(&input);
135 input.begin_frame();
136
137 let in_battle = game.battle().is_some();
138 // Battle-start edge: print the party it starts with (the carried-over
139 // state from battle 2 on) and load its agenda.
140 if in_battle && !was_in_battle {
141 let n = battles_done + 1;
142 let battle = game.battle().unwrap();
143 println!("battle {n} starts:");
144 for c in battle.party() {
145 let status = c.status.as_deref().unwrap_or("-");
146 println!(
147 " {} {}/{} MP {}/{} status {}",
148 c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
149 );
150 }
151 println!(" inventory: {:?}", battle.inventory());
152 agenda = agendas[battles_done].to_vec();
153 }
154 if in_battle {
155 last_log = game.battle().unwrap().log().to_vec();
156 }
157 // Battle-end edge: the runner has harvested the party state.
158 if was_in_battle && !in_battle {
159 battles_done += 1;
160 println!("\nbattle {battles_done} log:");
161 for line in &last_log {
162 println!(" {line}");
163 }
164 if let Some(party) = game.party_state() {
165 println!("party state after battle {battles_done}:");
166 for m in party {
167 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
168 }
169 }
170 if let Some(inv) = game.inventory() {
171 println!("inventory: {inv:?}");
172 }
173 println!();
174 last_log.clear();
175 if battles_done == 2 {
176 println!("both battles done — stopping");
177 break;
178 }
179 }
180 was_in_battle_prev = in_battle;
181 }
182
183 if battles_done < 2 {
184 eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
185 std::process::exit(1);
186 }
187}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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn party_state(&self) -> Option<&[PartyMemberState]>
pub fn party_state(&self) -> Option<&[PartyMemberState]>
The persistent party state, once a battle has completed or a save restored one (test/debug introspection).
Examples found in repository?
58fn print_party(game: &RunnerGame, when: &str) {
59 let Some(party) = game.party_state() else {
60 return;
61 };
62 println!("party state {when}:");
63 for m in party {
64 println!(
65 " {} level {} exp {} hp {} mp {} status {:?}",
66 m.id, m.level, m.exp, m.hp, m.mp, m.status
67 );
68 }
69}
70
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}More examples
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}46fn main() {
47 let mut args = std::env::args().skip(1);
48 let dir = args
49 .next()
50 .expect("usage: battle_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
66 // The agenda per battle (index = battles started so far − 1).
67 let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
68 let mut battles_done = 0usize;
69 let mut agenda: Vec<Pick> = Vec::new();
70 // Queued button presses; one key every other frame (a held key is not a
71 // fresh just-press, so keys alternate with idle frames).
72 let mut keys: Vec<GbButton> = Vec::new();
73 let mut last_log: Vec<String> = Vec::new();
74 let mut snapped: Vec<String> = Vec::new();
75 let mut was_in_battle_prev = false;
76
77 let mut input = InputState::new();
78 for frame in 0..6000u32 {
79 let was_in_battle = was_in_battle_prev;
80
81 // Decide this frame's input.
82 let mut mask = 0;
83 if frame % 2 == 0 {
84 if !keys.is_empty() {
85 mask = keys.remove(0).bit_mask();
86 } else if let Some(battle) = game.battle() {
87 if battle.in_menu() {
88 let items = battle.menu_items();
89 let label = if items.first().is_some_and(|i| i == "Fight") {
90 "root"
91 } else if items.iter().any(|i| i.contains('/')) {
92 "party"
93 } else {
94 "sub"
95 };
96 if let Some(dir) = &shot_dir {
97 let tag = format!("battle{}-{label}", battles_done + 1);
98 if !snapped.contains(&tag) {
99 snapped.push(tag.clone());
100 snap(&mut game, &dir.join(format!("{tag}.png")));
101 }
102 }
103 mask = match label {
104 "root" => {
105 let pick = agenda.first().copied().unwrap_or(Pick::Fight);
106 if !agenda.is_empty() {
107 agenda.remove(0);
108 }
109 match pick {
110 Pick::Fight => GbButton::A.bit_mask(),
111 Pick::Party => {
112 keys.push(GbButton::A);
113 GbButton::Down.bit_mask()
114 }
115 Pick::Item => {
116 keys.push(GbButton::Down);
117 keys.push(GbButton::A);
118 GbButton::Down.bit_mask()
119 }
120 }
121 }
122 // Submenus: confirm the entry under the cursor (the
123 // party cursor starts on the first switchable member).
124 _ => GbButton::A.bit_mask(),
125 };
126 } else if frame % 4 == 0 {
127 mask = GbButton::A.bit_mask(); // page narration
128 }
129 } else if frame % 4 == 0 {
130 mask = GbButton::A.bit_mask(); // page dialogue
131 }
132 }
133 input.set_from_bitmask(mask);
134 game.update(&input);
135 input.begin_frame();
136
137 let in_battle = game.battle().is_some();
138 // Battle-start edge: print the party it starts with (the carried-over
139 // state from battle 2 on) and load its agenda.
140 if in_battle && !was_in_battle {
141 let n = battles_done + 1;
142 let battle = game.battle().unwrap();
143 println!("battle {n} starts:");
144 for c in battle.party() {
145 let status = c.status.as_deref().unwrap_or("-");
146 println!(
147 " {} {}/{} MP {}/{} status {}",
148 c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
149 );
150 }
151 println!(" inventory: {:?}", battle.inventory());
152 agenda = agendas[battles_done].to_vec();
153 }
154 if in_battle {
155 last_log = game.battle().unwrap().log().to_vec();
156 }
157 // Battle-end edge: the runner has harvested the party state.
158 if was_in_battle && !in_battle {
159 battles_done += 1;
160 println!("\nbattle {battles_done} log:");
161 for line in &last_log {
162 println!(" {line}");
163 }
164 if let Some(party) = game.party_state() {
165 println!("party state after battle {battles_done}:");
166 for m in party {
167 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
168 }
169 }
170 if let Some(inv) = game.inventory() {
171 println!("inventory: {inv:?}");
172 }
173 println!();
174 last_log.clear();
175 if battles_done == 2 {
176 println!("both battles done — stopping");
177 break;
178 }
179 }
180 was_in_battle_prev = in_battle;
181 }
182
183 if battles_done < 2 {
184 eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
185 std::process::exit(1);
186 }
187}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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn inventory(&self) -> Option<&HashMap<String, u32>>
pub fn inventory(&self) -> Option<&HashMap<String, u32>>
The persistent battle inventory, once a battle has completed or a save restored one (test/debug introspection).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
46fn main() {
47 let mut args = std::env::args().skip(1);
48 let dir = args
49 .next()
50 .expect("usage: battle_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
66 // The agenda per battle (index = battles started so far − 1).
67 let agendas: &[&[Pick]] = &[&[Pick::Item, Pick::Party, Pick::Fight], &[Pick::Fight]];
68 let mut battles_done = 0usize;
69 let mut agenda: Vec<Pick> = Vec::new();
70 // Queued button presses; one key every other frame (a held key is not a
71 // fresh just-press, so keys alternate with idle frames).
72 let mut keys: Vec<GbButton> = Vec::new();
73 let mut last_log: Vec<String> = Vec::new();
74 let mut snapped: Vec<String> = Vec::new();
75 let mut was_in_battle_prev = false;
76
77 let mut input = InputState::new();
78 for frame in 0..6000u32 {
79 let was_in_battle = was_in_battle_prev;
80
81 // Decide this frame's input.
82 let mut mask = 0;
83 if frame % 2 == 0 {
84 if !keys.is_empty() {
85 mask = keys.remove(0).bit_mask();
86 } else if let Some(battle) = game.battle() {
87 if battle.in_menu() {
88 let items = battle.menu_items();
89 let label = if items.first().is_some_and(|i| i == "Fight") {
90 "root"
91 } else if items.iter().any(|i| i.contains('/')) {
92 "party"
93 } else {
94 "sub"
95 };
96 if let Some(dir) = &shot_dir {
97 let tag = format!("battle{}-{label}", battles_done + 1);
98 if !snapped.contains(&tag) {
99 snapped.push(tag.clone());
100 snap(&mut game, &dir.join(format!("{tag}.png")));
101 }
102 }
103 mask = match label {
104 "root" => {
105 let pick = agenda.first().copied().unwrap_or(Pick::Fight);
106 if !agenda.is_empty() {
107 agenda.remove(0);
108 }
109 match pick {
110 Pick::Fight => GbButton::A.bit_mask(),
111 Pick::Party => {
112 keys.push(GbButton::A);
113 GbButton::Down.bit_mask()
114 }
115 Pick::Item => {
116 keys.push(GbButton::Down);
117 keys.push(GbButton::A);
118 GbButton::Down.bit_mask()
119 }
120 }
121 }
122 // Submenus: confirm the entry under the cursor (the
123 // party cursor starts on the first switchable member).
124 _ => GbButton::A.bit_mask(),
125 };
126 } else if frame % 4 == 0 {
127 mask = GbButton::A.bit_mask(); // page narration
128 }
129 } else if frame % 4 == 0 {
130 mask = GbButton::A.bit_mask(); // page dialogue
131 }
132 }
133 input.set_from_bitmask(mask);
134 game.update(&input);
135 input.begin_frame();
136
137 let in_battle = game.battle().is_some();
138 // Battle-start edge: print the party it starts with (the carried-over
139 // state from battle 2 on) and load its agenda.
140 if in_battle && !was_in_battle {
141 let n = battles_done + 1;
142 let battle = game.battle().unwrap();
143 println!("battle {n} starts:");
144 for c in battle.party() {
145 let status = c.status.as_deref().unwrap_or("-");
146 println!(
147 " {} {}/{} MP {}/{} status {}",
148 c.name, c.hp, c.max_hp, c.mp, c.max_mp, status
149 );
150 }
151 println!(" inventory: {:?}", battle.inventory());
152 agenda = agendas[battles_done].to_vec();
153 }
154 if in_battle {
155 last_log = game.battle().unwrap().log().to_vec();
156 }
157 // Battle-end edge: the runner has harvested the party state.
158 if was_in_battle && !in_battle {
159 battles_done += 1;
160 println!("\nbattle {battles_done} log:");
161 for line in &last_log {
162 println!(" {line}");
163 }
164 if let Some(party) = game.party_state() {
165 println!("party state after battle {battles_done}:");
166 for m in party {
167 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
168 }
169 }
170 if let Some(inv) = game.inventory() {
171 println!("inventory: {inv:?}");
172 }
173 println!();
174 last_log.clear();
175 if battles_done == 2 {
176 println!("both battles done — stopping");
177 break;
178 }
179 }
180 was_in_battle_prev = in_battle;
181 }
182
183 if battles_done < 2 {
184 eprintln!("harness: only {battles_done} battle(s) completed within the frame cap");
185 std::process::exit(1);
186 }
187}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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn money(&self) -> u32
pub fn money(&self) -> u32
The player’s current money (test/debug introspection).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}The rows the open Start menu currently displays (root labels, party
detail lines, bag rows, target rows, or the note text); None when
the menu is closed (test/debug introspection).
Examples found in repository?
71fn main() {
72 let mut args = std::env::args().skip(1);
73 let dir = args
74 .next()
75 .expect("usage: levels_accept <project-dir> [shot-dir]");
76 let shot_dir = args.next().map(PathBuf::from);
77
78 let save_file = Path::new(&dir).join(".dotzuki-save.json");
79 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
80 let mut game = RunnerGame::new(
81 project,
82 RunnerOptions {
83 headless: true,
84 fresh: true,
85 write_saves: true,
86 external_saves: false,
87 save_file: Some(save_file.clone()),
88 // Deterministic battles: always hit, 89% variance, never crit.
89 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 // Auto-A drives everything: dialogue pages, battle narration, and the
96 // Fight → first-skill picks. Back-to-back `startBattle` commands swap
97 // Battle→Battle INSIDE one update, so a new battle is detected by the
98 // log resetting, not just by a not-in-battle gap.
99 let mut battle_no = 0usize;
100 let mut completed = 0usize;
101 let mut last_log: Vec<String> = Vec::new();
102 let mut was_in_battle = false;
103 let mut snapped_battle = false;
104
105 for frame_no in 0..6000u32 {
106 if frame_no % 4 == 0 {
107 frame(&mut game, GbButton::A.bit_mask());
108 } else {
109 idle(&mut game, 1);
110 }
111
112 let in_battle = game.battle().is_some();
113 if in_battle {
114 let log_len = game.battle().unwrap().log().len();
115 let new_battle = !was_in_battle || log_len < last_log.len();
116 if new_battle {
117 if was_in_battle {
118 // Seamless Battle→Battle swap: close the previous one
119 // out (the party state is already harvested).
120 completed += 1;
121 println!("\nbattle {completed} log:");
122 for line in &last_log {
123 println!(" {line}");
124 }
125 print_party(&game, &format!("after battle {completed}"));
126 println!();
127 if completed == 2 {
128 break;
129 }
130 }
131 battle_no += 1;
132 let battle = game.battle().unwrap();
133 println!("battle {battle_no} starts:");
134 for c in battle.party() {
135 println!(
136 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
137 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
138 );
139 }
140 if battle_no == 2 && !snapped_battle {
141 snapped_battle = true;
142 snap(&mut game, &shot_dir, "battle2-start");
143 }
144 }
145 last_log = game.battle().unwrap().log().to_vec();
146 } else if was_in_battle {
147 completed += 1;
148 println!("\nbattle {completed} log:");
149 for line in &last_log {
150 println!(" {line}");
151 }
152 print_party(&game, &format!("after battle {completed}"));
153 println!();
154 last_log.clear();
155 if completed == 2 {
156 break;
157 }
158 }
159 was_in_battle = in_battle;
160 }
161 if completed < 2 {
162 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
163 std::process::exit(1);
164 }
165
166 // Dismiss the post-battle text so the scene finishes (the save is
167 // written at that stable point).
168 for _ in 0..40 {
169 if game.dialogue_text().is_none() {
170 break;
171 }
172 press_a(&mut game);
173 }
174 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
175
176 // The Party view: Lv + EXP progress on the member rows.
177 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); // Party
180 println!("party view:");
181 for line in game.menu_lines().expect("party view open") {
182 println!(" {line}");
183 }
184 snap(&mut game, &shot_dir, "party-view");
185 frame(&mut game, GbButton::B.bit_mask());
186 idle(&mut game, 1);
187 frame(&mut game, GbButton::B.bit_mask());
188 idle(&mut game, 1);
189
190 // Save round trip: a fresh boot resumes level/exp from the save.
191 println!("\nsave round trip ({}):", save_file.display());
192 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
193 let game = RunnerGame::new(
194 project,
195 RunnerOptions {
196 headless: true,
197 save_file: Some(save_file),
198 rng_script: Some(vec![50, 100, 1]),
199 ..RunnerOptions::default()
200 },
201 )
202 .expect("boot from save");
203 print_party(&game, "resumed from the save");
204 assert!(
205 game.party_state().is_some_and(|p| p[0].level > 1),
206 "level must survive the save round trip"
207 );
208 println!("\nacceptance OK");
209}Sourcepub fn shop_lines(&self) -> Option<Vec<String>>
pub fn shop_lines(&self) -> Option<Vec<String>>
The item rows the open shop displays (×-prefixed when
unaffordable), plus the transient note as a final row when one is
showing; None when no shop is open (test/debug introspection).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}More examples
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.battle().unwrap().current_line().map(str::to_string);
172 if let (Some(dir), Some(line)) = (&shot_dir, line) {
173 let tag = match line.as_str() {
174 l if l.starts_with("Can't escape") => Some("run-blocked"),
175 l if l.starts_with("Got away") => Some("run-safe"),
176 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
177 Some("trainer-money")
178 }
179 _ => None,
180 };
181 if let Some(tag) = tag {
182 if !snapped.contains(&tag.to_string()) {
183 snapped.push(tag.to_string());
184 snap(&mut game, &dir.join(format!("{tag}.png")));
185 }
186 }
187 }
188 last_log = game.battle().unwrap().log().to_vec();
189 }
190 // Battle-end edge: the runner has harvested the state.
191 if was_in_battle && !in_battle {
192 battles_done += 1;
193 println!("\nbattle {battles_done} log:");
194 for line in &last_log {
195 println!(" {line}");
196 }
197 if battles_done == 1 {
198 assert!(
199 last_log
200 .iter()
201 .any(|l| l == "Can't escape from a trainer battle!"),
202 "the trainer Run attempt must be blocked (and the turn not consumed)"
203 );
204 }
205 println!("money after battle {battles_done}: {}", game.money());
206 if let Some(party) = game.party_state() {
207 for m in party {
208 println!(
209 " {} hp {} mp {} level {} exp {}",
210 m.id, m.hp, m.mp, m.level, m.exp
211 );
212 }
213 }
214 println!();
215 last_log.clear();
216 }
217 // Shop-exit edge.
218 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
219 shop_done = true;
220 println!(
221 "shop closed: money {}, inventory {:?}",
222 game.money(),
223 game.inventory()
224 );
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}Sourcepub fn whiteout_active(&self) -> bool
pub fn whiteout_active(&self) -> bool
true while the game-over whiteout owns the screen (blackout or
message phase; test/debug introspection).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}Sourcepub fn player_tile(&self) -> (i32, i32)
pub fn player_tile(&self) -> (i32, i32)
The player’s current tile.
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}Sourcepub fn player_elevation(&self) -> u8
pub fn player_elevation(&self) -> u8
The player’s current elevation level (multi-level maps; test/debug introspection).
Sourcepub fn is_blocked(&self, x: i32, y: i32) -> bool
pub fn is_blocked(&self, x: i32, y: i32) -> bool
true when tile (x, y) is map-solid on the current map (solid when
there is no map). NPC occupancy is not included; test/debug helper.
Sourcepub fn audio(&self) -> &RunnerAudio
pub fn audio(&self) -> &RunnerAudio
The audio subsystem (test/debug introspection).
Sourcepub fn render_audio(&mut self, frames: usize) -> Vec<f32>
pub fn render_audio(&mut self, frames: usize) -> Vec<f32>
Pull frames stereo PCM frames (44100 Hz, interleaved L/R f32,
length 2 * frames) from the audio engine — the render path for
hosts without an audio callback thread (the WASM shell feeding
WebAudio). Empty unless RunnerOptions::pcm_audio is on and a play
command has arrived.
Sourcepub fn debug_place(&mut self, x: i32, y: i32, facing: Direction)
pub fn debug_place(&mut self, x: i32, y: i32, facing: Direction)
Teleport the player (test/debug helper; no scene side effects).
Examples found in repository?
76fn main() {
77 let mut args = std::env::args().skip(1);
78 let dir = args
79 .next()
80 .expect("usage: menu_accept <project-dir> [shot-dir]");
81 let shot_dir = args.next().map(PathBuf::from);
82
83 let project = LoadedProject::load(Path::new(&dir)).expect("load project");
84 let mut game = RunnerGame::new(
85 project,
86 RunnerOptions {
87 headless: true,
88 fresh: true,
89 // Deterministic battles: always hit, 89% variance, never crit.
90 rng_script: Some(vec![50, 100, 1]),
91 ..RunnerOptions::default()
92 },
93 )
94 .expect("boot game");
95
96 dismiss(&mut game); // StartTown main
97 println!("== 1. the shop ==");
98 println!("money at boot: {} (manifest shop.startMoney)", game.money());
99
100 // Talk to the Shopkeeper at (13,10): the shop opens on the spot.
101 game.debug_place(12, 10, Direction::Right);
102 press_a(&mut game);
103 let rows = game.shop_lines().expect("shop open");
104 println!("shop shelf: {rows:?}");
105 snap(&mut game, &shot_dir, "shop");
106
107 // Buy one Potion: 100 → 80 G, inventory 3 → 4.
108 let before = game.money();
109 press_a(&mut game);
110 let rows = game.shop_lines().expect("shop still open");
111 println!(
112 "bought a Potion: money {before} → {} G; inventory {:?}",
113 game.money(),
114 game.inventory().expect("inventory materialized")
115 );
116 println!("shop note: {:?}", rows.last());
117 snap(&mut game, &shot_dir, "shop-bought");
118 assert_eq!(game.money(), 80, "20 G charged");
119 assert_eq!(game.inventory().unwrap().get("potion"), Some(&4));
120 press_a(&mut game); // dismiss the note
121
122 // B exits; the scene resumes with its follow-up line.
123 press(&mut game, GbButton::B);
124 let page = game.dialogue_text().expect("scene resumed after the shop");
125 println!("scene resumed with: {page:?}");
126 assert!(page.contains("Come again!"), "page: {page:?}");
127 dismiss(&mut game);
128
129 println!("\n== 2. the whiteout ==");
130 // Talk to the Hermit at (11,10) and lose on purpose (overwhelming Slime).
131 game.debug_place(12, 10, Direction::Left);
132 press_a(&mut game); // "Something stirs…"
133 press_a(&mut game); // → battle
134 let mut last_log: Vec<String> = Vec::new();
135 for _ in 0..120 {
136 let Some(battle) = game.battle() else { break };
137 last_log = battle.log().to_vec();
138 press_a(&mut game);
139 }
140 println!("lost battle log:");
141 for line in &last_log {
142 println!(" {line}");
143 }
144 assert_eq!(
145 last_log.last().map(String::as_str),
146 Some("You lost the battle…")
147 );
148
149 // The scene's post-lose text still plays…
150 let page = game.dialogue_text().expect("post-lose text");
151 println!("post-lose text: {page:?}");
152 assert!(page.contains("no match"), "page: {page:?}");
153 press_a(&mut game); // scene finishes → whiteout
154
155 // …then the whiteout: blackout first, then the collapsed line.
156 assert!(game.whiteout_active(), "whiteout armed");
157 idle(&mut game, 10);
158 snap(&mut game, &shot_dir, "whiteout-blackout");
159 idle(&mut game, 25);
160 let page = game.dialogue_text().expect("whiteout message");
161 println!("whiteout message: {page:?}");
162 assert!(page.contains("collapsed"), "page: {page:?}");
163 snap(&mut game, &shot_dir, "whiteout-message");
164
165 // Landing it: party healed to full, back at the entry spawn, flags kept.
166 press_a(&mut game);
167 assert!(!game.whiteout_active());
168 println!("after the whiteout:");
169 println!(
170 " map: {:?} tile: {:?}",
171 game.current_map_id(),
172 game.player_tile()
173 );
174 for m in game.party_state().expect("party state") {
175 println!(" {} hp {} mp {} status {:?}", m.id, m.hp, m.mp, m.status);
176 }
177 println!(
178 " __played_main_StartTown flag kept: {}",
179 game.flag("__played_main_StartTown")
180 );
181 assert_eq!(game.current_map_id(), Some("StartTown"));
182 assert!(game
183 .party_state()
184 .unwrap()
185 .iter()
186 .all(|m| m.hp > 0 && m.status.is_none()));
187 snap(&mut game, &shot_dir, "whiteout-after");
188
189 println!("\nacceptance OK");
190}Sourcepub fn frame_count(&self) -> u64
pub fn frame_count(&self) -> u64
Frames updated so far.
Trait Implementations§
Source§impl GameLoop for RunnerGame
Available on crate feature gpu and non-WebAssembly only.
impl GameLoop for RunnerGame
gpu and non-WebAssembly only.Source§type Fb = FrameBuffer
type Fb = FrameBuffer
FrameBuffer] (true-color games) or the indexed
crate::RgbaIndexedFrameBuffer (fixed-palette games).fn update(&mut self, input: &InputState)
fn draw(&mut self, frame_buffer: &mut FrameBuffer)
fn should_exit(&self) -> bool
Auto Trait Implementations§
impl !RefUnwindSafe for RunnerGame
impl !Send for RunnerGame
impl !Sync for RunnerGame
impl !UnwindSafe for RunnerGame
impl Freeze for RunnerGame
impl Unpin for RunnerGame
impl UnsafeUnpin for RunnerGame
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> FmtForward for T
impl<T> FmtForward for T
Source§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.Source§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.Source§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.Source§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.Source§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.Source§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.Source§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.Source§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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 moreimpl<T> MaybeSendSync for T
Source§impl<D> OwoColorize for D
impl<D> OwoColorize for D
Source§fn fg<C>(&self) -> FgColorDisplay<'_, C, Self>where
C: Color,
fn fg<C>(&self) -> FgColorDisplay<'_, C, Self>where
C: Color,
Source§fn bg<C>(&self) -> BgColorDisplay<'_, C, Self>where
C: Color,
fn bg<C>(&self) -> BgColorDisplay<'_, C, Self>where
C: Color,
Source§fn black(&self) -> FgColorDisplay<'_, Black, Self>
fn black(&self) -> FgColorDisplay<'_, Black, Self>
Source§fn on_black(&self) -> BgColorDisplay<'_, Black, Self>
fn on_black(&self) -> BgColorDisplay<'_, Black, Self>
Source§fn red(&self) -> FgColorDisplay<'_, Red, Self>
fn red(&self) -> FgColorDisplay<'_, Red, Self>
Source§fn on_red(&self) -> BgColorDisplay<'_, Red, Self>
fn on_red(&self) -> BgColorDisplay<'_, Red, Self>
Source§fn green(&self) -> FgColorDisplay<'_, Green, Self>
fn green(&self) -> FgColorDisplay<'_, Green, Self>
Source§fn on_green(&self) -> BgColorDisplay<'_, Green, Self>
fn on_green(&self) -> BgColorDisplay<'_, Green, Self>
Source§fn yellow(&self) -> FgColorDisplay<'_, Yellow, Self>
fn yellow(&self) -> FgColorDisplay<'_, Yellow, Self>
Source§fn on_yellow(&self) -> BgColorDisplay<'_, Yellow, Self>
fn on_yellow(&self) -> BgColorDisplay<'_, Yellow, Self>
Source§fn blue(&self) -> FgColorDisplay<'_, Blue, Self>
fn blue(&self) -> FgColorDisplay<'_, Blue, Self>
Source§fn on_blue(&self) -> BgColorDisplay<'_, Blue, Self>
fn on_blue(&self) -> BgColorDisplay<'_, Blue, Self>
Source§fn magenta(&self) -> FgColorDisplay<'_, Magenta, Self>
fn magenta(&self) -> FgColorDisplay<'_, Magenta, Self>
Source§fn on_magenta(&self) -> BgColorDisplay<'_, Magenta, Self>
fn on_magenta(&self) -> BgColorDisplay<'_, Magenta, Self>
Source§fn purple(&self) -> FgColorDisplay<'_, Magenta, Self>
fn purple(&self) -> FgColorDisplay<'_, Magenta, Self>
Source§fn on_purple(&self) -> BgColorDisplay<'_, Magenta, Self>
fn on_purple(&self) -> BgColorDisplay<'_, Magenta, Self>
Source§fn cyan(&self) -> FgColorDisplay<'_, Cyan, Self>
fn cyan(&self) -> FgColorDisplay<'_, Cyan, Self>
Source§fn on_cyan(&self) -> BgColorDisplay<'_, Cyan, Self>
fn on_cyan(&self) -> BgColorDisplay<'_, Cyan, Self>
Source§fn white(&self) -> FgColorDisplay<'_, White, Self>
fn white(&self) -> FgColorDisplay<'_, White, Self>
Source§fn on_white(&self) -> BgColorDisplay<'_, White, Self>
fn on_white(&self) -> BgColorDisplay<'_, White, Self>
Source§fn default_color(&self) -> FgColorDisplay<'_, Default, Self>
fn default_color(&self) -> FgColorDisplay<'_, Default, Self>
Source§fn on_default_color(&self) -> BgColorDisplay<'_, Default, Self>
fn on_default_color(&self) -> BgColorDisplay<'_, Default, Self>
Source§fn bright_black(&self) -> FgColorDisplay<'_, BrightBlack, Self>
fn bright_black(&self) -> FgColorDisplay<'_, BrightBlack, Self>
Source§fn on_bright_black(&self) -> BgColorDisplay<'_, BrightBlack, Self>
fn on_bright_black(&self) -> BgColorDisplay<'_, BrightBlack, Self>
Source§fn bright_red(&self) -> FgColorDisplay<'_, BrightRed, Self>
fn bright_red(&self) -> FgColorDisplay<'_, BrightRed, Self>
Source§fn on_bright_red(&self) -> BgColorDisplay<'_, BrightRed, Self>
fn on_bright_red(&self) -> BgColorDisplay<'_, BrightRed, Self>
Source§fn bright_green(&self) -> FgColorDisplay<'_, BrightGreen, Self>
fn bright_green(&self) -> FgColorDisplay<'_, BrightGreen, Self>
Source§fn on_bright_green(&self) -> BgColorDisplay<'_, BrightGreen, Self>
fn on_bright_green(&self) -> BgColorDisplay<'_, BrightGreen, Self>
Source§fn bright_yellow(&self) -> FgColorDisplay<'_, BrightYellow, Self>
fn bright_yellow(&self) -> FgColorDisplay<'_, BrightYellow, Self>
Source§fn on_bright_yellow(&self) -> BgColorDisplay<'_, BrightYellow, Self>
fn on_bright_yellow(&self) -> BgColorDisplay<'_, BrightYellow, Self>
Source§fn bright_blue(&self) -> FgColorDisplay<'_, BrightBlue, Self>
fn bright_blue(&self) -> FgColorDisplay<'_, BrightBlue, Self>
Source§fn on_bright_blue(&self) -> BgColorDisplay<'_, BrightBlue, Self>
fn on_bright_blue(&self) -> BgColorDisplay<'_, BrightBlue, Self>
Source§fn bright_magenta(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
fn bright_magenta(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
Source§fn on_bright_magenta(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
fn on_bright_magenta(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
Source§fn bright_purple(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
fn bright_purple(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
Source§fn on_bright_purple(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
fn on_bright_purple(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
Source§fn bright_cyan(&self) -> FgColorDisplay<'_, BrightCyan, Self>
fn bright_cyan(&self) -> FgColorDisplay<'_, BrightCyan, Self>
Source§fn on_bright_cyan(&self) -> BgColorDisplay<'_, BrightCyan, Self>
fn on_bright_cyan(&self) -> BgColorDisplay<'_, BrightCyan, Self>
Source§fn bright_white(&self) -> FgColorDisplay<'_, BrightWhite, Self>
fn bright_white(&self) -> FgColorDisplay<'_, BrightWhite, Self>
Source§fn on_bright_white(&self) -> BgColorDisplay<'_, BrightWhite, Self>
fn on_bright_white(&self) -> BgColorDisplay<'_, BrightWhite, Self>
Source§fn bold(&self) -> BoldDisplay<'_, Self>
fn bold(&self) -> BoldDisplay<'_, Self>
Source§fn dimmed(&self) -> DimDisplay<'_, Self>
fn dimmed(&self) -> DimDisplay<'_, Self>
Source§fn italic(&self) -> ItalicDisplay<'_, Self>
fn italic(&self) -> ItalicDisplay<'_, Self>
Source§fn underline(&self) -> UnderlineDisplay<'_, Self>
fn underline(&self) -> UnderlineDisplay<'_, Self>
Source§fn blink(&self) -> BlinkDisplay<'_, Self>
fn blink(&self) -> BlinkDisplay<'_, Self>
Source§fn blink_fast(&self) -> BlinkFastDisplay<'_, Self>
fn blink_fast(&self) -> BlinkFastDisplay<'_, Self>
Source§fn reversed(&self) -> ReversedDisplay<'_, Self>
fn reversed(&self) -> ReversedDisplay<'_, Self>
Source§fn strikethrough(&self) -> StrikeThroughDisplay<'_, Self>
fn strikethrough(&self) -> StrikeThroughDisplay<'_, Self>
Source§fn color<Color>(&self, color: Color) -> FgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
fn color<Color>(&self, color: Color) -> FgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
OwoColorize::fg or
a color-specific method, such as OwoColorize::green, Read moreSource§fn on_color<Color>(&self, color: Color) -> BgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
fn on_color<Color>(&self, color: Color) -> BgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
OwoColorize::bg or
a color-specific method, such as OwoColorize::on_yellow, Read moreSource§fn fg_rgb<const R: u8, const G: u8, const B: u8>(
&self,
) -> FgColorDisplay<'_, CustomColor<R, G, B>, Self>
fn fg_rgb<const R: u8, const G: u8, const B: u8>( &self, ) -> FgColorDisplay<'_, CustomColor<R, G, B>, Self>
Source§fn bg_rgb<const R: u8, const G: u8, const B: u8>(
&self,
) -> BgColorDisplay<'_, CustomColor<R, G, B>, Self>
fn bg_rgb<const R: u8, const G: u8, const B: u8>( &self, ) -> BgColorDisplay<'_, CustomColor<R, G, B>, Self>
Source§fn truecolor(&self, r: u8, g: u8, b: u8) -> FgDynColorDisplay<'_, Rgb, Self>
fn truecolor(&self, r: u8, g: u8, b: u8) -> FgDynColorDisplay<'_, Rgb, Self>
Source§fn on_truecolor(&self, r: u8, g: u8, b: u8) -> BgDynColorDisplay<'_, Rgb, Self>
fn on_truecolor(&self, r: u8, g: u8, b: u8) -> BgDynColorDisplay<'_, Rgb, Self>
Source§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
Source§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
Source§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
Source§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
Source§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
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
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
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
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T> Tap for T
impl<T> Tap for T
Source§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read moreSource§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read moreSource§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read moreSource§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read moreSource§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.Source§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.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
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.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
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.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
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.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
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.