fix(simulation): address PR #2 review findings

- Fix Cargo.toml edition 2024 → 2021 (Hoshe #1)
- Switch test runner to cargo-nextest in Makefile (Hoshe #2, D-030)
- Add debug_assert tick ordering enforcement in InputQueue::push (Hoshe #4)
- Add DayPhase Serialize/Deserialize derives (Tyre #6)
- Add day phase boundary comments clarifying half-open ranges (Hoshe #3)
- Add phase duration adjustability comment (Tyre #5)
- Document ObserverSnapshot planned fields as TODO (Tyre #1)
- Document entity_id as wire-format ID, not ECS Entity (Tyre #2)
- Change Relationship.target_name to target_id: u64 (Tyre #3)
- Clarify single app.update() is intentional boilerplate (Hoshe #8, Tyre #7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 17:53:01 +01:00
co-authored by Claude Opus 4.6
parent 9de1eb4aa3
commit 6bda4a7620
7 changed files with 59 additions and 7 deletions
+23 -1
View File
@@ -13,8 +13,16 @@ pub struct InputQueue {
}
impl InputQueue {
/// Add a new input to the queue
/// Add a new input to the queue.
/// Inputs must be pushed in tick order for deterministic processing.
/// Panics in debug builds if tick ordering is violated.
pub fn push(&mut self, input: PlayerInput) {
debug_assert!(
self.queue.back().is_none_or(|last| last.tick <= input.tick),
"InputQueue: tick ordering violated (last={}, new={})",
self.queue.back().map_or(0, |last| last.tick),
input.tick,
);
self.queue.push_back(input);
}
@@ -74,4 +82,18 @@ mod tests {
let inputs = queue.drain_for_tick(10);
assert!(inputs.is_empty());
}
#[test]
#[should_panic(expected = "tick ordering violated")]
fn push_rejects_out_of_order_in_debug() {
let mut queue = InputQueue::default();
queue.push(PlayerInput {
tick: 5,
action: PlayerAction::MoveNorth,
});
queue.push(PlayerInput {
tick: 2,
action: PlayerAction::MoveSouth,
});
}
}
+25 -1
View File
@@ -3,12 +3,14 @@
// Injectable time resource for deterministic replay (D-030)
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
pub const TICKS_PER_GAME_MINUTE: u64 = 10;
pub const MINUTES_PER_PHASE: u64 = 360;
pub const MINUTES_PER_DAY: u64 = 1440;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
// 4 equal 6-hour phases (adjustable — D-031 allows rebalancing phase durations)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DayPhase {
Morning,
Afternoon,
@@ -33,6 +35,7 @@ impl SimulationTime {
}
pub fn day_phase(&self) -> DayPhase {
let tod = self.time_of_day_minutes();
// Morning: [0, 360), Afternoon: [360, 720), Evening: [720, 1080), Night: [1080, 1440)
match tod {
0..360 => DayPhase::Morning,
360..720 => DayPhase::Afternoon,
@@ -114,4 +117,25 @@ mod tests {
schedule.run(&mut world);
assert_eq!(world.resource::<SimulationTime>().tick, 1);
}
#[test]
fn day_wraparound_at_midnight() {
// 1440 minutes = 1 full day, should wrap back to Morning
let time = SimulationTime {
tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE,
paused: false,
};
assert_eq!(time.day_phase(), DayPhase::Morning);
assert_eq!(time.time_of_day_minutes(), 0);
assert_eq!(time.day(), 1);
}
#[test]
fn day_calculation() {
let time = SimulationTime {
tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100,
paused: false,
};
assert_eq!(time.day(), 3);
}
}