use crate::physics::PhysicsWorld; use crate::protocol::*; #[derive(Debug, Clone, Copy, PartialEq)] pub enum GamePhase { Waiting, Playing, Simulating, Scoring, EndComplete, GameComplete, } pub struct Game { phase: GamePhase, end: u8, scores: [i32; 2], hammer: Team, turn_team: Team, physics: PhysicsWorld, active_stones: Vec, stones_team1: u8, stones_team2: u8, scoreboard: Vec, } pub struct ThrowOutcome { pub trajectories: Vec, pub state_message: ServerMessage, pub game_over: Option, } impl Game { pub fn new() -> Self { Self { phase: GamePhase::Waiting, end: 1, scores: [0, 0], hammer: Team::Team1, turn_team: Team::Team1, physics: PhysicsWorld::new(), active_stones: Vec::new(), stones_team1: STONES_PER_TEAM, stones_team2: STONES_PER_TEAM, scoreboard: Vec::new(), } } pub fn start(&mut self) { self.hammer = if rand::random() { Team::Team1 } else { Team::Team2 }; self.turn_team = self.hammer.other(); self.phase = GamePhase::Playing; self.end = 1; self.stones_team1 = STONES_PER_TEAM; self.stones_team2 = STONES_PER_TEAM; self.scores = [0, 0]; self.scoreboard.clear(); self.physics.reset(); self.physics.reset_stone_ids(); self.active_stones.clear(); } pub fn handle_throw( &mut self, team: Team, broom_x: f32, broom_y: f32, velocity: f32, curl: i8, friction: f32, ) -> Result, String> { if self.turn_team != team { return Err("Not your turn".to_string()); } if self.phase != GamePhase::Playing { return Err("Cannot throw now".to_string()); } self.active_stones.clear(); let trajectory = self.physics .throw(self.turn_team, broom_x, broom_y, velocity, curl, friction)?; self.active_stones = self.physics.current_stones(); self.phase = GamePhase::Simulating; match self.turn_team { Team::Team1 => self.stones_team1 = self.stones_team1.saturating_sub(1), Team::Team2 => self.stones_team2 = self.stones_team2.saturating_sub(1), } Ok(trajectory) } pub fn process_throw( &mut self, team: Team, broom_x: f32, broom_y: f32, velocity: f32, curl: i8, friction: f32, ) -> Result { let trajectories = self.handle_throw(team, broom_x, broom_y, velocity, curl, friction)?; self.finish_simulation(); let state_message = self.game_state_message(); let game_over = if self.phase == GamePhase::GameComplete { Some(self.game_over_message()) } else { None }; Ok(ThrowOutcome { trajectories, state_message, game_over, }) } pub fn finish_simulation(&mut self) { if self.phase != GamePhase::Simulating { return; } self.active_stones = self.physics.current_stones(); self.score_end_internal(false); } fn score_end_internal(&mut self, force: bool) { let end_done = force || (self.stones_team1 == 0 && self.stones_team2 == 0); if !end_done { self.phase = GamePhase::Playing; self.turn_team = self.turn_team.other(); return; } self.phase = GamePhase::Scoring; let end_hammer = self.hammer; let states = self.physics.stone_states_for_scoring(); let mut by_distance: Vec<_> = states .iter() .map(|(id, team, x, y)| { let dx = x - HOUSE_CENTER.0; let dy = y - HOUSE_CENTER.1; let dist = (dx * dx + dy * dy).sqrt(); (dist, *id, *team, *x, *y) }) .filter(|(dist, _, _, _, _)| *dist <= HOUSE_RADIUS) .collect(); by_distance.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); let scoring_team: Option = by_distance.first().map(|(_, _, team, _, _)| *team); let mut points = 0i32; if let Some(team) = scoring_team { for (_, _, t, _, _) in &by_distance { if *t == team { points += 1; } else { break; } } if points > 0 { self.scores[team.index()] += points; self.hammer = team.other(); } else { points = 0; } } let (team1_pts, team2_pts) = match scoring_team { Some(Team::Team1) if points > 0 => (points, 0), Some(Team::Team2) if points > 0 => (0, points), _ => (0, 0), }; self.scoreboard.push(EndScore { end: self.end, hammer: end_hammer, team1: team1_pts, team2: team2_pts, }); self.phase = GamePhase::EndComplete; self.advance_end_or_finish(); } fn advance_end_or_finish(&mut self) { let tied = self.scores[0] == self.scores[1]; let after_regulation = self.end >= ENDS; if after_regulation && !tied { self.phase = GamePhase::GameComplete; return; } self.end += 1; self.stones_team1 = STONES_PER_TEAM; self.stones_team2 = STONES_PER_TEAM; self.active_stones.clear(); self.physics.reset(); self.physics.reset_stone_ids(); self.turn_team = self.hammer.other(); self.phase = GamePhase::Playing; } pub fn game_state_message(&self) -> ServerMessage { ServerMessage::GameState { end: self.end, scores: self.scores, hammer: self.hammer, turn_team: self.turn_team, scoreboard: self.scoreboard.clone(), stones_remaining: [self.stones_team1, self.stones_team2], stones: self.active_stones.clone(), phase: match self.phase { GamePhase::Waiting => Phase::Waiting, GamePhase::Playing => Phase::Playing, GamePhase::Simulating => Phase::Simulating, GamePhase::Scoring => Phase::Scoring, GamePhase::EndComplete => Phase::EndComplete, GamePhase::GameComplete => Phase::GameComplete, }, } } pub fn game_over_message(&self) -> ServerMessage { let winner = if self.scores[0] > self.scores[1] { Some(Team::Team1) } else if self.scores[1] > self.scores[0] { Some(Team::Team2) } else { None }; ServerMessage::GameOver { scores: self.scores, winner, } } } pub struct Room { pub game: Game, pub tx: tokio::sync::broadcast::Sender, } impl Room { pub fn new(_id: &str) -> Self { let (tx, _) = tokio::sync::broadcast::channel(256); Self { game: Game::new(), tx, } } } #[cfg(test)] mod tests { use super::*; use crate::physics::DRAW_VELOCITY; #[test] fn starts_in_waiting_phase() { let game = Game::new(); assert!(matches!(game.phase, GamePhase::Waiting)); } #[test] fn starts_when_called() { let mut game = Game::new(); game.start(); assert!(matches!(game.phase, GamePhase::Playing)); assert_eq!(game.end, 1); assert_eq!(game.scores, [0, 0]); assert!(game.scoreboard.is_empty()); assert_eq!(game.stones_team1, STONES_PER_TEAM); assert_eq!(game.stones_team2, STONES_PER_TEAM); } #[test] fn rejects_throw_for_wrong_team() { let mut game = Game::new(); game.start(); let turn = game.turn_team; let wrong = turn.other(); let result = game.handle_throw(wrong, 0.5, 38.7, DRAW_VELOCITY, 1, 1.0); assert!(result.is_err()); } #[test] fn accepts_throw_for_turn_team_with_velocity() { let mut game = Game::new(); game.start(); let turn = game.turn_team; let result = game.handle_throw(turn, 0.5, 38.7, DRAW_VELOCITY, 1, 1.0); assert!(result.is_ok()); let paths = result.unwrap(); assert!(!paths.is_empty()); assert_eq!(paths[0].stone_id.n, 1); assert_eq!(paths[0].team, turn); } #[test] fn accepts_broom_outside_house() { let mut game = Game::new(); game.start(); let turn = game.turn_team; // (0.0, 30.0) is well outside HOUSE_RADIUS of HOUSE_CENTER let result = game.handle_throw(turn, 0.0, 30.0, DRAW_VELOCITY, 1, 1.0); assert!( result.is_ok(), "broom outside house should be allowed: {:?}", result.err() ); } #[test] fn process_throw_has_no_end_scored_and_includes_scoreboard_fields() { let mut game = Game::new(); game.start(); let turn = game.turn_team; let outcome = game .process_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0) .unwrap(); // ThrowOutcome must not carry end_scored assert!(!outcome.trajectories.is_empty()); match &outcome.state_message { ServerMessage::GameState { scoreboard, stones_remaining, .. } => { assert!(scoreboard.is_empty() || !scoreboard.is_empty()); // field present assert_eq!(stones_remaining.len(), 2); // One stone thrown let remaining = stones_remaining[turn.index()]; assert_eq!(remaining, STONES_PER_TEAM - 1); } other => panic!("expected GameState, got {:?}", other), } } #[test] fn game_state_message_exposes_scoreboard_and_stones_remaining() { let mut game = Game::new(); game.start(); match game.game_state_message() { ServerMessage::GameState { scoreboard, stones_remaining, stones, .. } => { assert!(scoreboard.is_empty()); assert_eq!(stones_remaining, [STONES_PER_TEAM, STONES_PER_TEAM]); assert!(stones.is_empty()); } other => panic!("expected GameState, got {:?}", other), } } #[test] fn decrements_stones_remaining_per_team() { let mut game = Game::new(); game.start(); let turn = game.turn_team; game.handle_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0) .unwrap(); match turn { Team::Team1 => assert_eq!(game.stones_team1, STONES_PER_TEAM - 1), Team::Team2 => assert_eq!(game.stones_team2, STONES_PER_TEAM - 1), } } }