diff --git a/simulator/src/board.rs b/simulator/src/board.rs index 1036052..7d7e50d 100644 --- a/simulator/src/board.rs +++ b/simulator/src/board.rs @@ -1,8 +1,4 @@ -use std::collections::HashSet; - -use crate::{ruleset::{Card, CardMetadata, Setup}, zobrist::{Feature, Zobrist}}; -use rand::Rng; -use rand::seq::SliceRandom; +use crate::{ruleset::{Card, CardMetadata, Setup}, smart_dealer::Deal, zobrist::{Feature, Zobrist}}; #[derive(Clone, Copy, Debug, Hash)] pub enum Move { @@ -28,7 +24,7 @@ impl<'a> Board<'a> { let mut wells = vec![]; // ..n_slots: normal - for i in 0..setup.ruleset.n_slots { + for _ in 0..setup.ruleset.n_slots { slots.push(Slot::new(setup, slots.len() as u8, 32)); } // n_slots: top cell @@ -107,55 +103,18 @@ impl<'a> Board<'a> { return true } - pub fn deal(&mut self, rng: &mut impl Rng) { + pub fn deal(&mut self, deal: Deal) { 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)); + for (w, c) in self.setup.deck.aces.iter().enumerate() { + self.wells[w].push(&mut self.zobrist, *c); } - // 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); + for (s, content) in deal.slots.iter().enumerate() { + let real_slot = if s < n_usable_slots as usize / 2 { s } else { s + 1 }; + for &card in content { + self.slots[real_slot].push(&mut self.zobrist, card) + } } } diff --git a/simulator/src/main.rs b/simulator/src/main.rs index eca5259..2bb0a3e 100644 --- a/simulator/src/main.rs +++ b/simulator/src/main.rs @@ -2,15 +2,22 @@ use board::Board; use ruleset::Ruleset; use seen::Seen; -use crate::{ruleset::Card, zobrist::Zobrist}; +use crate::smart_dealer::Deal; mod board; mod ruleset; mod seen; +mod smart_dealer; mod zobrist; fn main() { + let ruleset = Ruleset { + n_slots: 11, + n_suits: 5, + n_cards_per_suit: 10, + n_arcana: 25 + }; /* let ruleset = Ruleset { n_slots: 11, @@ -35,15 +42,17 @@ fn main() { 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.deal(Deal::deal(&setup, &mut rand::thread_rng())); board.display(); println!("is_winnable: {}", is_winnable(board)); diff --git a/simulator/src/ruleset.rs b/simulator/src/ruleset.rs index 605f94e..6230e5c 100644 --- a/simulator/src/ruleset.rs +++ b/simulator/src/ruleset.rs @@ -31,7 +31,7 @@ impl Ruleset { fn total_n_cards(&self) -> u8 { self.n_arcana + self.n_suits * self.n_cards_per_suit } - fn usable_n_cards(&self) -> u8 { + pub fn usable_n_cards(&self) -> u8 { self.total_n_cards() - self.n_suits } diff --git a/simulator/src/smart_dealer.rs b/simulator/src/smart_dealer.rs new file mode 100644 index 0000000..13c92df --- /dev/null +++ b/simulator/src/smart_dealer.rs @@ -0,0 +1,147 @@ +use rand::{seq::SliceRandom, Rng}; + +use crate::ruleset::{Card, Setup}; + +pub struct Deal { + pub slots: Vec>, +} + +impl Deal { + pub fn deal(setup: &Setup, rng: &mut impl Rng) -> Deal { + loop { + if let Some(d) = Self::deal1(setup, rng) { + return d + } + } + } + fn deal1(setup: &Setup, rng: &mut impl Rng) -> Option { + // don't use the middle slot + let n_slots = (setup.ruleset.n_slots - 1) as usize; + let n_usable_cards = setup.ruleset.usable_n_cards(); + let tower_height = n_usable_cards as usize / n_slots; + + let mut slots: Vec> = vec![vec![]; n_slots]; + + let split_point = + if setup.ruleset.n_arcana == 0 { 0 } + else { rng.gen_range(0..setup.ruleset.n_arcana) }; + + let n_wells = setup.ruleset.n_suits + 2; + + let mut virtual_wells: Vec> = vec![vec![]; n_wells as usize + 2]; + + // rely on the order of the deck + for r in 1..setup.ruleset.n_cards_per_suit { // skip aces + for s in 0..setup.ruleset.n_suits { + virtual_wells[s as usize].push(Card(setup.ruleset.n_cards_per_suit * s + r)); + } + } + + let first_arcana = setup.ruleset.n_suits * setup.ruleset.n_cards_per_suit; + for r in 0..split_point { + virtual_wells[setup.ruleset.n_suits as usize].push(Card(r+first_arcana)) + } + for r in (split_point..setup.ruleset.n_arcana).rev() { + virtual_wells[setup.ruleset.n_suits as usize + 1].push(Card(r+first_arcana)) + } + + let mut pops = vec![]; + for (well, contents) in virtual_wells.iter().enumerate() { + for _ in 0..contents.len() { + pops.push(well); + } + } + pops.shuffle(rng); + + while let Some(w) = pops.pop() { + fn find_home(card: Card, exclude: Option, setup: &Setup, slots: &mut [Vec], tower_height: usize, rng: &mut impl Rng) { + let mut acceptors = vec![]; + let mut not_full = vec![]; + for s in 0..slots.len() { + if Some(s) != exclude && slots[s].len() < tower_height { + if accepts(setup, slots[s].last().cloned(), card) { + acceptors.push(s) + } + not_full.push(s); + } + } + + acceptors.shuffle(rng); + not_full.shuffle(rng); + if rng.gen_bool(0.5) && acceptors.len() > 0 { + let a = acceptors.first().unwrap(); + slots[*a].push(card); + } else if let Some(a) = not_full.first() { + slots[*a].push(card); + } else if let Some(e) = exclude { + slots[e].push(card) + } else { + panic!("should not ever happen") + } + } + let card = virtual_wells[w].pop().expect("card must be present"); + find_home(card, None, setup, &mut slots, tower_height, rng); + + // move any card that is on an acceptor to a random slot + for _ in 0..4 { + let mut sources: Vec = (0..slots.len()).collect(); + sources.shuffle(rng); + for src in sources { + let mut iter = slots[src].iter().rev(); + let top = iter.next().cloned(); + let second_to_top = iter.next().cloned(); + + if let Some(t) = top { + if accepts(&setup, second_to_top, t) { + slots[src].pop(); + find_home(t, Some(src), setup, &mut slots, tower_height, rng); + } + } + } + } + } + + let mut instantly_accepted = vec![]; + for &a in &setup.deck.aces { + instantly_accepted.push(Card(a.0 + 1)) // twos + } + instantly_accepted.push(Card(first_arcana)); + instantly_accepted.push(Card(first_arcana + setup.ruleset.n_arcana - 1)); + + // NOTE: We never used the free cell. The free cell can dig one deeper + // So in theory most of these deals should be solvable using the freecell + for s in 0..slots.len() { + let mut iter = slots[s].iter().rev(); + let last = iter.next().cloned(); + let second_to_last = iter.next().cloned(); + + if let Some(last) = last { + if instantly_accepted.contains(&last) { + if let Some(c) = second_to_last { + if instantly_accepted.contains(&c) { + return None + } + + let n = slots[s].len(); + (slots[s][n-2],slots[s][n-1]) = (last, c); + } + else { + return None; + } + } + } + } + + return Some(Deal { slots }); + } + +} + +fn accepts(setup: &Setup, prev: Option, next: Card) -> bool { + let prev = prev.map(|p| setup.deck.cards[p.0 as usize]); + let next = setup.deck.cards[next.0 as usize]; + if let Some(p) = prev { + return p.suit == next.suit && (p.rank + 1 == next.rank || p.rank == next.rank + 1); + } + return true; +} \ No newline at end of file