This commit is contained in:
Jason Dekarske 2026-07-11 13:16:27 -07:00
parent 94c6949426
commit 5eefcd23bf
4 changed files with 302 additions and 713 deletions

View File

@ -71,7 +71,6 @@ impl Game {
broom_y: f32, broom_y: f32,
velocity: f32, velocity: f32,
curl: i8, curl: i8,
friction: f32,
) -> Result<Vec<StonePath>, String> { ) -> Result<Vec<StonePath>, String> {
if self.turn_team != team { if self.turn_team != team {
return Err("Not your turn".to_string()); return Err("Not your turn".to_string());
@ -81,9 +80,9 @@ impl Game {
} }
self.active_stones.clear(); self.active_stones.clear();
let trajectory = let trajectory = self
self.physics .physics
.throw(self.turn_team, broom_x, broom_y, velocity, curl, friction)?; .throw(self.turn_team, broom_x, broom_y, velocity, curl)?;
self.active_stones = self.physics.current_stones(); self.active_stones = self.physics.current_stones();
self.phase = GamePhase::Simulating; self.phase = GamePhase::Simulating;
@ -102,9 +101,8 @@ impl Game {
broom_y: f32, broom_y: f32,
velocity: f32, velocity: f32,
curl: i8, curl: i8,
friction: f32,
) -> Result<ThrowOutcome, String> { ) -> Result<ThrowOutcome, String> {
let trajectories = self.handle_throw(team, broom_x, broom_y, velocity, curl, friction)?; let trajectories = self.handle_throw(team, broom_x, broom_y, velocity, curl)?;
self.finish_simulation(); self.finish_simulation();
let state_message = self.game_state_message(); let state_message = self.game_state_message();
@ -259,7 +257,7 @@ impl Room {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::physics::DRAW_VELOCITY; use crate::protocol::DRAW_VELOCITY;
#[test] #[test]
fn starts_in_waiting_phase() { fn starts_in_waiting_phase() {
@ -285,7 +283,7 @@ mod tests {
game.start(); game.start();
let turn = game.turn_team; let turn = game.turn_team;
let wrong = turn.other(); let wrong = turn.other();
let result = game.handle_throw(wrong, 0.5, 38.7, DRAW_VELOCITY, 1, 1.0); let result = game.handle_throw(wrong, 0.5, 38.7, DRAW_VELOCITY, 1);
assert!(result.is_err()); assert!(result.is_err());
} }
@ -294,7 +292,7 @@ mod tests {
let mut game = Game::new(); let mut game = Game::new();
game.start(); game.start();
let turn = game.turn_team; let turn = game.turn_team;
let result = game.handle_throw(turn, 0.5, 38.7, DRAW_VELOCITY, 1, 1.0); let result = game.handle_throw(turn, 0.5, 38.7, DRAW_VELOCITY, 1);
assert!(result.is_ok()); assert!(result.is_ok());
let paths = result.unwrap(); let paths = result.unwrap();
assert!(!paths.is_empty()); assert!(!paths.is_empty());
@ -308,7 +306,7 @@ mod tests {
game.start(); game.start();
let turn = game.turn_team; let turn = game.turn_team;
// (0.0, 30.0) is well outside HOUSE_RADIUS of HOUSE_CENTER // (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); let result = game.handle_throw(turn, 0.0, 30.0, DRAW_VELOCITY, 1);
assert!( assert!(
result.is_ok(), result.is_ok(),
"broom outside house should be allowed: {:?}", "broom outside house should be allowed: {:?}",
@ -322,7 +320,7 @@ mod tests {
game.start(); game.start();
let turn = game.turn_team; let turn = game.turn_team;
let outcome = game let outcome = game
.process_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0) .process_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0)
.unwrap(); .unwrap();
// ThrowOutcome must not carry end_scored // ThrowOutcome must not carry end_scored
assert!(!outcome.trajectories.is_empty()); assert!(!outcome.trajectories.is_empty());
@ -366,7 +364,7 @@ mod tests {
let mut game = Game::new(); let mut game = Game::new();
game.start(); game.start();
let turn = game.turn_team; let turn = game.turn_team;
game.handle_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0) game.handle_throw(turn, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0)
.unwrap(); .unwrap();
match turn { match turn {
Team::Team1 => assert_eq!(game.stones_team1, STONES_PER_TEAM - 1), Team::Team1 => assert_eq!(game.stones_team1, STONES_PER_TEAM - 1),

View File

@ -180,12 +180,11 @@ fn spawn_message_handler(
broom_y, broom_y,
velocity, velocity,
curl, curl,
friction,
}) => { }) => {
let mut room_guard = room.lock().await; let mut room_guard = room.lock().await;
match room_guard match room_guard
.game .game
.process_throw(team, broom_x, broom_y, velocity, curl, friction) .process_throw(team, broom_x, broom_y, velocity, curl)
{ {
Ok(ThrowOutcome { Ok(ThrowOutcome {
trajectories, trajectories,

File diff suppressed because it is too large Load Diff

View File

@ -17,26 +17,45 @@ pub const SHEET_LENGTH: f32 = 45.0;
pub const HOUSE_CENTER: (f32, f32) = (0.0, 38.5); pub const HOUSE_CENTER: (f32, f32) = (0.0, 38.5);
pub const HOUSE_RADIUS: f32 = 6.0 * FEET_TO_METERS; // 12 ft diameter → 6 ft radius pub const HOUSE_RADIUS: f32 = 6.0 * FEET_TO_METERS; // 12 ft diameter → 6 ft radius
pub const HOG_LINE_Y: f32 = 21.0; pub const HOG_LINE_Y: f32 = 21.0;
pub const BACK_LINE_Y: f32 = 42.0; /// Backline touches the outer house ring (tee + 6 ft).
pub const BACK_LINE_Y: f32 = HOUSE_CENTER.1 + HOUSE_RADIUS;
pub const HACK_Y: f32 = 2.0; pub const HACK_Y: f32 = 2.0;
// Stone physical properties // Stone physical properties
pub const STONE_RADIUS: f32 = 0.15; pub const STONE_RADIUS: f32 = 0.15;
pub const STONE_MASS: f32 = 20.0; pub const STONE_MASS: f32 = 20.0;
pub const STONE_FRICTION: f32 = 0.015; pub const STONE_FRICTION: f32 = 0.015;
/// Newton restitution for stonestone contacts (Rapier, Average combine). /// Newton restitution for stonestone contacts (Rapier, Max/Average combiners).
/// Curling granite is nearly elastic on contact; low e makes takeouts feel like
/// putty (both limp together). ~0.9 → both keep going along impact direction.
pub const STONE_RESTITUTION: f32 = 0.9; pub const STONE_RESTITUTION: f32 = 0.9;
/// Soft guard end of the throw slider. Weight 1 → MIN_SPEED.
/// Calibrated with DRAW_VELOCITY so mid-slider (weight 5) lands near the tee. /// Stop offsets (feet relative to tee) for weights 1..=10 and hack.
/// Shared with the frontend; binary sim does not clamp on it (clients send free velocity). /// Positive = past tee toward backline (behind the tee).
pub const WEIGHT_STOP_OFFSET_FT: [f32; 10] =
[-11.0, -9.0, -7.0, -5.0, -3.0, -1.0, 0.0, 1.0, 3.0, 5.0];
pub const HACK_STOP_OFFSET_FT: f32 = 12.0;
/// m/s for weights 1..=10 (calibrated via open-ice stop distance).
pub const WEIGHT_SPEEDS: [f32; 10] = [
2.2573, 2.2783, 2.2992, 2.3199, 2.3404, 2.3608, 2.3709, 2.3810, 2.4012, 2.4211,
];
pub const HACK_SPEED: f32 = 2.4899;
/// Takeouts: open-ice stop at hack+N feet (N = 6,12,18,24).
pub const BOARD_SPEED: f32 = 2.5492;
pub const CONTROL_SPEED: f32 = 2.6408;
pub const NORMAL_SPEED: f32 = 2.7448;
pub const PEEL_SPEED: f32 = 2.8499;
pub const BOARD_STOP_OFFSET_FT: f32 = HACK_STOP_OFFSET_FT + 6.0;
pub const CONTROL_STOP_OFFSET_FT: f32 = HACK_STOP_OFFSET_FT + 15.0;
pub const NORMAL_STOP_OFFSET_FT: f32 = HACK_STOP_OFFSET_FT + 25.0;
pub const PEEL_STOP_OFFSET_FT: f32 = HACK_STOP_OFFSET_FT + 35.0;
/// Tee-line weight (category 7).
pub const DRAW_VELOCITY: f32 = WEIGHT_SPEEDS[6];
/// UI soft end (weight 1).
#[allow(dead_code)] #[allow(dead_code)]
pub const MIN_SPEED: f32 = 1.9; pub const MIN_SPEED: f32 = WEIGHT_SPEEDS[0];
/// Heavy end of the throw slider. Weight 10 → MAX_SPEED. /// UI heavy end of draw weights (weight 10). Peel/hack sit above this.
/// Keep span so weight 5 ≈ DRAW_VELOCITY (2.38).
#[allow(dead_code)] #[allow(dead_code)]
pub const MAX_SPEED: f32 = 3.0; pub const MAX_SPEED: f32 = WEIGHT_SPEEDS[9];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -89,17 +108,12 @@ pub enum ClientMessage {
velocity: f32, velocity: f32,
#[serde(default = "default_curl")] #[serde(default = "default_curl")]
curl: i8, curl: i8,
#[serde(default = "default_friction")]
friction: f32,
}, },
} }
fn default_curl() -> i8 { fn default_curl() -> i8 {
1 1
} }
fn default_friction() -> f32 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EndScore { pub struct EndScore {
@ -185,7 +199,7 @@ mod tests {
#[test] #[test]
fn throw_message_uses_velocity_not_weight() { fn throw_message_uses_velocity_not_weight() {
let json = r#"{"type":"throw","team":"team1","broom_x":0.5,"broom_y":38.5,"velocity":4.2,"curl":1,"friction":1.0}"#; let json = r#"{"type":"throw","team":"team1","broom_x":0.5,"broom_y":38.5,"velocity":4.2,"curl":1}"#;
let msg: ClientMessage = serde_json::from_str(json).unwrap(); let msg: ClientMessage = serde_json::from_str(json).unwrap();
match msg { match msg {
ClientMessage::Throw { ClientMessage::Throw {