use crate::app::{Sim, layout};
use crate::sim::{
Crab, CrabKind, Direction, GullState, MAX_PLAYERS, PlayerId, TideEvent, TileKind, castle_tier,
};
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
#[derive(Message, Clone, Debug)]
pub enum SimEvent {
CrabBanked {
owner: PlayerId,
pos: Vec2,
value: u32,
kind: CrabKind,
},
CrabEaten { pos: Vec2 },
CrabSpawned { pos: Vec2 },
CastleRaided {
owner: PlayerId,
pos: Vec2,
lost: u32,
},
GullArrived,
GullTookOff,
GullLanded { pos: Vec2 },
SignpostsChanged { delta: i32 },
TierUp { owner: PlayerId },
TideEventFired { event: TideEvent },
SurgeStarted,
RoundEnded,
}
#[derive(PartialEq, Debug)]
struct Origin {
width: u8,
height: u8,
seed: u64,
tiles: Vec<TileKind>,
crabs: Vec<(u32, u16, Direction, CrabKind)>,
gulls: Vec<(u32, u16)>,
}
impl Origin {
fn of(board: &crate::sim::Board) -> Option<Origin> {
(board.ticks() == 0).then(|| Origin {
width: board.width(),
height: board.height(),
seed: board.seed(),
tiles: board.tiles().map(|(_, _, kind)| kind).collect(),
crabs: board
.crabs()
.iter()
.map(|c| (c.id, c.tile, c.dir, c.kind))
.collect(),
gulls: board.gulls().iter().map(|g| (g.id, g.tile)).collect(),
})
}
}
#[derive(Default)]
pub struct Watch {
ticks: u64,
origin: Option<Origin>,
scores: [u32; MAX_PLAYERS],
tiers: [u8; MAX_PLAYERS],
posts: usize,
gulls: usize,
flying: usize,
event_at: Option<u64>,
last_event: Option<TideEvent>,
surging: bool,
over: bool,
crabs: HashMap<u32, Crab>,
gull_flying: HashMap<u32, (bool, Vec2)>,
}
impl Watch {
fn of(board: &crate::sim::Board) -> Watch {
let mut tiers = [0u8; MAX_PLAYERS];
for (tier, &score) in tiers.iter_mut().zip(board.scores().iter()) {
*tier = castle_tier(score);
}
Watch {
ticks: board.ticks(),
origin: Origin::of(board),
scores: *board.scores(),
tiers,
posts: (0..MAX_PLAYERS as u8)
.map(|p| board.signpost_count(p))
.sum(),
gulls: board.gulls().len(),
flying: board
.gulls()
.iter()
.filter(|g| matches!(g.state, GullState::Flying { .. }))
.count(),
event_at: board.last_event().map(|(_, at)| at),
last_event: board.last_event().map(|(event, _)| event),
surging: board.in_surge(),
over: board.round_over(),
crabs: board.crabs().iter().map(|c| (c.id, *c)).collect(),
gull_flying: board
.gulls()
.iter()
.map(|g| {
let pos = layout::creature_pos(board, g.tile, g.dir, g.progress);
(g.id, (matches!(g.state, GullState::Flying { .. }), pos))
})
.collect(),
}
}
}
fn crab_departure(board: &crate::sim::Board, prev: &Crab) -> SimEvent {
let pos = layout::creature_pos(board, prev.tile, prev.dir, prev.progress);
let (x, y) = board.coords_u8(prev.tile);
let (dx, dy) = prev.dir.offset();
let tile_or_empty = |x: i32, y: i32| {
if x >= 0 && y >= 0 && x < i32::from(board.width()) && y < i32::from(board.height()) {
board.tile_at(x as u8, y as u8)
} else {
TileKind::Empty
}
};
let here = tile_or_empty(i32::from(x), i32::from(y));
let entering = tile_or_empty(i32::from(x) + dx, i32::from(y) + dy);
match (here, entering) {
(TileKind::Castle(owner), _) | (_, TileKind::Castle(owner)) => SimEvent::CrabBanked {
owner,
pos,
value: prev.kind.value(),
kind: prev.kind,
},
_ => SimEvent::CrabEaten { pos },
}
}
fn crab_events(board: &crate::sim::Board, watch: &Watch, events: &mut Vec<SimEvent>) {
for (id, prev) in watch.crabs.iter() {
if board.crabs().iter().any(|crab| crab.id == *id) {
continue;
}
events.push(crab_departure(board, prev));
}
for crab in board.crabs() {
if watch.crabs.contains_key(&crab.id) {
continue;
}
let (x, y) = board.coords_u8(crab.tile);
if matches!(board.tile_at(x, y), TileKind::Spawner(_)) {
events.push(SimEvent::CrabSpawned {
pos: layout::creature_pos(board, crab.tile, crab.dir, crab.progress),
});
}
}
}
pub fn observe_sim(sim: Res<Sim>, mut watch: Local<Watch>, mut events: MessageWriter<SimEvent>) {
for event in diff(&sim.0, &mut watch) {
events.write(event);
}
}
pub fn diff(board: &crate::sim::Board, watch: &mut Watch) -> Vec<SimEvent> {
let next = Watch::of(board);
let swapped =
board.ticks() < watch.ticks || (board.ticks() == 0 && next.origin != watch.origin);
let events = if swapped {
Vec::new()
} else {
changes(board, watch, &next)
};
*watch = next;
events
}
fn changes(board: &crate::sim::Board, prev: &Watch, next: &Watch) -> Vec<SimEvent> {
let mut events = Vec::new();
crab_events(board, prev, &mut events);
for (seat, (&now, &before)) in next.scores.iter().zip(prev.scores.iter()).enumerate() {
if now >= before {
continue;
}
if let Some((x, y)) = board.castle_of(seat as PlayerId) {
events.push(SimEvent::CastleRaided {
owner: seat as PlayerId,
pos: layout::tile_center(board, x, y),
lost: before - now,
});
}
}
if next.gulls > prev.gulls {
events.push(SimEvent::GullArrived);
}
if next.flying > prev.flying {
events.push(SimEvent::GullTookOff);
}
for gull in board.gulls() {
let now_flying = matches!(gull.state, GullState::Flying { .. });
if let Some((was, _)) = prev.gull_flying.get(&gull.id)
&& *was
&& !now_flying
{
events.push(SimEvent::GullLanded {
pos: layout::creature_pos(board, gull.tile, gull.dir, gull.progress),
});
}
}
if next.posts != prev.posts {
events.push(SimEvent::SignpostsChanged {
delta: next.posts as i32 - prev.posts as i32,
});
}
for (seat, (&now, &before)) in next.tiers.iter().zip(prev.tiers.iter()).enumerate() {
if now > before {
events.push(SimEvent::TierUp {
owner: seat as PlayerId,
});
}
}
if next.event_at.is_some()
&& next.event_at != prev.event_at
&& let Some(event) = next.last_event
{
events.push(SimEvent::TideEventFired { event });
}
if next.surging && !prev.surging {
events.push(SimEvent::SurgeStarted);
}
if next.over && !prev.over {
events.push(SimEvent::RoundEnded);
}
events
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::{Board, CrabKind, Direction, Handedness};
fn synced(board: &Board) -> Watch {
let mut watch = Watch::default();
diff(board, &mut watch);
watch
}
#[test]
fn bank_and_eaten_classification() {
let mut board = Board::new(6, 4, 7);
board.set_tile(3, 1, crate::sim::TileKind::Castle(2));
board.spawn_crab(2, 1, Direction::Right, Handedness::Left, CrabKind::Giant);
let mut watch = synced(&board);
let mut banked = None;
for _ in 0..600 {
board.tick_idle();
let events = diff(&board, &mut watch);
if let Some(SimEvent::CrabBanked { owner, value, .. }) = events
.iter()
.find(|e| matches!(e, SimEvent::CrabBanked { .. }))
{
banked = Some((*owner, *value));
break;
}
assert!(
!events
.iter()
.any(|e| matches!(e, SimEvent::CrabEaten { .. })),
"a banking crab must never read as eaten"
);
}
assert_eq!(
banked,
Some((2, 10)),
"giant banks for its owner at value 10"
);
let mut board = Board::new(6, 4, 7);
board.spawn_crab(1, 2, Direction::Right, Handedness::Left, CrabKind::Common);
let mut watch = synced(&board);
board.spawn_gull(1, 2, Direction::Right);
let mut eaten = false;
for _ in 0..60 {
board.tick_idle();
let events = diff(&board, &mut watch);
assert!(
!events
.iter()
.any(|e| matches!(e, SimEvent::CrabBanked { .. })),
"an eaten crab must never read as banked"
);
if events
.iter()
.any(|e| matches!(e, SimEvent::CrabEaten { .. }))
{
eaten = true;
break;
}
}
assert!(eaten, "the gull never registered its meal");
}
#[test]
fn board_swap_is_silent() {
let mut board = Board::new(6, 4, 7);
board.spawn_crab(1, 1, Direction::Right, Handedness::Left, CrabKind::Common);
let mut watch = synced(&board);
for _ in 0..40 {
board.tick_idle();
diff(&board, &mut watch);
}
let fresh = Board::new(8, 6, 99);
assert!(
diff(&fresh, &mut watch).is_empty(),
"a board swap must not fire stale events"
);
}
#[test]
fn swap_between_unticked_boards_is_silent() {
let mut large = Board::new(9, 7, 3);
large.spawn_crab(8, 6, Direction::Right, Handedness::Left, CrabKind::Common);
large.spawn_crab(1, 1, Direction::Right, Handedness::Left, CrabKind::Giant);
let mut watch = synced(&large);
assert_eq!(watch.ticks, 0);
let mut small = Board::new(6, 4, 3);
small.spawn_crab(2, 2, Direction::Left, Handedness::Right, CrabKind::Common);
assert!(
diff(&small, &mut watch).is_empty(),
"a swap between un-ticked boards must be silent"
);
let mut other = Board::new(6, 4, 3);
other.spawn_crab(4, 1, Direction::Up, Handedness::Left, CrabKind::Common);
assert!(
diff(&other, &mut watch).is_empty(),
"a same-sized un-ticked board with other crabs is a swap"
);
assert!(other.place_signpost(0, 1, 1, Direction::Down));
let events = diff(&other, &mut watch);
assert!(
events
.iter()
.any(|e| matches!(e, SimEvent::SignpostsChanged { delta: 1 })),
"setup-phase signposts must still fire, got {events:?}"
);
}
#[test]
fn crab_departure_survives_a_smaller_board() {
let mut large = Board::new(9, 7, 3);
large.spawn_crab(8, 6, Direction::Right, Handedness::Left, CrabKind::Common);
let prev = large.crabs()[0];
let small = Board::new(6, 4, 3);
assert!(matches!(
crab_departure(&small, &prev),
SimEvent::CrabEaten { .. }
));
}
#[test]
fn observe_sim_runs_headless_in_an_app() {
let mut app = App::new();
app.add_message::<SimEvent>();
let mut board = Board::new(6, 4, 7);
board.set_tile(3, 1, crate::sim::TileKind::Castle(1));
board.spawn_crab(2, 1, Direction::Right, Handedness::Left, CrabKind::Common);
app.insert_resource(crate::app::Sim(board));
app.add_systems(Update, observe_sim);
app.update();
let mut banked = false;
for _ in 0..600 {
app.world_mut()
.resource_mut::<crate::app::Sim>()
.0
.tick_idle();
app.update();
let mut messages = app.world_mut().resource_mut::<Messages<SimEvent>>();
if messages
.drain()
.any(|e| matches!(e, SimEvent::CrabBanked { owner: 1, .. }))
{
banked = true;
break;
}
}
assert!(banked, "the system never reported the bank through the App");
}
#[test]
fn tier_up_names_its_owner() {
let mut board = Board::new(6, 4, 7);
let mut watch = synced(&board);
board.set_score(3, 10); board.tick_idle();
let events = diff(&board, &mut watch);
assert!(
events
.iter()
.any(|e| matches!(e, SimEvent::TierUp { owner: 3 })),
"expected TierUp for seat 3, got {events:?}"
);
}
}