diff --git a/brumby-soccer/src/interval.rs b/brumby-soccer/src/interval.rs index 6deba34..7e8d9e0 100644 --- a/brumby-soccer/src/interval.rs +++ b/brumby-soccer/src/interval.rs @@ -6,6 +6,7 @@ use brumby::hash_lookup::HashLookup; use crate::domain::{Player, Score, Side}; +pub mod assist; pub mod query; #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -223,7 +224,7 @@ pub fn explore(config: &IntervalConfig, include_intervals: Range) -> Explora } } player_lookup.push(Player::Other); - let other_player_index = config.player_probs.len(); + // let other_player_index = config.player_probs.len(); home_scorers.push(( config.player_probs.len(), 1.0 - combined_home_player_goal_prob, @@ -281,129 +282,131 @@ pub fn explore(config: &IntervalConfig, include_intervals: Range) -> Explora if current_prospect.ft_score.total() < config.prune_thresholds.max_total_goals { // only the home team scores for (scorer_index, player_score_prob) in &home_scorers { - let mut remaining_player_assist_prob = 1.0; - for (assister_index, player_assist_prob) in &home_assisters { - if *scorer_index != other_player_index && assister_index == scorer_index { - continue; - } - let player_assist_prob = if assister_index == &other_player_index { - remaining_player_assist_prob - } else { - remaining_player_assist_prob -= player_assist_prob; - *player_assist_prob - }; - - for (assister, assist_prob) in [ - (Some(*assister_index), config.team_probs.assists.home * player_assist_prob), - (None, (1.0 - config.team_probs.assists.home) * player_assist_prob) - ] { - if assist_prob == 0.0 { - continue; - } - merge( - &config.expansions, - &half, - ¤t_prospect, - current_prob, - PartialProspect { - home_scorer: Some(*scorer_index), - away_scorer: None, - home_assister: assister, - away_assister: None, - first_scoring_side: Some(&Side::Home), - prob: params.home - * player_score_prob - * assist_prob, - }, - &mut next_prospects, - ); - } + for (assister, player_assist_prob) in assist::Iter::new( + config.team_probs.assists.home, + &home_assisters, + *scorer_index, + ) { + merge( + &config.expansions, + &half, + ¤t_prospect, + current_prob, + PartialProspect { + home_scorer: Some(*scorer_index), + away_scorer: None, + home_assister: assister, + away_assister: None, + first_scoring_side: Some(&Side::Home), + prob: params.home * player_score_prob * player_assist_prob, + }, + &mut next_prospects, + ); } - - // // without assist - // if config.team_probs.assists.home != 1.0 { - // let partial = PartialProspect { - // home_scorer: Some(*scorer_index), - // away_scorer: None, - // home_assister: None, - // away_assister: None, - // first_scoring_side: Some(&Side::Home), - // prob: params.home - // * player_score_prob - // * (1.0 - config.team_probs.assists.home), + // let mut remaining_player_assist_prob = 1.0; + // for (assister_index, player_assist_prob) in &home_assisters { + // if *scorer_index != other_player_index && assister_index == scorer_index { + // continue; + // } + // let player_assist_prob = if assister_index == &other_player_index { + // remaining_player_assist_prob + // } else { + // remaining_player_assist_prob -= player_assist_prob; + // *player_assist_prob // }; - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // partial, - // &mut next_prospects, - // ); + // + // for (assister, assist_prob) in [ + // (Some(*assister_index), config.team_probs.assists.home * player_assist_prob), + // (None, (1.0 - config.team_probs.assists.home) * player_assist_prob) + // ] { + // if assist_prob == 0.0 { + // continue; + // } + // merge( + // &config.expansions, + // &half, + // ¤t_prospect, + // current_prob, + // PartialProspect { + // home_scorer: Some(*scorer_index), + // away_scorer: None, + // home_assister: assister, + // away_assister: None, + // first_scoring_side: Some(&Side::Home), + // prob: params.home + // * player_score_prob + // * assist_prob, + // }, + // &mut next_prospects, + // ); + // } // } } // only the away team scores for (scorer_index, player_score_prob) in &away_scorers { - let mut remaining_player_assist_prob = 1.0; - for (assister_index, player_assist_prob) in &away_assisters { - if *scorer_index != other_player_index && assister_index == scorer_index { - continue; - } - let player_assist_prob = if assister_index == &other_player_index { - remaining_player_assist_prob - } else { - remaining_player_assist_prob -= player_assist_prob; - *player_assist_prob - }; - - for (assister, assist_prob) in [ - (Some(*assister_index), config.team_probs.assists.away * player_assist_prob), - (None, (1.0 - config.team_probs.assists.away) * player_assist_prob) - ] { - if assist_prob == 0.0 { - continue; - } - merge( - &config.expansions, - &half, - ¤t_prospect, - current_prob, - PartialProspect { - home_scorer: None, - away_scorer: Some(*scorer_index), - home_assister: None, - away_assister: assister, - first_scoring_side: Some(&Side::Away), - prob: params.away - * player_score_prob - * assist_prob, - }, - &mut next_prospects, - ); - } + for (assister, player_assist_prob) in assist::Iter::new( + config.team_probs.assists.away, + &away_assisters, + *scorer_index, + ) { + merge( + &config.expansions, + &half, + ¤t_prospect, + current_prob, + PartialProspect { + home_scorer: None, + away_scorer: Some(*scorer_index), + home_assister: None, + away_assister: assister, + first_scoring_side: Some(&Side::Away), + prob: params.away * player_score_prob * player_assist_prob, + }, + &mut next_prospects, + ); } - - // // without assist - // if config.team_probs.assists.away != 1.0 { - // let partial = PartialProspect { - // home_scorer: None, - // away_scorer: Some(*scorer_index), - // home_assister: None, - // away_assister: None, - // first_scoring_side: Some(&Side::Away), - // prob: params.away - // * player_score_prob - // * (1.0 - config.team_probs.assists.away), + // let mut remaining_player_assist_prob = 1.0; + // for (assister_index, player_assist_prob) in &away_assisters { + // if *scorer_index != other_player_index && assister_index == scorer_index { + // continue; + // } + // let player_assist_prob = if assister_index == &other_player_index { + // remaining_player_assist_prob + // } else { + // remaining_player_assist_prob -= player_assist_prob; + // *player_assist_prob // }; - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // partial, - // &mut next_prospects, - // ); + // + // for (assister, assist_prob) in [ + // ( + // Some(*assister_index), + // config.team_probs.assists.away * player_assist_prob, + // ), + // ( + // None, + // (1.0 - config.team_probs.assists.away) * player_assist_prob, + // ), + // ] { + // if assist_prob == 0.0 { + // continue; + // } + // merge( + // &config.expansions, + // &half, + // ¤t_prospect, + // current_prob, + // PartialProspect { + // home_scorer: None, + // away_scorer: Some(*scorer_index), + // home_assister: None, + // away_assister: assister, + // first_scoring_side: Some(&Side::Away), + // prob: params.away * player_score_prob * assist_prob, + // }, + // &mut next_prospects, + // ); + // } // } } } else { @@ -416,244 +419,133 @@ pub fn explore(config: &IntervalConfig, include_intervals: Range) -> Explora for (home_scorer_index, home_player_score_prob) in &home_scorers { for (away_scorer_index, away_player_score_prob) in &away_scorers { for first_scoring_side in [&Side::Home, &Side::Away] { - let mut remaining_home_player_assist_prob = 1.0; - for (home_assister_index, home_player_assist_prob) in &home_assisters { - if *home_scorer_index != other_player_index - && home_assister_index == home_scorer_index - { - continue; - } - let home_player_assist_prob = if home_assister_index - == &other_player_index - { - remaining_home_player_assist_prob - } else { - remaining_home_player_assist_prob -= home_player_assist_prob; - *home_player_assist_prob - }; - - // with assist - let mut remaining_away_player_assist_prob = 1.0; - for (away_assister_index, away_player_assist_prob) in - &away_assisters - { - if *away_scorer_index != other_player_index - && away_assister_index == away_scorer_index - { - continue; - } - let away_player_assist_prob = - if away_assister_index == &other_player_index { - remaining_away_player_assist_prob - } else { - remaining_away_player_assist_prob -= - away_player_assist_prob; - *away_player_assist_prob - }; - - for (home_assister, away_assister, assist_prob) in [ - ( - Some(*home_assister_index), - Some(*away_assister_index), - config.team_probs.assists.home - * home_player_assist_prob - * config.team_probs.assists.away - * away_player_assist_prob, - ), - ( - Some(*home_assister_index), - None, - config.team_probs.assists.home - * home_player_assist_prob - * (1.0 - config.team_probs.assists.away) - * away_player_assist_prob, - ), - ( - None, - Some(*away_assister_index), - (1.0 - config.team_probs.assists.home) - * home_player_assist_prob - * config.team_probs.assists.away - * away_player_assist_prob, - ), - ( - None, - None, - (1.0 - config.team_probs.assists.home) + for (home_assister, home_player_assist_prob) in assist::Iter::new( + config.team_probs.assists.home, + &home_assisters, + *home_scorer_index, + ) { + for (away_assister, away_player_assist_prob) in assist::Iter::new( + config.team_probs.assists.away, + &away_assisters, + *away_scorer_index, + ) { + merge( + &config.expansions, + &half, + ¤t_prospect, + current_prob, + PartialProspect { + home_scorer: Some(*home_scorer_index), + away_scorer: Some(*away_scorer_index), + home_assister, + away_assister, + first_scoring_side: Some(first_scoring_side), + prob: params.common + * 0.5 + * home_player_score_prob + * away_player_score_prob * home_player_assist_prob - * (1.0 - config.team_probs.assists.away) * away_player_assist_prob, - ), - ] { - if assist_prob == 0.0 { - continue; - } - - merge( - &config.expansions, - &half, - ¤t_prospect, - current_prob, - PartialProspect { - home_scorer: Some(*home_scorer_index), - away_scorer: Some(*away_scorer_index), - home_assister, - away_assister, - first_scoring_side: Some(first_scoring_side), - prob: params.common - * 0.5 - * home_player_score_prob - * away_player_score_prob - * assist_prob, - }, - &mut next_prospects, - ); - } - - // // with home and away assist - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: Some(*home_assister_index), - // away_assister: Some(*away_assister_index), - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * config.team_probs.assists.home - // * home_player_assist_prob - // * config.team_probs.assists.away - // * away_player_assist_prob, - // }, - // &mut next_prospects, - // ); - // - // // with home assist only - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: Some(*home_assister_index), - // away_assister: None, - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * config.team_probs.assists.home - // * home_player_assist_prob - // * (1.0 - config.team_probs.assists.away) - // * away_player_assist_prob, - // }, - // &mut next_prospects, - // ); - // - // // with away assist only - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: None, - // away_assister: Some(*away_assister_index), - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * (1.0 - config.team_probs.assists.home) - // * home_player_assist_prob - // * config.team_probs.assists.away - // * away_player_assist_prob, - // }, - // &mut next_prospects, - // ); - // - // // without assist - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: None, - // away_assister: None, - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * (1.0 - config.team_probs.assists.home) - // * home_player_assist_prob - // * (1.0 - config.team_probs.assists.away) - // * away_player_assist_prob, - // }, - // &mut next_prospects, - // ); + }, + &mut next_prospects, + ); } - // - // // without assist - // let partial = PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: Some(*home_assister_index), - // away_assister: None, - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * config.team_probs.assists.home - // * home_player_assist_prob - // * (1.0 - config.team_probs.assists.away) - // }; - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // partial, - // &mut next_prospects, - // ); } - - // // without assist - // let partial = PartialProspect { - // home_scorer: Some(*home_scorer_index), - // away_scorer: Some(*away_scorer_index), - // home_assister: None, - // away_assister: None, - // first_scoring_side: Some(first_scoring_side), - // prob: params.common - // * 0.5 - // * home_player_score_prob - // * away_player_score_prob - // * (1.0 - config.team_probs.assists.home) - // * (1.0 - config.team_probs.assists.away) - // }; - // merge( - // &config.expansions, - // &half, - // ¤t_prospect, - // current_prob, - // partial, - // &mut next_prospects, - // ); + // let mut remaining_home_player_assist_prob = 1.0; + // for (home_assister_index, home_player_assist_prob) in &home_assisters { + // if *home_scorer_index != other_player_index + // && home_assister_index == home_scorer_index + // { + // continue; + // } + // let home_player_assist_prob = if home_assister_index + // == &other_player_index + // { + // remaining_home_player_assist_prob + // } else { + // remaining_home_player_assist_prob -= home_player_assist_prob; + // *home_player_assist_prob + // }; + // + // // with assist + // let mut remaining_away_player_assist_prob = 1.0; + // for (away_assister_index, away_player_assist_prob) in + // &away_assisters + // { + // if *away_scorer_index != other_player_index + // && away_assister_index == away_scorer_index + // { + // continue; + // } + // let away_player_assist_prob = + // if away_assister_index == &other_player_index { + // remaining_away_player_assist_prob + // } else { + // remaining_away_player_assist_prob -= + // away_player_assist_prob; + // *away_player_assist_prob + // }; + // + // for (home_assister, away_assister, assist_prob) in [ + // ( + // Some(*home_assister_index), + // Some(*away_assister_index), + // config.team_probs.assists.home + // * home_player_assist_prob + // * config.team_probs.assists.away + // * away_player_assist_prob, + // ), + // ( + // Some(*home_assister_index), + // None, + // config.team_probs.assists.home + // * home_player_assist_prob + // * (1.0 - config.team_probs.assists.away) + // * away_player_assist_prob, + // ), + // ( + // None, + // Some(*away_assister_index), + // (1.0 - config.team_probs.assists.home) + // * home_player_assist_prob + // * config.team_probs.assists.away + // * away_player_assist_prob, + // ), + // ( + // None, + // None, + // (1.0 - config.team_probs.assists.home) + // * home_player_assist_prob + // * (1.0 - config.team_probs.assists.away) + // * away_player_assist_prob, + // ), + // ] { + // if assist_prob == 0.0 { + // continue; + // } + // + // merge( + // &config.expansions, + // &half, + // ¤t_prospect, + // current_prob, + // PartialProspect { + // home_scorer: Some(*home_scorer_index), + // away_scorer: Some(*away_scorer_index), + // home_assister, + // away_assister, + // first_scoring_side: Some(first_scoring_side), + // prob: params.common + // * 0.5 + // * home_player_score_prob + // * away_player_score_prob + // * assist_prob, + // }, + // &mut next_prospects, + // ); + // } + // } + // } } } } diff --git a/brumby-soccer/src/interval/assist.rs b/brumby-soccer/src/interval/assist.rs new file mode 100644 index 0000000..f45b200 --- /dev/null +++ b/brumby-soccer/src/interval/assist.rs @@ -0,0 +1,154 @@ +pub struct Iter<'a> { + assist_prob: f64, + assisters: &'a[(usize, f64)], + scorer_index: usize, + other_player_index: usize, + remaining_player_assist_prob: f64, + pos: usize +} +impl<'a> Iter<'a> { + pub fn new(assist_prob: f64, + assisters: &'a[(usize, f64)], + scorer_index: usize) -> Self { + Self { + assist_prob, + assisters, + scorer_index, + other_player_index: assisters[assisters.len() - 1].0, + remaining_player_assist_prob: 1.0, + pos: 0, + } + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = (Option, f64); + + fn next(&mut self) -> Option { + loop { + let response = if self.pos == self.assisters.len() + 1 { + // iterator exhausted + None + } else if self.pos == self.assisters.len() { + // probability of no assist happening + self.pos += 1; + Some((None, 1.0 - self.assist_prob)) + } else { + // probability of a player assisting + let (assister, player_assist_prob) = { + let (assister, player_assist_prob) = self.assisters[self.pos]; + // scorer cannot assist to self, unless assister is 'other' player + if assister != self.other_player_index && assister == self.scorer_index { + self.pos += 1; + self.assisters[self.pos] + } else { + (assister, player_assist_prob) + } + }; + self.pos += 1; + + let player_assist_prob = if assister == self.other_player_index { + self.remaining_player_assist_prob + } else { + self.remaining_player_assist_prob -= player_assist_prob; + player_assist_prob + }; + Some((Some(assister), self.assist_prob * player_assist_prob)) + }; + + match response { + None => return None, + Some((_, prob)) if prob > 0.0 => return response, + _ => {} + } + } + } +} + +#[cfg(test)] +mod tests { + use assert_float_eq::*; + use super::*; + + #[test] + fn scored_by_player_not_among_assisters() { + let assisters = [ + (10, 0.1), + (20, 0.2), + (99, f64::NAN), + ]; + let mut iter = Iter::new(0.7, &assisters, 5); + assert_relative_eq(Some((Some(10), 0.07)), iter.next()); + assert_relative_eq(Some((Some(20), 0.14)), iter.next()); + assert_relative_eq(Some((Some(99), 0.49)), iter.next()); + assert_relative_eq(Some((None, 0.3)), iter.next()); + assert_relative_eq(None, iter.next()); + } + + #[test] + fn scored_by_player_among_assisters() { + let assisters = [ + (10, 0.1), + (20, 0.2), + (99, f64::NAN), + ]; + let mut iter = Iter::new(0.7, &assisters, 20); + assert_relative_eq(Some((Some(10), 0.07)), iter.next()); + assert_relative_eq(Some((Some(99), 0.63)), iter.next()); + assert_relative_eq(Some((None, 0.3)), iter.next()); + assert_relative_eq(None, iter.next()); + } + + #[test] + fn scored_by_other() { + let assisters = [ + (10, 0.1), + (20, 0.2), + (99, f64::NAN), + ]; + let mut iter = Iter::new(0.7, &assisters, 99); + assert_relative_eq(Some((Some(10), 0.07)), iter.next()); + assert_relative_eq(Some((Some(20), 0.14)), iter.next()); + assert_relative_eq(Some((Some(99), 0.49)), iter.next()); + assert_relative_eq(Some((None, 0.3)), iter.next()); + assert_relative_eq(None, iter.next()); + } + + #[test] + fn skip_zero_prob() { + let assisters = [ + (10, 0.1), + (20, 0.2), + (99, f64::NAN), + ]; + let mut iter = Iter::new(1.0, &assisters, 5); + assert_relative_eq(Some((Some(10), 0.1)), iter.next()); + assert_relative_eq(Some((Some(20), 0.2)), iter.next()); + assert_relative_eq(Some((Some(99), 0.7)), iter.next()); + assert_relative_eq(None, iter.next()); + } + + fn assert_relative_eq(left: Option<(Option, f64)>, right: Option<(Option, f64)>) { + match left { + None => { + match right { + None => {} + Some(_) => { + panic!("left: {left:?}, right: {right:?}"); + } + } + } + Some(left_player_prob) => { + match right { + None => { + panic!("left: {left:?}, right: {right:?}"); + } + Some(right_player_prob) => { + assert_eq!(left_player_prob.0, right_player_prob.0, "left: {left:?}, right: {right:?}"); + assert_float_relative_eq!(left_player_prob.1, right_player_prob.1); + } + } + } + } + } +} \ No newline at end of file