Last active
April 4, 2021 10:04
-
-
Save teryror/193237542a1bb6867dd762e356861918 to your computer and use it in GitHub Desktop.
Finding the minimum win rates required to "go infinite" in MTG Arena
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fn binomial_coefficient(n: u32, k: u32) -> u128 { | |
let mut res = 1u128; | |
for i in 0..k { | |
res = (res * (n - i) as u128) / (i + 1) as u128; | |
} | |
res | |
} | |
fn neg_binom_pmf(k: u32, r: u32, p: f64) -> f64 { | |
assert!(r > 0); | |
binomial_coefficient(k + r - 1, k) as f64 * (1.0 - p).powi(r as i32) * p.powi(k as i32) | |
} | |
fn binomial_pmf(k: u32, n: u32, p: f64) -> f64 { | |
binomial_coefficient(n, k) as f64 * p.powi(k as i32) * (1.0 - p).powi((n - k) as i32) | |
} | |
#[derive(Copy, Clone, PartialEq)] | |
enum MatchType { | |
BestOfOne, | |
BestOfThree, | |
} | |
impl MatchType { | |
fn match_win_rate(&self, game_win_rate: f64) -> f64 { | |
match self { | |
&Self::BestOfOne => game_win_rate, | |
&Self::BestOfThree => 3.0 * game_win_rate.powi(2) - 2.0 * game_win_rate.powi(3), | |
} | |
} | |
} | |
#[derive(Copy, Clone)] | |
enum EndCondition { | |
NMatches, | |
NWinsOrRLosses(u32) | |
} | |
struct Event { | |
name: &'static str, | |
entry_fee: u32, | |
style: MatchType, | |
end_con: EndCondition, | |
payouts: &'static [u32], | |
} | |
impl Event { | |
fn new(name: &'static str, entry_fee: u32, style: MatchType, end_con: EndCondition, payouts: &'static [u32]) -> Self { | |
Event { | |
name, entry_fee, style, end_con, payouts, | |
} | |
} | |
fn expected_value(&self, game_win_rate: f64) -> f64 { | |
let p_win = self.style.match_win_rate(game_win_rate); | |
let mut expected_payout = 0.0; | |
match self.end_con { | |
EndCondition::NWinsOrRLosses(r) => { | |
let mut cumulative_p = 0.0; | |
for k in 0..self.payouts.len() - 1 { | |
let p = neg_binom_pmf(k as u32, r, p_win); | |
expected_payout += p * self.payouts[k] as f64; | |
cumulative_p += p; | |
} | |
let p_n_wins = 1.0 - cumulative_p; | |
expected_payout += p_n_wins * *self.payouts.last().unwrap() as f64; | |
}, | |
EndCondition::NMatches => { | |
let n = self.payouts.len() - 1; | |
for k in 0..=n { | |
let p = binomial_pmf(k as u32, n as u32, p_win); | |
expected_payout += p * self.payouts[k] as f64; | |
} | |
} | |
} | |
expected_payout - self.entry_fee as f64 | |
} | |
fn break_even_point(&self) -> f64 { | |
let mut p = (0.0f64, 1.0f64); | |
while (p.0 - p.1).abs() > 0.001 { | |
let p_mid = (p.0 + p.1) / 2.0; | |
let ev = self.expected_value(p_mid); | |
assert!(ev.is_finite()); | |
if ev < 0.0 { | |
p.0 = p_mid; | |
} else if ev > 0.0 { | |
p.1 = p_mid; | |
} else { | |
return p_mid; | |
} | |
} | |
(p.0 + p.1) / 2.0 | |
} | |
} | |
fn main() { | |
use MatchType::*; | |
use EndCondition::*; | |
let event_types = [ | |
Event::new("Traditional Event", 1000, BestOfThree, NWinsOrRLosses(2), &[0, 500, 1000, 1500, 1700, 2100]), | |
Event::new("Standard Event", 500, BestOfOne, NWinsOrRLosses(3), &[100, 200, 300, 400, 500, 600, 800, 1000]), | |
Event::new("Traditional Draft", 1500, BestOfThree, NMatches, &[0, 0, 1000, 3000]), | |
Event::new("Traditional Cube", 4000, BestOfThree, NMatches, &[0, 0, 4000, 6000]), | |
Event::new("Historic Challenge", 10000, BestOfThree, NWinsOrRLosses(3), &[0, 1000, 2000, 3000, 4000, 6000, 8000, 10000, 15000]), | |
Event::new("Premier Draft", 1500, BestOfOne, NWinsOrRLosses(3), &[50, 100, 250, 1000, 1400, 1600, 1800, 2200]), | |
Event::new("Arena Cube Draft", 4000, BestOfOne, NWinsOrRLosses(3), &[0, 500, 1000, 2000, 3000, 4000, 5000, 6000]), | |
Event::new("Quick Draft", 750, BestOfOne, NWinsOrRLosses(3), &[50, 100, 200, 300, 450, 650, 850, 950]), | |
Event::new("Sealed Deck", 2000, BestOfOne, NWinsOrRLosses(3), &[200, 400, 600, 1200, 1400, 1600, 2000, 2200]), | |
]; | |
for event in &event_types { | |
let p_win_to_go_infinite = event.break_even_point(); | |
print!("{:18}:{:5.1}%", event.name, p_win_to_go_infinite * 100.0); | |
if event.style == BestOfThree { | |
let match_win_rate = BestOfThree.match_win_rate(p_win_to_go_infinite); | |
print!(" ({:4.1}%)", match_win_rate * 100.0); | |
} | |
println!(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment