Spectrum system (src/ai/spectrum.rs): - 6 spectrums: materials, temperature, light, entities, density, velocity - Each renders a different ASCII view of the same world state - materials: material display chars + entity chars - temperature: heat levels as .-=+oxX# - light: brightness as .:+oO* - entities: entity positions only, background dots - density: material density as .:+oO# - velocity: entity speed + CA activity as .:+oO, Tape recording (src/ai/tape.rs): - --mode tape CLI with --tape-interval, --tape-output, --tape-json - Records all 6 spectrums + world state every N ticks - Each frame: tick, depth, kills, score, camera, player HP/pos, entity count - Outputs human-readable text and JSON formats Pipe protocol additions: - get_spectrum: single spectrum view by name - get_all_spectrums: all 6 spectrums in one response - Response.spectrums field added Architecture priorities updated: - Graphics mode is PRIMARY renderer - ASCII mode is DEBUG backend - Terminal mode is LEGACY - AI uses multi-spectrum ASCII layers for observation All 171 tests + 14 scenarios pass.
195 lines
5.6 KiB
Rust
195 lines
5.6 KiB
Rust
use crate::game::Game;
|
|
use crate::render::lighting;
|
|
use std::io::Write;
|
|
|
|
pub struct TapeRecorder {
|
|
frames: Vec<TapeFrame>,
|
|
interval: u32,
|
|
last_recorded_tick: u64,
|
|
view_w: usize,
|
|
view_h: usize,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct TapeFrame {
|
|
pub tick: u64,
|
|
pub depth: u32,
|
|
pub kills: u32,
|
|
pub score: u32,
|
|
pub cam_x: i32,
|
|
pub cam_y: i32,
|
|
pub player_hp: f32,
|
|
pub player_max_hp: f32,
|
|
pub player_pos: [f32; 2],
|
|
pub entity_count: usize,
|
|
pub spectrums: Vec<(String, String)>,
|
|
}
|
|
|
|
impl TapeRecorder {
|
|
pub fn new(interval: u32, view_w: usize, view_h: usize) -> Self {
|
|
Self {
|
|
frames: Vec::new(),
|
|
interval,
|
|
last_recorded_tick: 0,
|
|
view_w,
|
|
view_h,
|
|
}
|
|
}
|
|
|
|
pub fn should_record(&self, tick: u64) -> bool {
|
|
tick - self.last_recorded_tick >= self.interval as u64
|
|
}
|
|
|
|
pub fn record(&mut self, game: &Game) {
|
|
if !self.should_record(game.tick) {
|
|
return;
|
|
}
|
|
self.last_recorded_tick = game.tick;
|
|
|
|
let (px, py) = game.player.center(&game.entities);
|
|
let cam_x = px as i32 - (self.view_w as i32 / 2);
|
|
let cam_y = py as i32 - (self.view_h as i32 / 2);
|
|
|
|
let light = lighting::compute_lighting(
|
|
&game.grid,
|
|
cam_x,
|
|
cam_y,
|
|
self.view_w,
|
|
self.view_h,
|
|
lighting::ambient_light(),
|
|
);
|
|
|
|
let spectrums = crate::ai::spectrum::render_all_spectrums(
|
|
&game.grid,
|
|
&game.entities,
|
|
Some(&light),
|
|
cam_x,
|
|
cam_y,
|
|
self.view_w,
|
|
self.view_h,
|
|
);
|
|
|
|
let (hp, max_hp) = game
|
|
.player
|
|
.entity(&game.entities)
|
|
.map(|e| (e.health, e.max_health))
|
|
.unwrap_or((0.0, 0.0));
|
|
|
|
let frame = TapeFrame {
|
|
tick: game.tick,
|
|
depth: game.depth,
|
|
kills: game.kills,
|
|
score: game.score,
|
|
cam_x,
|
|
cam_y,
|
|
player_hp: hp,
|
|
player_max_hp: max_hp,
|
|
player_pos: [px, py],
|
|
entity_count: game.entities.all().len(),
|
|
spectrums,
|
|
};
|
|
self.frames.push(frame);
|
|
}
|
|
|
|
pub fn frame_count(&self) -> usize {
|
|
self.frames.len()
|
|
}
|
|
|
|
pub fn save_to_file(&self, path: &str) -> std::io::Result<()> {
|
|
let mut f = std::fs::File::create(path)?;
|
|
writeln!(f, "=== Verbatim Tape Recording ===")?;
|
|
writeln!(f, "frames: {}", self.frames.len())?;
|
|
writeln!(f, "view: {}x{}", self.view_w, self.view_h)?;
|
|
writeln!(f)?;
|
|
|
|
for frame in &self.frames {
|
|
writeln!(
|
|
f,
|
|
"==== FRAME tick={} depth={} kills={} score={} ====",
|
|
frame.tick, frame.depth, frame.kills, frame.score
|
|
)?;
|
|
writeln!(f, "cam: ({}, {})", frame.cam_x, frame.cam_y)?;
|
|
writeln!(
|
|
f,
|
|
"player: hp={:.1}/{:.1} pos=({:.1},{:.1}) entities={}",
|
|
frame.player_hp,
|
|
frame.player_max_hp,
|
|
frame.player_pos[0],
|
|
frame.player_pos[1],
|
|
frame.entity_count
|
|
)?;
|
|
writeln!(f)?;
|
|
for (name, view) in &frame.spectrums {
|
|
writeln!(f, "--- {} ---", name)?;
|
|
write!(f, "{}", view)?;
|
|
writeln!(f)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn save_json_to_file(&self, path: &str) -> std::io::Result<()> {
|
|
let json = serde_json::to_string_pretty(
|
|
&self
|
|
.frames
|
|
.iter()
|
|
.map(|f| {
|
|
serde_json::json!({
|
|
"tick": f.tick,
|
|
"depth": f.depth,
|
|
"kills": f.kills,
|
|
"score": f.score,
|
|
"cam": [f.cam_x, f.cam_y],
|
|
"player": {
|
|
"hp": f.player_hp,
|
|
"max_hp": f.player_max_hp,
|
|
"pos": f.player_pos,
|
|
},
|
|
"entity_count": f.entity_count,
|
|
"spectrums": f.spectrums.iter().map(|(name, view)| {
|
|
serde_json::json!({
|
|
"name": name,
|
|
"view": view,
|
|
})
|
|
}).collect::<Vec<_>>(),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
let mut f = std::fs::File::create(path)?;
|
|
f.write_all(json.as_bytes())?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn run_tape_mode(ticks: u32, interval: u32, output: &str, json_output: Option<&str>) {
|
|
let mut game = Game::new();
|
|
game.init_world();
|
|
|
|
let view_w = 80usize;
|
|
let view_h = 25usize;
|
|
let mut recorder = TapeRecorder::new(interval, view_w, view_h);
|
|
|
|
recorder.record(&game);
|
|
|
|
for _ in 0..ticks {
|
|
game.fixed_update();
|
|
recorder.record(&game);
|
|
}
|
|
|
|
let frame_count = recorder.frame_count();
|
|
match recorder.save_to_file(output) {
|
|
Ok(_) => eprintln!("Tape: {} frames recorded, saved to {}", frame_count, output),
|
|
Err(e) => eprintln!("Tape save failed: {}", e),
|
|
}
|
|
|
|
if let Some(json_path) = json_output {
|
|
match recorder.save_json_to_file(json_path) {
|
|
Ok(_) => eprintln!("Tape JSON saved to {}", json_path),
|
|
Err(e) => eprintln!("Tape JSON save failed: {}", e),
|
|
}
|
|
}
|
|
}
|