Jason Dekarske 2e11f005dc fix: allow broom aim anywhere, not only house
Ultraworked

Co-authored-by: Sisyphus <sisyphus@ohmyopenagent.com>
2026-07-10 23:17:15 -07:00

300 lines
8.5 KiB
Rust

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<StoneState>,
stones_red: u8,
stones_yellow: u8,
last_end_scored: Option<(u8, i32, Option<Team>)>,
}
pub struct ThrowOutcome {
pub trajectory: Vec<StoneTrajectory>,
pub end_scored: Option<ServerMessage>,
pub state_message: ServerMessage,
pub game_over: Option<ServerMessage>,
}
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<Vec<StoneTrajectory>, 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, weight, 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<ThrowOutcome, String> {
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<Team> = 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<ServerMessage> {
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<ServerMessage>,
}
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());
}
}