mod domain;
mod error;
mod handlers;
mod sim;
mod views;
use domain::types::Direction;
use sim::Game;
use distributed::HashMapRepository;
const SMALL_MAP: &str = "\
#######
#1 . 2#
# # # #
# . #
# # # #
#3 . 4#
#######";
#[tokio::test]
async fn game_setup_and_movement() {
let repo = HashMapRepository::new();
let game = Game::new(&repo, "game-1", SMALL_MAP).await.unwrap();
let p1 = game.sim("p1", "Alice");
let p2 = game.sim("p2", "Bob");
p1.join(0).await.unwrap();
p2.join(1).await.unwrap();
p1.move_dir(Direction::South).await.unwrap();
let result = p1.move_dir(Direction::West).await;
assert!(result.is_err());
let board = game.board().await.unwrap();
assert_eq!(board.data.players.len(), 2);
let alice = board
.data
.players
.iter()
.find(|p| p.name == "Alice")
.unwrap();
assert_eq!(alice.x, 1);
assert_eq!(alice.y, 2);
assert!(alice.alive);
let bob = board.data.players.iter().find(|p| p.name == "Bob").unwrap();
assert_eq!(bob.x, 5);
assert_eq!(bob.y, 1);
assert!(bob.alive);
}
#[tokio::test]
async fn invalid_spawn_index_returns_error() {
let repo = HashMapRepository::new();
let game = Game::new(&repo, "game-invalid-spawn", SMALL_MAP)
.await
.unwrap();
let player = game.sim("p1", "Alice");
let result = player.join(99).await;
assert!(matches!(
result,
Err(error::GameError::InvalidSpawnIndex(99))
));
}
#[tokio::test]
async fn bomb_destroys_blocks() {
let repo = HashMapRepository::new();
let game = Game::new(&repo, "game-2", SMALL_MAP).await.unwrap();
let p1 = game.sim("p1", "Alice");
p1.join(0).await.unwrap();
p1.move_dir(Direction::East).await.unwrap();
p1.place_bomb().await.unwrap();
p1.move_dir(Direction::West).await.unwrap(); p1.move_dir(Direction::South).await.unwrap();
let board = game.board().await.unwrap();
assert_eq!(board.data.bombs.len(), 1);
game.tick().await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
let tick_result = game.tick().await.unwrap();
assert!(!tick_result.blocks_destroyed.is_empty());
let board = game.board().await.unwrap();
assert_eq!(board.data.bombs.len(), 0);
assert!(p1.is_alive().await.unwrap());
let player = p1.player().await.unwrap();
assert_eq!(player.active_bombs, 0);
}
#[tokio::test]
async fn player_killed_by_bomb() {
let repo2 = HashMapRepository::new();
let open_map = "\
###########
#1 #
# #
# #
# #
# #
# 2#
###########";
let game2 = Game::new(&repo2, "game-3b", open_map).await.unwrap();
let alice = game2.sim("p1", "Alice");
let bob = game2.sim("p2", "Bob");
alice.join(0).await.unwrap(); bob.join(1).await.unwrap();
bob.move_dir(Direction::North).await.unwrap(); bob.move_dir(Direction::North).await.unwrap(); bob.move_dir(Direction::North).await.unwrap(); bob.move_dir(Direction::North).await.unwrap(); bob.move_dir(Direction::North).await.unwrap(); bob.move_dir(Direction::West).await.unwrap(); bob.move_dir(Direction::West).await.unwrap(); bob.move_dir(Direction::West).await.unwrap(); bob.move_dir(Direction::West).await.unwrap();
alice.move_dir(Direction::East).await.unwrap(); alice.move_dir(Direction::East).await.unwrap();
alice.place_bomb().await.unwrap();
alice.move_dir(Direction::South).await.unwrap(); alice.move_dir(Direction::South).await.unwrap(); alice.move_dir(Direction::South).await.unwrap();
game2.tick().await.unwrap();
game2.tick().await.unwrap();
game2.tick().await.unwrap();
game2.tick().await.unwrap();
let tick_result = game2.tick().await.unwrap();
assert!(!bob.is_alive().await.unwrap());
assert!(alice.is_alive().await.unwrap());
assert!(tick_result
.players_killed
.contains(&"player:p2".to_string()));
use distributed::{OutboxMessageStatus, OutboxStore};
let pending = repo2
.outbox_store()
.messages_by_status(OutboxMessageStatus::Pending)
.unwrap();
assert!(!pending.is_empty());
assert_eq!(pending[0].event_type, "PlayerKilled");
}
#[tokio::test]
async fn chain_reaction() {
let repo = HashMapRepository::new();
let tall_map = "\
###########
#1 #
# #
# #
# #
# #
# #
# 2#
###########";
let game = Game::new(&repo, "game-4", tall_map).await.unwrap();
let p1 = game.sim("p1", "Alice");
let p2 = game.sim("p2", "Bob");
p1.join(0).await.unwrap(); p2.join(1).await.unwrap();
p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.place_bomb().await.unwrap();
p1.move_dir(Direction::South).await.unwrap(); p1.move_dir(Direction::South).await.unwrap(); p1.move_dir(Direction::South).await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
bob_move_to_position(&game, "p2", "Bob", 9, 7, 5, 1).await;
p2.place_bomb().await.unwrap(); p2.move_dir(Direction::South).await.unwrap(); p2.move_dir(Direction::South).await.unwrap(); p2.move_dir(Direction::South).await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
let tick_result = game.tick().await.unwrap();
assert!(
!tick_result.chain_detonations.is_empty() || tick_result.detonations.len() >= 2,
"Expected chain reaction or multiple detonations, got {} detonations, {} chains",
tick_result.detonations.len(),
tick_result.chain_detonations.len()
);
assert!(p1.is_alive().await.unwrap());
assert!(p2.is_alive().await.unwrap());
}
async fn bob_move_to_position<R>(
game: &Game<'_, R>,
id: &str,
_name: &str,
from_x: i32,
from_y: i32,
to_x: i32,
to_y: i32,
) where
R: distributed::AsyncGetStream
+ distributed::AsyncTransactionalCommit
+ distributed::AsyncReadModelWritePlanStore
+ distributed::AsyncRelationalReadModelQueryStore,
{
let sim = game.sim(id, _name);
let mut cx = from_x;
let mut cy = from_y;
while cy != to_y {
if cy > to_y {
sim.move_dir(Direction::North).await.unwrap();
cy -= 1;
} else {
sim.move_dir(Direction::South).await.unwrap();
cy += 1;
}
}
while cx != to_x {
if cx > to_x {
sim.move_dir(Direction::West).await.unwrap();
cx -= 1;
} else {
sim.move_dir(Direction::East).await.unwrap();
cx += 1;
}
}
}
#[tokio::test]
async fn concurrent_bomb_placement() {
use distributed::{AsyncReadModelWorkspaceExt, RowKey, RowValue};
use std::sync::Arc;
let repo = Arc::new(HashMapRepository::new());
let open_map = "\
#######
#1 2#
# #
#3 4#
#######";
handlers::create_game(&*repo, "game-5", open_map)
.await
.unwrap();
handlers::join_game(&*repo, "p1", "Alice", "game-5", 0)
.await
.unwrap();
handlers::join_game(&*repo, "p2", "Bob", "game-5", 1)
.await
.unwrap();
let repo2 = repo.clone();
let repo3 = repo.clone();
let (r1, r2) = tokio::join!(
handlers::place_bomb(&*repo2, "p1", "game-5"),
handlers::place_bomb(&*repo3, "p2", "game-5"),
);
r1.unwrap();
r2.unwrap();
let _board = repo
.workspace_async()
.load_async::<views::BoardView>(RowKey::new([(
"game_id",
RowValue::String("game-5".into()),
)]))
.one()
.await
.unwrap()
.unwrap();
let alice: domain::player::Player =
handlers::get_aggregate::<_, domain::player::Player>(&*repo, "player:p1")
.await
.unwrap()
.unwrap();
assert_eq!(alice.active_bombs, 1);
let bob: domain::player::Player =
handlers::get_aggregate::<_, domain::player::Player>(&*repo, "player:p2")
.await
.unwrap()
.unwrap();
assert_eq!(bob.active_bombs, 1);
let bomb1: Option<domain::bomb::Bomb> =
handlers::get_aggregate::<_, domain::bomb::Bomb>(&*repo, "bomb:p1:1")
.await
.unwrap();
let bomb2: Option<domain::bomb::Bomb> =
handlers::get_aggregate::<_, domain::bomb::Bomb>(&*repo, "bomb:p2:1")
.await
.unwrap();
assert!(bomb1.is_some(), "Alice's bomb should exist");
assert!(bomb2.is_some(), "Bob's bomb should exist");
}
#[tokio::test]
async fn full_game_to_winner() {
let repo = HashMapRepository::new();
let arena = "\
#############
#1 2#
# #
# #
# #
# #
# #
# #
# #
# #
# #
#3 4#
#############";
let game = Game::new(&repo, "game-6", arena).await.unwrap();
let p1 = game.sim("p1", "Alice");
let p2 = game.sim("p2", "Bob");
let p3 = game.sim("p3", "Charlie");
let p4 = game.sim("p4", "Diana");
p1.join(0).await.unwrap(); p2.join(1).await.unwrap(); p3.join(2).await.unwrap(); p4.join(3).await.unwrap();
let board = game.board().await.unwrap();
assert_eq!(board.data.players.len(), 4);
assert!(board.data.players.iter().all(|p| p.alive));
p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap(); p1.move_dir(Direction::East).await.unwrap();
p1.place_bomb().await.unwrap();
p1.move_dir(Direction::South).await.unwrap(); p1.move_dir(Direction::South).await.unwrap(); p1.move_dir(Direction::South).await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap(); game.tick().await.unwrap(); let r1 = game.tick().await.unwrap();
assert!(
!p2.is_alive().await.unwrap(),
"Bob should be dead from Alice's bomb"
);
assert!(r1.players_killed.contains(&"player:p2".to_string()));
p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap(); p3.move_dir(Direction::East).await.unwrap();
p3.place_bomb().await.unwrap();
p3.move_dir(Direction::North).await.unwrap(); p3.move_dir(Direction::North).await.unwrap(); p3.move_dir(Direction::North).await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap(); game.tick().await.unwrap(); let r2 = game.tick().await.unwrap();
assert!(!p4.is_alive().await.unwrap(), "Diana should be dead");
assert!(r2.players_killed.contains(&"player:p4".to_string()));
p1.move_dir(Direction::South).await.unwrap(); p1.move_dir(Direction::South).await.unwrap();
p1.place_bomb().await.unwrap();
p1.move_dir(Direction::North).await.unwrap(); p1.move_dir(Direction::North).await.unwrap(); p1.move_dir(Direction::North).await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap();
game.tick().await.unwrap(); game.tick().await.unwrap(); let r3 = game.tick().await.unwrap();
assert!(!p3.is_alive().await.unwrap(), "Charlie should be dead");
assert!(r3.game_over, "Game should be over with only Alice alive");
assert_eq!(r3.winner.as_deref(), Some("Alice"));
let board = game.board().await.unwrap();
let alive_count = board.data.players.iter().filter(|p| p.alive).count();
assert_eq!(alive_count, 1);
let winner_state = board.data.players.iter().find(|p| p.alive).unwrap();
assert_eq!(winner_state.name, "Alice");
}