Skip to content

Instantly share code, notes, and snippets.

@teryror
Last active April 4, 2021 10:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save teryror/193237542a1bb6867dd762e356861918 to your computer and use it in GitHub Desktop.
Save teryror/193237542a1bb6867dd762e356861918 to your computer and use it in GitHub Desktop.
Finding the minimum win rates required to "go infinite" in MTG Arena
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