use super::*;
fn frame_info(frame_number: usize, time: f32) -> FrameInfo {
FrameInfo {
frame_number,
time,
dt: 1.0 / 30.0,
seconds_remaining: None,
}
}
fn touch(frame: usize, time: f32, player: PlayerId, team_is_team_0: bool) -> TouchEvent {
TouchEvent {
touch_id: None,
time,
frame,
team_is_team_0,
player: Some(player),
player_position: None,
closest_approach_distance: Some(0.0),
contact_local_ball_position: None,
contact_local_hitbox_point: None,
contact_world_hitbox_point: None,
dodge_contact: false,
}
}
fn p(id: u64) -> PlayerId {
PlayerId::Steam(id)
}
struct LooseSim {
resolver: LoosePossessionResolver,
resolved: Vec<ResolvedPossession>,
}
impl LooseSim {
fn new() -> Self {
let mut resolver = LoosePossessionResolver::default();
resolver.begin(&frame_info(0, 0.0));
Self {
resolver,
resolved: Vec::new(),
}
}
fn step(&mut self, frame: usize, time: f32, touches: &[TouchEvent]) {
let touched_team_zero = touches.iter().any(|t| t.team_is_team_0);
let touched_team_one = touches.iter().any(|t| !t.team_is_team_0);
let team_zero_player = touches
.iter()
.rfind(|t| t.team_is_team_0)
.and_then(|t| t.player.clone());
let team_one_player = touches
.iter()
.rfind(|t| !t.team_is_team_0)
.and_then(|t| t.player.clone());
self.resolver.update(
&frame_info(frame, time),
&team_zero_player,
&team_one_player,
touched_team_zero,
touched_team_one,
);
self.resolved
.extend(std::mem::take(&mut self.resolver.newly_resolved));
}
fn finish(&mut self, frame: usize, time: f32) {
self.resolver.flush(time, frame);
self.resolved
.extend(std::mem::take(&mut self.resolver.newly_resolved));
}
fn team_segments(&self) -> Vec<(bool, f32, f32)> {
self.resolved
.iter()
.filter_map(|segment| match segment.label {
PossessionLabel::TeamZero => Some((true, segment.start_time, segment.end_time)),
PossessionLabel::TeamOne => Some((false, segment.start_time, segment.end_time)),
PossessionLabel::Neutral => None,
})
.collect()
}
}
#[test]
fn single_touch_grants_loose_possession_until_opponent_takes_over() {
let mut sim = LooseSim::new();
sim.step(10, 1.0, &[touch(10, 1.0, p(1), true)]); sim.step(13, 1.3, &[touch(13, 1.3, p(1), true)]); sim.step(20, 2.0, &[touch(20, 2.0, p(2), false)]); sim.step(24, 2.4, &[touch(24, 2.4, p(2), false)]); sim.finish(40, 4.0);
assert_eq!(
sim.team_segments(),
vec![(true, 1.0, 2.0), (false, 2.0, 4.0)]
);
}
#[test]
fn lone_touch_then_loose_stays_with_last_toucher() {
let mut sim = LooseSim::new();
sim.step(10, 1.0, &[touch(10, 1.0, p(1), true)]);
sim.step(90, 3.0, &[]); sim.finish(120, 4.0);
assert_eq!(sim.team_segments(), vec![(true, 1.0, 4.0)]);
}
#[test]
fn repelled_poke_keeps_holder_continuous() {
let mut sim = LooseSim::new();
sim.step(10, 1.0, &[touch(10, 1.0, p(1), true)]);
sim.step(13, 1.3, &[touch(13, 1.3, p(1), true)]);
sim.step(20, 2.0, &[touch(20, 2.0, p(2), false)]); sim.step(23, 2.3, &[touch(23, 2.3, p(1), true)]); sim.finish(40, 4.0);
assert_eq!(sim.team_segments(), vec![(true, 1.0, 4.0)]);
}
#[test]
fn unfollowed_challenge_turns_over_to_challenger() {
let mut sim = LooseSim::new();
sim.step(10, 1.0, &[touch(10, 1.0, p(1), true)]);
sim.step(20, 2.0, &[touch(20, 2.0, p(2), false)]); sim.step(80, 3.7, &[]); sim.finish(120, 4.0);
assert_eq!(
sim.team_segments(),
vec![(true, 1.0, 2.0), (false, 2.0, 4.0)]
);
}
#[test]
fn loose_possession_covers_the_whole_live_stretch_except_the_lead_in() {
let mut sim = LooseSim::new();
sim.step(10, 1.0, &[touch(10, 1.0, p(1), true)]);
sim.step(20, 2.0, &[touch(20, 2.0, p(2), false)]);
sim.step(24, 2.4, &[touch(24, 2.4, p(2), false)]);
sim.finish(40, 4.0);
let neutral: f32 = sim
.resolved
.iter()
.filter(|s| s.label == PossessionLabel::Neutral)
.map(|s| s.end_time - s.start_time)
.sum();
let credited: f32 = sim
.team_segments()
.iter()
.map(|(_, start, end)| end - start)
.sum();
assert!(
(neutral - 1.0).abs() < 1e-6,
"neutral lead-in should be 1.0s"
);
assert!(
(credited - 3.0).abs() < 1e-6,
"rest of the stretch is credited"
);
}