// Copyright (C) 2017 Steve Sprang
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Gather statistics for simulated games of SET.
//!
//! Results on *AMD Ryzen 7 1800x @ 3.9GHz* with 16 threads:
//!
//! Simulating 1_000_000_000 games. This may take some time...
//! PT1280.803583379S seconds elapsed.
//!
//! hand | sets | no sets | total | ratio | % with no sets
//! ------+----------------+---------------+----------------+---------+----------------
//! 6 | 12_229_414 | 468_288_234 | 480_517_648 | 1:38 | 97.45495 %
//! 9 | 480_517_648 | 445_045_030 | 925_562_678 | 1:1 | 48.08373 %
//! 12 | 22_385_130_429 | 1_597_587_780 | 23_982_718_209 | 14:1 | 6.66141 %
//! 15 | 1_523_150_458 | 17_280_709 | 1_540_431_167 | 88:1 | 1.12181 %
//! 18 | 16_513_441 | 1_082 | 16_514_523 | 15262:1 | 0.00655 %
//!
//! cards left | occurrences | % of games
//! ------------+-------------+------------
//! 0 | 12_229_414 | 1.22294 %
//! 6 | 468_288_234 | 46.82882 %
//! 9 | 445_045_030 | 44.50450 %
//! 12 | 73_670_054 | 7.36701 %
//! 15 | 766_796 | 0.07668 %
//! 18 | 472 | 0.00005 %
//!
//! As an optimization, this program makes use of the fact that there is an
//! isomorphism between a `core::Card` and its index. It only uses `core::Card`
//! objects directly when initializing the `SETS` lookup table, and otherwise just
//! works with the cards by index.
#[macro_use]
extern crate clap;
extern crate core;
extern crate num_cpus;
#[macro_use]
extern crate prettytable;
extern crate rand;
extern crate time;
use prettytable::Table;
use prettytable::format::consts;
use rand::{thread_rng, Rng};
use std::cmp;
use std::sync::mpsc;
use std::thread;
use time::PreciseTime;
use core::card::*;
use core::deck::cards;
use core::pair_iter::PairIter;
use core::shuffle::Shuffle;
use core::utils::*;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const NUM_GAMES: u64 = 1_000_000;
const INITIAL_DEAL: usize = 12;
const MAX_DEAL: usize = 22;
const SET_SIZE: usize = 3;
////////////////////////////////////////////////////////////////////////////////
// Counts
////////////////////////////////////////////////////////////////////////////////
struct Counts {
/// Playable hand.
sets: [u64; MAX_DEAL],
/// Stuck hand.
no_sets: [u64; MAX_DEAL],
/// Game over.
remainder: [u64; MAX_DEAL],
}
impl Counts {
fn zero() -> Counts {
Counts {
sets: [0; MAX_DEAL],
no_sets: [0; MAX_DEAL],
remainder: [0; MAX_DEAL],
}
}
fn add(&mut self, other: &Counts) {
for i in 0..MAX_DEAL {
self.sets[i] += other.sets[i];
self.no_sets[i] += other.no_sets[i];
self.remainder[i] += other.remainder[i];
}
}
fn num_simulated(&self) -> u64 {
self.remainder.iter().sum()
}
fn print_hand_stats(&self) {
let mut table = Table::new();
table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(row![r => "hand", "sets", "no sets", "total", "ratio", "% with no sets"]);
let iter = self.sets.iter().zip(self.no_sets.iter()).enumerate();
for (hand_size, (&sets, &no_sets)) in iter {
if hand_size == 0 || no_sets == 0 {
continue;
}
let total_hands = sets + no_sets;
// no sets as a percentage of all hands of this size
let percentage = (no_sets as f64 / total_hands as f64) * 100.0;
// ratio of sets : no sets
let ratio = sets as f64 / no_sets as f64;
let ratio_string = if ratio > 1.0 {
format!("{}:1", ratio.round() as usize)
} else {
format!("1:{}", (1.0 / ratio).round() as usize)
};
table.add_row(row![r => &hand_size.to_string(),
&pretty_print(sets),
&pretty_print(no_sets),
&pretty_print(total_hands),
&ratio_string,
&format!("{:.5} %", percentage)]);
}
table.printstd();
}
fn print_end_game_stats(&self) {
let mut table = Table::new();
table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(row![r => "cards left", "occurrences", "% of games"]);
let num_games = self.num_simulated();
for (hand_size, &count) in self.remainder.iter().enumerate() {
if count == 0 {
continue;
}
let percentage = (count as f64 / num_games as f64) * 100.0;
table.add_row(row![r => &hand_size.to_string(),
&pretty_print(count),
&format!("{:.5} %", percentage)]);
}
table.printstd();
}
}
////////////////////////////////////////////////////////////////////////////////
// IndexDeck
////////////////////////////////////////////////////////////////////////////////
#[derive(Default, Clone)]
pub struct IndexDeck {
stock: Vec,
}
// A deck of indices rather than cards. It didn't seem worth
// generalizing `core::Deck` for the optimizations used in this
// program, so a bit of code duplication here.
impl IndexDeck {
/// Returns a shuffled `IndexDeck`.
pub fn new() -> IndexDeck {
let mut indices = (0..81).collect::>();
indices.shuffle();
IndexDeck { stock: indices }
}
pub fn is_empty(&self) -> bool {
self.stock.is_empty()
}
pub fn remainder(&self) -> usize {
self.stock.len()
}
pub fn draw(&mut self, n: usize) -> Vec {
let r = self.remainder();
let x = cmp::min(n, r);
self.stock.split_off(r - x)
}
}
////////////////////////////////////////////////////////////////////////////////
// Support Functions
////////////////////////////////////////////////////////////////////////////////
/// Lookup table for Sets.
static mut SETS: [[usize; 81]; 81] = [[0; 81]; 81];
fn build_lookup() {
let cards = cards();
for (&a, &b) in (0..81).collect::>().pairs() {
let c = (cards[a], cards[b]).complete_set().index();
unsafe {
SETS[a][b] = c;
// `complete_set()` is commutative
SETS[b][a] = c;
}
}
}
#[inline(always)]
fn is_set(a: usize, b: usize, c: usize) -> bool {
unsafe { *SETS.get_unchecked(a).get_unchecked(b) == c }
}
fn find_random_set(hand: &[usize]) -> Option<(usize, usize, usize)> {
let mut sets = Vec::new();
for x in 2..hand.len() {
let a = hand[x];
for y in 1..x {
let b = hand[y];
for &c in hand.iter().take(y) {
if is_set(a, b, c) {
sets.push((a, b, c));
}
}
}
}
if sets.is_empty() {
None
} else {
let mut rng = thread_rng();
let random_ix = rng.gen_range(0, sets.len());
Some(sets[random_ix])
}
}
////////////////////////////////////////////////////////////////////////////////
// Simulate
////////////////////////////////////////////////////////////////////////////////
fn simulate_game(counts: &mut Counts) {
let mut deck = IndexDeck::new();
let mut hand = deck.draw(INITIAL_DEAL);
'game: loop {
if let Some((a, b, c)) = find_random_set(&hand) {
counts.sets[hand.len()] += 1;
// remove the set
hand.retain(|&x| x != a && x != b && x != c);
if hand.len() < INITIAL_DEAL {
// deal more cards to replace removed set
hand.append(&mut deck.draw(SET_SIZE));
}
} else {
counts.no_sets[hand.len()] += 1;
if deck.is_empty() {
// no sets and no stock remaining: game over
counts.remainder[hand.len()] += 1;
break 'game;
} else {
// deal more cards to increase odds of set
hand.append(&mut deck.draw(SET_SIZE));
}
}
}
}
fn run_simulations(num_games: u64, num_threads: u64) {
let start_time = PreciseTime::now();
let (tx, rx) = mpsc::channel();
let (thread_chunk, rem) = (num_games / num_threads, num_games % num_threads);
// initialize set lookup table
build_lookup();
// launch threads
for ix in 0..num_threads {
let tx = tx.clone();
let num = thread_chunk + if ix == 0 { rem } else { 0 };
thread::spawn(move || {
let mut counts = Counts::zero();
for _ in 0..num {
simulate_game(&mut counts)
}
tx.send(counts).unwrap();
});
}
// collate results
let mut totals = Counts::zero();
for _ in 0..num_threads {
let counts = rx.recv().unwrap();
totals.add(&counts);
}
// summary
println!("{} seconds elapsed.\n", start_time.to(PreciseTime::now()));
totals.print_hand_stats();
println!();
totals.print_end_game_stats();
}
////////////////////////////////////////////////////////////////////////////////
// main
////////////////////////////////////////////////////////////////////////////////
fn main() {
let games_help = &format!(
"Sets number of games to simulate (default: {})",
pretty_print(NUM_GAMES)
);
let matches = clap_app!(simulate =>
(version: VERSION)
(about: "Gather statistics for simulated games of SET.")
(@arg GAMES: -g --games +takes_value games_help)
(@arg THREADS: -t --threads +takes_value "Sets number of threads")
).get_matches();
let num_games = value_t!(matches, "GAMES", u64).unwrap_or(NUM_GAMES);
let num_threads = value_t!(matches, "THREADS", u64).unwrap_or(num_cpus::get() as u64);
println!(
"Simulating {} games. This may take some time...",
pretty_print(num_games)
);
run_simulations(num_games, num_threads);
}