simulate/
simulate.rs

1// Copyright (C) 2017 Steve Sprang
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16//! Gather statistics for simulated games of SET.
17//!
18//! Results on *AMD Ryzen 7 1800x @ 3.9GHz* with 16 threads:
19//!
20//! Simulating 1_000_000_000 games. This may take some time...
21//! PT1280.803583379S seconds elapsed.
22//!
23//!  hand |           sets |       no sets |          total |   ratio | % with no sets
24//! ------+----------------+---------------+----------------+---------+----------------
25//!     6 |     12_229_414 |   468_288_234 |    480_517_648 |    1:38 |     97.45495 %
26//!     9 |    480_517_648 |   445_045_030 |    925_562_678 |     1:1 |     48.08373 %
27//!    12 | 22_385_130_429 | 1_597_587_780 | 23_982_718_209 |    14:1 |      6.66141 %
28//!    15 |  1_523_150_458 |    17_280_709 |  1_540_431_167 |    88:1 |      1.12181 %
29//!    18 |     16_513_441 |         1_082 |     16_514_523 | 15262:1 |      0.00655 %
30//!
31//!  cards left | occurrences | % of games
32//! ------------+-------------+------------
33//!           0 |  12_229_414 |  1.22294 %
34//!           6 | 468_288_234 | 46.82882 %
35//!           9 | 445_045_030 | 44.50450 %
36//!          12 |  73_670_054 |  7.36701 %
37//!          15 |     766_796 |  0.07668 %
38//!          18 |         472 |  0.00005 %
39//!
40//! As an optimization, this program makes use of the fact that there is an
41//! isomorphism between a `core::Card` and its index. It only uses `core::Card`
42//! objects directly when initializing the `SETS` lookup table, and otherwise just
43//! works with the cards by index.
44
45#[macro_use]
46extern crate clap;
47extern crate core;
48extern crate num_cpus;
49#[macro_use]
50extern crate prettytable;
51extern crate rand;
52extern crate time;
53
54use prettytable::Table;
55use prettytable::format::consts;
56use rand::{thread_rng, Rng};
57use std::cmp;
58use std::sync::mpsc;
59use std::thread;
60use time::PreciseTime;
61
62use core::card::*;
63use core::deck::cards;
64use core::pair_iter::PairIter;
65use core::shuffle::Shuffle;
66use core::utils::*;
67
68const VERSION: &str = env!("CARGO_PKG_VERSION");
69const NUM_GAMES: u64 = 1_000_000;
70const INITIAL_DEAL: usize = 12;
71const MAX_DEAL: usize = 22;
72const SET_SIZE: usize = 3;
73
74////////////////////////////////////////////////////////////////////////////////
75// Counts
76////////////////////////////////////////////////////////////////////////////////
77
78struct Counts {
79    /// Playable hand.
80    sets: [u64; MAX_DEAL],
81    /// Stuck hand.
82    no_sets: [u64; MAX_DEAL],
83    /// Game over.
84    remainder: [u64; MAX_DEAL],
85}
86
87impl Counts {
88    fn zero() -> Counts {
89        Counts {
90            sets: [0; MAX_DEAL],
91            no_sets: [0; MAX_DEAL],
92            remainder: [0; MAX_DEAL],
93        }
94    }
95
96    fn add(&mut self, other: &Counts) {
97        for i in 0..MAX_DEAL {
98            self.sets[i] += other.sets[i];
99            self.no_sets[i] += other.no_sets[i];
100            self.remainder[i] += other.remainder[i];
101        }
102    }
103
104    fn num_simulated(&self) -> u64 {
105        self.remainder.iter().sum()
106    }
107
108    fn print_hand_stats(&self) {
109        let mut table = Table::new();
110        table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
111        table.set_titles(row![r => "hand", "sets", "no sets", "total", "ratio", "% with no sets"]);
112
113        let iter = self.sets.iter().zip(self.no_sets.iter()).enumerate();
114
115        for (hand_size, (&sets, &no_sets)) in iter {
116            if hand_size == 0 || no_sets == 0 {
117                continue;
118            }
119
120            let total_hands = sets + no_sets;
121            // no sets as a percentage of all hands of this size
122            let percentage = (no_sets as f64 / total_hands as f64) * 100.0;
123
124            // ratio of sets : no sets
125            let ratio = sets as f64 / no_sets as f64;
126            let ratio_string = if ratio > 1.0 {
127                format!("{}:1", ratio.round() as usize)
128            } else {
129                format!("1:{}", (1.0 / ratio).round() as usize)
130            };
131
132            table.add_row(row![r => &hand_size.to_string(),
133                               &pretty_print(sets),
134                               &pretty_print(no_sets),
135                               &pretty_print(total_hands),
136                               &ratio_string,
137                               &format!("{:.5} %", percentage)]);
138        }
139
140        table.printstd();
141    }
142
143    fn print_end_game_stats(&self) {
144        let mut table = Table::new();
145        table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
146        table.set_titles(row![r => "cards left", "occurrences", "% of games"]);
147
148        let num_games = self.num_simulated();
149
150        for (hand_size, &count) in self.remainder.iter().enumerate() {
151            if count == 0 {
152                continue;
153            }
154
155            let percentage = (count as f64 / num_games as f64) * 100.0;
156            table.add_row(row![r => &hand_size.to_string(),
157                               &pretty_print(count),
158                               &format!("{:.5} %", percentage)]);
159        }
160
161        table.printstd();
162    }
163}
164
165////////////////////////////////////////////////////////////////////////////////
166// IndexDeck
167////////////////////////////////////////////////////////////////////////////////
168
169#[derive(Default, Clone)]
170pub struct IndexDeck {
171    stock: Vec<usize>,
172}
173
174// A deck of indices rather than cards. It didn't seem worth
175// generalizing `core::Deck` for the optimizations used in this
176// program, so a bit of code duplication here.
177impl IndexDeck {
178    /// Returns a shuffled `IndexDeck`.
179    pub fn new() -> IndexDeck {
180        let mut indices = (0..81).collect::<Vec<_>>();
181        indices.shuffle();
182        IndexDeck { stock: indices }
183    }
184
185    pub fn is_empty(&self) -> bool {
186        self.stock.is_empty()
187    }
188
189    pub fn remainder(&self) -> usize {
190        self.stock.len()
191    }
192
193    pub fn draw(&mut self, n: usize) -> Vec<usize> {
194        let r = self.remainder();
195        let x = cmp::min(n, r);
196        self.stock.split_off(r - x)
197    }
198}
199
200////////////////////////////////////////////////////////////////////////////////
201// Support Functions
202////////////////////////////////////////////////////////////////////////////////
203
204/// Lookup table for Sets.
205static mut SETS: [[usize; 81]; 81] = [[0; 81]; 81];
206
207fn build_lookup() {
208    let cards = cards();
209
210    for (&a, &b) in (0..81).collect::<Vec<_>>().pairs() {
211        let c = (cards[a], cards[b]).complete_set().index();
212        unsafe {
213            SETS[a][b] = c;
214            // `complete_set()` is commutative
215            SETS[b][a] = c;
216        }
217    }
218}
219
220#[inline(always)]
221fn is_set(a: usize, b: usize, c: usize) -> bool {
222    unsafe { *SETS.get_unchecked(a).get_unchecked(b) == c }
223}
224
225fn find_random_set(hand: &[usize]) -> Option<(usize, usize, usize)> {
226    let mut sets = Vec::new();
227
228    for x in 2..hand.len() {
229        let a = hand[x];
230        for y in 1..x {
231            let b = hand[y];
232            for &c in hand.iter().take(y) {
233                if is_set(a, b, c) {
234                    sets.push((a, b, c));
235                }
236            }
237        }
238    }
239
240    if sets.is_empty() {
241        None
242    } else {
243        let mut rng = thread_rng();
244        let random_ix = rng.gen_range(0, sets.len());
245        Some(sets[random_ix])
246    }
247}
248
249////////////////////////////////////////////////////////////////////////////////
250// Simulate
251////////////////////////////////////////////////////////////////////////////////
252
253fn simulate_game(counts: &mut Counts) {
254    let mut deck = IndexDeck::new();
255    let mut hand = deck.draw(INITIAL_DEAL);
256
257    'game: loop {
258        if let Some((a, b, c)) = find_random_set(&hand) {
259            counts.sets[hand.len()] += 1;
260
261            // remove the set
262            hand.retain(|&x| x != a && x != b && x != c);
263
264            if hand.len() < INITIAL_DEAL {
265                // deal more cards to replace removed set
266                hand.append(&mut deck.draw(SET_SIZE));
267            }
268        } else {
269            counts.no_sets[hand.len()] += 1;
270
271            if deck.is_empty() {
272                // no sets and no stock remaining: game over
273                counts.remainder[hand.len()] += 1;
274                break 'game;
275            } else {
276                // deal more cards to increase odds of set
277                hand.append(&mut deck.draw(SET_SIZE));
278            }
279        }
280    }
281}
282
283fn run_simulations(num_games: u64, num_threads: u64) {
284    let start_time = PreciseTime::now();
285    let (tx, rx) = mpsc::channel();
286    let (thread_chunk, rem) = (num_games / num_threads, num_games % num_threads);
287
288    // initialize set lookup table
289    build_lookup();
290
291    // launch threads
292    for ix in 0..num_threads {
293        let tx = tx.clone();
294        let num = thread_chunk + if ix == 0 { rem } else { 0 };
295
296        thread::spawn(move || {
297            let mut counts = Counts::zero();
298            for _ in 0..num {
299                simulate_game(&mut counts)
300            }
301            tx.send(counts).unwrap();
302        });
303    }
304
305    // collate results
306    let mut totals = Counts::zero();
307    for _ in 0..num_threads {
308        let counts = rx.recv().unwrap();
309        totals.add(&counts);
310    }
311
312    // summary
313    println!("{} seconds elapsed.\n", start_time.to(PreciseTime::now()));
314    totals.print_hand_stats();
315    println!();
316    totals.print_end_game_stats();
317}
318
319////////////////////////////////////////////////////////////////////////////////
320// main
321////////////////////////////////////////////////////////////////////////////////
322
323fn main() {
324    let games_help = &format!(
325        "Sets number of games to simulate (default: {})",
326        pretty_print(NUM_GAMES)
327    );
328
329    let matches = clap_app!(simulate =>
330        (version: VERSION)
331        (about: "Gather statistics for simulated games of SET.")
332        (@arg GAMES: -g --games +takes_value games_help)
333        (@arg THREADS: -t --threads +takes_value "Sets number of threads")
334    ).get_matches();
335
336    let num_games = value_t!(matches, "GAMES", u64).unwrap_or(NUM_GAMES);
337    let num_threads = value_t!(matches, "THREADS", u64).unwrap_or(num_cpus::get() as u64);
338
339    println!(
340        "Simulating {} games. This may take some time...",
341        pretty_print(num_games)
342    );
343    run_simulations(num_games, num_threads);
344}