use crate::protocol::*; use crate::physics::PhysicsWorld; #[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_red: u8, stones_yellow: u8, last_end_scored: Option<(u8, i32, Option)>, } pub struct ThrowOutcome { pub trajectory: Vec, pub end_scored: Option, 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::Red, turn_team: Team::Red, physics: PhysicsWorld::new(), active_stones: Vec::new(), stones_red: STONES_PER_TEAM, stones_yellow: STONES_PER_TEAM, last_end_scored: None, } } pub fn start(&mut self) { self.hammer = if rand::random() { Team::Red } else { Team::Yellow }; self.turn_team = self.hammer.other(); self.phase = GamePhase::Playing; self.end = 1; self.stones_red = STONES_PER_TEAM; self.stones_yellow = STONES_PER_TEAM; self.scores = [0, 0]; 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, weight: u8, 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(); // B1: physics takes pure velocity (m/s). Map legacy weight 1..=10 until B3. let w = weight.clamp(1, 10) as f32; let t = (w - 1.0) / 9.0; let velocity = MIN_SPEED + t * (MAX_SPEED - MIN_SPEED); 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::Red => self.stones_red = self.stones_red.saturating_sub(1), Team::Yellow => self.stones_yellow = self.stones_yellow.saturating_sub(1), } Ok(trajectory) } pub fn process_throw( &mut self, team: Team, broom_x: f32, broom_y: f32, weight: u8, curl: i8, friction: f32, ) -> Result { let trajectory = self.handle_throw(team, broom_x, broom_y, weight, curl, friction)?; self.finish_simulation(); let end_scored = self.take_last_end_scored(); let state_message = self.game_state_message(); let game_over = if self.phase == GamePhase::GameComplete { Some(self.game_over_message()) } else { None }; Ok(ThrowOutcome { trajectory, end_scored, 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_red == 0 && self.stones_yellow == 0); if !end_done { self.phase = GamePhase::Playing; self.turn_team = self.turn_team.other(); return; } self.phase = GamePhase::Scoring; 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 = 0; if let Some(team) = scoring_team { for (_, _, t, _, _) in &by_distance { if *t == team { points += 1; } else { break; } } if points > 0 { let team_idx = match team { Team::Red => 0, Team::Yellow => 1, }; self.scores[team_idx] += points; self.hammer = team.other(); } else { points = 0; } } self.last_end_scored = Some((self.end, points, scoring_team)); 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_red = STONES_PER_TEAM; self.stones_yellow = 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 take_last_end_scored(&mut self) -> Option { let msg = self.last_end_scored.map(|(end, points, scoring_team)| { ServerMessage::EndScored { end, points, scoring_team } }); self.last_end_scored = None; msg } pub fn game_state_message(&self) -> ServerMessage { ServerMessage::GameState { end: self.end, scores: self.scores, hammer: self.hammer, turn_team: self.turn_team, 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::Red) } else if self.scores[1] > self.scores[0] { Some(Team::Yellow) } 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::*; #[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]); } #[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, 7, 1, 1.0); assert!(result.is_err()); } #[test] fn accepts_throw_for_turn_team() { let mut game = Game::new(); game.start(); let turn = game.turn_team; let result = game.handle_throw(turn, 0.5, 38.7, 7, 1, 1.0); assert!(result.is_ok()); } #[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, 7, 1, 1.0); assert!(result.is_ok(), "broom outside house should be allowed: {:?}", result.err()); } }