const BLOCKS_PER_PAGE: usize = 1000;
const DISTS_PER_ENTRY: usize = 32;
const BLOCKS_PER_ENTRY: usize = 125;
const TT_BYTES: usize = 4;
const TT_TRICKS: usize = 12;
const TT_HANDS: usize = 4;
const TT_SUITS: usize = 4;
const TT_HASH_BUCKETS: usize = 256;
pub const DEFAULT_MEMORY_MB: u32 = 160;
pub const MAX_MEMORY_MB: u32 = 256;
#[derive(Clone, Copy, Debug, Default)]
pub struct NodeCards {
pub ubound: i8,
pub lbound: i8,
pub best_move_suit: u8,
pub best_move_rank: u8,
pub least_win: [u8; TT_SUITS],
}
#[derive(Clone, Copy, Debug, Default)]
struct WinMatch {
xor_set: u32,
top_set: [u32; TT_BYTES],
top_mask: [u32; TT_BYTES],
mask_index: i32,
last_mask_no: i32,
first: NodeCards,
}
#[derive(Clone, Copy, Debug)]
struct WinBlock {
next_match_no: i32,
next_write_no: i32,
timestamp_read: i32,
list: [WinMatch; BLOCKS_PER_ENTRY],
}
impl WinBlock {
fn new() -> Self {
Self {
next_match_no: 0,
next_write_no: 0,
timestamp_read: 0,
list: [WinMatch::default(); BLOCKS_PER_ENTRY],
}
}
const fn reset(&mut self) {
self.next_match_no = 0;
self.next_write_no = 0;
self.timestamp_read = 0;
}
}
#[derive(Clone, Copy, Debug, Default)]
struct PosSearch {
pos_block: BlockId,
key: i64,
}
#[derive(Clone, Copy, Debug)]
struct DistHash {
next_no: i32,
next_write_no: i32,
list: [PosSearch; DISTS_PER_ENTRY],
}
impl Default for DistHash {
fn default() -> Self {
Self {
next_no: 0,
next_write_no: 0,
list: [PosSearch::default(); DISTS_PER_ENTRY],
}
}
}
type DistHashBuckets = Box<[DistHash; TT_HASH_BUCKETS]>;
#[derive(Clone, Copy, Debug, Default)]
struct Aggr {
aggr_ranks: [u32; TT_SUITS],
aggr_bytes: [[u32; TT_BYTES]; TT_SUITS],
}
type Page = Box<[WinBlock; BLOCKS_PER_PAGE]>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct BlockId(u32);
impl BlockId {
const fn from_indices(page: u32, slot: u32) -> Self {
Self(page * BLOCKS_PER_PAGE as u32 + slot)
}
const fn page(self) -> u32 {
self.0 / BLOCKS_PER_PAGE as u32
}
const fn slot(self) -> u32 {
self.0 % BLOCKS_PER_PAGE as u32
}
}
static TT_LOWEST_RANK: std::sync::LazyLock<Box<[i32; 8192]>> = std::sync::LazyLock::new(|| {
let mut t = Box::new([0i32; 8192]);
t[0] = 15;
let mut top_bit_rank: usize = 1;
for ind in 1..8192 {
if ind >= (top_bit_rank << 1) {
top_bit_rank <<= 1;
}
t[ind] = t[ind ^ top_bit_rank] - 1;
}
t
});
static MASK_BYTES: std::sync::LazyLock<Box<[[[u32; TT_BYTES]; TT_SUITS]; 8192]>> =
std::sync::LazyLock::new(|| {
let mut t = Box::new([[[0u32; TT_BYTES]; TT_SUITS]; 8192]);
let mut win_mask = vec![0u32; 8192];
let mut top_bit_rank: usize = 1;
for ind in 1..8192 {
if ind >= (top_bit_rank << 1) {
top_bit_rank <<= 1;
}
win_mask[ind] = (win_mask[ind ^ top_bit_rank] >> 2) | (3 << 24);
let w = win_mask[ind];
t[ind][0][0] = (w << 6) & 0xff00_0000;
t[ind][0][1] = (w << 14) & 0xff00_0000;
t[ind][0][2] = (w << 22) & 0xff00_0000;
t[ind][0][3] = (w << 30) & 0xff00_0000;
t[ind][1][0] = (w >> 2) & 0x00ff_0000;
t[ind][1][1] = (w << 6) & 0x00ff_0000;
t[ind][1][2] = (w << 14) & 0x00ff_0000;
t[ind][1][3] = (w << 22) & 0x00ff_0000;
t[ind][2][0] = (w >> 10) & 0x0000_ff00;
t[ind][2][1] = (w >> 2) & 0x0000_ff00;
t[ind][2][2] = (w << 6) & 0x0000_ff00;
t[ind][2][3] = (w << 14) & 0x0000_ff00;
t[ind][3][0] = (w >> 18) & 0x0000_00ff;
t[ind][3][1] = (w >> 10) & 0x0000_00ff;
t[ind][3][2] = (w >> 2) & 0x0000_00ff;
t[ind][3][3] = (w << 6) & 0x0000_00ff;
}
t
});
pub struct TransTable {
pages_default: u32,
pages_maximum: u32,
pages: Vec<Page>,
next_slot: u32,
tt_root: [[DistHashBuckets; TT_HANDS]; TT_TRICKS],
last_block_seen: [[Option<BlockId>; TT_HANDS]; TT_TRICKS],
aggr: Box<[Aggr; 8192]>,
aggr_ready: bool,
timestamp: i32,
}
impl Default for TransTable {
fn default() -> Self {
Self::new()
}
}
impl TransTable {
pub(crate) fn new() -> Self {
Self::with_memory(DEFAULT_MEMORY_MB, MAX_MEMORY_MB)
}
pub(crate) fn with_memory(default_mb: u32, max_mb: u32) -> Self {
Self {
pages_default: Self::mb_to_pages(default_mb),
pages_maximum: Self::mb_to_pages(max_mb),
pages: Vec::new(),
next_slot: BLOCKS_PER_PAGE as u32, tt_root: std::array::from_fn(|_| {
std::array::from_fn(|_| {
vec![DistHash::default(); TT_HASH_BUCKETS]
.into_boxed_slice()
.try_into()
.unwrap_or_else(|_| unreachable!())
})
}),
last_block_seen: [[None; TT_HANDS]; TT_TRICKS],
aggr: Box::new([Aggr::default(); 8192]),
aggr_ready: false,
timestamp: 0,
}
}
fn mb_to_pages(megabytes: u32) -> u32 {
const WIN_BLOCK_BYTES: usize = std::mem::size_of::<WinBlock>();
let block_kib = (BLOCKS_PER_PAGE * WIN_BLOCK_BYTES) as f64 / 1024.0;
((1024.0 * f64::from(megabytes)) / block_kib) as u32
}
#[allow(dead_code)]
pub(crate) fn set_memory_default(&mut self, megabytes: u32) {
self.pages_default = Self::mb_to_pages(megabytes);
}
#[allow(dead_code)]
pub(crate) fn set_memory_maximum(&mut self, megabytes: u32) {
self.pages_maximum = Self::mb_to_pages(megabytes);
}
pub(crate) fn init(&mut self, hand_lookup: &[[i32; 15]; TT_SUITS]) {
for s in 0..TT_SUITS {
self.aggr[0].aggr_ranks[s] = 0;
for b in 0..TT_BYTES {
self.aggr[0].aggr_bytes[s][b] = 0;
}
}
let mut top_bit_rank: usize = 1;
let mut top_bit_no: usize = 2;
for ind in 1..8192 {
if ind >= (top_bit_rank << 1) {
top_bit_rank <<= 1;
top_bit_no += 1;
}
self.aggr[ind] = self.aggr[ind ^ top_bit_rank];
for (rank_slot, suit_lookup) in
self.aggr[ind].aggr_ranks.iter_mut().zip(hand_lookup.iter())
{
let h = suit_lookup[top_bit_no] as u32;
*rank_slot = (*rank_slot >> 2) | (h << 24);
}
let ar = self.aggr[ind].aggr_ranks;
let ab = &mut self.aggr[ind].aggr_bytes;
ab[0][0] = (ar[0] << 6) & 0xff00_0000;
ab[0][1] = (ar[0] << 14) & 0xff00_0000;
ab[0][2] = (ar[0] << 22) & 0xff00_0000;
ab[0][3] = (ar[0] << 30) & 0xff00_0000;
ab[1][0] = (ar[1] >> 2) & 0x00ff_0000;
ab[1][1] = (ar[1] << 6) & 0x00ff_0000;
ab[1][2] = (ar[1] << 14) & 0x00ff_0000;
ab[1][3] = (ar[1] << 22) & 0x00ff_0000;
ab[2][0] = (ar[2] >> 10) & 0x0000_ff00;
ab[2][1] = (ar[2] >> 2) & 0x0000_ff00;
ab[2][2] = (ar[2] << 6) & 0x0000_ff00;
ab[2][3] = (ar[2] << 14) & 0x0000_ff00;
ab[3][0] = (ar[3] >> 18) & 0x0000_00ff;
ab[3][1] = (ar[3] >> 10) & 0x0000_00ff;
ab[3][2] = (ar[3] >> 2) & 0x0000_00ff;
ab[3][3] = (ar[3] << 6) & 0x0000_00ff;
}
self.aggr_ready = true;
}
fn init_tt(&mut self) {
for t in 0..TT_TRICKS {
for h in 0..TT_HANDS {
let buckets = &mut self.tt_root[t][h];
for i in 0..TT_HASH_BUCKETS {
buckets[i].next_no = 0;
buckets[i].next_write_no = 0;
}
self.last_block_seen[t][h] = None;
}
}
}
pub(crate) fn reset(&mut self) {
if self.pages.is_empty() {
self.init_tt();
self.timestamp = 0;
return;
}
if self.pages.len() as u32 > self.pages_default {
self.pages.truncate(self.pages_default as usize);
}
self.next_slot = if self.pages.is_empty() {
BLOCKS_PER_PAGE as u32
} else {
self.pages.truncate(1);
0
};
self.init_tt();
self.timestamp = 0;
}
fn get_next_card_block(&mut self) -> BlockId {
if self.next_slot < BLOCKS_PER_PAGE as u32 {
let page_idx = (self.pages.len() - 1) as u32;
let slot = self.next_slot;
self.next_slot += 1;
let block_id = BlockId::from_indices(page_idx, slot);
self.pages[page_idx as usize][slot as usize].reset();
return block_id;
}
if (self.pages.len() as u32) < self.pages_maximum {
self.pages.push(Self::new_page());
self.next_slot = 1;
let page_idx = (self.pages.len() - 1) as u32;
let block_id = BlockId::from_indices(page_idx, 0);
self.pages[page_idx as usize][0].reset();
block_id
} else {
self.reset();
if self.pages.is_empty() {
self.pages.push(Self::new_page());
self.next_slot = 0;
}
self.get_next_card_block()
}
}
fn new_page() -> Page {
let v = vec![WinBlock::new(); BLOCKS_PER_PAGE];
v.into_boxed_slice()
.try_into()
.unwrap_or_else(|_| unreachable!())
}
#[inline]
fn block(&self, id: BlockId) -> &WinBlock {
&self.pages[id.page() as usize][id.slot() as usize]
}
#[inline]
fn block_mut(&mut self, id: BlockId) -> &mut WinBlock {
&mut self.pages[id.page() as usize][id.slot() as usize]
}
#[inline]
fn hash8(hand_dist: &[i32; TT_HANDS]) -> usize {
let h = i64::from(hand_dist[0])
^ (i64::from(hand_dist[1]).wrapping_mul(5))
^ (i64::from(hand_dist[2]).wrapping_mul(25))
^ (i64::from(hand_dist[3]).wrapping_mul(125));
let h = h as i32;
((h ^ (h >> 5)) & 0xff) as usize
}
#[inline]
fn suit_lengths_key(hand_dist: &[i32; TT_HANDS]) -> i64 {
(i64::from(hand_dist[0]) << 36)
| (i64::from(hand_dist[1]) << 24)
| (i64::from(hand_dist[2]) << 12)
| i64::from(hand_dist[3])
}
#[inline]
pub(crate) fn lookup(
&mut self,
trick: i32,
hand: i32,
aggr_target: &[u32; TT_SUITS],
hand_dist: &[i32; TT_HANDS],
limit: i32,
lower_flag: &mut bool,
) -> Option<&NodeCards> {
let trick = trick as usize;
let hand = hand as usize;
let key = Self::suit_lengths_key(hand_dist);
let hashkey = Self::hash8(hand_dist);
let (block_id, empty) = self.lookup_suit(trick, hand, hashkey, key);
self.last_block_seen[trick][hand] = Some(block_id);
if empty {
return None;
}
let ab0 = &self.aggr[aggr_target[0] as usize].aggr_bytes[0];
let ab1 = &self.aggr[aggr_target[1] as usize].aggr_bytes[1];
let ab2 = &self.aggr[aggr_target[2] as usize].aggr_bytes[2];
let ab3 = &self.aggr[aggr_target[3] as usize].aggr_bytes[3];
let top_set = [
ab0[0] | ab1[0] | ab2[0] | ab3[0],
ab0[1] | ab1[1] | ab2[1] | ab3[1],
ab0[2] | ab1[2] | ab2[2] | ab3[2],
ab0[3] | ab1[3] | ab2[3] | ab3[3],
];
self.lookup_cards(block_id, &top_set, limit, lower_flag)
}
#[inline]
fn lookup_suit(
&mut self,
trick: usize,
hand: usize,
hashkey: usize,
key: i64,
) -> (BlockId, bool) {
{
let dp = &self.tt_root[trick][hand][hashkey];
for i in 0..(dp.next_no as usize) {
if dp.list[i].key == key {
return (dp.list[i].pos_block, false);
}
}
}
let n = self.tt_root[trick][hand][hashkey].next_no as usize;
let write_no = self.tt_root[trick][hand][hashkey].next_write_no as usize;
let (slot_idx, block_id, needs_alloc) = if n == DISTS_PER_ENTRY {
let m = if write_no == DISTS_PER_ENTRY {
0
} else {
write_no
};
let bid = self.tt_root[trick][hand][hashkey].list[m].pos_block;
(m, bid, false)
} else {
let bid = self.get_next_card_block();
(n, bid, true)
};
let timestamp = self.timestamp;
if needs_alloc {
let bucket = &mut self.tt_root[trick][hand][hashkey];
let n_now = bucket.next_no as usize;
let m = if n_now == DISTS_PER_ENTRY {
let w = bucket.next_write_no as usize;
if w == DISTS_PER_ENTRY { 0 } else { w }
} else {
bucket.next_no += 1;
n_now
};
bucket.next_write_no = (m + 1) as i32;
if bucket.next_write_no > DISTS_PER_ENTRY as i32 {
bucket.next_write_no = 1;
}
bucket.list[m].pos_block = block_id;
bucket.list[m].key = key;
self.block_mut(block_id).timestamp_read = timestamp;
self.block_mut(block_id).next_match_no = 0;
self.block_mut(block_id).next_write_no = 0;
return (block_id, true);
}
{
let bucket = &mut self.tt_root[trick][hand][hashkey];
if bucket.next_write_no == DISTS_PER_ENTRY as i32 {
bucket.next_write_no = 1;
} else {
bucket.next_write_no += 1;
}
bucket.list[slot_idx].key = key;
bucket.list[slot_idx].pos_block = block_id;
}
self.block_mut(block_id).next_match_no = 0;
self.block_mut(block_id).next_write_no = 0;
(block_id, true)
}
#[inline]
fn lookup_cards(
&mut self,
block_id: BlockId,
top_set: &[u32; TT_BYTES],
limit: i32,
lower_flag: &mut bool,
) -> Option<&NodeCards> {
let bp = self.block(block_id);
let n = bp.next_write_no - 1;
let n2 = bp.next_match_no - 1;
let mut found: Option<i32> = None;
let mut found_lower = false;
for i in (0..=n).rev() {
if let Some(lower) = Self::match_entry(&bp.list[i as usize], top_set, limit) {
found = Some(i);
found_lower = lower;
break;
}
}
if found.is_none() {
for i in ((n + 1)..=n2).rev() {
if let Some(lower) = Self::match_entry(&bp.list[i as usize], top_set, limit) {
found = Some(i);
found_lower = lower;
break;
}
}
}
if let Some(idx) = found {
self.timestamp += 1;
let ts = self.timestamp;
let bp = self.block_mut(block_id);
bp.timestamp_read = ts;
*lower_flag = found_lower;
Some(&self.block(block_id).list[idx as usize].first)
} else {
None
}
}
#[inline]
fn match_entry(wp: &WinMatch, top_set: &[u32; TT_BYTES], limit: i32) -> Option<bool> {
if (wp.top_set[0] ^ top_set[0]) & wp.top_mask[0] != 0 {
return None;
}
if wp.last_mask_no != 1 {
if (wp.top_set[1] ^ top_set[1]) & wp.top_mask[1] != 0 {
return None;
}
if wp.last_mask_no != 2 && (wp.top_set[2] ^ top_set[2]) & wp.top_mask[2] != 0 {
return None;
}
}
let n = &wp.first;
if i32::from(n.lbound) > limit {
return Some(true);
}
if i32::from(n.ubound) <= limit {
return Some(false);
}
None
}
#[inline]
pub(crate) fn add(
&mut self,
trick: i32,
hand: i32,
aggr_target: &[u32; TT_SUITS],
win_ranks: [u16; TT_SUITS],
cards: NodeCards,
lower_flag: bool,
) {
let trick = trick as usize;
let hand = hand as usize;
let Some(block_id) = self.last_block_seen[trick][hand] else {
return; };
let mut ab: [[u32; TT_BYTES]; TT_SUITS] = [[0; TT_BYTES]; TT_SUITS];
let mut mb: [[u32; TT_BYTES]; TT_SUITS] = [[0; TT_BYTES]; TT_SUITS];
let mut low: [i32; TT_SUITS] = [0; TT_SUITS];
let mut entry = WinMatch {
first: cards,
xor_set: 0,
top_set: [0; TT_BYTES],
top_mask: [0; TT_BYTES],
mask_index: 0,
last_mask_no: 0,
};
for ss in 0..TT_SUITS {
let w = i32::from(win_ranks[ss]);
if w == 0 {
ab[ss] = self.aggr[0].aggr_bytes[ss];
mb[ss] = MASK_BYTES[0][ss];
low[ss] = 15;
entry.first.least_win[ss] = 0;
} else {
let lowest_bit = w & w.wrapping_neg();
let high_mask = lowest_bit.wrapping_neg() as u32;
let ag = (aggr_target[ss] & high_mask) as usize & 0x1fff;
ab[ss] = self.aggr[ag].aggr_bytes[ss];
mb[ss] = MASK_BYTES[ag][ss];
low[ss] = TT_LOWEST_RANK[ag];
entry.first.least_win[ss] = (15 - low[ss]) as u8;
entry.xor_set ^= self.aggr[ag].aggr_ranks[ss];
}
}
for b in 0..TT_BYTES {
entry.top_set[b] = ab[0][b] | ab[1][b] | ab[2][b] | ab[3][b];
entry.top_mask[b] = mb[0][b] | mb[1][b] | mb[2][b] | mb[3][b];
}
entry.mask_index = (low[0] << 12) | (low[1] << 8) | (low[2] << 4) | low[3];
entry.last_mask_no = if entry.top_mask[1] == 0 {
1
} else if entry.top_mask[2] == 0 {
2
} else if entry.top_mask[3] == 0 {
3
} else {
4
};
self.create_or_update(block_id, &entry, lower_flag);
}
fn create_or_update(&mut self, block_id: BlockId, search: &WinMatch, flag: bool) {
let bp = self.block_mut(block_id);
let n = bp.next_match_no as usize;
for i in 0..n {
let wp = &bp.list[i];
if wp.xor_set != search.xor_set {
continue;
}
if wp.mask_index != search.mask_index {
continue;
}
if wp.top_set[0] != search.top_set[0] {
continue;
}
if wp.top_set[1] != search.top_set[1] {
continue;
}
if wp.top_set[2] != search.top_set[2] {
continue;
}
let dst = &mut bp.list[i].first;
if search.first.lbound > dst.lbound {
dst.lbound = search.first.lbound;
}
if search.first.ubound < dst.ubound {
dst.ubound = search.first.ubound;
}
dst.best_move_suit = search.first.best_move_suit;
dst.best_move_rank = search.first.best_move_rank;
return;
}
if n == BLOCKS_PER_ENTRY {
if bp.next_write_no >= BLOCKS_PER_ENTRY as i32 {
bp.next_write_no = 0;
}
} else {
bp.next_match_no += 1;
}
let slot = bp.next_write_no as usize;
bp.list[slot] = *search;
if !flag {
bp.list[slot].first.best_move_suit = 0;
bp.list[slot].first.best_move_rank = 0;
}
bp.next_write_no += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_hand_lookup() -> [[i32; 15]; TT_SUITS] {
[[0; 15]; TT_SUITS]
}
#[test]
fn new_has_default_memory_limits() {
let tt = TransTable::new();
assert!(tt.pages_default >= 20);
assert!(tt.pages_maximum >= tt.pages_default);
assert!(tt.pages_maximum <= 45);
assert!(tt.pages.is_empty());
}
#[test]
fn with_memory_respects_arguments() {
let tt = TransTable::with_memory(60, 100);
let smaller = TransTable::with_memory(30, 50);
assert!(tt.pages_default > smaller.pages_default);
assert!(tt.pages_maximum > smaller.pages_maximum);
}
#[test]
fn hash8_known_values() {
let h1 = TransTable::hash8(&[0, 0, 0, 0]);
let h2 = TransTable::hash8(&[0x111, 0x222, 0x333, 0x444]);
let h3 = TransTable::hash8(&[0x444, 0x333, 0x222, 0x111]);
assert!(h1 < 256);
assert!(h2 < 256);
assert!(h3 < 256);
assert_ne!(h2, h3);
}
#[test]
fn add_then_lookup_round_trip() {
let mut tt = TransTable::new();
tt.init(&dummy_hand_lookup());
let trick = 5;
let hand = 0;
let aggr_target = [0x1fff_u32; 4]; let hand_dist = [0x0433, 0x0334, 0x0232, 0x0531];
let win_ranks = [0x1c00_u16; 4]; let cards = NodeCards {
ubound: 9,
lbound: 9,
best_move_suit: 1,
best_move_rank: 14,
least_win: [0; 4],
};
let mut lf = false;
let first = tt.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf);
assert!(first.is_none());
tt.add(trick, hand, &aggr_target, win_ranks, cards, true);
let mut lf2 = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf2);
let res = tt
.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf2)
.copied();
assert!(res.is_some(), "lookup after add must return the entry");
let got = res.unwrap();
assert_eq!(got.ubound, 9);
assert_eq!(got.lbound, 9);
assert_eq!(got.best_move_suit, 1);
assert_eq!(got.best_move_rank, 14);
assert!(lf2, "lbound > limit, so lower_flag must be set");
}
#[test]
fn lookup_with_different_hand_dist_misses() {
let mut tt = TransTable::new();
tt.init(&dummy_hand_lookup());
let trick = 7;
let hand = 1;
let aggr_target = [0x1fff_u32; 4];
let hd_a = [0x0433, 0x0334, 0x0232, 0x0531];
let hd_b = [0x0532, 0x0334, 0x0232, 0x0431]; let win_ranks = [0x1c00_u16; 4];
let cards = NodeCards {
ubound: 10,
lbound: 10,
best_move_suit: 2,
best_move_rank: 11,
least_win: [0; 4],
};
let mut lf = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hd_a, 5, &mut lf);
tt.add(trick, hand, &aggr_target, win_ranks, cards, true);
let mut lf = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hd_a, 5, &mut lf);
let res_a = tt
.lookup(trick, hand, &aggr_target, &hd_a, 5, &mut lf)
.copied();
assert!(res_a.is_some());
let mut lf = false;
let res_b = tt
.lookup(trick, hand, &aggr_target, &hd_b, 5, &mut lf)
.copied();
assert!(
res_b.is_none(),
"different hand_dist must not return entry from hd_a"
);
}
#[test]
fn add_two_entries_with_different_dists_both_retrievable() {
let mut tt = TransTable::new();
tt.init(&dummy_hand_lookup());
let trick = 8;
let hand = 2;
let aggr_target = [0x1fff_u32; 4];
let hd_a = [0x0433, 0x0334, 0x0232, 0x0531];
let hd_b = [0x0532, 0x0334, 0x0232, 0x0431];
let win_ranks = [0x1c00_u16; 4];
let mut lf = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hd_a, 5, &mut lf);
tt.add(
trick,
hand,
&aggr_target,
win_ranks,
NodeCards {
ubound: 7,
lbound: 7,
best_move_suit: 0,
best_move_rank: 5,
least_win: [0; 4],
},
true,
);
let _ = tt.lookup(trick, hand, &aggr_target, &hd_b, 5, &mut lf);
tt.add(
trick,
hand,
&aggr_target,
win_ranks,
NodeCards {
ubound: 8,
lbound: 8,
best_move_suit: 1,
best_move_rank: 6,
least_win: [0; 4],
},
true,
);
let mut lf = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hd_a, 3, &mut lf);
let got_a = tt
.lookup(trick, hand, &aggr_target, &hd_a, 3, &mut lf)
.copied();
assert_eq!(got_a.map(|n| n.best_move_rank), Some(5));
let _ = tt.lookup(trick, hand, &aggr_target, &hd_b, 3, &mut lf);
let got_b = tt
.lookup(trick, hand, &aggr_target, &hd_b, 3, &mut lf)
.copied();
assert_eq!(got_b.map(|n| n.best_move_rank), Some(6));
}
#[test]
fn reset_clears_all_entries() {
let mut tt = TransTable::new();
tt.init(&dummy_hand_lookup());
let trick = 6;
let hand = 3;
let aggr_target = [0x1fff_u32; 4];
let hand_dist = [0x0433, 0x0334, 0x0232, 0x0531];
let win_ranks = [0x1c00_u16; 4];
let mut lf = false;
let _ = tt.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf);
tt.add(
trick,
hand,
&aggr_target,
win_ranks,
NodeCards {
ubound: 10,
lbound: 10,
best_move_suit: 0,
best_move_rank: 12,
least_win: [0; 4],
},
true,
);
let _ = tt.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf);
let res = tt
.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf)
.copied();
assert!(res.is_some(), "pre-reset lookup must hit");
tt.reset();
assert!(tt.last_block_seen[trick as usize][hand as usize].is_none());
let mut lf = false;
let res = tt
.lookup(trick, hand, &aggr_target, &hand_dist, 5, &mut lf)
.copied();
assert!(res.is_none(), "post-reset lookup must miss");
}
#[test]
fn lookup_without_init_returns_none() {
let mut tt = TransTable::new();
let mut lf = false;
let res = tt.lookup(0, 0, &[0; 4], &[0; 4], 0, &mut lf);
assert!(res.is_none());
}
#[test]
fn set_memory_changes_budget() {
let mut tt = TransTable::new();
let original_default = tt.pages_default;
let original_max = tt.pages_maximum;
tt.set_memory_default(40);
tt.set_memory_maximum(80);
assert_ne!(tt.pages_default, original_default);
assert_ne!(tt.pages_maximum, original_max);
assert!(tt.pages_default < original_default);
assert!(tt.pages_maximum < original_max);
}
#[test]
fn page_allocation_grows_pool() {
let mut tt = TransTable::with_memory(10, 20);
tt.init(&dummy_hand_lookup());
let aggr_target = [0x1fff_u32; 4];
let win_ranks = [0x1c00_u16; 4];
let cards = NodeCards {
ubound: 5,
lbound: 5,
best_move_suit: 0,
best_move_rank: 5,
least_win: [0; 4],
};
let initial_pages = tt.pages.len();
for i in 0..500i32 {
let hd = [i & 0xfff, (i * 2) & 0xfff, (i * 3) & 0xfff, (i * 5) & 0xfff];
let mut lf = false;
let _ = tt.lookup(5, 0, &aggr_target, &hd, 3, &mut lf);
tt.add(5, 0, &aggr_target, win_ranks, cards, true);
}
assert!(tt.pages.len() > initial_pages);
}
}