~bsprague/chaos

e4928b7215a1444ad96eb9cb5394841783a90eda — Brandon Sprague 3 months ago
Initial commit of game + Monte Carlo
4 files changed, 242 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.lock
A Cargo.toml
A src/main.rs
A  => .gitignore +1 -0
@@ 1,1 @@
/target

A  => Cargo.lock +75 -0
@@ 1,75 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chaos-game"
version = "0.1.0"
dependencies = [
 "rand",
]

[[package]]
name = "getrandom"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"

[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"

[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc",
 "rand_chacha",
 "rand_core",
]

[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
 "ppv-lite86",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
 "getrandom",
]

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

A  => Cargo.toml +9 -0
@@ 1,9 @@
[package]
name = "chaos-game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"

A  => src/main.rs +157 -0
@@ 1,157 @@
use rand::Rng;
use std::collections::HashMap;

fn main() -> Result<(), PlayError> {
    let mut rng = rand::thread_rng();

    let mut wins = HashMap::from([(Player::Blue, 0), (Player::Orange, 0)]);

    // Play a 100000 random games
    for _ in 0..100000 {
        let mut b = Board::new();
        while let None = b.winner() {
            let m = Move {
                x: rng.gen_range(0..=MAX_DIM),
                y: rng.gen_range(0..=MAX_DIM),
            };
            b.play(m)?;
        }
        wins.entry(b.winner().unwrap()).and_modify(|v| *v += 1);
    }

    println!("Results: {:?}", wins);

    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Player {
    Blue,
    Orange,
}

impl Player {
    fn topple_deltas(&self) -> [(isize, isize); 4] {
        match self {
            Player::Blue => [(0, -1), (1, 0), (0, 1), (-1, 0)],
            Player::Orange => [(0, 1), (-1, 0), (0, -1), (1, 0)],
        }
    }
}

const BOARD_SIZE: usize = 3;
const MAX_DIM: u8 = (BOARD_SIZE as u8) - 1;

#[derive(Debug)]
enum PlayError {
    MoveOutOfBounds,
    Internal(String),
}

#[derive(Debug, Clone)]
struct Move {
    x: u8,
    y: u8,
}

#[derive(Debug)]
struct Board {
    next_to_play: Player,
    stacks: [[Vec<Player>; BOARD_SIZE]; BOARD_SIZE],
    held_pieces: HashMap<Player, u32>,
}

// Both players use the same coordinate system, e.g.

// 1 2 3
// 4 5 6
// 7 8 9

// Where '1' is (0, 0), '8' is (1, 2) and '9' is (2, 2)

impl Board {
    fn new() -> Board {
        Board {
            next_to_play: Player::Blue,
            stacks: Default::default(),
            held_pieces: HashMap::from([(Player::Blue, 12), (Player::Orange, 12)]),
        }
    }

    fn play(&mut self, mv: Move) -> Result<(), PlayError> {
        if mv.x > MAX_DIM || mv.y > MAX_DIM {
            return Err(PlayError::MoveOutOfBounds);
        }
        // Remove from hand
        self.held_pieces
            .entry(self.next_to_play.clone())
            .and_modify(|v| *v -= 1);
        self.stacks[usize::from(mv.x)][usize::from(mv.y)].push(self.next_to_play.clone());
        self.handle_topples(0)?;
        self.next_to_play = match self.next_to_play {
            Player::Blue => Player::Orange,
            Player::Orange => Player::Blue,
        };
        Ok(())
    }

    fn handle_topples(&mut self, depth: usize) -> Result<(), PlayError> {
        if depth > 10 {
            return Err(PlayError::Internal("toppled more than 10 times for a single move, which feels like it should be impossible".to_owned()));
        }

        let mut toppled_anything = false;
        for x in 0..self.stacks.len() {
            for y in 0..self.stacks[x].len() {
                // Topple this stack
                if self.stacks[x][y].len() >= 4 {
                    self.topple(x, y)?;
                    toppled_anything = true;
                }
            }
        }

        // This seems like an excellent way to get stuck in an infinite loop, so we include a depth parameter.
        if toppled_anything {
            self.handle_topples(depth + 1)?;
        }

        Ok(())
    }

    fn topple(&mut self, x: usize, y: usize) -> Result<(), PlayError> {
        let ix = isize::try_from(x)
            .map_err(|_| PlayError::Internal(format!("failed to parse x {:?} as isize", x)))?;
        let iy = isize::try_from(y)
            .map_err(|_| PlayError::Internal(format!("failed to parse y {:?} as isize", y)))?;
        for (dx, dy) in self.next_to_play.topple_deltas() {
            let toppling_piece = self.stacks[x][y].pop().unwrap();

            let new_x = ix + dx;
            let new_y = iy + dy;
            if new_x < 0
                || new_x >= BOARD_SIZE as isize
                || new_y < 0
                || new_y >= BOARD_SIZE as isize
            {
                // Return to hand
                self.held_pieces
                    .entry(toppling_piece)
                    .and_modify(|v| *v += 1);
                continue;
            }
            // Put at the new location.
            self.stacks[new_x as usize][new_y as usize].push(toppling_piece);
        }
        Ok(())
    }

    fn winner(&self) -> Option<Player> {
        for (player, cnt) in &self.held_pieces {
            if *cnt == 0 {
                return Some(player.clone());
            }
        }
        return None;
    }
}