diff --git a/src/bits.rs b/src/bits.rs index 39f3fa6..2363f36 100644 --- a/src/bits.rs +++ b/src/bits.rs @@ -19,3 +19,20 @@ pub(crate) fn fill_ones_below_lowest_bit_set(x: u64) -> u64 { debug_assert!(x != 0); x | x.wrapping_sub(1) } + +#[inline] +pub(crate) fn extract_highest_bit_set(x: u64) -> u64 { + debug_assert!(x != 0); + 1 << (63 - x.leading_zeros()) +} + +#[inline] +pub(crate) fn reset_highest_bit_set(x: u64) -> u64 { + debug_assert!(x != 0); + x & !extract_highest_bit_set(x) +} + +#[inline] +pub(crate) fn mask_from_highest_bit_set(x: u64) -> u64 { + mask_up_to_lowest_bit_set(x.reverse_bits()).reverse_bits() +} diff --git a/src/cards.rs b/src/cards.rs index 08a2e66..995a277 100644 --- a/src/cards.rs +++ b/src/cards.rs @@ -9,7 +9,8 @@ use static_assertions::assert_eq_size; use crate::{ bits::{ - extract_lowest_bit_set, fill_ones_below_lowest_bit_set, mask_up_to_lowest_bit_set, + extract_highest_bit_set, extract_lowest_bit_set, fill_ones_below_lowest_bit_set, + mask_from_highest_bit_set, mask_up_to_lowest_bit_set, reset_highest_bit_set, reset_lowest_bit_set, }, ByPlayer, Player, PlayerMap, @@ -94,7 +95,7 @@ impl TryFrom for Suit { #[derive(Clone, Copy, PartialEq, Eq)] #[repr(u8)] #[allow(clippy::unreadable_literal)] -pub enum CardValue { +pub enum Rank { _2 = 0b_0000, _3 = 0b_0001, _4 = 0b_0010, @@ -110,22 +111,22 @@ pub enum CardValue { _A = 0b_1100, } -impl CardValue { +impl Rank { fn symbol(self) -> char { match self { - CardValue::_2 => '2', - CardValue::_3 => '3', - CardValue::_4 => '4', - CardValue::_5 => '5', - CardValue::_6 => '6', - CardValue::_7 => '7', - CardValue::_8 => '8', - CardValue::_9 => '9', - CardValue::_T => 'T', - CardValue::_J => 'J', - CardValue::_Q => 'Q', - CardValue::_K => 'K', - CardValue::_A => 'A', + Rank::_2 => '2', + Rank::_3 => '3', + Rank::_4 => '4', + Rank::_5 => '5', + Rank::_6 => '6', + Rank::_7 => '7', + Rank::_8 => '8', + Rank::_9 => '9', + Rank::_T => 'T', + Rank::_J => 'J', + Rank::_Q => 'Q', + Rank::_K => 'K', + Rank::_A => 'A', } } @@ -137,25 +138,25 @@ impl CardValue { self as u8 } - unsafe fn from_index(value: u8) -> CardValue { + unsafe fn from_index(value: u8) -> Rank { debug_assert!(value < 13); unsafe { std::mem::transmute(value) } } } -impl Debug for CardValue { +impl Debug for Rank { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_char(self.symbol()) } } -impl From for u8 { - fn from(value: CardValue) -> Self { +impl From for u8 { + fn from(value: Rank) -> Self { value as Self } } -impl TryFrom for CardValue { +impl TryFrom for Rank { type Error = String; fn try_from(ch: char) -> Result { @@ -363,13 +364,13 @@ impl Card { std::mem::transmute(mask) } - fn new(value: CardValue, suit: Suit) -> Self { + fn new(value: Rank, suit: Suit) -> Self { unsafe { Self::from_mask(value.mask() | suit.card_mask()) } } #[must_use] - pub(crate) fn value(self) -> CardValue { - unsafe { CardValue::from_index(self.mask() & VALUE_MASK) } + pub(crate) fn value(self) -> Rank { + unsafe { Rank::from_index(self.mask() & VALUE_MASK) } } fn mask(self) -> u8 { @@ -384,7 +385,7 @@ impl Card { unsafe fn from_bit_index(bit_index: usize) -> Card { debug_assert!(bit_index < 64); let index = *unsafe { Card::BIT_INDEX_TO_INDEX.get_unchecked(bit_index) }; - debug_assert!(index < 52); + debug_assert!(index < 52, "{index} >= 52 {bit_index}"); *unsafe { Card::ALL.get_unchecked(index as usize) } } @@ -470,7 +471,13 @@ assert_eq_size!([u64; 2], SetItem); impl SetItem { pub(crate) fn card(&self) -> Card { - self.card_set.lowest_card().unwrap() + self.card_set.lo_card().unwrap() + } +} + +impl Debug for SetItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_char(self.card().symbol()) } } @@ -487,7 +494,7 @@ impl Iterator for CardIterator { return None; } - let card_set = self.set.lowest_card_set(); + let card_set = self.set.lo_card_set(); while !card_set.intersects(self.suit) { self.suit = self.suit << 16; } @@ -579,6 +586,10 @@ impl CardSet { mask: 0b_0001_1111_1111_1111_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000, }; + pub const _A: CardSet = CardSet { + mask: 0b_0001_0000_0000_0000_0001_0000_0000_0000_0001_0000_0000_0000_0001_0000_0000_0000, + }; + // order of Suit values const SUITS: [CardSet; 4] = [Self::C, Self::D, Self::H, Self::S]; const SUIT_MASKS: u64x4 = @@ -618,7 +629,7 @@ impl CardSet { for (i, suit) in Self::BPN_SUITS.iter().enumerate() { for ch in suits[i].chars() { - let card = Card::new(CardValue::try_from(ch)?, *suit); + let card = Card::new(Rank::try_from(ch)?, *suit); set = set | card; } } @@ -631,6 +642,12 @@ impl CardSet { suits.join(".") } + #[must_use] + pub(crate) fn to_bpn_masked(self, mask: CardSet) -> String { + let suits = Self::BPN_SUITS.map(|s| self.to_bpn_masked_single_suit(s, mask)); + suits.join(".") + } + fn to_bpn_single_suit(self, suit: Suit) -> String { let mut result = String::new(); let mut cards: Vec = self.filter_suit(suit).iter().map(|s| s.card()).collect(); @@ -641,6 +658,20 @@ impl CardSet { result } + fn to_bpn_masked_single_suit(self, suit: Suit, mask: CardSet) -> String { + let mut result = String::new(); + let mut cards: Vec = self.filter_suit(suit).iter().map(|s| s.card()).collect(); + cards.reverse(); + for c in cards { + if mask.contains(c) { + result.push(c.value().symbol()); + } else { + result.push('x'); + } + } + result + } + #[must_use] pub(crate) fn size(self) -> u32 { self.mask.count_ones() @@ -659,7 +690,7 @@ impl CardSet { } } - fn lowest_card(self) -> Option { + fn lo_card(self) -> Option { if self.is_empty() { return None; } @@ -669,38 +700,77 @@ impl CardSet { Some(card) } - fn lowest_card_set(self) -> CardSet { + fn lo_card_set(self) -> CardSet { debug_assert!(!self.is_empty()); CardSet { mask: extract_lowest_bit_set(self.mask), } } + pub(crate) fn hi_card_rel_mask(self, suit: CardSet) -> CardSet { + debug_assert!(!self.is_empty()); + let mask = mask_from_highest_bit_set(self.mask); + CardSet::from(mask & suit.mask) + } + pub(crate) fn gt(self, cards: CardSet) -> bool { self.mask > cards.mask } /// compute number of high cards in sequence - pub(crate) fn high_card_seq(self) -> u8 { - self.high_card_suit_seq(CardSet::S) - + self.high_card_suit_seq(CardSet::H) - + self.high_card_suit_seq(CardSet::D) - + self.high_card_suit_seq(CardSet::C) + pub(crate) fn high_card_seq(self) -> (u8, CardSet) { + let s = self.high_card_suit_seq(CardSet::S); + let h = self.high_card_suit_seq(CardSet::H); + let d = self.high_card_suit_seq(CardSet::D); + let c = self.high_card_suit_seq(CardSet::C); + (s.0 + h.0 + d.0 + c.0, s.1 | h.1 | d.1 | c.1) } #[allow(clippy::cast_possible_truncation)] - pub(crate) fn high_card_suit_seq(self, suit: CardSet) -> u8 { + pub(crate) fn high_card_suit_seq(self, suit: CardSet) -> (u8, CardSet) { let cards = self & suit; if cards.is_empty() { - return 0; + return (0, CardSet::default()); } let ones = !suit.mask | cards.mask; - ones.leading_ones() as u8 - suit.mask.leading_zeros() as u8 + let rel_mask = reset_lowest_bit_set(mask_from_highest_bit_set(!ones)) & suit.mask; + ( + (ones.leading_ones() as u8 - suit.mask.leading_zeros() as u8), + rel_mask.into(), + ) } fn splat(self) -> u64x4 { Simd::splat(self.mask) } + + pub(crate) fn unpromote(self, cards: CardSet) -> CardSet { + let mut result = self.mask; + for suit in CardSet::SUITS { + if (suit & cards).is_empty() { + continue; + } + let mut suit_cards = (suit & cards).mask; + let mut work = self.mask | !suit.mask; + + while suit_cards != 0 { + let bit = extract_highest_bit_set(suit_cards); + if bit & work == 0 { + break; + } + suit_cards = reset_highest_bit_set(suit_cards); + work = (work >> 1) | work | 0x8000_0000_0000_0000; + } + + work &= suit.mask; + result = (result & !suit.mask) | work; + } + result.into() + } + + fn contains(self, c: Card) -> bool { + !(CardSet::from(c) & self).is_empty() + } } impl From for CardSet { @@ -842,6 +912,10 @@ impl Deal { self.promote_masked(CardSet::ALL52) } + pub(crate) fn present(&self) -> CardSet { + self.hands.reduce_or().into() + } + #[must_use] pub(crate) fn promote_masked(&self, mask: CardSet) -> Deal { let mut hands = self.hands; @@ -1032,8 +1106,9 @@ impl Deal { n.join("\n") + "\n" + &mid.join("\n") + "\n" + &s.join("\n") } + #[must_use] #[allow(clippy::cast_possible_truncation)] - pub(crate) fn count(&self) -> [u8; 4] { + pub fn count(&self) -> [u8; 4] { [ self.hands[0].count_ones() as u8, self.hands[1].count_ones() as u8, @@ -1041,6 +1116,16 @@ impl Deal { self.hands[3].count_ones() as u8, ] } + + pub(crate) fn format_bpn_masked(&self, mask: CardSet) -> String { + format!( + "{{ {} {} {} {} }}", + self.get(Player::W).to_bpn_masked(mask), + self.get(Player::N).to_bpn_masked(mask), + self.get(Player::E).to_bpn_masked(mask), + self.get(Player::S).to_bpn_masked(mask) + ) + } } impl BitAnd for &Deal { @@ -1053,9 +1138,19 @@ impl BitAnd for &Deal { } } +impl BitAnd<&Deal> for &Deal { + type Output = Deal; + + fn bitand(self, rhs: &Deal) -> Self::Output { + Deal { + hands: self.hands & rhs.hands, + } + } +} + impl std::fmt::Debug for Deal { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Deal { ")?; + f.write_str("{ ")?; f.write_str(&CardSet::from(self.hands[0]).to_bpn())?; f.write_str(" ")?; f.write_str(&CardSet::from(self.hands[1]).to_bpn())?; @@ -1199,7 +1294,7 @@ mod tests { }) .unwrap(); assert_eq!( - "Deal { 854.Q873.Q984.65 KQ32.T6.T72.AJ93 9.AJ542.J653.T87 AJT76.K9.AK.KQ42 }", + "{ 854.Q873.Q984.65 KQ32.T6.T72.AJ93 9.AJ542.J653.T87 AJT76.K9.AK.KQ42 }", format!("{deal:?}") ); } @@ -1217,7 +1312,7 @@ mod tests { assert_eq!("CardMap { from_absolute: \"🂡🂮🂭🂫🂪🂩🂨🂧🂦🂥🂤🂣🂢🂱🂾🂽🂻🂺🂹🂸🂷🂶🂵🂴🂳🂲🃁🃎🃍🃋🃊🃉🃈🃇🃆🃅🃄🃃🃂🃑🃞🃝🃛🃚🃙🃘🃗🃖🃕🃔🃓🃒\", to_absolute: \"🂡🂮🂭🂫🂪🂩🂨🂧🂦🂥🂤🂣🂢🂱🂾🂽🂻🂺🂹🂸🂷🂶🂵🂴🂳🂲🃁🃎🃍🃋🃊🃉🃈🃇🃆🃅🃄🃃🃂🃑🃞🃝🃛🃚🃙🃘🃗🃖🃕🃔🃓🃒\" }", &format!("{map:?}")); assert_eq!( - "Deal { 65.Q873.Q984.854 AJ93.T6.T72.KQ32 T87.AJ542.J653.9 KQ42.K9.AK.AJT76 }", + "{ 65.Q873.Q984.854 AJ93.T6.T72.KQ32 T87.AJ542.J653.9 KQ42.K9.AK.AJT76 }", format!("{:?}", deal.swap_suits(Suit::C, Suit::S, &mut map)) ); assert_eq!("CardMap { from_absolute: \"🃑🃞🃝🃛🃚🃙🃘🃗🃖🃕🃔🃓🃒🂱🂾🂽🂻🂺🂹🂸🂷🂶🂵🂴🂳🂲🃁🃎🃍🃋🃊🃉🃈🃇🃆🃅🃄🃃🃂🂡🂮🂭🂫🂪🂩🂨🂧🂦🂥🂤🂣🂢\", to_absolute: \"🃑🃞🃝🃛🃚🃙🃘🃗🃖🃕🃔🃓🃒🂱🂾🂽🂻🂺🂹🂸🂷🂶🂵🂴🂳🂲🃁🃎🃍🃋🃊🃉🃈🃇🃆🃅🃄🃃🃂🂡🂮🂭🂫🂪🂩🂨🂧🂦🂥🂤🂣🂢\" }", @@ -1239,7 +1334,7 @@ mod tests { &format!("{map:?}") ); assert_eq!( - "Deal { AJT76.K9.AK.KQ42 854.Q873.Q984.65 KQ32.T6.T72.AJ93 9.AJ542.J653.T87 }", + "{ AJT76.K9.AK.KQ42 854.Q873.Q984.65 KQ32.T6.T72.AJ93 9.AJ542.J653.T87 }", format!("{:?}", deal.rotate_to_west(Player::S, Some(&mut map))) ); assert_eq!( @@ -1260,7 +1355,7 @@ mod tests { .unwrap(); assert_eq!( - "Deal { .Q9.. .T8.. .AJ.. A.K.. }", + "{ .Q9.. .T8.. .AJ.. A.K.. }", format!("{:?}", deal2.promote_all(&mut map)) ); assert_eq!( @@ -1286,7 +1381,7 @@ mod tests { }) .unwrap(); assert_eq!( - "Deal { .Q87.. .T6.A. .AJ5.. A.K9.. }", + "{ .Q87.. .T6.A. .AJ5.. A.K9.. }", format!("{:?}", deal3.promote_all(&mut map)) ); assert_eq!( @@ -1312,7 +1407,7 @@ mod tests { .unwrap(); let mut map = CardMap::default(); assert_eq!( - "Deal { .Q873.AQ. Q.T6.T.KJ .AJ54.KJ. AK.K9..AQ }", + "{ .Q873.AQ. Q.T6.T.KJ .AJ54.KJ. AK.K9..AQ }", format!("{:?}", deal6.promote_all(&mut map)) ); assert_eq!( @@ -1334,7 +1429,7 @@ mod tests { ) .unwrap(); assert_eq!( - "Deal { QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 }", + "{ QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 }", format!("{deal:?}") ); @@ -1343,7 +1438,7 @@ mod tests { ) .unwrap(); assert_eq!( - "Deal { AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 }", + "{ AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 }", format!("{deal:?}") ); @@ -1352,7 +1447,7 @@ mod tests { ) .unwrap(); assert_eq!( - "Deal { K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 873.J97.AT764.Q4 }", + "{ K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 873.J97.AT764.Q4 }", format!("{deal:?}") ); @@ -1361,7 +1456,7 @@ mod tests { ) .unwrap(); assert_eq!( - "Deal { 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 }", + "{ 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 QJ6.K652.J85.T98 }", format!("{deal:?}") ); @@ -1370,7 +1465,7 @@ mod tests { ) .unwrap(); assert_eq!( - "Deal { QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 }", + "{ QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 }", format!("{deal:?}") ); } @@ -1388,10 +1483,10 @@ mod tests { assert_eq!( "ByPlayer { ".to_owned() - + "w: BySuit { s: 1, h: 1, d: 0, c: 0 }, " - + "n: BySuit { s: 0, h: 0, d: 0, c: 0 }, " - + "e: BySuit { s: 0, h: 0, d: 1, c: 0 }, " - + "s: BySuit { s: 0, h: 0, d: 0, c: 1 } }", + + "w: BySuit { s: (1, CardSet { mask: \"0001000000000000000000000000000000000000000000000000000000000000\" }), h: (1, CardSet { mask: \"0000000000000000000100000000000000000000000000000000000000000000\" }), d: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), c: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }) }, " + + "n: BySuit { s: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), h: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), d: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), c: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }) }, " + + "e: BySuit { s: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), h: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), d: (1, CardSet { mask: \"0000000000000000000000000000000000010000000000000000000000000000\" }), c: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }) }, " + + "s: BySuit { s: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), h: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), d: (0, CardSet { mask: \"0000000000000000000000000000000000000000000000000000000000000000\" }), c: (1, CardSet { mask: \"0000000000000000000000000000000000000000000000000001000000000000\" }) } }", format!("{high_cards:?}") ); } diff --git a/src/lib.rs b/src/lib.rs index 8761c4b..662966a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,13 +10,13 @@ pub use play::*; pub mod analyze; mod bits; pub mod counter; +pub mod observers; pub mod search; -pub mod stats; mod trans_table; mod ui; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(u8)] pub enum Player { W = 0, diff --git a/src/main.rs b/src/main.rs index 5616348..3962e5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use bridgitte::{ analyze::{analyze_all, analyze_player, analyze_strain, ByStrain, Strain}, - search::{self, Config, InitialPosition}, + search::{Config, InitialPosition}, Deal, Player, Suit, }; use clap::{Parser, Subcommand}; @@ -38,21 +38,16 @@ pub fn main() { println!("{}", deal.format_as_table()); println!(); - let config = Config { - stats, - ..Config::default() - }; + let config = Config { stats }; if let Some(declarer) = declarer { if let Some(strain) = strain { - let mut s = search::create( - &InitialPosition { - deal, - trumps: strain.trumps, - declarer, - }, - &config, - ); + let mut s = InitialPosition { + deal, + trumps: strain.trumps, + declarer, + } + .search(&config); let analysis = s.run_unbounded(); println!(); println!(" {strain:?}",); diff --git a/src/observers.rs b/src/observers.rs new file mode 100644 index 0000000..98e829f --- /dev/null +++ b/src/observers.rs @@ -0,0 +1,294 @@ +use std::time::{Duration, Instant}; + +use crate::{ + search::{InitialPosition, SearchResult}, + trans_table::AB, + ui::render_sparkline, + Card, CardSet, PlayOfCards, PlayState, +}; + +pub(crate) trait Observer { + fn node_enter(&mut self, target: u8, state: &PlayState); + fn node_exit( + &mut self, + target: u8, + state: &PlayState, + result: &SearchResult, + ); + fn child(&mut self, card: Card); + fn update_table(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB); + + fn guess_iter(&mut self); + fn max_tricks_cutoff(&mut self); + fn quick_tricks_cutoff(&mut self); + fn search_cutoff(&mut self); + fn search_finished(&mut self, init: &InitialPosition); + fn search_started(&mut self); + fn tt_hit(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB); + fn tt_lower_cutoff(&mut self); + fn tt_upper_cutoff(&mut self); +} + +#[derive(Debug, Default)] +pub struct Empty {} + +impl Observer for Empty { + fn node_enter(&mut self, _target: u8, _state: &PlayState) {} + fn node_exit( + &mut self, + _target: u8, + _state: &PlayState, + _result: &SearchResult, + ) { + } + + fn tt_lower_cutoff(&mut self) {} + + fn tt_upper_cutoff(&mut self) {} + + fn guess_iter(&mut self) {} + + fn max_tricks_cutoff(&mut self) {} + + fn quick_tricks_cutoff(&mut self) {} + + fn search_cutoff(&mut self) {} + + fn search_finished(&mut self, _init: &InitialPosition) {} + + fn search_started(&mut self) {} + + fn tt_hit(&mut self, _state: &PlayState, _rel_mask: CardSet, _ab: AB) {} + + fn child(&mut self, _card: Card) {} + + fn update_table( + &mut self, + _state: &PlayState, + _rel_mask: CardSet, + _ab: AB, + ) { + } +} + +#[derive(Debug, Default)] +pub struct UnsyncStats { + depth: usize, + start: Option, + elapsed: Option, + guess_iter: u64, + max_tricks_cutoff: u64, + nodes: [u64; 13], + quick_tricks_cutoff: u64, + search_cutoff: u64, + search: u64, + tt_hit: u64, + tt_lower_cutoff: u64, + tt_upper_cutoff: u64, +} + +impl UnsyncStats { + #[allow(clippy::cast_possible_truncation)] + fn print(&self, init: &InitialPosition) { + let total_nodes: u64 = self.nodes.iter().sum(); + let duration = u64::try_from(self.elapsed.unwrap().as_millis()).unwrap_or(u64::MAX); + println!("Search Statistics:"); + println!( + " contract: {}{}", + init.declarer.symbol(), + init.trumps.map_or("N", |s| s.symbol()) + ); + println!(" duration: {duration:12} ms"); + println!(" searches: {:12}", self.search); + println!(" guesses: {:12}", self.guess_iter); + println!( + " nodes: {:12} {}/ms |{}|", + total_nodes, + total_nodes / duration, + render_sparkline(&self.nodes) + ); + println!( + " cuts: {:12} {}%", + self.search_cutoff, + self.search_cutoff * 100 / total_nodes + ); + println!( + " tt hits: {:12} {}%", + self.tt_hit, + self.tt_hit * 100 / total_nodes + ); + println!( + " tt lower: {:12} {}%", + self.tt_lower_cutoff, + self.tt_lower_cutoff * 100 / total_nodes + ); + println!( + " tt upper: {:12} {}%", + self.tt_upper_cutoff, + self.tt_upper_cutoff * 100 / total_nodes + ); + println!( + " max cut: {:12} {}%", + self.max_tricks_cutoff, + self.max_tricks_cutoff * 100 / total_nodes + ); + println!( + " quick tr: {:12} {}%", + self.quick_tricks_cutoff, + self.quick_tricks_cutoff * 100 / total_nodes + ); + } +} + +impl Observer for UnsyncStats { + fn node_enter(&mut self, _target: u8, _state: &PlayState) { + self.nodes[self.depth / 4] += 1; + self.depth += 1; + } + + fn node_exit( + &mut self, + _target: u8, + _state: &PlayState, + _result: &SearchResult, + ) { + self.depth -= 1; + } + + fn tt_lower_cutoff(&mut self) { + self.tt_lower_cutoff += 1; + } + + fn tt_upper_cutoff(&mut self) { + self.tt_upper_cutoff += 1; + } + + fn guess_iter(&mut self) { + self.guess_iter += 1; + } + + fn max_tricks_cutoff(&mut self) { + self.max_tricks_cutoff += 1; + } + + fn quick_tricks_cutoff(&mut self) { + self.quick_tricks_cutoff += 1; + } + + fn search_cutoff(&mut self) { + self.search_cutoff += 1; + } + + fn search_finished(&mut self, init: &InitialPosition) { + self.elapsed = Some(self.start.unwrap().elapsed()); + self.print(init); + } + + fn search_started(&mut self) { + self.search += 1; + self.start = Some(Instant::now()); + } + + fn tt_hit(&mut self, _state: &PlayState, _rel_mask: CardSet, _ab: AB) { + self.tt_hit += 1; + } + + fn child(&mut self, _card: Card) {} + + fn update_table( + &mut self, + _state: &PlayState, + _rel_mask: CardSet, + _ab: AB, + ) { + } +} + +#[derive(Default)] +pub struct Logger { + depth: usize, +} + +impl Logger { + fn indent(&self) { + for _i in 0..self.depth { + print!(" "); + } + } +} + +impl Observer for Logger { + fn guess_iter(&mut self) {} + + fn max_tricks_cutoff(&mut self) {} + + fn node_enter(&mut self, target: u8, state: &PlayState) { + self.indent(); + if state.play.is_empty() { + print!("* "); + } else { + print!("> "); + } + println!("{target} {:?} {:?}", state.next, state.deal); + self.depth += 1; + } + + fn node_exit( + &mut self, + target: u8, + state: &PlayState, + result: &SearchResult, + ) { + self.depth -= 1; + self.indent(); + println!( + "< {target} {:?} {} {} {:?}", + state.next, + state.deal.format_bpn_masked(result.rel_mask), + result.possible, + result.ab + ); + } + + fn child(&mut self, card: Card) { + self.indent(); + println!("{card:?}"); + } + + fn quick_tricks_cutoff(&mut self) {} + + fn search_cutoff(&mut self) {} + + fn search_finished(&mut self, _init: &InitialPosition) {} + + fn search_started(&mut self) {} + + fn tt_hit(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB) { + self.indent(); + println!( + "T {:?} {} {:?}", + state.next, + state.deal.format_bpn_masked(rel_mask), + ab + ); + } + + fn tt_lower_cutoff(&mut self) {} + + fn tt_upper_cutoff(&mut self) {} + + fn update_table( + &mut self, + state: &PlayState, + rel_mask: CardSet, + ab: AB, + ) { + self.indent(); + println!( + "! {:?} {} {:?}", + state.next, + state.deal.format_bpn_masked(rel_mask), + ab + ); + } +} diff --git a/src/play.rs b/src/play.rs index b0c3ab9..c08a523 100644 --- a/src/play.rs +++ b/src/play.rs @@ -2,9 +2,11 @@ use static_assertions::assert_eq_size; use crate::{CardSet, Deal, Pair, Player, SetItem, Suit}; +#[derive(Debug)] pub struct Trick { pub winner: Player, pub cards: CardSet, + first_suit: CardSet, } impl Trick { @@ -20,7 +22,8 @@ pub trait PlayOfCards: Default + Clone + std::fmt::Debug { #[must_use] fn add(&self, c: &SetItem, p: Player) -> Self; fn trick(&self) -> Option; - fn quick_tricks(d: &Deal, p: Player) -> u8; + fn quick_tricks(d: &Deal, p: Player) -> (u8, CardSet); + fn rel_mask(trick: &Trick, rel_mask: CardSet) -> CardSet; } /// 1-4 cards that were played during 1 trick @@ -67,6 +70,7 @@ impl PlayOfCards for PlayedCardsNT { self.winner.map(|winner| Trick { winner, cards: self.cards, + first_suit: self.first_suit, }) } else { None @@ -81,9 +85,25 @@ impl PlayOfCards for PlayedCardsNT { self.first_suit.is_empty() } - fn quick_tricks(d: &Deal, p: Player) -> u8 { + fn quick_tricks(d: &Deal, p: Player) -> (u8, CardSet) { d.get(p).high_card_seq() } + + fn rel_mask(trick: &Trick, mut rel_mask: CardSet) -> CardSet { + if !rel_mask.is_empty() { + rel_mask = rel_mask.unpromote(trick.cards); + } + + let first_suit_cards = trick.cards & trick.first_suit; + let rank_win = first_suit_cards.size() > 1; + + if rank_win { + let first_suit_cards = trick.cards & trick.first_suit; + first_suit_cards.hi_card_rel_mask(trick.first_suit) | rel_mask + } else { + rel_mask + } + } } /// trump suite is allways SPADES @@ -130,6 +150,7 @@ impl PlayOfCards for PlayedCardsT { self.winner.map(|winner| Trick { winner, cards: self.cards, + first_suit: self.first_suit, }) } else { None @@ -144,22 +165,48 @@ impl PlayOfCards for PlayedCardsT { self.first_suit.is_empty() } - fn quick_tricks(d: &Deal, p: Player) -> u8 { - let hand = d.get(p); - let trump_count = (d & CardSet::S).count(); + fn quick_tricks(deal: &Deal, player: Player) -> (u8, CardSet) { + let hand = deal.get(player); + let trump_count = (deal & CardSet::S).count(); - let opp_trumps = max2(Pair::from(p).opposite().filter(trump_count)); - let quick_trumps = hand.high_card_suit_seq(CardSet::S); + let opp_trumps = max2(Pair::from(player).opposite().filter(trump_count)); + let s = hand.high_card_suit_seq(CardSet::S); - if opp_trumps > quick_trumps { + if opp_trumps > s.0 { // opponents have too many trumps to be sure - return quick_trumps; + return s; } - quick_trumps - + hand.high_card_suit_seq(CardSet::H) - + hand.high_card_suit_seq(CardSet::D) - + hand.high_card_suit_seq(CardSet::C) + let h = hand.high_card_suit_seq(CardSet::H); + let d = hand.high_card_suit_seq(CardSet::D); + let c = hand.high_card_suit_seq(CardSet::C); + (s.0 + h.0 + d.0 + c.0, s.1 | h.1 | d.1 | c.1) + } + + fn rel_mask(trick: &Trick, mut rel_mask: CardSet) -> CardSet { + if !rel_mask.is_empty() { + rel_mask = rel_mask.unpromote(trick.cards); + } + let trump_cards = trick.cards & CardSet::S; + let trump_win = !(trump_cards).is_empty(); + + if trump_win { + let rank_win = trump_cards.size() > 1; + if rank_win { + trump_cards.hi_card_rel_mask(CardSet::S) | rel_mask + } else { + rel_mask + } + } else { + let first_suit_cards = trick.cards & trick.first_suit; + let rank_win = first_suit_cards.size() > 1; + + if rank_win { + first_suit_cards.hi_card_rel_mask(trick.first_suit) | rel_mask + } else { + rel_mask + } + } } } @@ -262,7 +309,7 @@ impl PlayState { } /// Number of quick tricks possible without any intervention for current player. - pub(crate) fn quick_tricks(&self) -> u8 { + pub(crate) fn quick_tricks(&self) -> (u8, CardSet) { debug_assert!(self.play.is_empty()); PoC::quick_tricks(&self.deal, self.next) } diff --git a/src/search.rs b/src/search.rs index 211f918..bfc44a2 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,8 +1,8 @@ use std::cmp::max; use crate::{ - stats::{EmptyStats, Stats, UnsyncStats}, - trans_table::{Empty, TransTable, UnsyncCache}, + observers::{Empty, Observer, UnsyncStats}, + trans_table::{TransTable, UnsyncTable, AB}, Card, CardMap, CardSet, Deal, Pair, PlayOfCards, PlayState, PlayedCardsNT, PlayedCardsT, Player, PlayerMap, SetItem, Suit, Trick, }; @@ -12,39 +12,50 @@ pub trait Search { fn optimal_line(&mut self, target: u8) -> Vec<(Player, [Card; 4])>; } -pub trait Lattice { - fn lower(z: T) -> (T, T); - fn upper(z: T) -> (T, T); - fn default_result() -> bool; +trait ABLattice { + fn default_possible() -> bool; + fn initial(target: u8, max: u8) -> AB; + fn search_cutoff(min: u8, target: u8, max: u8) -> AB; } -pub struct Max {} -impl Lattice for Max { - fn lower(z: u8) -> (u8, u8) { - (z, u8::MAX) +struct Max {} +impl ABLattice for Max { + fn default_possible() -> bool { + false } - fn upper(z: u8) -> (u8, u8) { - (u8::MIN, z) + fn search_cutoff(_min: u8, target: u8, max: u8) -> AB { + // we can guarantee target tricks + AB { a: target, b: max } } - fn default_result() -> bool { - false + fn initial(target: u8, _max: u8) -> AB { + debug_assert!(target > 0); + // if we don't find any positive solution the maximum we can expect is target - 1 + AB { + a: 0, + b: target - 1, + } } } -pub struct Min {} -impl Lattice for Min { - fn lower(z: u8) -> (u8, u8) { - (u8::MIN, z) +struct Min {} +impl ABLattice for Min { + fn default_possible() -> bool { + true } - fn upper(z: u8) -> (u8, u8) { - (z, u8::MAX) + fn search_cutoff(min: u8, target: u8, _max: u8) -> AB { + debug_assert!(target > 0); + // we can deny target tricks, so at most target -1 tricks are possible + AB { + a: min, + b: target - 1, + } } - fn default_result() -> bool { - true + fn initial(target: u8, max: u8) -> AB { + AB { a: target, b: max } } } @@ -55,23 +66,59 @@ pub struct InitialPosition { pub declarer: Player, } +impl InitialPosition { + fn search_impl( + self: &InitialPosition, + table: impl TransTable + 'static, + observer: impl Observer + 'static, + ) -> Box { + if let Some(_trumps) = self.trumps { + Box::new(SearchImpl::::new(self, table, observer)) + } else { + Box::new(SearchImpl::::new( + self, table, observer, + )) + } + } + + #[must_use] + pub fn search(self: &InitialPosition, config: &Config) -> Box { + let table = UnsyncTable::new(1_000_000); + + if config.stats { + let stats = UnsyncStats::default(); + self.search_impl(table, stats) + } else { + let stats = Empty::default(); + self.search_impl(table, stats) + } + } +} + /// zero-window search with transposition table -struct SearchImpl +struct SearchImpl where PoC: PlayOfCards, - TT: TransTable, - ST: Stats, + TT: TransTable, + O: Observer, { init: InitialPosition, state: PlayState, table: TT, - stats: ST, + observer: O, cards: CardMap, players: PlayerMap, } -impl, ST: Stats> SearchImpl { - fn new(init: &InitialPosition, table: TT, stats: ST) -> Self { +#[derive(Debug)] +pub(crate) struct SearchResult { + pub(crate) possible: bool, + pub(crate) rel_mask: CardSet, + pub(crate) ab: AB, +} + +impl SearchImpl { + fn new(init: &InitialPosition, table: TT, observer: O) -> Self { let mut cards = CardMap::default(); let mut players = PlayerMap::default(); @@ -90,65 +137,98 @@ impl, ST: Stats> SearchImpl { init: init.clone(), state: PlayState::::new(&deal, next), table, - stats, + observer, cards, players, } } /// returns true if _max_ pair can take `target` tricks. - fn visit(&mut self, target: u8) -> bool { + fn visit(&mut self, target: u8) -> SearchResult { debug_assert!(target > 0); if self.state.deal.is_empty() { - return false; + return SearchResult { + possible: false, + rel_mask: CardSet::default(), + ab: AB { a: 0, b: 0 }, + }; } - self.stats.node_enter(); + self.observer.node_enter(target, &self.state); let result = match Pair::from(self.state.next) { Pair::NS => self.visit_impl::(target), Pair::WE => self.visit_impl::(target), }; - self.stats.node_exit(); + self.observer.node_exit(target, &self.state, &result); result } - fn visit_impl>(&mut self, target: u8) -> bool { + fn visit_impl(&mut self, target: u8) -> SearchResult { + let max_tricks = self.state.deal.max_tricks(); + // start of the round if self.state.play.is_empty() { - if let Some((lower, upper)) = self.table.get(&self.state) { - self.stats.tt_hit(); - if target <= lower { - self.stats.tt_lower_cutoff(); - return true; + if let Some((rel_mask, ab)) = self.table.get(&self.state) { + self.observer.tt_hit(&self.state, rel_mask, ab); + if target <= ab.a { + self.observer.tt_lower_cutoff(); + return SearchResult { + possible: true, + rel_mask, + ab, + }; } - if target > upper { - self.stats.tt_upper_cutoff(); - return false; + if target > ab.b { + self.observer.tt_upper_cutoff(); + return SearchResult { + possible: false, + rel_mask, + ab, + }; } } - let max_tricks = self.state.deal.max_tricks(); - // declarare can't take more tricks than available if target > max_tricks { - self.stats.max_tricks_cutoff(); - return false; + self.observer.max_tricks_cutoff(); + return SearchResult { + possible: false, + rel_mask: CardSet::default(), + ab: AB { + a: 0, + b: max_tricks, + }, + }; } // check quick tricks cut-off - let quick_tricks = self.state.quick_tricks(); + let (quick_tricks, rel_mask) = self.state.quick_tricks(); match Pair::from(self.state.next) { Pair::NS => { if target <= quick_tricks { // max side can quickly take `target` tricks - self.stats.quick_tricks_cutoff(); - return true; + self.observer.quick_tricks_cutoff(); + return SearchResult { + possible: true, + rel_mask, + ab: AB { + a: quick_tricks, + b: max_tricks, + }, + }; } } Pair::WE => { if target > (max_tricks - quick_tricks) { - self.stats.quick_tricks_cutoff(); - return false; + self.observer.quick_tricks_cutoff(); + return SearchResult { + possible: false, + rel_mask, + ab: AB { + a: 0, + b: max_tricks - quick_tricks, + }, + }; } } } @@ -158,44 +238,67 @@ impl, ST: Stats> SearchImpl { let plays = self.state.plays(); debug_assert!(!plays.is_empty()); - let mut result = L::default_result(); - let mut bound = L::upper(target - 1); + // max: possible=false unless we found winning move with possible = true + // the initial bound should be (0, max_tricks) + + // min: possible=true unless we found denying move with possible = false + // the default bound should be (target, max_tricks) + + let mut possible = L::default_possible(); + let mut ab = L::initial(target, max_tricks); + let mut rel_mask = CardSet::ALL52; for c in seach_plays_iter(plays) { - let z = self.visit_child(&c, target); - if z != result { - self.stats.search_cutoff(); - bound = L::lower(target); - result = z; + self.observer.child(c.card()); + let result = self.visit_child(&c, target); + if result.possible != possible { + // counter-example + self.observer.search_cutoff(); + ab = L::search_cutoff(result.ab.a, target, result.ab.b); + possible = !possible; + rel_mask = result.rel_mask; break; + } else if result.rel_mask.size() < rel_mask.size() { + rel_mask = result.rel_mask; } } + if self.state.play.is_empty() { - self.table.update(&self.state, bound); + self.observer.update_table(&self.state, rel_mask, ab); + self.table.update(&self.state, rel_mask, ab); + } + SearchResult { + possible, + rel_mask, + ab, } - result } - fn visit_child(&mut self, c: &SetItem, target: u8) -> bool { + fn visit_child(&mut self, c: &SetItem, target: u8) -> SearchResult { let (trick, undo) = self.state.apply_mut(c); - let decrement = trick.as_ref().map(Trick::target_inc).unwrap_or_default(); - let target = target - decrement; - let z = if target > 0 { self.visit(target) } else { true }; + let target_inc = trick.as_ref().map(Trick::target_inc).unwrap_or_default(); + let target = target - target_inc; + let mut result = if target > 0 { + self.visit(target) + } else { + SearchResult { + possible: true, + rel_mask: CardSet::default(), + ab: AB { + a: 0, + b: self.state.deal.max_tricks(), + }, + } + }; self.state.undo(undo); - z + if let Some(trick) = trick { + result.rel_mask = PoC::rel_mask(&trick, result.rel_mask); + result.ab.a += target_inc; + result.ab.b += target_inc; + } + result } - // fn optimal_line_impl_child(&mut self, c: &SetItem, target: u8, moves: &mut [Card]) { - // println!("{:?}", c.card()); - // moves[0] = c.card(); - // let (win, undo) = self.state.apply_mut(&c); - // let decrement = win.map(MinMax::trick_increment).unwrap_or_default(); - // let target = target - decrement; - // assert!(target != 0, "sz: {}", self.state.deal.size()); - // self.optimal_line_impl(target, &mut moves[1..]); - // self.state.undo(undo); - // } - fn optimal_line_impl(&mut self, mut target: u8) -> Vec<(Player, [Card; 4])> { let initial_deal = self.state.deal.clone(); @@ -207,16 +310,16 @@ impl, ST: Stats> SearchImpl { debug_assert!(!plays.is_empty()); for c in seach_plays_iter(plays) { - let z = self.visit_child(&c, target); + let child = self.visit_child(&c, target); let pick = match Pair::from(self.state.next) { Pair::WE => { // min side shouldn't be able to prevent optimal play - assert!(z); + assert!(child.possible); // pick the play that prevents too many tricks - !self.visit_child(&c, target + 1) + !self.visit_child(&c, target + 1).possible } - Pair::NS => z, + Pair::NS => child.possible, }; if pick { @@ -275,11 +378,11 @@ impl, ST: Stats> SearchImpl { } } -impl, ST: Stats> Search for SearchImpl { +impl Search for SearchImpl { fn run_unbounded(&mut self) -> u8 { assert_eq!(0, self.state.deal.size() % 4); - self.stats.search_started(); + self.observer.search_started(); let mut upper = self.state.deal.max_tricks(); let mut lower = 0u8; @@ -288,10 +391,10 @@ impl, ST: Stats> Search for SearchImpl= lower && guess <= upper); while lower < upper { - self.stats.guess_iter(); + self.observer.guess_iter(); let target = max(guess, lower + 1); let result = self.visit(target); - if result { + if result.possible { // we were able to achieve `target` tricks, this is our new lower bound then lower = target; guess = lower; @@ -301,7 +404,7 @@ impl, ST: Stats> Search for SearchImpl, ST: Stats> Search for SearchImpl, -} - -impl Default for Config { - fn default() -> Self { - Self { - stats: false, - trans_table_size: Some(100_000_000), - } - } -} - -fn create_with_poc( - init: &InitialPosition, - config: &Config, -) -> Box { - if let Some(size) = config.trans_table_size { - let table = UnsyncCache::new(size); - if config.stats { - let stats = UnsyncStats::default(); - Box::new(SearchImpl::::new(init, table, stats)) - } else { - let stats = EmptyStats::default(); - Box::new(SearchImpl::::new(init, table, stats)) - } - } else { - let table = Empty {}; - if config.stats { - let stats = UnsyncStats::default(); - Box::new(SearchImpl::::new(init, table, stats)) - } else { - let stats = EmptyStats::default(); - Box::new(SearchImpl::::new(init, table, stats)) - } - } -} - -#[must_use] -pub fn create(init: &InitialPosition, config: &Config) -> Box { - if let Some(_trumps) = init.trumps { - create_with_poc::(init, config) - } else { - create_with_poc::(init, config) - } } #[must_use] pub(crate) fn search(init: &InitialPosition, config: &Config) -> u8 { - create(init, config).run_unbounded() + init.search(config).run_unbounded() } struct PlayIterator> { @@ -397,11 +457,17 @@ fn seach_plays_iter(set: CardSet) -> impl Iterator { #[cfg(test)] mod tests { + use std::collections::HashSet; + use crate::{ - search::{create, seach_plays_iter, search, Config, InitialPosition}, - ByPlayer, CardSet, Deal, Player, Suit, + observers::{Empty, Logger}, + search::{seach_plays_iter, search, Config, InitialPosition}, + trans_table::{TransTable, UnsyncTable}, + ByPlayer, CardSet, Deal, PlayOfCards, PlayState, Player, Suit, }; + use super::AB; + #[test] fn play_iterator() { let hand = CardSet::try_from_bpn(".63.AKQ987.A9732").unwrap(); @@ -411,6 +477,63 @@ mod tests { assert_eq!("🂶🂳🃁🃉🃑🃙🃗🃓", rev.join("")); } + struct TestTable { + verified_entries: HashSet, + table: UnsyncTable, + } + + impl TestTable { + fn new(verified_entries: &[&str]) -> Self { + Self { + verified_entries: HashSet::from_iter( + verified_entries.iter().map(|s| s.to_string()), + ), + table: UnsyncTable::new(100), + } + } + } + + impl TransTable for TestTable { + fn get(&self, state: &PlayState) -> Option<(CardSet, AB)> { + self.table.get(state) + } + + fn update(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB) { + let str = format!( + "{:?} {} = ({},{})", + state.next, + state.deal.format_bpn_masked(rel_mask), + ab.a, + ab.b + ); + assert!( + self.verified_entries.contains(&str), + "unverified entry: {str}" + ); + self.table.update(state, rel_mask, ab); + } + } + + #[test] + fn small_deal_1() { + let deal = Deal::try_from(ByPlayer::<&str> { + w: "K...", + n: "A...", + e: "T...", + s: "7...", + }) + .unwrap(); + let table = TestTable::new(&["W { x... A... x... x... } = (1,1)"]); + let mut search = InitialPosition { + deal, + trumps: None, + declarer: Player::S, + } + .search_impl(table, Logger::default()); + let tricks = search.run_unbounded(); + assert_eq!(1, tricks); + } + #[test] fn small_deal_2() { let deal = Deal::try_from(ByPlayer::<&str> { @@ -420,14 +543,17 @@ mod tests { s: "72...", }) .unwrap(); - let tricks = search( - &InitialPosition { - deal, - trumps: None, - declarer: Player::S, - }, - &Config::default(), - ); + let table = TestTable::new(&[ + "N { A... x... x... x... } = (0,0)", + "W { Kx... AQ... xx... xx... } = (2,2)", + ]); + let mut search = InitialPosition { + deal, + trumps: None, + declarer: Player::S, + } + .search_impl(table, Logger::default()); + let tricks = search.run_unbounded(); assert_eq!(2, tricks); } @@ -440,68 +566,72 @@ mod tests { s: "752...", }) .unwrap(); - assert_eq!( - 2, - search( - &InitialPosition { - deal, - trumps: None, - declarer: Player::S - }, - &Config::default() - ) - ); + + let table = TestTable::new(&[ + "N { Ax... xx... xx... xx... } = (0,1)", + "N { A... x... x... x... } = (0,0)", + "N { Kx... Ax... xx... xx... } = (0,1)", + "W { Kxx... Axx... xxx... xxx... } = (0,2)", + "W { x... A... x... x... } = (1,1)", + "N { Ax... Kx... xx... xx... } = (1,2)", + "W { Kxx... AQx... xxx... xxx... } = (2,3)", + ]); + let mut search = InitialPosition { + deal, + trumps: None, + declarer: Player::S, + } + .search_impl(table, Logger::default()); + + assert_eq!(2, search.run_unbounded()); } #[test] - fn bermuda_1983() { - let config = Config { - stats: false, - trans_table_size: None, - }; - let trumps = Some(crate::Suit::S); - - // walk one minmax line of play backwards - let deal2 = Deal::try_from(ByPlayer::<&str> { + fn bermuda_2() { + let deal = Deal::try_from(ByPlayer::<&str> { w: ".Q8..", n: ".T6..", e: ".AJ..", s: "6.K..", }) .unwrap(); - assert_eq!( - 1, - search( - &InitialPosition { - deal: deal2.clone(), - trumps: trumps, - declarer: Player::N - }, - &config - ) - ); - assert_eq!( - 0, - search( - &InitialPosition { - deal: deal2.clone(), - trumps: Some(Suit::C), - declarer: Player::N - }, - &config - ) - ); - assert_eq!( - 0, - search( - &InitialPosition { - deal: deal2.clone(), - trumps: None, - declarer: Player::N - }, - &config - ) - ); + + let table = TestTable::new(&[ + "W { .Ax.. x.x.. .xx.. .xx.. } = (0,1)", + "W { .x.. x... .x.. .x.. } = (1,1)", + "W { .Ax.. x.x.. .xx.. .xx.. } = (1,2)", + ]); + let mut search = InitialPosition { + deal: deal.clone(), + trumps: Some(crate::Suit::S), + declarer: Player::N, + } + .search_impl(table, Logger::default()); + assert_eq!(1, search.run_unbounded()); + + let table = TestTable::new(&["W { .AJ.. .K..x .Qx.. .xx.. } = (0,0)"]); + let mut search = InitialPosition { + deal: deal.clone(), + trumps: Some(crate::Suit::C), + declarer: Player::N, + } + .search_impl(table, Logger::default()); + assert_eq!(0, search.run_unbounded()); + + let table = TestTable::new(&["W { .AJ.. x.K.. .Qx.. .xx.. } = (0,0)"]); + let mut search = InitialPosition { + deal: deal.clone(), + trumps: None, + declarer: Player::N, + } + .search_impl(table, Logger::default()); + assert_eq!(0, search.run_unbounded()); + } + + #[test] + fn bermuda_1983() { + let config = Config { stats: false }; + let trumps = Some(crate::Suit::S); let deal3 = Deal::try_from(ByPlayer::<&str> { w: ".Q87..", @@ -659,25 +789,21 @@ mod tests { } #[test] - fn bermuda_table() { - let config = Config::default(); - let trumps = Some(crate::Suit::S); - - let deal6 = Deal::try_from(ByPlayer::<&str> { + fn bermuda_6() { + let deal = Deal::try_from(ByPlayer::<&str> { w: ".Q873.84.", n: "2.T6.2.93", e: ".AJ54.53.", s: "76.K9..Q4", }) .unwrap(); - let mut search = create( - &InitialPosition { - deal: deal6, - trumps, - declarer: Player::N, - }, - &config, - ); + let table = UnsyncTable::new(100); + let mut search = InitialPosition { + deal, + trumps: Some(Suit::S), + declarer: Player::N, + } + .search_impl(table, Empty {}); assert_eq!(5, search.run_unbounded()); assert_eq!( diff --git a/src/stats.rs b/src/stats.rs deleted file mode 100644 index 44e93e4..0000000 --- a/src/stats.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::time::{Duration, Instant}; - -use crate::{search::InitialPosition, ui::render_sparkline}; - -pub trait Stats { - fn guess_iter(&mut self); - fn max_tricks_cutoff(&mut self); - fn node_enter(&mut self); - fn node_exit(&mut self); - fn quick_tricks_cutoff(&mut self); - fn search_cutoff(&mut self); - fn search_finished(&mut self, init: &InitialPosition); - fn search_started(&mut self); - fn tt_hit(&mut self); - fn tt_lower_cutoff(&mut self); - fn tt_upper_cutoff(&mut self); -} - -#[derive(Debug, Default)] -pub struct EmptyStats {} - -impl Stats for EmptyStats { - fn node_enter(&mut self) {} - fn node_exit(&mut self) {} - - fn tt_lower_cutoff(&mut self) {} - - fn tt_upper_cutoff(&mut self) {} - - fn guess_iter(&mut self) {} - - fn max_tricks_cutoff(&mut self) {} - - fn quick_tricks_cutoff(&mut self) {} - - fn search_cutoff(&mut self) {} - - fn search_finished(&mut self, _init: &InitialPosition) {} - - fn search_started(&mut self) {} - - fn tt_hit(&mut self) {} -} - -#[derive(Debug, Default)] -pub struct UnsyncStats { - depth: usize, - start: Option, - elapsed: Option, - guess_iter: u64, - max_tricks_cutoff: u64, - nodes: [u64; 13], - quick_tricks_cutoff: u64, - search_cutoff: u64, - search: u64, - tt_hit: u64, - tt_lower_cutoff: u64, - tt_upper_cutoff: u64, -} -impl UnsyncStats { - #[allow(clippy::cast_possible_truncation)] - fn print(&self, init: &InitialPosition) { - let total_nodes: u64 = self.nodes.iter().sum(); - let duration = u64::try_from(self.elapsed.unwrap().as_millis()).unwrap_or(u64::MAX); - println!("Search Statistics:"); - println!( - " contract: {}{}", - init.declarer.symbol(), - init.trumps.map_or("N", |s| s.symbol()) - ); - println!(" duration: {duration:12} ms"); - println!(" searches: {:12}", self.search); - println!(" guesses: {:12}", self.guess_iter); - println!( - " nodes: {:12} {}/ms |{}|", - total_nodes, - total_nodes / duration, - render_sparkline(&self.nodes) - ); - println!( - " cuts: {:12} {}%", - self.search_cutoff, - self.search_cutoff * 100 / total_nodes - ); - println!( - " tt hits: {:12} {}%", - self.tt_hit, - self.tt_hit * 100 / total_nodes - ); - println!( - " tt lower: {:12} {}%", - self.tt_lower_cutoff, - self.tt_lower_cutoff * 100 / total_nodes - ); - println!( - " tt upper: {:12} {}%", - self.tt_upper_cutoff, - self.tt_upper_cutoff * 100 / total_nodes - ); - println!( - " max cut: {:12} {}%", - self.max_tricks_cutoff, - self.max_tricks_cutoff * 100 / total_nodes - ); - println!( - " quick tr: {:12} {}%", - self.quick_tricks_cutoff, - self.quick_tricks_cutoff * 100 / total_nodes - ); - } -} - -impl Stats for UnsyncStats { - fn node_enter(&mut self) { - self.nodes[self.depth / 4] += 1; - self.depth += 1; - } - - fn node_exit(&mut self) { - self.depth -= 1; - } - - fn tt_lower_cutoff(&mut self) { - self.tt_lower_cutoff += 1; - } - - fn tt_upper_cutoff(&mut self) { - self.tt_upper_cutoff += 1; - } - - fn guess_iter(&mut self) { - self.guess_iter += 1; - } - - fn max_tricks_cutoff(&mut self) { - self.max_tricks_cutoff += 1; - } - - fn quick_tricks_cutoff(&mut self) { - self.quick_tricks_cutoff += 1; - } - - fn search_cutoff(&mut self) { - self.search_cutoff += 1; - } - - fn search_finished(&mut self, init: &InitialPosition) { - self.elapsed = Some(self.start.unwrap().elapsed()); - self.print(init); - } - - fn search_started(&mut self) { - self.search += 1; - self.start = Some(Instant::now()); - } - - fn tt_hit(&mut self) { - self.tt_hit += 1; - } -} diff --git a/src/trans_table.rs b/src/trans_table.rs index 3f3390a..24e7131 100644 --- a/src/trans_table.rs +++ b/src/trans_table.rs @@ -1,102 +1,147 @@ -use std::cmp::{max, min}; +use std::{ + cmp::{max, min}, + collections::HashMap, + hash::Hash, +}; + +use crate::{CardSet, Deal, PlayOfCards, PlayState, Player}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct AB { + /// guaranteed to make <= `a` + pub(crate) a: u8, + /// impossible to make > `b` + pub(crate) b: u8, +} +impl AB { + fn intersect(self, other: AB) -> AB { + debug_assert!(self.a <= self.b); + debug_assert!(other.a <= other.b); + let result = AB { + a: max(self.a, other.a), + b: min(self.b, other.b), + }; + debug_assert!(result.a <= result.b); + result + } +} -use crate::{Deal, PlayOfCards, PlayState}; +impl std::fmt::Debug for AB { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.a, self.b) + } +} -pub trait TransTable { - fn get(&self, state: &PlayState) -> Option<(u8, u8)>; - fn update(&mut self, state: &PlayState, ab: (u8, u8)); +pub trait TransTable { + fn get(&self, state: &PlayState) -> Option<(CardSet, AB)>; + fn update(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB); } pub struct Empty {} -impl TransTable for Empty { - fn get(&self, _state: &PlayState) -> Option<(u8, u8)> { +impl TransTable for Empty { + fn get(&self, _state: &PlayState) -> Option<(CardSet, AB)> { None } - fn update(&mut self, _state: &PlayState, _ab: (u8, u8)) {} + fn update(&mut self, _state: &PlayState, _rel_mask: CardSet, _ab: AB) {} } -#[derive(PartialEq, Eq, Hash, Clone)] +#[derive(Eq, Hash, PartialEq, PartialOrd, Ord)] struct Key { - // rotated deal - deal: Deal, + distr: u64, + next: Player, } impl From<&PlayState> for Key { fn from(state: &PlayState) -> Self { - debug_assert!(state.play.is_empty()); - Key { - deal: state.deal.rotate_to_west(state.next, None), - } + let deal = &state.deal; + let next = state.next; + let c = (deal & CardSet::C).count(); + let d = (deal & CardSet::D).count(); + let h = (deal & CardSet::H).count(); + let s = (deal & CardSet::S).count(); + + let c = + u64::from(c[0]) | u64::from(c[1]) << 4 | u64::from(c[2]) << 8 | u64::from(c[3]) << 12; + let d = + u64::from(d[0]) | u64::from(d[1]) << 4 | u64::from(d[2]) << 8 | u64::from(d[3]) << 12; + let h = + u64::from(h[0]) | u64::from(h[1]) << 4 | u64::from(h[2]) << 8 | u64::from(h[3]) << 12; + let s = + u64::from(s[0]) | u64::from(s[1]) << 4 | u64::from(s[2]) << 8 | u64::from(s[3]) << 12; + + let distr = (c) | (d) << 16 | (h) << 32 | (s) << 48; + Key { distr, next } } } -#[derive(Debug, Clone)] -struct Value { - ab: (u8, u8), -} -impl Value { - fn intersect(&self, other: (u8, u8)) -> Value { - Value { - ab: (max(self.ab.0, other.0), min(self.ab.1, other.1)), - } - } -} - -pub struct SyncCache { - cache: quick_cache::sync::Cache, -} - -impl SyncCache { - #[allow(dead_code)] - pub(crate) fn new(size: usize) -> Self { - Self { - cache: quick_cache::sync::Cache::new(size), - } +impl std::fmt::Debug for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}{:16x}", self.next, self.distr) } } -impl TransTable for SyncCache { - fn get(&self, state: &PlayState) -> Option<(u8, u8)> { - self.cache.get(&Key::from(state)).map(|v| v.ab) - } - - fn update(&mut self, state: &PlayState, ab: (u8, u8)) { - let key = Key::from(state); - match self.cache.get_value_or_guard(&key, None) { - quick_cache::sync::GuardResult::Value(value) => { - self.cache.insert(key, value.intersect(ab)); - } - quick_cache::sync::GuardResult::Guard(guard) => _ = guard.insert(Value { ab }), - quick_cache::sync::GuardResult::Timeout => panic!("unexpected timeout"), - } - } +#[derive(Debug)] +struct Entry { + deal_mask: Deal, + ab: AB, } -pub struct UnsyncCache { - cache: quick_cache::unsync::Cache, +pub struct UnsyncTable { + cache: HashMap>, } -impl UnsyncCache { +impl UnsyncTable { pub(crate) fn new(size: usize) -> Self { Self { - cache: quick_cache::unsync::Cache::new(size), + cache: HashMap::with_capacity(size), } } } -impl TransTable for UnsyncCache { - fn get(&self, state: &PlayState) -> Option<(u8, u8)> { - self.cache.get(&Key::from(state)).map(|v| v.ab) +impl TransTable for UnsyncTable { + fn get(&self, state: &PlayState) -> Option<(CardSet, AB)> { + let entries = self.cache.get(&Key::from(state))?; + let deal = &state.deal; + let mut result: Option<(CardSet, AB)> = None; + for entry in entries { + if (&entry.deal_mask & deal) == entry.deal_mask { + if let Some(prev_result) = result { + result = Some(( + entry.deal_mask.present() | prev_result.0, + entry.ab.intersect(prev_result.1), + )); + } else { + result = Some((entry.deal_mask.present(), entry.ab)); + } + } + } + result } - fn update(&mut self, state: &PlayState, ab: (u8, u8)) { + fn update(&mut self, state: &PlayState, rel_mask: CardSet, ab: AB) { + debug_assert!(ab.b <= 13); let key = Key::from(state); - if let Some(mut existing) = self.cache.get_mut(&key) { - *existing = existing.intersect(ab); + + let deal = &state.deal; + let deal = deal & rel_mask; + debug_assert_eq!(deal.present(), rel_mask); + let new_entry = Entry { + deal_mask: deal, + ab, + }; + let Some(entries) = self.cache.get_mut(&key) else { + let entries = vec![new_entry]; + self.cache.insert(key, entries); return; }; - - self.cache.insert(key, Value { ab }); + for entry in entries.iter_mut() { + if entry.deal_mask == new_entry.deal_mask { + let ab = entry.ab.intersect(new_entry.ab); + debug_assert_ne!(ab, entry.ab, "suboptimal table insert"); + entry.ab = ab; + } + } + entries.push(new_entry); } }