From 0e08e21c9b008127dcbd7200c3d6a8e696a7a293 Mon Sep 17 00:00:00 2001 From: Jason Dekarske Date: Fri, 10 Jul 2026 23:16:06 -0700 Subject: [PATCH] refactor(backend): team-based throws without player slots Anyone identifying as the turn team can throw; room slots for 1v1 removed. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/src/game.rs | 118 +++++++++++++------------------------------- 1 file changed, 35 insertions(+), 83 deletions(-) diff --git a/backend/src/game.rs b/backend/src/game.rs index 785a63c..5888960 100644 --- a/backend/src/game.rs +++ b/backend/src/game.rs @@ -12,8 +12,6 @@ pub enum GamePhase { } pub struct Game { - red: Option, - yellow: Option, phase: GamePhase, end: u8, scores: [i32; 2], @@ -27,7 +25,7 @@ pub struct Game { } pub struct ThrowOutcome { - pub trajectory: Vec<(f32, f32, f32)>, + pub trajectory: Vec, pub end_scored: Option, pub state_message: ServerMessage, pub game_over: Option, @@ -36,8 +34,6 @@ pub struct ThrowOutcome { impl Game { pub fn new() -> Self { Self { - red: None, - yellow: None, phase: GamePhase::Waiting, end: 1, scores: [0, 0], @@ -51,57 +47,7 @@ impl Game { } } - pub fn add_player(&mut self, id: String, preferred: Option) -> Option { - let team = preferred - .filter(|t| self.slot_for(t).is_none()) - .or_else(|| self.first_open_team())?; - - let player = Player { id, team, connected: true }; - *self.slot_for(&team) = Some(player); - Some(team) - } - - fn slot_for(&mut self, team: &Team) -> &mut Option { - match team { - Team::Red => &mut self.red, - Team::Yellow => &mut self.yellow, - } - } - - fn first_open_team(&self) -> Option { - if self.red.is_none() { - Some(Team::Red) - } else if self.yellow.is_none() { - Some(Team::Yellow) - } else { - None - } - } - - pub fn remove_player(&mut self, id: &str) -> Option { - if let Some(ref p) = self.red { - if p.id == id { - self.red = None; - return Some(Team::Red); - } - } - if let Some(ref p) = self.yellow { - if p.id == id { - self.yellow = None; - return Some(Team::Yellow); - } - } - None - } - - pub fn can_start(&self) -> bool { - self.red.is_some() && self.yellow.is_some() - } - pub fn start(&mut self) { - if !self.can_start() { - return; - } self.hammer = if rand::random() { Team::Red } else { Team::Yellow }; self.turn_team = self.hammer.other(); self.phase = GamePhase::Playing; @@ -114,17 +60,16 @@ impl Game { self.active_stones.clear(); } - pub fn current_player_id(&self) -> Option<&str> { - let p = match self.turn_team { - Team::Red => self.red.as_ref()?, - Team::Yellow => self.yellow.as_ref()?, - }; - Some(&p.id) - } - - pub fn handle_throw(&mut self, player_id: &str, broom_x: f32, broom_y: f32, weight: u8, curl: i8, friction: f32) -> Result, String> { - let current_id = self.current_player_id().ok_or("No current player")?; - if current_id != player_id { + 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 { @@ -138,7 +83,7 @@ impl Game { } self.active_stones.clear(); - let path = self.physics.throw(self.turn_team, broom_x, broom_y, weight, curl, friction)?; + let trajectory = self.physics.throw(self.turn_team, broom_x, broom_y, weight, curl, friction)?; self.active_stones = self.physics.current_stones(); self.phase = GamePhase::Simulating; @@ -147,19 +92,19 @@ impl Game { Team::Yellow => self.stones_yellow = self.stones_yellow.saturating_sub(1), } - Ok(path) + Ok(trajectory) } pub fn process_throw( &mut self, - player_id: &str, + team: Team, broom_x: f32, broom_y: f32, weight: u8, curl: i8, friction: f32, ) -> Result { - let trajectory = self.handle_throw(player_id, broom_x, broom_y, weight, curl, friction)?; + let trajectory = self.handle_throw(team, broom_x, broom_y, weight, curl, friction)?; self.finish_simulation(); let end_scored = self.take_last_end_scored(); @@ -223,7 +168,7 @@ impl Game { Team::Red => 0, Team::Yellow => 1, }; - self.scores[team_idx] += points as i32; + self.scores[team_idx] += points; self.hammer = team.other(); } else { points = 0; @@ -315,29 +260,36 @@ mod tests { use super::*; #[test] - fn prefers_red_when_requested_and_free() { - let mut game = Game::new(); - assert_eq!(game.add_player("p1".into(), Some(Team::Red)), Some(Team::Red)); + fn starts_in_waiting_phase() { + let game = Game::new(); + assert!(matches!(game.phase, GamePhase::Waiting)); } #[test] - fn prefers_yellow_when_requested_and_free() { + fn starts_when_called() { let mut game = Game::new(); - assert_eq!(game.add_player("p1".into(), Some(Team::Yellow)), Some(Team::Yellow)); + game.start(); + assert!(matches!(game.phase, GamePhase::Playing)); + assert_eq!(game.end, 1); + assert_eq!(game.scores, [0, 0]); } #[test] - fn falls_back_when_preferred_taken() { + fn rejects_throw_for_wrong_team() { let mut game = Game::new(); - assert_eq!(game.add_player("p1".into(), Some(Team::Red)), Some(Team::Red)); - assert_eq!(game.add_player("p2".into(), Some(Team::Red)), Some(Team::Yellow)); + 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 legacy_order_without_preference() { + fn accepts_throw_for_turn_team() { let mut game = Game::new(); - assert_eq!(game.add_player("p1".into(), None), Some(Team::Red)); - assert_eq!(game.add_player("p2".into(), None), Some(Team::Yellow)); - assert_eq!(game.add_player("p3".into(), None), None); + 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()); } }