Last active
August 6, 2025 23:50
-
-
Save haritonch/1e793a68f574c5410e1efd632b484390 to your computer and use it in GitHub Desktop.
Monte Carlo simulation for Tzoker
This file contains hidden or 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
| use rand::{seq::SliceRandom, Rng}; | |
| use rayon::prelude::*; | |
| use rust_decimal::{prelude::FromPrimitive, Decimal}; | |
| use std::time::Instant; | |
| use std::{cmp::min, str::FromStr}; | |
| const EXPECTED_TOTAL_COUPONS_PLAYED: u32 = 7_000_000; | |
| const N_SIMULATIONS: u32 = 10_000; | |
| const JACKPOT: u32 = 22_000_000; | |
| const SECOND_CATEGORY_POT: u32 = 2_000_000; | |
| type Five = [u8; 5]; | |
| type One = u8; | |
| struct Coupon { | |
| pub five: Five, | |
| pub one: One, | |
| } | |
| fn random_one() -> One { | |
| let mut rng = rand::rng(); | |
| rng.random_range(1..=20) | |
| } | |
| fn random_five() -> Five { | |
| let mut numbers: Vec<u8> = (1..=45).collect(); | |
| let mut rng = rand::rng(); | |
| numbers.shuffle(&mut rng); | |
| let five: Five = numbers[..5].try_into().unwrap(); | |
| five | |
| } | |
| fn random_coupon() -> Coupon { | |
| let five = random_five(); | |
| let one = random_one(); | |
| Coupon { five, one } | |
| } | |
| fn n_matches(five: &Five, other_five: &Five) -> u32 { | |
| let mut matches = 0; | |
| for &num in five { | |
| if other_five.contains(&num) { | |
| matches += 1; | |
| } | |
| } | |
| matches | |
| } | |
| #[derive(Clone, Copy, Hash, PartialEq, Eq)] | |
| enum WinningCategory { | |
| First, | |
| Second, | |
| Third, | |
| Fourth, | |
| Fifth, | |
| Sixth, | |
| Seventh, | |
| Eighth, | |
| Ninth, | |
| None, | |
| } | |
| impl From<&WinningCategory> for Decimal { | |
| fn from(value: &WinningCategory) -> Self { | |
| match value { | |
| WinningCategory::First => Decimal::from_u32(JACKPOT).unwrap(), | |
| WinningCategory::Second => Decimal::from_str("100000").unwrap(), | |
| WinningCategory::Third => Decimal::from_str("2500").unwrap(), | |
| WinningCategory::Fourth => Decimal::from_str("50").unwrap(), | |
| WinningCategory::Fifth => Decimal::from_str("50").unwrap(), | |
| WinningCategory::Sixth => Decimal::from_str("2").unwrap(), | |
| WinningCategory::Seventh => Decimal::from_str("2").unwrap(), | |
| WinningCategory::Eighth => Decimal::from_str("1.5").unwrap(), | |
| WinningCategory::Ninth => Decimal::from_str("1").unwrap(), | |
| WinningCategory::None => Decimal::ZERO, | |
| } | |
| } | |
| } | |
| impl WinningCategory { | |
| fn payout(&self) -> Decimal { | |
| Decimal::from(self) | |
| } | |
| } | |
| fn winning_category(coupon: &Coupon, winning_coupon: &Coupon) -> WinningCategory { | |
| let five_matches = n_matches(&coupon.five, &winning_coupon.five); | |
| let one_match = if coupon.one == winning_coupon.one { | |
| 1 | |
| } else { | |
| 0 | |
| }; | |
| match (five_matches, one_match) { | |
| (5, 1) => WinningCategory::First, | |
| (5, 0) => WinningCategory::Second, | |
| (4, 1) => WinningCategory::Third, | |
| (4, 0) => WinningCategory::Fourth, | |
| (3, 1) => WinningCategory::Fifth, | |
| (3, 0) => WinningCategory::Sixth, | |
| (2, 1) => WinningCategory::Seventh, | |
| (1, 1) => WinningCategory::Eighth, | |
| (2, 0) => WinningCategory::Ninth, | |
| _ => WinningCategory::None, | |
| } | |
| } | |
| fn num_category_winners( | |
| played_coupons: &[Coupon], | |
| winning_coupon: &Coupon, | |
| target_category: WinningCategory, | |
| ) -> u32 { | |
| let mut count = 0; | |
| for coupon in played_coupons { | |
| let coupon_category = winning_category(coupon, winning_coupon); | |
| if coupon_category == target_category { | |
| count += 1; | |
| } | |
| } | |
| count | |
| } | |
| // Returns the round payout | |
| fn round_payout() -> Decimal { | |
| let my_coupon = random_coupon(); | |
| let played_coupons: Vec<Coupon> = (0..EXPECTED_TOTAL_COUPONS_PLAYED) | |
| .map(|_| random_coupon()) | |
| .collect(); | |
| let winning_coupon = random_coupon(); | |
| let my_category = winning_category(&my_coupon, &winning_coupon); | |
| match my_category { | |
| c @ WinningCategory::First => { | |
| let num_category_winners = | |
| Decimal::from_u32(num_category_winners(&played_coupons, &winning_coupon, c)) | |
| .unwrap(); | |
| println!("Winner!!!"); | |
| c.payout().checked_div(num_category_winners).unwrap() | |
| } | |
| c @ WinningCategory::Second => { | |
| let num_category_winners = | |
| Decimal::from_u32(num_category_winners(&played_coupons, &winning_coupon, c)) | |
| .unwrap(); | |
| min( | |
| c.payout(), | |
| Decimal::from_u32(SECOND_CATEGORY_POT) | |
| .unwrap() | |
| .checked_div(num_category_winners) | |
| .unwrap(), | |
| ) | |
| } | |
| c => c.payout(), | |
| } | |
| } | |
| fn main() { | |
| let start = Instant::now(); | |
| let total_payout = (0..N_SIMULATIONS) | |
| .into_par_iter() | |
| .map(|_| round_payout()) | |
| .reduce(|| Decimal::ZERO, |a, b| a.checked_add(b).unwrap()); | |
| let expected_payout = total_payout | |
| .checked_div(Decimal::from_u32(N_SIMULATIONS).unwrap()) | |
| .unwrap(); | |
| println!("Average payout after {N_SIMULATIONS} simulations: {expected_payout}"); | |
| let duration = start.elapsed(); | |
| println!("Execution time: {:.2?}", duration); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment