222 lines
7.7 KiB
Rust
222 lines
7.7 KiB
Rust
//! Local original-data acceptance probe; run using validate.sh from any directory.
|
|
use raptor_replica::{
|
|
assets::Catalog,
|
|
content::Registry,
|
|
modding::{self, ActiveMods},
|
|
persistence::Pilot,
|
|
simulation::{Input, Outcome, World},
|
|
};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::path::PathBuf;
|
|
|
|
const MISSION: &str = "iron_coast_mission_5:mission_05";
|
|
const IDS: [&str; 8] = [
|
|
"iron_coast_scout",
|
|
"iron_coast_turret",
|
|
"iron_coast_boss_5",
|
|
"pulse_cannon",
|
|
"seeker_swarm",
|
|
"ion_lance",
|
|
"iron_coast_mission_5",
|
|
"iron_coast_music",
|
|
];
|
|
|
|
fn build_active(base: &Registry, mods: &PathBuf, catalog: &Catalog) -> ActiveMods {
|
|
let discovery = modding::discover(mods);
|
|
assert!(discovery.errors.is_empty(), "{:?}", discovery.errors);
|
|
ActiveMods::build(base, &discovery, &IDS.map(str::to_string), Some(catalog)).unwrap()
|
|
}
|
|
|
|
fn play(active: &mut ActiveMods, shield: u32) -> World {
|
|
active.scripts.states.clear();
|
|
active.scripts.rng.clear();
|
|
let mut pilot = Pilot::default();
|
|
pilot.inventory.insert("base:energy".into(), shield);
|
|
pilot.credits = if shield == 75 { 10_000 } else { 0 };
|
|
pilot.difficulty = 2;
|
|
pilot.mission = MISSION.into();
|
|
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
|
|
let mut used = 0;
|
|
let mut native_first = BTreeMap::new();
|
|
let mut native_seen = BTreeSet::new();
|
|
let mut native_moved = BTreeSet::new();
|
|
for tick in 0..3600 {
|
|
let target = world
|
|
.enemies
|
|
.iter()
|
|
.find(|e| e.health > 0 && e.definition.0 == "iron_coast_mission_5:foundry");
|
|
let target_x = target.map(|e| e.x + 8).unwrap_or(144);
|
|
let input = Input {
|
|
fire: true,
|
|
dx: (target_x - world.player.x).signum(),
|
|
special: world.player.shield <= 65 && tick % 2 == 0,
|
|
..Input::default()
|
|
};
|
|
let before = world
|
|
.player
|
|
.inventory
|
|
.get(&"iron_coast_mission_5:coolant".into())
|
|
.copied()
|
|
.unwrap_or(0);
|
|
world
|
|
.step(input, &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
for enemy in &world.enemies {
|
|
if enemy.definition.0.starts_with("base:enemy_") {
|
|
native_seen.insert(enemy.definition.0.clone());
|
|
let first = native_first
|
|
.entry(enemy.entity)
|
|
.or_insert((enemy.x, enemy.y));
|
|
if *first != (enemy.x, enemy.y) {
|
|
native_moved.insert(enemy.definition.0.clone());
|
|
}
|
|
}
|
|
}
|
|
if tick == 0 {
|
|
world.player.selected = Some("iron_coast_mission_5:coolant".into());
|
|
}
|
|
let after = world
|
|
.player
|
|
.inventory
|
|
.get(&"iron_coast_mission_5:coolant".into())
|
|
.copied()
|
|
.unwrap_or(0);
|
|
if after < before {
|
|
used += before - after;
|
|
}
|
|
if world.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
println!(
|
|
"start_shield={shield} tick={} outcome={:?} shield={} credits={} coolant_used={used} foundries={} boss={} digest={}",
|
|
world.tick,
|
|
world.outcome,
|
|
world.player.shield,
|
|
world.player.credits,
|
|
world
|
|
.objectives
|
|
.get("iron_coast_mission_5:foundries_destroyed")
|
|
.copied()
|
|
.unwrap_or(0),
|
|
world
|
|
.objectives
|
|
.get("iron_coast_boss_5:defeated")
|
|
.copied()
|
|
.unwrap_or(0),
|
|
world.digest()
|
|
);
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert!(world.player.shield > 0);
|
|
assert_eq!(
|
|
world.objectives["iron_coast_mission_5:foundries_destroyed"],
|
|
2
|
|
);
|
|
assert!(world.objectives["iron_coast_boss_5:defeated"] >= 1);
|
|
assert!(world.player.credits >= pilot.credits + 15_000);
|
|
assert!(
|
|
native_seen.len() >= 6,
|
|
"all six native libraries should appear"
|
|
);
|
|
assert!(
|
|
native_moved.len() >= 6,
|
|
"native library paths must move in actual simulation"
|
|
);
|
|
println!("classic seen={native_seen:?}; moving={native_moved:?}");
|
|
world
|
|
}
|
|
|
|
fn main() {
|
|
let args = std::env::args().collect::<Vec<_>>();
|
|
let catalog = Catalog::open(PathBuf::from(&args[1])).unwrap();
|
|
let base = Registry::base(&catalog).unwrap();
|
|
let mods = PathBuf::from(&args[2]);
|
|
let mut active = build_active(&base, &mods, &catalog);
|
|
assert_eq!(active.pins.len(), 8);
|
|
let a = play(&mut active, 75);
|
|
let b = play(&mut active, 75);
|
|
assert_eq!(
|
|
a.digest(),
|
|
b.digest(),
|
|
"repeat with same inputs must be deterministic"
|
|
);
|
|
play(&mut active, 100);
|
|
|
|
for reserves in [0, 4] {
|
|
active.scripts.states.clear();
|
|
active.scripts.rng.clear();
|
|
let mut pilot = Pilot::default();
|
|
if reserves > 0 {
|
|
pilot.inventory.insert("base:super_shield".into(), 100);
|
|
pilot.super_shield_reserves = vec![100; reserves];
|
|
}
|
|
for id in active.registry.items.keys() {
|
|
if pilot.inventory_slots() == 20 {
|
|
break;
|
|
}
|
|
if id.0 != "base:super_shield" {
|
|
pilot.inventory.entry(id.clone()).or_insert(1);
|
|
}
|
|
}
|
|
assert_eq!(pilot.inventory_slots(), 20);
|
|
let before = pilot.inventory.clone();
|
|
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.player.inventory.len(), before.len());
|
|
assert_eq!(
|
|
world.player.super_shield_reserves,
|
|
pilot.super_shield_reserves
|
|
);
|
|
for (id, count) in before {
|
|
assert!(world.player.inventory[&id] >= count);
|
|
}
|
|
println!("full inventory start: 20 physical slots preserved, reserves={reserves}");
|
|
}
|
|
|
|
// The encounter timer and killing only the cruiser cannot award victory.
|
|
active.scripts.states.clear();
|
|
active.scripts.rng.clear();
|
|
let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
world
|
|
.objectives
|
|
.insert("iron_coast_boss_5:defeated".into(), 1);
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Playing);
|
|
world
|
|
.objectives
|
|
.insert("iron_coast_mission_5:foundries_destroyed".into(), 2);
|
|
let credits = world.player.credits;
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert_eq!(world.player.credits, credits + 15_000);
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.player.credits, credits + 15_000);
|
|
println!("objectives and one-time reward: passed");
|
|
|
|
// Re-enter with completed script state retained. The start callback must
|
|
// clear encounter flags/counters instead of inheriting the former victory.
|
|
let mut retry = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap();
|
|
retry
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(retry.outcome, Outcome::Playing);
|
|
assert_eq!(
|
|
retry.objectives["iron_coast_mission_5:foundries_destroyed"],
|
|
0
|
|
);
|
|
assert_eq!(retry.objectives["iron_coast_boss_5:defeated"], 0);
|
|
assert_eq!(retry.objectives["iron_coast_mission_5:complete"], 0);
|
|
println!("mission restart with saved state: reset correctly");
|
|
}
|