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,
}
}
struct ResolverSim {
tracker: PossessionTracker,
resolved: Vec<ResolvedPossession>,
}
impl ResolverSim {
fn new() -> Self {
let mut tracker = PossessionTracker::default();
tracker.begin_resolver(&frame_info(0, 0.0));
Self {
tracker,
resolved: Vec::new(),
}
}
fn step(&mut self, frame: usize, time: f32, touches: &[TouchEvent]) {
let state = self.tracker.update(&frame_info(frame, time), touches);
self.resolved.extend(state.newly_resolved);
}
fn finish(&mut self, frame: usize, time: f32) {
self.resolved
.extend(self.tracker.flush_resolver(&frame_info(frame, time)));
}
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()
}
}
fn p(id: u64) -> PlayerId {
PlayerId::Steam(id)
}
#[test]
fn single_kickoff_touch_then_lost_credits_no_possession_to_first_toucher() {
let mut sim = ResolverSim::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(25, 2.5, &[touch(25, 2.5, p(2), false)]); sim.finish(40, 4.0);
assert_eq!(sim.team_segments(), vec![(false, 2.0, 2.5)]);
}
#[test]
fn brief_control_with_no_follow_up_is_backdated_to_last_touch() {
let mut sim = ResolverSim::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(90, 3.0, &[]); sim.finish(120, 4.0);
assert_eq!(sim.team_segments(), vec![(true, 1.0, 1.3)]);
}
#[test]
fn sustained_dribble_is_one_continuous_segment() {
let mut sim = ResolverSim::new();
for (frame, time) in [
(10, 1.0),
(13, 1.3),
(16, 1.6),
(19, 1.9),
(22, 2.2),
(25, 2.5),
] {
sim.step(frame, time, &[touch(frame, time, p(1), true)]);
}
sim.finish(40, 4.0);
assert_eq!(sim.team_segments(), vec![(true, 1.0, 2.5)]);
}
#[test]
fn unconfirmed_opponent_clear_grants_nothing_and_keeps_holder_continuous() {
let mut sim = ResolverSim::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(18, 1.8, &[touch(18, 1.8, 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, 2.3)]);
}
#[test]
fn confirmed_turnover_credits_winner_from_first_touch_with_neutral_gap() {
let mut sim = ResolverSim::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.resolved
.iter()
.map(|s| (s.label, s.start_time, s.end_time))
.collect::<Vec<_>>(),
vec![
(PossessionLabel::Neutral, 0.0, 1.0),
(PossessionLabel::TeamZero, 1.0, 1.3),
(PossessionLabel::Neutral, 1.3, 2.0),
(PossessionLabel::TeamOne, 2.0, 2.4),
(PossessionLabel::Neutral, 2.4, 4.0),
]
);
}
#[test]
fn tracker_uses_latest_touch_player_for_team_independent_of_slice_order() {
let earlier_player = PlayerId::Steam(1);
let later_player = PlayerId::Steam(2);
let mut tracker = PossessionTracker::default();
let later_touch = touch(20, 2.0, later_player.clone(), true);
let earlier_touch = touch(10, 1.0, earlier_player, true);
let state = tracker.update(&frame_info(20, 2.0), &[later_touch, earlier_touch]);
assert_eq!(state.current_team_is_team_0, Some(true));
assert_eq!(state.current_player, Some(later_player));
}