encounter_accept/
encounter_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
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum Pick {
31 Fight,
32 Run,
33}
34
35fn snap(game: &mut RunnerGame, path: &Path) {
37 let mut fb = FrameBuffer::new(
38 RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
39 Rgba::BLACK,
40 );
41 game.draw(&mut fb);
42 save_png(&fb, path).expect("screenshot");
43 println!(" [shot] {}", path.display());
44}
45
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 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 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 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 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 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 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() };
117 } else if frame % 4 == 0 {
118 mask = GbButton::A.bit_mask(); }
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); keys.push(GbButton::A);
126 }
127 1 => {
128 println!("sell view: {:?}", game.shop_lines().unwrap());
129 keys.push(GbButton::A); }
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); }
139 3 => keys.push(GbButton::B), _ => keys.push(GbButton::B), }
142 shop_step += 1;
143 } else if frame % 4 == 0 {
144 mask = GbButton::A.bit_mask(); }
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 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 let line = game
172 .battle()
173 .unwrap()
174 .current_line()
175 .map(str::to_string);
176 if let (Some(dir), Some(line)) = (&shot_dir, line) {
177 let tag = match line.as_str() {
178 l if l.starts_with("Can't escape") => Some("run-blocked"),
179 l if l.starts_with("Got away") => Some("run-safe"),
180 l if l.starts_with("Got ") && l.ends_with("for winning!") => {
181 Some("trainer-money")
182 }
183 _ => None,
184 };
185 if let Some(tag) = tag {
186 if !snapped.contains(&tag.to_string()) {
187 snapped.push(tag.to_string());
188 snap(&mut game, &dir.join(format!("{tag}.png")));
189 }
190 }
191 }
192 last_log = game.battle().unwrap().log().to_vec();
193 }
194 if was_in_battle && !in_battle {
196 battles_done += 1;
197 println!("\nbattle {battles_done} log:");
198 for line in &last_log {
199 println!(" {line}");
200 }
201 if battles_done == 1 {
202 assert!(
203 last_log
204 .iter()
205 .any(|l| l == "Can't escape from a trainer battle!"),
206 "the trainer Run attempt must be blocked (and the turn not consumed)"
207 );
208 }
209 println!("money after battle {battles_done}: {}", game.money());
210 if let Some(party) = game.party_state() {
211 for m in party {
212 println!(
213 " {} hp {} mp {} level {} exp {}",
214 m.id, m.hp, m.mp, m.level, m.exp
215 );
216 }
217 }
218 println!();
219 last_log.clear();
220 }
221 if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
223 shop_done = true;
224 println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
225 }
226 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 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}