Rust simulator
This commit is contained in:
parent
bdc8c91078
commit
4a0673d085
1
simulator/.gitignore
vendored
Normal file
1
simulator/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
89
simulator/Cargo.lock
generated
Normal file
89
simulator/Cargo.lock
generated
Normal 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
11
simulator/Cargo.toml
Normal 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
335
simulator/src/board.rs
Normal 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
89
simulator/src/main.rs
Normal 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
94
simulator/src/ruleset.rs
Normal 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
33
simulator/src/seen.rs
Normal 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
36
simulator/src/zobrist.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
29
simulator/src/zobrist_test.old
Normal file
29
simulator/src/zobrist_test.old
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user