From 4a0673d0854fc21709ae3105436fdd31393c904d Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Mon, 5 Feb 2024 21:37:01 -0800 Subject: [PATCH] Rust simulator --- simulator/.gitignore | 1 + simulator/Cargo.lock | 89 +++++++++ simulator/Cargo.toml | 11 ++ simulator/src/board.rs | 335 +++++++++++++++++++++++++++++++++ simulator/src/main.rs | 89 +++++++++ simulator/src/ruleset.rs | 94 +++++++++ simulator/src/seen.rs | 33 ++++ simulator/src/zobrist.rs | 36 ++++ simulator/src/zobrist_test.old | 29 +++ 9 files changed, 717 insertions(+) create mode 100644 simulator/.gitignore create mode 100644 simulator/Cargo.lock create mode 100644 simulator/Cargo.toml create mode 100644 simulator/src/board.rs create mode 100644 simulator/src/main.rs create mode 100644 simulator/src/ruleset.rs create mode 100644 simulator/src/seen.rs create mode 100644 simulator/src/zobrist.rs create mode 100644 simulator/src/zobrist_test.old diff --git a/simulator/.gitignore b/simulator/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/simulator/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/simulator/Cargo.lock b/simulator/Cargo.lock new file mode 100644 index 0000000..ba9f81c --- /dev/null +++ b/simulator/Cargo.lock @@ -0,0 +1,89 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +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 = "simulator" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand", + "xxhash-rust", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "xxhash-rust" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml new file mode 100644 index 0000000..6867df2 --- /dev/null +++ b/simulator/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simulator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.79" +rand = "0.8.5" +xxhash-rust = { version = "0.8.8", features = ["xxh3"] } diff --git a/simulator/src/board.rs b/simulator/src/board.rs new file mode 100644 index 0000000..1036052 --- /dev/null +++ b/simulator/src/board.rs @@ -0,0 +1,335 @@ +use std::collections::HashSet; + +use crate::{ruleset::{Card, CardMetadata, Setup}, zobrist::{Feature, Zobrist}}; +use rand::Rng; +use rand::seq::SliceRandom; + +#[derive(Clone, Copy, Debug, Hash)] +pub enum Move { + ToSlot { from_slot: u8, to_slot: u8 }, + ToWell { from_slot: u8, to_well: u8 } +} + + +pub struct Board<'a> { + zobrist: Zobrist, + + checkpoints: Vec, + log: Vec, + + setup: &'a Setup, + slots: Vec>, + wells: Vec>, +} + +impl<'a> Board<'a> { + pub fn new(setup: &'a Setup) -> Self { + let mut slots = vec![]; + let mut wells = vec![]; + + // ..n_slots: normal + for i in 0..setup.ruleset.n_slots { + slots.push(Slot::new(setup, slots.len() as u8, 32)); + } + // n_slots: top cell + slots.push(Slot::new(setup, slots.len() as u8, 1)); + + // ..n_suits: conventional wells + for _ in &setup.deck.suits { + wells.push(Well::new(setup, wells.len() as u8, Box::new( + |prev: Option, new: CardMetadata| { + if let Some(p) = prev { + if new.suit != p.suit { return false } + return new.suit == p.suit && new.rank == p.rank + 1 + } + panic!("well should have had an ace or something to start") + } + ))) + } + + if setup.ruleset.n_arcana > 0 { + // n_suits: arcana ascending + wells.push(Well::new(setup, wells.len() as u8, Box::new( + |prev, new| { + if new.suit != b'a' { return false } + if let Some(p) = prev { + return new.rank == p.rank + 1; + } + return new.rank == 0; + } + ))); + + // n_suits+1: arcana descending + let top_rank = setup.ruleset.n_arcana - 1; + wells.push(Well::new(setup, wells.len() as u8, Box::new( + move |prev, new| { + if new.suit != b'a' { return false } + if let Some(p) = prev { + return new.rank + 1 == p.rank; + } + return new.rank == top_rank; + } + ))) + } + Board { + zobrist: Zobrist::new(), + + checkpoints: vec![], + log: vec![], + + setup, + slots, + wells, + } + } + + pub fn display(&self) { + println!("Wells:"); + for w in &self.wells { + println!("- {:?}", w.contents); + } + println!("\nSlots:"); + for s in &self.slots { + println!("- {:?}", s.contents); + } + } + + pub fn zobrist_key(&self) -> u64 { + self.zobrist.value + } + + pub fn is_won(&self) -> bool { + for s in self.slots.iter() { + if s.peek().is_some() { + return false + } + } + return true + } + + pub fn deal(&mut self, rng: &mut impl Rng) { + let n_usable_slots = self.setup.ruleset.n_slots - 1; + + let mut available = HashSet::new(); + for c in 0..self.setup.deck.cards.len() { + available.insert(Card(c as u8)); + } + + // place aces in wells + for i in 0..self.setup.deck.aces.len() { + let ace = self.setup.deck.aces[i]; + self.wells[i].push(&mut self.zobrist, self.setup.deck.aces[i]); + available.remove(&ace); + } + + let mut eligible_bottom_row: Vec = + (0..self.setup.deck.cards.len()) + .map(|i| Card(i as u8)) + .filter(|card| { + if !available.contains(card) { + return false; + } + + for w in self.wells.iter() { + if w.would_accept(*card) { + return false + } + } + return true + }) + .collect(); + + eligible_bottom_row.shuffle(rng); + let bottom_row: Vec = (&eligible_bottom_row[..n_usable_slots as usize]).iter().cloned().collect(); + for i in bottom_row.iter() { + available.remove(i); + } + + let mut eligible: Vec = (0..self.setup.deck.cards.len()) + .map(|i| Card(i as u8)) + .filter(|c| available.contains(&c)) + .collect(); + eligible.shuffle(rng); + eligible.extend(bottom_row); + + for (i, card) in eligible.iter().cloned().enumerate() { + let i = i as u8 % n_usable_slots; + let real_slot = if i < n_usable_slots / 2 { i } else { i + 1 }; + self.slots[real_slot as usize].push(&mut self.zobrist, card); + } + } + + fn settle(&mut self) { + while self.settle1() { } + } + + fn settle1(&mut self) -> bool { + let special_slot_full = self.slots[self.setup.ruleset.n_slots as usize].peek().is_some(); + for s in 0..self.slots.len() { + if let Some(top) = self.slots[s].peek() { + for w in 0..self.wells.len() { + if special_slot_full && (w as u8) < self.setup.ruleset.n_suits { + // top wells are blocked + } else if self.wells[w].would_accept(top) { + self.internal_perform(Move::ToWell { from_slot: s as u8, to_well: w as u8 }); + return true + } + } + } + } + return false; + } + + // TODO: Do this without allocs? + pub fn legal_moves(&self) -> Vec { + let mut moves = vec![]; + for i in 0..self.slots.len() { + if let Some(top) = self.slots[i].peek() { + for j in 0..self.slots.len() { + if i == j { continue; } + if !self.slots[j].would_accept(top) { continue; } + moves.push(Move::ToSlot { from_slot: i as u8, to_slot: j as u8 }) + } + } + } + moves + } + + fn create_checkpoint(&mut self) { + self.checkpoints.push(self.log.len()); + } + + pub fn perform(&mut self, m: Move) { + self.create_checkpoint(); + self.internal_perform(m); + self.settle(); + } + + pub fn undo(&mut self) { + let cp = self.checkpoints.pop().expect("undo() without perform()"); + while self.log.len() > cp { + let m = self.log.pop().unwrap(); + self.internal_unperform(m) + } + } + + fn internal_perform(&mut self, m: Move) { + // println!("internal perform: {:?}", m); + match m { + Move::ToWell { from_slot, to_well } => { + let top = self.slots[from_slot as usize].pop(&mut self.zobrist).expect("move from empty slot"); + self.wells[to_well as usize].push(&mut self.zobrist, top); + } + Move::ToSlot { from_slot, to_slot } => { + let top = self.slots[from_slot as usize].pop(&mut self.zobrist).expect("move from empty slot"); + self.slots[to_slot as usize].push(&mut self.zobrist, top); + } + } + self.log.push(m); + } + + fn internal_unperform(&mut self, m: Move) { + // println!("internal unperform: {:?}", m); + match m { + Move::ToWell { from_slot, to_well } => { + let top = self.wells[to_well as usize].pop(&mut self.zobrist).expect("move from empty well"); + self.slots[from_slot as usize].push(&mut self.zobrist, top); + } + Move::ToSlot { from_slot, to_slot } => { + let top = self.slots[to_slot as usize].pop(&mut self.zobrist).expect("move from empty slot"); + self.slots[from_slot as usize].push(&mut self.zobrist, top); + } + } + } +} + +struct Slot<'a> { + ix: u8, + setup: &'a Setup, + max_n: u8, + contents: Vec, +} + +impl<'a> Slot<'a> { + fn new(setup: &'a Setup, ix: u8, max_n: u8) -> Self { + Slot { + setup, + ix, + contents: Vec::with_capacity(max_n as usize), + max_n, + } + } + + fn push(&mut self, zobrist: &mut Zobrist, new: Card) { + zobrist.toggle(Feature::CardAt { card: new, slot: self.ix, depth: self.contents.len() as u8}); + self.contents.push(new); + } + + fn peek(&self) -> Option { + self.contents.last().cloned() + } + + fn pop(&mut self, zobrist: &mut Zobrist) -> Option { + let card = self.contents.pop(); + let depth = self.contents.len() as u8; + if let Some(card) = card { + zobrist.toggle(Feature::CardAt { card, slot: self.ix, depth }); + } + return card + } + + fn would_accept(&self, top: Card) -> bool { + if self.contents.len() as u8 >= self.max_n { + return false; + } + + let last = self.contents.last(); + if let Some(l) = last { + let l = self.setup.deck.cards[l.0 as usize]; + let n = self.setup.deck.cards[top.0 as usize]; + return l.suit == n.suit && (l.rank + 1 == n.rank || l.rank == n.rank + 1); + } else { + return true; + } + } +} +type AcceptCb = Box, CardMetadata) -> bool>; + +struct Well<'a> { + setup: &'a Setup, + ix: u8, + accept_cb: AcceptCb, + contents: Vec, +} + +impl<'a> Well<'a> { + fn new(setup: &'a Setup, ix: u8, accept_cb: AcceptCb) -> Self { + Well { + setup, + ix, + accept_cb, + contents: vec![] + } + } + + fn push(&mut self, zobrist: &mut Zobrist, new: Card) { + zobrist.toggle(Feature::WellHas { card: new, well: self.ix }); + self.contents.push(new); + } + + fn pop(&mut self, zobrist: &mut Zobrist) -> Option { + let card = self.contents.pop(); + if let Some(c) = card { + zobrist.toggle(Feature::WellHas { card: c, well: self.ix }); + } + card + } + + fn would_accept(&self, new: Card) -> bool { + let prev = self.contents.last().map(|p| + self.setup.deck.cards[p.0 as usize] + ); + let new = self.setup.deck.cards[new.0 as usize]; + return (self.accept_cb)(prev, new); + } +} \ No newline at end of file diff --git a/simulator/src/main.rs b/simulator/src/main.rs new file mode 100644 index 0000000..eca5259 --- /dev/null +++ b/simulator/src/main.rs @@ -0,0 +1,89 @@ +use board::Board; +use ruleset::Ruleset; +use seen::Seen; + +use crate::{ruleset::Card, zobrist::Zobrist}; + +mod board; +mod ruleset; +mod seen; +mod zobrist; + + +fn main() { + /* + let ruleset = Ruleset { + n_slots: 11, + n_suits: 4, + n_cards_per_suit: 13, + n_arcana: 22 + }; + */ + /* + let ruleset = Ruleset { + n_slots: 5, + n_suits: 1, + n_cards_per_suit: 9, + n_arcana: 0 + }; + */ + /* + let ruleset = Ruleset { + n_slots: 7, + n_suits: 2, + n_cards_per_suit: 9, + n_arcana: 8 + }; + */ + let ruleset = Ruleset { + n_slots: 9, + n_suits: 3, + n_cards_per_suit: 11, + n_arcana: 18 + }; + let setup = ruleset.compile().expect("compilation should succeed"); + let mut board = Board::new(&setup); + board.deal(&mut rand::thread_rng()); + board.display(); + + println!("is_winnable: {}", is_winnable(board)); + + // println!("Legal moves: {:#?}", board.legal_moves()); + +} + +fn is_winnable(mut board: Board<'_>) -> bool { + let mut seen = Seen::new(); + + explore(0, &mut board, &mut seen) +} + +fn explore(depth: usize, board: &mut Board<'_>, seen: &mut Seen) -> bool { + if depth > 200 { + return false; + } + + if seen.contains(board.zobrist_key()) { + return false + } + seen.add(board.zobrist_key()); + + if board.is_won() { + board.display(); + return true + } + + for m in board.legal_moves() { + let hash_1 = board.zobrist_key(); + board.perform(m); + // println!("try: {:X?} {:?}", board.zobrist_key(), m); + if explore(depth + 1, board, seen) { + return true; + } + // println!("undo: {:X?} {:?}", board.zobrist_key(), m); + board.undo(); + let hash_2 = board.zobrist_key(); + assert_eq!(hash_1, hash_2) + } + return false; +} \ No newline at end of file diff --git a/simulator/src/ruleset.rs b/simulator/src/ruleset.rs new file mode 100644 index 0000000..605f94e --- /dev/null +++ b/simulator/src/ruleset.rs @@ -0,0 +1,94 @@ +use anyhow::bail; + +pub struct Ruleset { + pub n_slots: u8, + pub n_suits: u8, + pub n_cards_per_suit: u8, + pub n_arcana: u8, +} + +pub struct Deck { + pub aces: Vec, + pub suits: Vec, + pub cards: Vec, +} + +pub struct Setup { + pub ruleset: Ruleset, + pub deck: Deck +} + + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Card(pub u8); + + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct CardMetadata { pub suit: u8, pub rank: u8 } + +impl Ruleset { + fn usable_slots(&self) -> u8 { self.n_slots - 1 } + fn total_n_cards(&self) -> u8 { + self.n_arcana + self.n_suits * self.n_cards_per_suit + } + fn usable_n_cards(&self) -> u8 { + self.total_n_cards() - self.n_suits + } + + pub fn compile(self) -> anyhow::Result { + self.validate()?; + let deck = self.create_deck(); + Ok(Setup { + ruleset: self, + deck + }) + } + fn validate(&self) -> anyhow::Result<()> { + if self.n_slots > 11 { bail!("max 11 slots") } + if self.n_suits > 5 { bail!("max 5 suits") } + if self.n_cards_per_suit > 13 { bail!("max 13 cards per suit") } + if self.n_arcana > 99 { bail!("max 99 arcana") } + if self.usable_slots() % 2 != 0 { + bail!("must have even number of usable slots") + } + if self.usable_n_cards() % self.usable_slots() != 0 { + bail!( + "needs {} more cards for an even deal", + self.usable_slots() - (self.usable_n_cards() % self.usable_slots()) + ) + } + let instantly_placed_cards = self.n_suits + + if self.n_arcana == 0 { 0 } + else if self.n_arcana == 1 { 1 } + else { 2 }; + if self.total_n_cards() - instantly_placed_cards < self.usable_slots() { + bail!("can't place all cards without automoves"); + } + Ok(()) + } + + fn create_deck(&self) -> Deck { + let possible_suits = b"pscwb"; + let mut aces = vec![]; + let mut suits: Vec = vec![]; + suits.extend(possible_suits[..self.n_suits as usize].into_iter()); + let mut cards = vec![]; + + // suited cards + for &suit in &suits { + for rank in 1..=self.n_cards_per_suit { + if rank == 1 { + aces.push(Card(cards.len() as u8)) + } + cards.push(CardMetadata { suit, rank: rank as u8 }) + } + } + + // arcana + for rank in 0..self.n_arcana { + cards.push(CardMetadata {suit: b'a', rank}); + } + + Deck { aces, suits, cards } + } +} \ No newline at end of file diff --git a/simulator/src/seen.rs b/simulator/src/seen.rs new file mode 100644 index 0000000..5153720 --- /dev/null +++ b/simulator/src/seen.rs @@ -0,0 +1,33 @@ + + +const SIZE: usize = 0x800000; +const SIZE_MASK: usize = SIZE - 1; + +pub struct Seen { + pub bitarr: Vec, +} + +impl Seen { + pub fn new() -> Seen { + Seen { + bitarr: vec![0; SIZE] + } + } + + fn fix(&self, ix: u64) -> (usize, u8) { + let hi = (ix >> 3) & SIZE_MASK as u64; + let lo = 1 << (ix & 0x7); + (hi as usize, lo) + } + + // TODO: Elide bounds checks + pub fn add(&mut self, ix: u64) { + let (hi, lo) = self.fix(ix); + self.bitarr[hi] |= lo; + } + + pub fn contains(&self, ix: u64) -> bool { + let (hi, lo) = self.fix(ix); + self.bitarr[hi] & lo != 0 + } +} \ No newline at end of file diff --git a/simulator/src/zobrist.rs b/simulator/src/zobrist.rs new file mode 100644 index 0000000..f459737 --- /dev/null +++ b/simulator/src/zobrist.rs @@ -0,0 +1,36 @@ +use crate::ruleset::Card; +use xxhash_rust::xxh3::xxh3_64; + +const ZOBRIST_CONSTANT: u32 = 0xba7f00d5; + +pub struct Zobrist { + pub value: u64 +} + +impl Zobrist { + pub fn new() -> Zobrist { + Zobrist { value: 0 } + } + pub fn toggle(&mut self, feature: Feature) { + self.value ^= feature.zobrist() + } +} + + +#[derive(Clone, Copy, Debug)] +pub enum Feature { + CardAt { card: Card, slot: u8, depth: u8 }, + WellHas { card: Card, well: u8 } +} + +impl Feature { + pub fn zobrist(&self) -> u64 { + let mut msg: [u8; 8] = [0; 8]; + msg[0..4].copy_from_slice(&ZOBRIST_CONSTANT.to_le_bytes()); + match *self { + Self::CardAt { card, slot, depth } => msg[4..8].copy_from_slice(&[0, card.0, slot, depth]), + Self::WellHas { card, well } => msg[4..7].copy_from_slice(&[1, card.0, well]), + } + xxh3_64(&msg) + } +} \ No newline at end of file diff --git a/simulator/src/zobrist_test.old b/simulator/src/zobrist_test.old new file mode 100644 index 0000000..5c7fc2f --- /dev/null +++ b/simulator/src/zobrist_test.old @@ -0,0 +1,29 @@ +use crate::{ruleset::Card, zobrist::Zobrist}; + +mod board; +mod ruleset; +mod seen; +mod zobrist; + + +fn main() { + println!("Hello, world!"); + let mut zob = Zobrist::new(); + for feat in [ + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 0 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 1 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 2 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 3 }, + + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 0 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 1 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 2 }, + zobrist::Feature::CardAt { card: Card(0), slot: 0, depth: 3 }, + ] { + let old_zob = zob.value; + println!("{:?} => {:016X?}!", feat, feat.zobrist()); + zob.toggle(feat); + let new_zob = zob.value; + println!("Zobrist: {:016X?} => {:016X?}", old_zob, new_zob); + } +}