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 external_saves: false,
87 save_file: Some(save_file.clone()),
88 rng_script: Some(vec![50, 100, 1]),
90 ..RunnerOptions::default()
91 },
92 )
93 .expect("boot game");
94
95 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 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 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 frame(&mut game, GbButton::Start.bit_mask());
178 idle(&mut game, 1);
179 press_a(&mut game); 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 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}