Rust simulator

This commit is contained in:
Pyrex 2024-02-05 21:37:01 -08:00
parent bdc8c91078
commit 4a0673d085
9 changed files with 717 additions and 0 deletions

1
simulator/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

89
simulator/Cargo.lock generated Normal file
View File

@ -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"

11
simulator/Cargo.toml Normal file
View File

@ -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"] }

335
simulator/src/board.rs Normal file
View File

@ -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<usize>,
log: Vec<Move>,
setup: &'a Setup,
slots: Vec<Slot<'a>>,
wells: Vec<Well<'a>>,
}
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<CardMetadata>, 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<Card> =
(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<Card> = (&eligible_bottom_row[..n_usable_slots as usize]).iter().cloned().collect();
for i in bottom_row.iter() {
available.remove(i);
}
let mut eligible: Vec<Card> = (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<Move> {
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<Card>,
}
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<Card> {
self.contents.last().cloned()
}
fn pop(&mut self, zobrist: &mut Zobrist) -> Option<Card> {
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<dyn Fn(Option<CardMetadata>, CardMetadata) -> bool>;
struct Well<'a> {
setup: &'a Setup,
ix: u8,
accept_cb: AcceptCb,
contents: Vec<Card>,
}
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<Card> {
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);
}
}

89
simulator/src/main.rs Normal file
View File

@ -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;
}

94
simulator/src/ruleset.rs Normal file
View File

@ -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<Card>,
pub suits: Vec<u8>,
pub cards: Vec<CardMetadata>,
}
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<Setup> {
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<u8> = 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 }
}
}

33
simulator/src/seen.rs Normal file
View File

@ -0,0 +1,33 @@
const SIZE: usize = 0x800000;
const SIZE_MASK: usize = SIZE - 1;
pub struct Seen {
pub bitarr: Vec<u8>,
}
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
}
}

36
simulator/src/zobrist.rs Normal file
View File

@ -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)
}
}

View File

@ -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);
}
}