use std::path::{Path, PathBuf};
use dotzuki_engine::render::{FrameBuffer, Rgba};
use dotzuki_engine::render_config::RenderConfig;
use dotzuki_renderer::input::{GbButton, InputState};
use dotzuki_runner::headless::save_png;
use dotzuki_runner::{LoadedProject, RunnerGame, RunnerOptions, SCREEN_H, SCREEN_W};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pick {
Fight,
Run,
}
fn snap(game: &mut RunnerGame, path: &Path) {
let mut fb = FrameBuffer::new(
RenderConfig::new(SCREEN_W as u32, SCREEN_H as u32),
Rgba::BLACK,
);
game.draw(&mut fb);
save_png(&fb, path).expect("screenshot");
println!(" [shot] {}", path.display());
}
fn main() {
let mut args = std::env::args().skip(1);
let dir = args
.next()
.expect("usage: encounter_accept <project-dir> [shot-dir]");
let shot_dir = args.next().map(PathBuf::from);
let project = LoadedProject::load(Path::new(&dir)).expect("load project");
let mut game = RunnerGame::new(
project,
RunnerOptions {
headless: true,
fresh: true,
rng_script: Some(vec![50, 100, 1]),
..RunnerOptions::default()
},
)
.expect("boot game");
println!("money at boot: {}", game.money());
let agendas: &[&[Pick]] = &[&[Pick::Run, Pick::Fight], &[Pick::Run]];
let mut battles_done = 0usize;
let mut agenda: Vec<Pick> = agendas[0].to_vec();
let mut keys: Vec<GbButton> = Vec::new();
let mut last_log: Vec<String> = Vec::new();
let mut was_in_battle_prev = false;
let mut snapped: Vec<String> = Vec::new();
let mut shop_step = 0usize;
let mut shop_done = false;
let mut input = InputState::new();
for frame in 0..6000u32 {
let was_in_battle = was_in_battle_prev;
let mut mask = 0;
if frame % 2 == 0 {
if !keys.is_empty() {
mask = keys.remove(0).bit_mask();
} else if let Some(battle) = game.battle() {
if battle.in_menu() {
let is_root = battle.menu_items().first().is_some_and(|i| i == "Fight");
mask = if is_root {
let pick = agenda.first().copied().unwrap_or(Pick::Fight);
if !agenda.is_empty() {
agenda.remove(0);
}
match pick {
Pick::Fight => GbButton::A.bit_mask(),
Pick::Run => {
keys.push(GbButton::Down);
keys.push(GbButton::Down);
keys.push(GbButton::A);
GbButton::Down.bit_mask()
}
}
} else {
GbButton::A.bit_mask() };
} else if frame % 4 == 0 {
mask = GbButton::A.bit_mask(); }
} else if game.shop_lines().is_some() && battles_done == 2 && !shop_done {
match shop_step {
0 => {
println!("shop root: {:?}", game.shop_lines().unwrap());
keys.push(GbButton::Down); keys.push(GbButton::A);
}
1 => {
println!("sell view: {:?}", game.shop_lines().unwrap());
keys.push(GbButton::A); }
2 => {
let lines = game.shop_lines().unwrap();
println!("sell note: {lines:?}");
if let Some(dir) = &shot_dir {
snap(&mut game, &dir.join("shop-sold.png"));
}
keys.push(GbButton::A); }
3 => keys.push(GbButton::B), _ => keys.push(GbButton::B), }
shop_step += 1;
} else if frame % 4 == 0 {
mask = GbButton::A.bit_mask(); }
}
input.set_from_bitmask(mask);
game.update(&input);
input.begin_frame();
let in_battle = game.battle().is_some();
if in_battle && !was_in_battle {
let n = battles_done + 1;
let battle = game.battle().unwrap();
println!(
"battle {n} starts: {} vs {} (queued {}, trainer {}, reward {})",
battle.player().name,
battle.enemy().name,
battle.enemies_remaining(),
battle.is_trainer(),
battle.trainer_money(),
);
if battles_done > 0 {
agenda = agendas[battles_done].to_vec();
}
}
if in_battle {
let line = game
.battle()
.unwrap()
.current_line()
.map(str::to_string);
if let (Some(dir), Some(line)) = (&shot_dir, line) {
let tag = match line.as_str() {
l if l.starts_with("Can't escape") => Some("run-blocked"),
l if l.starts_with("Got away") => Some("run-safe"),
l if l.starts_with("Got ") && l.ends_with("for winning!") => {
Some("trainer-money")
}
_ => None,
};
if let Some(tag) = tag {
if !snapped.contains(&tag.to_string()) {
snapped.push(tag.to_string());
snap(&mut game, &dir.join(format!("{tag}.png")));
}
}
}
last_log = game.battle().unwrap().log().to_vec();
}
if was_in_battle && !in_battle {
battles_done += 1;
println!("\nbattle {battles_done} log:");
for line in &last_log {
println!(" {line}");
}
if battles_done == 1 {
assert!(
last_log
.iter()
.any(|l| l == "Can't escape from a trainer battle!"),
"the trainer Run attempt must be blocked (and the turn not consumed)"
);
}
println!("money after battle {battles_done}: {}", game.money());
if let Some(party) = game.party_state() {
for m in party {
println!(
" {} hp {} mp {} level {} exp {}",
m.id, m.hp, m.mp, m.level, m.exp
);
}
}
println!();
last_log.clear();
}
if shop_step >= 5 && game.shop_lines().is_none() && !shop_done {
shop_done = true;
println!("shop closed: money {}, inventory {:?}", game.money(), game.inventory());
}
if battles_done == 2 && shop_done && game.battle().is_none() {
println!("acceptance complete");
break;
}
was_in_battle_prev = in_battle;
}
let money = game.money();
let potions = game.inventory().and_then(|inv| inv.get("potion").copied());
println!("final: money {money}, potions {potions:?}");
assert_eq!(battles_done, 2, "both battles must complete");
assert!(shop_done, "the shop sell must complete");
assert_eq!(money, 142, "100 start + 32 trainer + 10 sell (20/2)");
assert_eq!(potions, Some(2), "3 starting − 1 sold");
}