only tick nonwaiting objects

This commit is contained in:
2026-07-11 12:04:33 -05:00
parent b1b723fd1b
commit f1eaaae5d0
7 changed files with 55 additions and 13 deletions
+31
View File
@@ -383,6 +383,37 @@ fn a_later_object_sees_an_earlier_objects_move_this_tick() {
assert_eq!(log_texts(&game), vec!["blocked"]);
}
#[test]
fn tick_is_gated_on_an_empty_queue() {
// `tick` fires only when the object's output queue is empty — so an *unguarded*
// tick that just `move`s east paces itself one step per drained move instead of
// piling up. `move` enqueues a Move plus a 0.25s Delay; while that Delay is still
// draining the engine skips re-running `tick`. With 100 ms frames the object steps
// once (x=1) then waits ~0.25s (the pending Delay) before the next call fires.
// Under the old every-frame model an unguarded tick would enqueue a fresh move each
// frame, racing the object east far faster.
let obj = scripted_object(0, 0, "m");
let board = open_board(6, 1, (5, 0), vec![obj]);
let mut game = GameState::with_scripts(
board,
// Note: no `if m.queue.length == 0` guard — the engine provides it.
scripts_from(&[("m", "fn tick(m, dt) { move(East); }")]),
);
game.run_init();
// Over 0.3s only the first move has resolved; the pending Delay suppresses the
// re-tick, so the object is still at x=1 rather than having stacked more moves.
for _ in 0..3 {
game.tick(Duration::from_millis(100));
}
assert_eq!(game.board().objects[&1].x, 1);
// Once the Delay fully drains the queue empties, so the next frame calls `tick`
// again and the object takes its second step.
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[&1].x, 2);
}
#[test]
fn a_send_cycle_terminates_via_the_called_guard() {
// Two objects send "go" to each other in a cycle. Without the per-invocation