Add Brain enum with hunting foxes; move the A* into the sim
NPC strategy is now a Brain field on the entity (Idle, Wander, Hunt(Prey)) that Sim::think matches on, so behavior is data independent of species. Hunt follows the first step of an A* route to the nearest prey in sight, replanned every window; the pathfinder moves from game/ to sim/ for that. A greedy step-toward-target was tried first and oscillated at walls. Bumps between the same species no longer fight, and six foxes join the hens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+80
-8
@@ -1,11 +1,10 @@
|
||||
mod pathfind;
|
||||
mod pixelhelper;
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use crate::assets::{Image, Tile};
|
||||
use crate::input::{GameAction, InputState};
|
||||
use sim::entity::{Entity, EntityKind};
|
||||
use sim::entity::{Brain, Entity, EntityKind, Prey};
|
||||
use sim::map::TileMap;
|
||||
use sim::{delta_action, load_world, step_allowed, Event, Sim, TICK_HZ, TICKS_PER_MOVE};
|
||||
|
||||
@@ -44,6 +43,18 @@ const CAM_SMOOTH_TAU_MS: f32 = 120.0;
|
||||
/// A position correction farther than this is a teleport — snap instead of easing.
|
||||
const CAM_SNAP_PX: f32 = 96.0;
|
||||
|
||||
/// Animals scattered over the map at start, one per free tile picked at random.
|
||||
const HEN_COUNT: usize = 150;
|
||||
const FOX_COUNT: usize = 6;
|
||||
|
||||
/// Sprite indices in `entities.png`.
|
||||
const SPRITE_PLAYER: u16 = 0;
|
||||
const SPRITE_HEN: u16 = 1;
|
||||
const SPRITE_FOX: u16 = 2;
|
||||
|
||||
/// Interval of the population report on stdout.
|
||||
const REPORT_MS: usize = 5000;
|
||||
|
||||
/// Render-side smoothing state for one entity: the previous tile and how long ago the
|
||||
/// current one was taken. Positions are tiles; rendering lerps between them in pixels
|
||||
/// over one movement interval.
|
||||
@@ -74,6 +85,10 @@ pub struct Game {
|
||||
/// Per-entity interpolation state, keyed by entity id. Purely cosmetic — all game
|
||||
/// logic keeps using the sim's tile positions.
|
||||
lerp: HashMap<u32, EntityLerp>,
|
||||
/// Running totals for the population report.
|
||||
attacks: u32,
|
||||
deaths: u32,
|
||||
report_ms: usize,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -87,8 +102,25 @@ impl Game {
|
||||
println!("loaded {map_path}: {}×{} tiles in {:.1} ms",
|
||||
map.width, map.height, t0.elapsed().as_secs_f64() * 1e3);
|
||||
let mut sim = Sim::new(world);
|
||||
let player_id = sim.world.spawn_entity(EntityKind::Player, 0, (0, 0), 100, 3);
|
||||
sim.world.spawn_entity(EntityKind::Npc, 1, (6, 6), 5, 1);
|
||||
let player_id = sim.world.spawn_entity(
|
||||
EntityKind::Player, SPRITE_PLAYER, Brain::Idle, (0, 0), 100, 3);
|
||||
|
||||
// Scatter animals over free tiles. A cheap LCG is plenty for placement.
|
||||
let mut seed: u32 = 12345;
|
||||
let mut scatter = |sim: &mut Sim, count: usize, sprite: u16, brain: Brain, hp: u16, attack: u16| {
|
||||
let mut placed = 0;
|
||||
while placed < count {
|
||||
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
|
||||
let x = ((seed >> 8) % map.width as u32) as i32;
|
||||
let y = ((seed >> 20) % map.height as u32) as i32;
|
||||
if !sim.world.blocked(x, y) {
|
||||
sim.world.spawn_entity(EntityKind::Npc, sprite, brain, (x as i16, y as i16), hp, attack);
|
||||
placed += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
scatter(&mut sim, HEN_COUNT, SPRITE_HEN, Brain::Wander, 5, 1);
|
||||
scatter(&mut sim, FOX_COUNT, SPRITE_FOX, Brain::Hunt(Prey::Kind(SPRITE_HEN)), 8, 2);
|
||||
|
||||
let mut game = Game {
|
||||
tileset,
|
||||
@@ -100,6 +132,9 @@ impl Game {
|
||||
goal: None,
|
||||
cam: (0.0, 0.0),
|
||||
lerp: HashMap::new(),
|
||||
attacks: 0,
|
||||
deaths: 0,
|
||||
report_ms: 0,
|
||||
};
|
||||
game.cam = game.cam_target();
|
||||
game.track_lerp();
|
||||
@@ -144,21 +179,41 @@ impl Game {
|
||||
self.step_lerp(dt);
|
||||
self.step_camera(dt);
|
||||
self.render_viewport(render_frame);
|
||||
|
||||
self.report_ms += dt;
|
||||
if self.report_ms >= REPORT_MS {
|
||||
self.report_ms = 0;
|
||||
self.print_report();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn print_report(&self) {
|
||||
let count = |sprite| self.sim.world.entities.values().filter(|e| e.type_id == sprite).count();
|
||||
println!("hens: {:3} foxes: {:2} attacks: {:4} deaths: {:3}",
|
||||
count(SPRITE_HEN), count(SPRITE_FOX), self.attacks, self.deaths);
|
||||
}
|
||||
|
||||
/// React to what the sim reported for the last window. Returns `true` once the
|
||||
/// player is dead — the one event the game cannot continue past.
|
||||
fn handle_events(&mut self) -> bool {
|
||||
let mut dead = false;
|
||||
// Only the player's own fights are logged; the rest is tallied for the report.
|
||||
for ev in self.sim.take_events() {
|
||||
match ev {
|
||||
Event::Attacked { by, target, damage } => {
|
||||
println!("#{by} hits #{target} for {damage}");
|
||||
self.attacks += 1;
|
||||
if by == self.player_id || target == self.player_id {
|
||||
println!("#{by} hits #{target} for {damage}");
|
||||
}
|
||||
}
|
||||
Event::Died { id, at } => {
|
||||
println!("#{id} dies at {at:?}");
|
||||
dead |= id == self.player_id;
|
||||
self.deaths += 1;
|
||||
if id == self.player_id {
|
||||
dead = true;
|
||||
} else {
|
||||
println!("#{id} dies at {at:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +371,7 @@ impl Game {
|
||||
if self.sim.world.solid(goal.0, goal.1) {
|
||||
return None;
|
||||
}
|
||||
pathfind::find_path(self.player_pos(), goal, |x, y| {
|
||||
sim::pathfind::find_path(self.player_pos(), goal, |x, y| {
|
||||
(x, y) != goal && self.sim.world.blocked(x, y)
|
||||
})
|
||||
}
|
||||
@@ -433,6 +488,23 @@ impl Game {
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot {
|
||||
/// Runs the world headlessly for `$SECONDS` (default 60) of sim time and prints the
|
||||
/// population report — to watch the hens without a window:
|
||||
/// `cargo test -p game population -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn population() {
|
||||
std::env::set_current_dir("..").unwrap();
|
||||
let secs: usize = std::env::var("SECONDS").ok().and_then(|s| s.parse().ok()).unwrap_or(60);
|
||||
let mut g = super::Game::start("assets/map_test");
|
||||
let input = crate::input::InputState::new();
|
||||
let mut frame = vec![0u16; 320 * 240];
|
||||
for _ in 0..secs * 60 {
|
||||
g.update(&mut frame, 16, &input);
|
||||
}
|
||||
g.print_report();
|
||||
}
|
||||
|
||||
/// Renders the first frame headlessly to the PNG named by `$SNAPSHOT` — a way to
|
||||
/// eyeball the renderer without a window. `$SNAPSHOT_MAP` picks the map:
|
||||
/// `SNAPSHOT=/tmp/frame.png cargo test -p game snapshot -- --ignored`
|
||||
|
||||
Reference in New Issue
Block a user