feat(physics): µ(v) friction, pure velocity, open boundaries
Replace weight-scaled rapier damping with pure initial velocity (m/s), piecewise-linear ice µ(v) table, and post-step a=−µ_eff·g·unit(v) drag. Remove left/right/back wall colliders; OOB stones leave via prune only. game.rs maps legacy weight→velocity via MIN/MAX until B3. Calibrated DRAW_VELOCITY=2.38 m/s lands near tee (±0.8 m). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
473e18a751
commit
72ffa15489
@ -77,7 +77,13 @@ impl Game {
|
||||
}
|
||||
|
||||
self.active_stones.clear();
|
||||
let trajectory = self.physics.throw(self.turn_team, broom_x, broom_y, weight, curl, friction)?;
|
||||
// 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;
|
||||
|
||||
|
||||
@ -2,14 +2,50 @@ use rapier2d::prelude::*;
|
||||
|
||||
use crate::protocol::*;
|
||||
|
||||
const LINEAR_DAMPING: f32 = 0.142;
|
||||
const ANGULAR_DAMPING: f32 = 0.18;
|
||||
const MAX_SIM_TIME: f32 = 30.0;
|
||||
const REST_SPEED: f32 = 0.04;
|
||||
const REST_ANGULAR_SPEED: f32 = 0.05;
|
||||
// Rotation rate of the velocity vector, in rad/s.
|
||||
// Positive curl_sign = right curl -> curves toward +x when moving up-sheet.
|
||||
const CURL_RATE: f32 = 0.010;
|
||||
const G: f32 = 9.80665;
|
||||
|
||||
/// Calibrated initial speed (m/s) for a mid draw that stops near the tee line
|
||||
/// with friction_scalar = 1.0, curl = 0, broom aimed at HOUSE_CENTER.
|
||||
pub const DRAW_VELOCITY: f32 = 2.38;
|
||||
|
||||
/// Ice friction coefficient µ as a function of speed (m/s).
|
||||
/// Piecewise-linear interpolation of the binding table; µ(v≥2.5) = 0.0081.
|
||||
pub fn mu(v: f32) -> f32 {
|
||||
// Binding µ(v) table (v_m/s, µ)
|
||||
const KNOTS: [(f32, f32); 7] = [
|
||||
(0.0, 0.016),
|
||||
(0.1482, 0.014),
|
||||
(0.3005, 0.0116),
|
||||
(0.4486, 0.0098),
|
||||
(0.7371, 0.0079),
|
||||
(1.0098, 0.0073),
|
||||
(2.5, 0.0081),
|
||||
];
|
||||
|
||||
let speed = v.abs();
|
||||
if speed >= 2.5 {
|
||||
return 0.0081;
|
||||
}
|
||||
for i in 0..KNOTS.len() - 1 {
|
||||
let (v0, mu0) = KNOTS[i];
|
||||
let (v1, mu1) = KNOTS[i + 1];
|
||||
if speed >= v0 && speed <= v1 {
|
||||
let t = if (v1 - v0).abs() < f32::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(speed - v0) / (v1 - v0)
|
||||
};
|
||||
return mu0 + t * (mu1 - mu0);
|
||||
}
|
||||
}
|
||||
0.016
|
||||
}
|
||||
|
||||
pub struct PhysicsWorld {
|
||||
gravity: Vector,
|
||||
@ -24,7 +60,8 @@ pub struct PhysicsWorld {
|
||||
multibody_joints: MultibodyJointSet,
|
||||
ccd_solver: CCDSolver,
|
||||
next_stone_id: u32,
|
||||
stone_handles: Vec<(u32, RigidBodyHandle, Team, i8)>,
|
||||
/// (id, handle, team, curl_sign, friction_scalar)
|
||||
stone_handles: Vec<(u32, RigidBodyHandle, Team, i8, f32)>,
|
||||
}
|
||||
|
||||
impl Default for PhysicsWorld {
|
||||
@ -75,52 +112,39 @@ impl PhysicsWorld {
|
||||
self.next_stone_id = 1;
|
||||
}
|
||||
|
||||
/// No wall colliders: stones leave play via prune_out_of_play only.
|
||||
fn build_sheet(&mut self) {
|
||||
let half = SHEET_WIDTH / 2.0 + 0.1;
|
||||
let left = ColliderBuilder::cuboid(0.1, SHEET_LENGTH / 2.0 + 1.0)
|
||||
.translation(Vector::new(-half, SHEET_LENGTH / 2.0))
|
||||
.friction(0.0)
|
||||
.restitution(0.0)
|
||||
.build();
|
||||
self.colliders.insert(left);
|
||||
|
||||
let right = ColliderBuilder::cuboid(0.1, SHEET_LENGTH / 2.0 + 1.0)
|
||||
.translation(Vector::new(half, SHEET_LENGTH / 2.0))
|
||||
.friction(0.0)
|
||||
.restitution(0.0)
|
||||
.build();
|
||||
self.colliders.insert(right);
|
||||
|
||||
let back = ColliderBuilder::cuboid(SHEET_WIDTH / 2.0 + 1.0, 0.1)
|
||||
.translation(Vector::new(0.0, SHEET_LENGTH + 0.1))
|
||||
.friction(0.0)
|
||||
.restitution(0.1)
|
||||
.build();
|
||||
self.colliders.insert(back);
|
||||
// Intentionally empty — open boundaries (no left/right/back bounce).
|
||||
}
|
||||
|
||||
/// Throw a stone with pure initial velocity (m/s).
|
||||
/// `friction_scalar` is clamped to 0.5..=1.5 and multiplies µ.
|
||||
pub fn throw(
|
||||
&mut self,
|
||||
team: Team,
|
||||
broom_x: f32,
|
||||
broom_y: f32,
|
||||
weight: u8,
|
||||
velocity: f32,
|
||||
curl: i8,
|
||||
friction: f32,
|
||||
friction_scalar: f32,
|
||||
) -> Result<Vec<StoneTrajectory>, String> {
|
||||
let weight = weight.clamp(1, 10) as f32;
|
||||
let t = (weight - 1.0) / 9.0;
|
||||
let speed = MIN_SPEED + t * (MAX_SPEED - MIN_SPEED);
|
||||
let speed = velocity.max(0.0);
|
||||
let dx = broom_x;
|
||||
let dy = broom_y - HACK_Y;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(0.01);
|
||||
let vx = dx / len * speed;
|
||||
let vy = dy / len * speed;
|
||||
|
||||
let curl_sign = if curl < 0 { -1 } else { 1 };
|
||||
let damping_mult = friction.clamp(0.5, 2.0);
|
||||
let curl_sign = if curl < 0 {
|
||||
-1
|
||||
} else if curl > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let friction_scalar = friction_scalar.clamp(0.5, 1.5);
|
||||
|
||||
self.spawn_stone(team, 0.0, HACK_Y, vx, vy, curl_sign, damping_mult)
|
||||
self.spawn_stone(team, 0.0, HACK_Y, vx, vy, curl_sign, friction_scalar)
|
||||
}
|
||||
|
||||
fn spawn_stone(
|
||||
@ -131,7 +155,7 @@ impl PhysicsWorld {
|
||||
vx: f32,
|
||||
vy: f32,
|
||||
curl_sign: i8,
|
||||
damping_mult: f32,
|
||||
friction_scalar: f32,
|
||||
) -> Result<Vec<StoneTrajectory>, String> {
|
||||
let id = self.next_stone_id;
|
||||
self.next_stone_id += 1;
|
||||
@ -140,8 +164,8 @@ impl PhysicsWorld {
|
||||
.translation(Vector::new(x, y))
|
||||
.linvel(Vector::new(vx, vy))
|
||||
.angvel(0.0)
|
||||
.linear_damping(LINEAR_DAMPING * damping_mult)
|
||||
.angular_damping(ANGULAR_DAMPING)
|
||||
.linear_damping(0.0)
|
||||
.angular_damping(0.0)
|
||||
.can_sleep(false)
|
||||
.build();
|
||||
|
||||
@ -155,8 +179,10 @@ impl PhysicsWorld {
|
||||
.density(STONE_MASS / (std::f32::consts::PI * STONE_RADIUS * STONE_RADIUS))
|
||||
.build();
|
||||
|
||||
self.colliders.insert_with_parent(collider, handle, &mut self.bodies);
|
||||
self.stone_handles.push((id, handle, team, curl_sign));
|
||||
self.colliders
|
||||
.insert_with_parent(collider, handle, &mut self.bodies);
|
||||
self.stone_handles
|
||||
.push((id, handle, team, curl_sign, friction_scalar));
|
||||
|
||||
self.simulate_until_rest(id)
|
||||
}
|
||||
@ -174,7 +200,7 @@ impl PhysicsWorld {
|
||||
let mut paths: Vec<(u32, RigidBodyHandle, Vec<(f32, f32, f32)>)> = self
|
||||
.stone_handles
|
||||
.iter()
|
||||
.map(|(id, handle, _, _)| (*id, *handle, Vec::new()))
|
||||
.map(|(id, handle, _, _, _)| (*id, *handle, Vec::new()))
|
||||
.collect();
|
||||
|
||||
// Record the initial sample at t=0 for every stone.
|
||||
@ -190,6 +216,7 @@ impl PhysicsWorld {
|
||||
|
||||
loop {
|
||||
self.step();
|
||||
self.apply_ice_friction();
|
||||
self.apply_curl();
|
||||
time += PHYSICS_DT;
|
||||
sample_accum += PHYSICS_DT;
|
||||
@ -234,10 +261,38 @@ impl PhysicsWorld {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Apply a = −µ_eff * g * unit(v) after each physics step.
|
||||
/// µ_eff = mu(|v|) * friction_scalar. If velocity would reverse, stop.
|
||||
fn apply_ice_friction(&mut self) {
|
||||
for (_, handle, _, _, friction_scalar) in &self.stone_handles {
|
||||
let body = match self.bodies.get_mut(*handle) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let v = body.linvel();
|
||||
let speed = (v.x * v.x + v.y * v.y).sqrt();
|
||||
if speed < 1e-6 {
|
||||
body.set_linvel(Vector::new(0.0, 0.0), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mu_eff = mu(speed) * *friction_scalar;
|
||||
let a = mu_eff * G;
|
||||
let dv = a * PHYSICS_DT;
|
||||
if dv >= speed {
|
||||
body.set_linvel(Vector::new(0.0, 0.0), true);
|
||||
} else {
|
||||
let scale = (speed - dv) / speed;
|
||||
body.set_linvel(Vector::new(v.x * scale, v.y * scale), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate each stone's velocity slightly based on its selected curl direction.
|
||||
// Right curl (curl_sign = +1) curves toward +x when moving up-sheet (positive y).
|
||||
fn apply_curl(&mut self) {
|
||||
for (_, handle, _, curl_sign) in &self.stone_handles {
|
||||
for (_, handle, _, curl_sign, _) in &self.stone_handles {
|
||||
let body = match self.bodies.get_mut(*handle) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
@ -260,16 +315,23 @@ impl PhysicsWorld {
|
||||
|
||||
fn prune_out_of_play(&mut self) {
|
||||
let mut keep = Vec::new();
|
||||
for (id, handle, team, curl) in self.stone_handles.drain(..) {
|
||||
for (id, handle, team, curl, friction_scalar) in self.stone_handles.drain(..) {
|
||||
if let Some(body) = self.bodies.get(handle) {
|
||||
let pos = body.translation();
|
||||
let beyond_back = pos.y > BACK_LINE_Y;
|
||||
let short_of_hog = pos.y < HOG_LINE_Y;
|
||||
let outside = pos.x.abs() > SHEET_WIDTH / 2.0;
|
||||
if beyond_back || short_of_hog || outside {
|
||||
self.bodies.remove(handle, &mut self.islands, &mut self.colliders, &mut self.impulse_joints, &mut self.multibody_joints, true);
|
||||
self.bodies.remove(
|
||||
handle,
|
||||
&mut self.islands,
|
||||
&mut self.colliders,
|
||||
&mut self.impulse_joints,
|
||||
&mut self.multibody_joints,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
keep.push((id, handle, team, curl));
|
||||
keep.push((id, handle, team, curl, friction_scalar));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,7 +356,7 @@ impl PhysicsWorld {
|
||||
}
|
||||
|
||||
fn all_stones_at_rest(&self) -> bool {
|
||||
for (_, handle, _, _) in &self.stone_handles {
|
||||
for (_, handle, _, _, _) in &self.stone_handles {
|
||||
if let Some(body) = self.bodies.get(*handle) {
|
||||
let v = body.linvel();
|
||||
let speed = (v.x * v.x + v.y * v.y).sqrt();
|
||||
@ -308,7 +370,7 @@ impl PhysicsWorld {
|
||||
|
||||
pub fn current_stones(&self) -> Vec<StoneState> {
|
||||
let mut states = Vec::new();
|
||||
for (id, handle, team, _) in &self.stone_handles {
|
||||
for (id, handle, team, _, _) in &self.stone_handles {
|
||||
if let Some(body) = self.bodies.get(*handle) {
|
||||
let pos = body.translation();
|
||||
states.push(StoneState {
|
||||
@ -326,7 +388,7 @@ impl PhysicsWorld {
|
||||
|
||||
pub fn stone_states_for_scoring(&self) -> Vec<(u32, Team, f32, f32)> {
|
||||
let mut out = Vec::new();
|
||||
for (id, handle, team, _) in &self.stone_handles {
|
||||
for (id, handle, team, _, _) in &self.stone_handles {
|
||||
if let Some(body) = self.bodies.get(*handle) {
|
||||
let pos = body.translation();
|
||||
out.push((*id, *team, pos.x, pos.y));
|
||||
@ -341,9 +403,11 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn final_y(world: &PhysicsWorld, id: u32) -> f32 {
|
||||
world.stone_handles.iter()
|
||||
.find(|(sid, _, _, _)| *sid == id)
|
||||
.map(|(_, h, _, _)| {
|
||||
world
|
||||
.stone_handles
|
||||
.iter()
|
||||
.find(|(sid, _, _, _, _)| *sid == id)
|
||||
.map(|(_, h, _, _, _)| {
|
||||
let b = &world.bodies[*h];
|
||||
b.translation().y
|
||||
})
|
||||
@ -351,9 +415,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn final_x(world: &PhysicsWorld, id: u32) -> f32 {
|
||||
world.stone_handles.iter()
|
||||
.find(|(sid, _, _, _)| *sid == id)
|
||||
.map(|(_, h, _, _)| {
|
||||
world
|
||||
.stone_handles
|
||||
.iter()
|
||||
.find(|(sid, _, _, _, _)| *sid == id)
|
||||
.map(|(_, h, _, _, _)| {
|
||||
let b = &world.bodies[*h];
|
||||
b.translation().x
|
||||
})
|
||||
@ -361,30 +427,95 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weight_7_lands_on_tee_line() {
|
||||
fn mu_at_rest_is_0_016() {
|
||||
assert!((mu(0.0) - 0.016).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mu_interpolates_between_knots() {
|
||||
// Midpoint between 0.1482 (0.014) and 0.3005 (0.0116)
|
||||
let v = (0.1482 + 0.3005) / 2.0;
|
||||
let expected = (0.014 + 0.0116) / 2.0;
|
||||
let got = mu(v);
|
||||
assert!(
|
||||
(got - expected).abs() < 1e-5,
|
||||
"mu({}) = {}, expected ~{}",
|
||||
v,
|
||||
got,
|
||||
expected
|
||||
);
|
||||
// High-speed plateau
|
||||
assert!((mu(2.5) - 0.0081).abs() < 1e-6);
|
||||
assert!((mu(5.0) - 0.0081).abs() < 1e-6);
|
||||
// Exact knot
|
||||
assert!((mu(1.0098) - 0.0073).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn velocity_draw_lands_on_tee_line() {
|
||||
let mut world = PhysicsWorld::new();
|
||||
world.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 1, 1.0).unwrap();
|
||||
// curl=0 so lateral drift does not push the stone OOB before rest.
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
let id = world.next_stone_id - 1;
|
||||
let y = final_y(&world, id);
|
||||
println!("weight 7 final y={}", y);
|
||||
println!("DRAW_VELOCITY={} final y={}", DRAW_VELOCITY, y);
|
||||
assert!(
|
||||
(y - HOUSE_CENTER.1).abs() <= 0.5,
|
||||
"weight-7 draw shot should finish on the tee line, got y={}",
|
||||
y
|
||||
(y - HOUSE_CENTER.1).abs() <= 0.8,
|
||||
"draw shot should finish near tee line, got y={} (tee={})",
|
||||
y,
|
||||
HOUSE_CENTER.1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_friction_or_low_v_prunes_before_hog() {
|
||||
// Low velocity + high friction_scalar ⇒ short of hog, pruned.
|
||||
let mut world = PhysicsWorld::new();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 1.0, 0, 1.5)
|
||||
.unwrap();
|
||||
let stones = world.current_stones();
|
||||
assert!(
|
||||
stones.is_empty(),
|
||||
"low-v high-friction throw should be pruned short of hog"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sideline_aim_goes_out_not_bounce() {
|
||||
// Aim so the stone crosses |x| > SHEET_WIDTH/2; with open boundaries it
|
||||
// must be pruned (not bounce off a wall and remain in play).
|
||||
let mut world = PhysicsWorld::new();
|
||||
world
|
||||
.throw(Team::Red, 4.0, 15.0, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
let stones = world.current_stones();
|
||||
assert!(
|
||||
stones.is_empty(),
|
||||
"sideline-bound stone should be pruned, not bounce; remaining={:?}",
|
||||
stones
|
||||
.iter()
|
||||
.map(|s| (s.x, s.y))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curl_direction_mirrors_x_offset() {
|
||||
// Use trajectory last sample (pre-prune): strong curl can exit the sheet.
|
||||
let mut right = PhysicsWorld::new();
|
||||
right.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 1, 1.0).unwrap();
|
||||
let right_id = right.next_stone_id - 1;
|
||||
let right_x = final_x(&right, right_id);
|
||||
let right_traj = right
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 1, 1.0)
|
||||
.unwrap();
|
||||
let right_x = right_traj[0].path.last().map(|p| p.0).unwrap_or(f32::NAN);
|
||||
|
||||
let mut left = PhysicsWorld::new();
|
||||
left.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, -1, 1.0).unwrap();
|
||||
let left_id = left.next_stone_id - 1;
|
||||
let left_x = final_x(&left, left_id);
|
||||
let left_traj = left
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, -1, 1.0)
|
||||
.unwrap();
|
||||
let left_x = left_traj[0].path.last().map(|p| p.0).unwrap_or(f32::NAN);
|
||||
|
||||
println!("right curl final x={} left curl final x={}", right_x, left_x);
|
||||
assert!(
|
||||
@ -398,8 +529,13 @@ mod tests {
|
||||
#[test]
|
||||
fn stones_persist_after_multiple_throws() {
|
||||
let mut world = PhysicsWorld::new();
|
||||
world.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 1, 1.0).unwrap();
|
||||
world.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, -1, 1.0).unwrap();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
// Slight lateral aim so stones don't stack identically; still in-bounds.
|
||||
world
|
||||
.throw(Team::Red, 0.3, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
let stones = world.current_stones();
|
||||
assert_eq!(stones.len(), 2, "both stones should remain in the physics world");
|
||||
@ -409,9 +545,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn out_of_play_stone_is_pruned() {
|
||||
// A very light, high-friction throw should stop short of the hog line and be removed.
|
||||
// A very slow, high-friction throw should stop short of the hog line and be removed.
|
||||
let mut world = PhysicsWorld::new();
|
||||
world.throw(Team::Red, 0.0, HOUSE_CENTER.1, 1, 0, 2.0).unwrap();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 0.8, 0, 1.5)
|
||||
.unwrap();
|
||||
let stones = world.current_stones();
|
||||
assert!(stones.is_empty(), "stones short of the hog line should be pruned");
|
||||
}
|
||||
@ -420,32 +558,31 @@ mod tests {
|
||||
fn collision_records_trajectories_for_both_stones() {
|
||||
// Place a stationary stone on the center line and throw a second stone
|
||||
// straight at it so they collide. Both stones must have sampled paths.
|
||||
let mut world = PhysicsWorld::new();
|
||||
let takeout_v = DRAW_VELOCITY * 1.4;
|
||||
|
||||
// First stone: place it far enough up-sheet to stay in play after impact.
|
||||
let mut world = PhysicsWorld::new();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 0, 1.0)
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
let first_id = world.next_stone_id - 1;
|
||||
|
||||
// Second stone: aimed directly at the first stone's final position.
|
||||
let target_y = final_y(&world, first_id);
|
||||
let target_x = final_x(&world, first_id);
|
||||
world
|
||||
.throw(Team::Yellow, target_x, target_y, 10, 0, 1.0)
|
||||
.throw(Team::Yellow, target_x, target_y, takeout_v, 0, 1.0)
|
||||
.unwrap();
|
||||
let second_id = world.next_stone_id - 1;
|
||||
|
||||
// Re-run the collision throw and capture trajectories.
|
||||
let mut world = PhysicsWorld::new();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 0, 1.0)
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, DRAW_VELOCITY, 0, 1.0)
|
||||
.unwrap();
|
||||
let first_id = world.next_stone_id - 1;
|
||||
let target_y = final_y(&world, first_id);
|
||||
let target_x = final_x(&world, first_id);
|
||||
let trajectories = world
|
||||
.throw(Team::Yellow, target_x, target_y, 10, 0, 1.0)
|
||||
.throw(Team::Yellow, target_x, target_y, takeout_v, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
let by_id: std::collections::HashMap<u32, Vec<(f32, f32, f32)>> = trajectories
|
||||
@ -479,6 +616,9 @@ mod tests {
|
||||
|
||||
// Both paths should share the same t=0 reference (the thrown stone's release).
|
||||
assert_eq!(first_path[0].2, 0.0, "first stone path should start at t=0");
|
||||
assert_eq!(second_path[0].2, 0.0, "thrown stone path should start at t=0");
|
||||
assert_eq!(
|
||||
second_path[0].2, 0.0,
|
||||
"thrown stone path should start at t=0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user