levels_accept/
levels_accept.rs1use std::path::{Path, PathBuf};
21
22use dotzuki_engine::render::{FrameBuffer, Rgba};
23use dotzuki_engine::render_config::RenderConfig;
24use dotzuki_renderer::input::{GbButton, InputState};
25use dotzuki_runner::headless::save_png;
26use dotzuki_runner::{LoadedProject, RunnerGame, RunnerOptions, SCREEN_H, SCREEN_W};
27
28fn snap(game: &mut RunnerGame, dir: &Option<PathBuf>, name: &str) {
30 let Some(dir) = dir else { return };
31 let mut fb = FrameBuffer::new(
32 RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
33 Rgba::BLACK,
34 );
35 game.draw(&mut fb);
36 let path = dir.join(format!("{name}.png"));
37 save_png(&fb, &path).expect("screenshot");
38 println!(" [shot] {}", path.display());
39}
40
41fn frame(game: &mut RunnerGame, mask: u8) {
42 let mut input = InputState::new();
43 input.set_from_bitmask(mask);
44 game.update(&input);
45}
46
47fn idle(game: &mut RunnerGame, n: u32) {
48 for _ in 0..n {
49 frame(game, 0);
50 }
51}
52
53fn press_a(game: &mut RunnerGame) {
54 frame(game, GbButton::A.bit_mask());
55 idle(game, 1);
56}
57
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 save_file: Some(save_file.clone()),
87 rng_script: Some(vec![50, 100, 1]),
89 ..RunnerOptions::default()
90 },
91 )
92 .expect("boot game");
93
94 let mut battle_no = 0usize;
99 let mut completed = 0usize;
100 let mut last_log: Vec<String> = Vec::new();
101 let mut was_in_battle = false;
102 let mut snapped_battle = false;
103
104 for frame_no in 0..6000u32 {
105 if frame_no % 4 == 0 {
106 frame(&mut game, GbButton::A.bit_mask());
107 } else {
108 idle(&mut game, 1);
109 }
110
111 let in_battle = game.battle().is_some();
112 if in_battle {
113 let log_len = game.battle().unwrap().log().len();
114 let new_battle = !was_in_battle || log_len < last_log.len();
115 if new_battle {
116 if was_in_battle {
117 completed += 1;
120 println!("\nbattle {completed} log:");
121 for line in &last_log {
122 println!(" {line}");
123 }
124 print_party(&game, &format!("after battle {completed}"));
125 println!();
126 if completed == 2 {
127 break;
128 }
129 }
130 battle_no += 1;
131 let battle = game.battle().unwrap();
132 println!("battle {battle_no} starts:");
133 for c in battle.party() {
134 println!(
135 " {} level {} exp {} hp {}/{} mp {}/{} atk {}",
136 c.name, c.level, c.exp, c.hp, c.max_hp, c.mp, c.max_mp, c.attack
137 );
138 }
139 if battle_no == 2 && !snapped_battle {
140 snapped_battle = true;
141 snap(&mut game, &shot_dir, "battle2-start");
142 }
143 }
144 last_log = game.battle().unwrap().log().to_vec();
145 } else if was_in_battle {
146 completed += 1;
147 println!("\nbattle {completed} log:");
148 for line in &last_log {
149 println!(" {line}");
150 }
151 print_party(&game, &format!("after battle {completed}"));
152 println!();
153 last_log.clear();
154 if completed == 2 {
155 break;
156 }
157 }
158 was_in_battle = in_battle;
159 }
160 if completed < 2 {
161 eprintln!("harness: only {completed} battle(s) completed within the frame cap");
162 std::process::exit(1);
163 }
164
165 for _ in 0..40 {
168 if game.dialogue_text().is_none() {
169 break;
170 }
171 press_a(&mut game);
172 }
173 assert!(game.flag("SLIMES_BEATEN"), "the scene ran to its end");
174
175 frame(&mut game, GbButton::Start.bit_mask());
177 idle(&mut game, 1);
178 press_a(&mut game); println!("party view:");
180 for line in game.menu_lines().expect("party view open") {
181 println!(" {line}");
182 }
183 snap(&mut game, &shot_dir, "party-view");
184 frame(&mut game, GbButton::B.bit_mask());
185 idle(&mut game, 1);
186 frame(&mut game, GbButton::B.bit_mask());
187 idle(&mut game, 1);
188
189 println!("\nsave round trip ({}):", save_file.display());
191 let project = LoadedProject::load(Path::new(&dir)).expect("reload project");
192 let game = RunnerGame::new(
193 project,
194 RunnerOptions {
195 headless: true,
196 save_file: Some(save_file),
197 rng_script: Some(vec![50, 100, 1]),
198 ..RunnerOptions::default()
199 },
200 )
201 .expect("boot from save");
202 print_party(&game, "resumed from the save");
203 assert!(
204 game.party_state().is_some_and(|p| p[0].level > 1),
205 "level must survive the save round trip"
206 );
207 println!("\nacceptance OK");
208}