|
const NUM_ITERATIONS: u32 = 5_000_000; |
|
|
|
mod mono_color { |
|
#[derive(Copy, Clone, Eq, PartialEq)] |
|
enum CardType { |
|
NonLand, |
|
ColoredLand, |
|
ColorlessLand, |
|
} |
|
|
|
#[derive(Copy, Clone)] |
|
struct MonoColorDeck { |
|
total_cards: u32, |
|
total_lands: u32, |
|
color_lands: u32, |
|
} |
|
|
|
impl MonoColorDeck { |
|
fn new(deck_size: u32, total_lands: u32, colored_sources: u32) -> Self { |
|
MonoColorDeck { |
|
total_cards: deck_size, |
|
total_lands, |
|
color_lands: colored_sources, |
|
} |
|
} |
|
|
|
fn draw_card(&mut self) -> CardType { |
|
use rand::prelude::*; |
|
|
|
let mut rng = thread_rng(); |
|
let random_int_between_one_and_deck_size = rng.gen_range(0, self.total_cards) + 1; |
|
|
|
if random_int_between_one_and_deck_size <= self.color_lands { |
|
self.color_lands -= 1; |
|
self.total_lands -= 1; |
|
self.total_cards -= 1; |
|
|
|
CardType::ColoredLand |
|
} else if random_int_between_one_and_deck_size <= self.total_lands { |
|
self.total_lands -= 1; |
|
self.total_cards -= 1; |
|
|
|
CardType::ColorlessLand |
|
} else { |
|
self.total_cards -= 1; |
|
CardType::NonLand |
|
} |
|
} |
|
|
|
fn draw_seven(&mut self) -> (u32, u32) { |
|
let mut lands_drawn_total = 0; |
|
let mut colored_lands_drawn = 0; |
|
|
|
for _ in 0..7 { |
|
let card_type = self.draw_card(); |
|
if card_type != CardType::NonLand { |
|
lands_drawn_total += 1; |
|
if card_type == CardType::ColoredLand { |
|
colored_lands_drawn += 1; |
|
} |
|
} |
|
} |
|
|
|
(colored_lands_drawn, lands_drawn_total) |
|
} |
|
} |
|
|
|
fn run_sim(deck_size: u32, total_lands: u32) { |
|
use super::NUM_ITERATIONS; |
|
let conditions_on_total_lands = [2..=3, 2..=4, 3..=4]; |
|
|
|
let consistency_cutoff = 0.95; |
|
let mut minima_one_or_more = [total_lands; 3]; |
|
let mut minima_two_or_more = [total_lands; 3]; |
|
|
|
for num_colored_sources in (1..total_lands).rev() { |
|
let mut count_one_or_more_colored_sources = [0, 0, 0]; |
|
let mut count_two_or_more_colored_sources = [0, 0, 0]; |
|
|
|
let mut count_conditional = [0, 0, 0]; |
|
|
|
for _ in 0..NUM_ITERATIONS { |
|
let mut deck = MonoColorDeck::new(deck_size, total_lands, num_colored_sources); |
|
let (colored_lands_drawn, lands_drawn_total) = deck.draw_seven(); |
|
|
|
for (i, condition) in conditions_on_total_lands.iter().enumerate() { |
|
if condition.contains(&lands_drawn_total) { |
|
count_conditional[i] += 1; |
|
if colored_lands_drawn >= 1 { |
|
count_one_or_more_colored_sources[i] += 1; |
|
if colored_lands_drawn >= 2 { |
|
count_two_or_more_colored_sources[i] += 1; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
let mut all_freqs_below_cutoff = true; |
|
|
|
for i in 0..3 { |
|
let count_one_or_more = count_one_or_more_colored_sources[i]; |
|
let count_two_or_more = count_two_or_more_colored_sources[i]; |
|
|
|
let freq_one_or_more = count_one_or_more as f64 / count_conditional[i] as f64; |
|
let freq_two_or_more = count_two_or_more as f64 / count_conditional[i] as f64; |
|
|
|
if freq_one_or_more >= consistency_cutoff { |
|
minima_one_or_more[i] = num_colored_sources; |
|
all_freqs_below_cutoff = false; |
|
} |
|
|
|
if freq_two_or_more >= consistency_cutoff { |
|
minima_two_or_more[i] = num_colored_sources; |
|
all_freqs_below_cutoff = false; |
|
} |
|
} |
|
|
|
if all_freqs_below_cutoff { |
|
break; |
|
} |
|
} |
|
|
|
print!("| {:5} | ", total_lands); |
|
print!("{:4} / {:2} | ", minima_one_or_more[0], minima_two_or_more[0]); |
|
print!("{:4} / {:2} | ", minima_one_or_more[1], minima_two_or_more[1]); |
|
print!("{:4} / {:2} |", minima_one_or_more[2], minima_two_or_more[2]); |
|
println!(); |
|
} |
|
|
|
pub fn run_all_sims() { |
|
println!("# Mono-colored decks"); |
|
println!("## Limited decks (40 cards)"); |
|
println!("| Lands | 2-3 lands | 2-4 lands | 3-4 lands |"); |
|
println!("|------:|----------:|----------:|----------:|"); |
|
for num_lands in 10..=20 { |
|
run_sim(40, num_lands); |
|
} |
|
|
|
println!(); |
|
println!("## Constructed decks (60 cards)"); |
|
println!("| Lands | 2-3 lands | 2-4 lands | 3-4 lands |"); |
|
println!("|------:|----------:|----------:|----------:|"); |
|
for num_lands in 15..=30 { |
|
run_sim(60, num_lands); |
|
} |
|
|
|
println!(); |
|
println!("## Yorion decks (80 cards)"); |
|
println!("| Lands | 2-3 lands | 2-4 lands | 3-4 lands |"); |
|
println!("|------:|----------:|----------:|----------:|"); |
|
for num_lands in 20..=40 { |
|
run_sim(80, num_lands); |
|
} |
|
|
|
println!(); |
|
println!("## Commander decks (99 cards)"); |
|
println!("| Lands | 2-3 lands | 2-4 lands | 3-4 lands |"); |
|
println!("|------:|----------:|----------:|----------:|"); |
|
for num_lands in 24..=49 { |
|
run_sim(99, num_lands); |
|
} |
|
} |
|
} |
|
|
|
mod dual_color { |
|
use rand::{thread_rng, Rng}; |
|
|
|
#[derive(Copy, Clone, Eq, PartialEq)] |
|
enum CardType { |
|
NonLand, |
|
ColorlessLand, |
|
MonoColorLandA, |
|
MonoColorLandB, |
|
DualColorLand, |
|
} |
|
|
|
#[derive(Copy, Clone)] |
|
struct Deck { |
|
total_cards: u32, |
|
cards_by_card_type: [u32; 5], |
|
} |
|
|
|
impl Deck { |
|
fn new(total_cards: u32, colorless_sources: u32, colored_sources_a: u32, colored_sources_b: u32, two_color_sources: u32) -> Self { |
|
let non_land_cards = total_cards - (colorless_sources + colored_sources_a + colored_sources_b + two_color_sources); |
|
|
|
Deck { |
|
total_cards, |
|
cards_by_card_type: [non_land_cards, colorless_sources, colored_sources_a, colored_sources_b, two_color_sources], |
|
} |
|
} |
|
|
|
fn draw_card(&mut self) -> CardType { |
|
let mut rng = thread_rng(); |
|
let mut random_int_between_one_and_deck_size = rng.gen_range(0, self.total_cards) + 1; |
|
|
|
if random_int_between_one_and_deck_size <= self.cards_by_card_type[0] { |
|
self.total_cards -= 1; |
|
self.cards_by_card_type[0] -= 1; |
|
|
|
return CardType::NonLand; |
|
} |
|
|
|
random_int_between_one_and_deck_size -= self.cards_by_card_type[0]; |
|
if random_int_between_one_and_deck_size <= self.cards_by_card_type[1] { |
|
self.total_cards -= 1; |
|
self.cards_by_card_type[1] -= 1; |
|
|
|
return CardType::ColorlessLand; |
|
} |
|
|
|
random_int_between_one_and_deck_size -= self.cards_by_card_type[1]; |
|
if random_int_between_one_and_deck_size <= self.cards_by_card_type[2] { |
|
self.total_cards -= 1; |
|
self.cards_by_card_type[2] -= 1; |
|
|
|
return CardType::MonoColorLandA; |
|
} |
|
|
|
random_int_between_one_and_deck_size -= self.cards_by_card_type[2]; |
|
if random_int_between_one_and_deck_size <= self.cards_by_card_type[3] { |
|
self.total_cards -= 1; |
|
self.cards_by_card_type[3] -= 1; |
|
|
|
return CardType::MonoColorLandB; |
|
} |
|
|
|
random_int_between_one_and_deck_size -= self.cards_by_card_type[3]; |
|
if random_int_between_one_and_deck_size <= self.cards_by_card_type[4] { |
|
self.total_cards -= 1; |
|
self.cards_by_card_type[4] -= 1; |
|
|
|
return CardType::DualColorLand; |
|
} |
|
|
|
unreachable!() |
|
} |
|
} |
|
|
|
fn run_sim(deck_size: u32, land_count: u32, max_off_color_land_count: u32, off_color_land_count_step: usize) { |
|
use super::NUM_ITERATIONS; |
|
let conditions_on_total_lands = [2..=3, 2..=4, 3..=4]; |
|
|
|
print!("| {:5} |", land_count); |
|
for num_off_color_lands in (0..=max_off_color_land_count).into_iter().step_by(off_color_land_count_step) { |
|
let num_on_color_lands = land_count - num_off_color_lands; |
|
|
|
let mut num_duals_minima = [u32::max_value(); 3]; |
|
|
|
for num_dual_lands in (0..=num_on_color_lands).rev() { |
|
let other_lands = num_on_color_lands - num_dual_lands; |
|
let num_mono_color_lands_b = other_lands / 2; |
|
let num_mono_color_lands_a = other_lands - num_mono_color_lands_b; |
|
|
|
let mut count_conditional = [0; 3]; |
|
let mut count_ok = [0; 3]; |
|
|
|
for _ in 0..NUM_ITERATIONS { |
|
let mut deck = Deck::new( |
|
deck_size, |
|
num_off_color_lands, |
|
num_mono_color_lands_a, |
|
num_mono_color_lands_b, |
|
num_dual_lands); |
|
|
|
let mut total_lands_drawn = 0; |
|
let mut sources_drawn_a = 0; |
|
let mut sources_drawn_b = 0; |
|
|
|
for _ in 0..7 { |
|
let card_type = deck.draw_card(); |
|
|
|
if card_type != CardType::NonLand { |
|
total_lands_drawn += 1; |
|
if card_type == CardType::MonoColorLandA || card_type == CardType::DualColorLand { |
|
sources_drawn_a += 1; |
|
} |
|
if card_type == CardType::MonoColorLandB || card_type == CardType::DualColorLand { |
|
sources_drawn_b += 1; |
|
} |
|
} |
|
} |
|
|
|
for (i, condition) in conditions_on_total_lands.iter().enumerate() { |
|
if condition.contains(&total_lands_drawn) { |
|
count_conditional[i] += 1; |
|
if sources_drawn_a > 0 && sources_drawn_b > 0 { |
|
count_ok[i] += 1; |
|
} |
|
} |
|
} |
|
} |
|
|
|
let mut all_failed = true; |
|
for (i, (count_ok, count_conditional)) in count_ok.iter().zip(count_conditional.iter()).enumerate() { |
|
let freq_ok = *count_ok as f64 / *count_conditional as f64; |
|
if freq_ok > 0.95 { |
|
num_duals_minima[i] = num_dual_lands; |
|
all_failed = false; |
|
} |
|
} |
|
|
|
if all_failed { break; } |
|
} |
|
|
|
print!(" {:2} / {:2} / {:2} |", num_duals_minima[0], num_duals_minima[1], num_duals_minima[2]); |
|
} |
|
|
|
println!(); |
|
} |
|
|
|
pub fn run_all_sims() { |
|
println!("# Two-color Manabases"); |
|
println!("## Limited decks (40 cards):"); |
|
println!("| Lands | 0 off-color | 1 off-color | 2 off-color | 3 off-color |"); |
|
println!("|------:|-------------:|-------------:|-------------:|-------------:|"); |
|
for land_count in 10..=20 { |
|
run_sim(40, land_count, 3, 1); |
|
} |
|
|
|
println!(); |
|
println!("## Constructed decks (60 cards):"); |
|
println!("| Lands | 0 off-color | 1 off-color | 2 off-color | 3 off-color | 4 off-color |"); |
|
println!("|------:|-------------:|-------------:|-------------:|-------------:|-------------:|"); |
|
for land_count in 15..=30 { |
|
run_sim(60, land_count, 4, 1); |
|
} |
|
|
|
println!(); |
|
println!("## Yorion decks (80 cards):"); |
|
println!("| Lands | 0 off-color | 1 off-color | 2 off-color | 3 off-color | 4 off-color |"); |
|
println!("|------:|-------------:|-------------:|-------------:|-------------:|-------------:|"); |
|
for land_count in 20..=40 { |
|
run_sim(80, land_count, 4, 1); |
|
} |
|
|
|
println!(); |
|
println!("## Commander decks (99 cards):"); |
|
println!("| Lands | 0 off-color | 2 off-color | 4 off-color | 6 off-color | 8 off-color |"); |
|
println!("|------:|-------------:|-------------:|-------------:|-------------:|-------------:|"); |
|
for land_count in 24..=49 { |
|
run_sim(99, land_count, 8, 2); |
|
} |
|
} |
|
} |
|
|
|
mod three_colors { |
|
use rand::Rng; |
|
|
|
#[repr(usize)] |
|
#[derive(Copy, Clone, Eq, PartialEq)] |
|
enum CardType { |
|
NonLand = 0, |
|
FabledPassage = 1, |
|
BasicLandA = 2, |
|
BasicLandB = 3, |
|
BasicLandC = 4, |
|
DualLandAB = 5, |
|
DualLandAC = 6, |
|
DualLandBC = 7, |
|
TriLand = 8, |
|
} |
|
|
|
#[derive(Copy, Clone)] |
|
struct Deck { |
|
total_cards: u32, |
|
num_cards_by_type: [u32; 9] |
|
} |
|
|
|
impl Deck { |
|
fn with_balanced_duals(deck_size: u32, land_count: u32, num_tri_lands: u32) -> Deck { |
|
let non_land_cards = deck_size - land_count; |
|
let other_lands = land_count - num_tri_lands; |
|
|
|
let dual_lands_ab = other_lands / 3 + if other_lands % 3 > 0 { 1 } else { 0 }; |
|
let dual_lands_ac = other_lands / 3 + if other_lands % 3 > 1 { 1 } else { 0 }; |
|
let dual_lands_bc = other_lands / 3; |
|
|
|
assert_eq!(land_count, dual_lands_ab + dual_lands_ac + dual_lands_bc + num_tri_lands); |
|
|
|
Deck { |
|
total_cards: deck_size, |
|
num_cards_by_type: [non_land_cards, 0, 0, 0, 0, dual_lands_ab, dual_lands_ac, dual_lands_bc, num_tri_lands], |
|
} |
|
} |
|
|
|
fn with_skewed_duals(deck_size: u32, land_count: u32, num_tri_lands: u32) -> Deck { |
|
let non_land_cards = deck_size - land_count; |
|
let other_lands = land_count - num_tri_lands; |
|
|
|
let dual_lands_ab = other_lands / 2; |
|
let dual_lands_ac = other_lands - dual_lands_ab; |
|
|
|
assert_eq!(land_count, num_tri_lands + dual_lands_ab + dual_lands_ac); |
|
|
|
Deck { |
|
total_cards: deck_size, |
|
num_cards_by_type: [non_land_cards, 0, 0, 0, 0, dual_lands_ab, dual_lands_ac, 0, num_tri_lands], |
|
} |
|
} |
|
|
|
fn with_basics_and_fabled_passage(deck_size: u32, land_count: u32, num_tri_lands: u32) -> Deck { |
|
let non_land_cards = deck_size - land_count; |
|
let other_lands = land_count - (num_tri_lands + 10); // account for Fabled Passage + fetchable basics |
|
|
|
let dual_lands_ab = other_lands / 3 + if other_lands % 3 > 0 { 1 } else { 0 }; |
|
let dual_lands_ac = other_lands / 3 + if other_lands % 3 > 1 { 1 } else { 0 }; |
|
let dual_lands_bc = other_lands / 3; |
|
|
|
assert_eq!(land_count, num_tri_lands + dual_lands_ab + dual_lands_ac + dual_lands_bc + 10); |
|
|
|
Deck { |
|
total_cards: deck_size, |
|
num_cards_by_type: [non_land_cards, 4, 2, 2, 2, dual_lands_ab, dual_lands_ac, dual_lands_bc, num_tri_lands], |
|
} |
|
} |
|
|
|
fn with_basics_and_fetch_lands(deck_size: u32, land_count: u32, num_basics: u32) -> Deck { |
|
// For simplicity, we'll count proper fetches as tri-lands, |
|
// since they're just as good at fixing opening hands as long as there's no colorless lands |
|
|
|
let non_land_cards = deck_size - land_count; |
|
|
|
let other_lands = land_count - (8 + num_basics); |
|
let dual_lands_ab = other_lands / 3 + if other_lands % 3 > 0 { 1 } else { 0 }; |
|
let dual_lands_ac = other_lands / 3 + if other_lands % 3 > 1 { 1 } else { 0 }; |
|
let dual_lands_bc = other_lands / 3; |
|
|
|
let basics = if num_basics == 5 { |
|
(3, 1, 1) |
|
} else if num_basics == 6 { |
|
(2, 2, 2) |
|
} else { |
|
unreachable!() |
|
}; |
|
|
|
assert_eq!(land_count, 8 + num_basics + dual_lands_ab + dual_lands_ac + dual_lands_bc); |
|
|
|
Deck { |
|
total_cards: deck_size, |
|
num_cards_by_type: [non_land_cards, 0, basics.0, basics.1, basics.2, dual_lands_ab, dual_lands_ac, dual_lands_bc, 8], |
|
} |
|
} |
|
|
|
fn draw_card(&mut self) -> CardType { |
|
let mut random_int_between_one_and_deck_size = rand::thread_rng().gen_range(0, self.total_cards) + 1; |
|
|
|
for (idx, count) in self.num_cards_by_type.iter_mut().enumerate() { |
|
if random_int_between_one_and_deck_size <= *count { |
|
self.total_cards -= 1; |
|
*count -= 1; |
|
|
|
return unsafe { std::mem::transmute(idx) }; |
|
} |
|
|
|
random_int_between_one_and_deck_size -= *count; |
|
} |
|
|
|
unreachable!() |
|
} |
|
|
|
fn draw_seven(&mut self) -> (u32, bool) { |
|
let mut total_lands_drawn = 0; |
|
let mut sources_drawn_a = 0; |
|
let mut sources_drawn_b = 0; |
|
let mut sources_drawn_c = 0; |
|
let mut passages_drawn = 0; |
|
|
|
for _ in 0..7 { |
|
use CardType::*; |
|
|
|
let card_type = self.draw_card(); |
|
if card_type == NonLand { continue; } |
|
total_lands_drawn += 1; |
|
|
|
if card_type == FabledPassage { |
|
passages_drawn += 1; |
|
} |
|
|
|
if card_type == BasicLandA || card_type == DualLandAB || card_type == DualLandAC || card_type == TriLand { |
|
sources_drawn_a += 1; |
|
} |
|
|
|
if card_type == BasicLandB || card_type == DualLandAB || card_type == DualLandBC || card_type == TriLand { |
|
sources_drawn_b += 1; |
|
} |
|
|
|
if card_type == BasicLandC || card_type == DualLandAC || card_type == DualLandBC || card_type == TriLand { |
|
sources_drawn_c += 1; |
|
} |
|
} |
|
|
|
while passages_drawn > 0 { |
|
passages_drawn -= 1; |
|
|
|
if sources_drawn_a == 0 { |
|
sources_drawn_a += 1; |
|
} else if sources_drawn_b == 0 { |
|
sources_drawn_b += 1; |
|
} else { |
|
sources_drawn_c += 1; |
|
} |
|
} |
|
|
|
let has_all_colors = sources_drawn_a > 0 && sources_drawn_b > 0 && sources_drawn_c > 0; |
|
(total_lands_drawn, has_all_colors) |
|
} |
|
} |
|
|
|
fn run_sim(deck_size: u32, land_count: u32, num_tri_lands_to_compare_to_zero: u32) { |
|
let decks_to_test = [ |
|
Deck::with_balanced_duals(deck_size, land_count, 0), |
|
Deck::with_balanced_duals(deck_size, land_count, num_tri_lands_to_compare_to_zero), |
|
Deck::with_skewed_duals(deck_size, land_count, 0), |
|
Deck::with_skewed_duals(deck_size, land_count, num_tri_lands_to_compare_to_zero), |
|
Deck::with_basics_and_fabled_passage(deck_size, land_count, 0), |
|
Deck::with_basics_and_fabled_passage(deck_size, land_count, 4), |
|
Deck::with_basics_and_fetch_lands(deck_size, land_count, 5), |
|
Deck::with_basics_and_fetch_lands(deck_size, land_count, 6), |
|
]; |
|
|
|
let conditions_on_lands_in_hand = [2..=3, 2..=4, 3..=4]; |
|
|
|
print!("| {:5} |", land_count); |
|
for deck_to_test in &decks_to_test { |
|
let mut count_conditional = [0; 3]; |
|
let mut count_ok = [0; 3]; |
|
|
|
for _ in 0..super::NUM_ITERATIONS { |
|
let mut deck = *deck_to_test; |
|
assert_eq!(deck.total_cards, deck_size); |
|
|
|
let (total_lands_drawn, has_all_colors) = deck.draw_seven(); |
|
|
|
for (i, condition) in conditions_on_lands_in_hand.iter().enumerate() { |
|
if condition.contains(&total_lands_drawn) { |
|
count_conditional[i] += 1; |
|
if has_all_colors { |
|
count_ok[i] += 1; |
|
} |
|
} |
|
} |
|
} |
|
|
|
let freq_ok = count_ok[0] as f64 / count_conditional[0] as f64 * 100.0; |
|
print!(" {:5.1}% / ", freq_ok); |
|
|
|
let freq_ok = count_ok[1] as f64 / count_conditional[1] as f64 * 100.0; |
|
print!("{:5.1}% / ", freq_ok); |
|
|
|
let freq_ok = count_ok[2] as f64 / count_conditional[2] as f64 * 100.0; |
|
print!("{:5.1}% |", freq_ok) |
|
} |
|
|
|
println!(); |
|
} |
|
|
|
pub fn run_all_sims() { |
|
println!("# Tri-color Manabases"); |
|
println!("## Constructed decks (60 cards):"); |
|
println!("| Lands | even duals, no triomes | even duals, 4 triomes | skewed duals, no triomes | skewed duals, 4 triomes | Passage, no triomes | Passage, 4 triomes | 8 Fetches, 5 Basics | 8 Fetches, 6 Basics |"); |
|
println!("|------:|-------------------------:|-------------------------:|-------------------------:|-------------------------:|-------------------------:|--------------------------|-------------------------:|-------------------------:|"); |
|
for land_count in 20..=30 { |
|
run_sim(60, land_count, 4); |
|
} |
|
} |
|
} |
|
|
|
fn main() { |
|
mono_color::run_all_sims(); |
|
dual_color::run_all_sims(); |
|
three_colors::run_all_sims(); |
|
} |