1use crate::convert::dds_trump_from_strain;
35use crate::later_tricks::{later_tricks_max, later_tricks_min};
36use crate::lookup::{BIT_MAP_RANK, WIN_RANKS};
37use crate::move_type::{HighCard, MoveType};
38use crate::moves::{DDS_NOTRUMP, Moves, RelRanks};
39use crate::pos::{MAX_DEPTH, Pos};
40use crate::quick_tricks::{MAXNODE, MINNODE, quick_tricks, quick_tricks_second_hand};
41use crate::tt::{NodeCards, TransTable};
42use contract_bridge::Strain;
43
44const DDS_SUITS: usize = 4;
45const DDS_HANDS: usize = 4;
46
47macro_rules! stat {
53 ($($body:tt)*) => {{
54 #[cfg(feature = "profiling")]
55 {
56 $($body)*
57 }
58 }};
59}
60
61#[derive(Clone, Copy, Debug, Default)]
74pub struct SearchStats {
75 pub node0_entries: u64,
78 pub exit_tt_early: u64,
80 pub exit_trivial: u64,
82 pub exit_leaf: u64,
84 pub exit_quick: u64,
86 pub exit_later: u64,
88 pub exit_tt_late: u64,
90 pub reached_moveloop0: u64,
92
93 pub tt_lookups: u64,
96 pub tt_hits: u64,
98 pub tt_stores: u64,
100
101 pub cutoff_nodes: u64,
104 pub allnode_nodes: u64,
106 pub cutoff_first: u64,
108 pub cutoff_index_sum: u64,
110 pub cutoff_hist: [u64; 8],
112}
113
114#[cfg(feature = "profiling")]
115impl SearchStats {
116 #[inline]
119 fn note_cutoff(&mut self, move_index: u32) {
120 self.cutoff_nodes += 1;
121 self.cutoff_index_sum += u64::from(move_index);
122 if move_index == 1 {
123 self.cutoff_first += 1;
124 }
125 let bucket = (move_index.clamp(1, 8) - 1) as usize;
126 self.cutoff_hist[bucket] += 1;
127 }
128}
129
130const HAND_DELTA: [i32; DDS_SUITS] = [256, 16, 1, 0];
133
134#[derive(Clone, Copy, Debug, Default)]
140struct WinnerEntry {
141 suit: i32,
142 winner_rank: i32,
143 winner_hand: i32,
144 second_rank: i32,
145 second_hand: i32,
146}
147
148#[derive(Clone, Copy, Debug, Default)]
152struct Winners {
153 number: i32,
154 winner: [WinnerEntry; 4],
155}
156
157pub struct Engine {
160 pub moves: Moves,
161 pub node_type_store: [i32; DDS_HANDS],
162 pub ini_depth: i32,
163 pub trump: i32,
164
165 best_move: [MoveType; MAX_DEPTH],
167 best_move_tt: [MoveType; MAX_DEPTH],
168
169 lowest_win: [[u16; DDS_SUITS]; MAX_DEPTH],
171
172 forbidden_moves: [MoveType; 14],
174
175 winners: [Winners; 13],
177
178 rel: Box<[RelRanks; 8192]>,
182
183 pub search_target_calls: u64,
190 pub bisection_iters: u64,
191
192 pub iter1_nanos: u128,
198 pub later_nanos: u128,
199
200 pub stats: SearchStats,
204}
205
206impl Engine {
207 pub(crate) fn new(strain: Strain) -> Self {
211 let trump = dds_trump_from_strain(strain);
212 let mut moves = Moves::new();
213 moves.set_trump(trump);
214
215 let node_type_store = [MAXNODE, MINNODE, MAXNODE, MINNODE];
217
218 Self {
219 moves,
220 node_type_store,
221 ini_depth: 0,
222 trump,
223 best_move: [MoveType::default(); MAX_DEPTH],
224 best_move_tt: [MoveType::default(); MAX_DEPTH],
225 lowest_win: [[0u16; DDS_SUITS]; MAX_DEPTH],
226 forbidden_moves: [MoveType::default(); 14],
227 winners: [Winners::default(); 13],
228 rel: vec![RelRanks::default(); 8192]
229 .into_boxed_slice()
230 .try_into()
231 .unwrap_or_else(|_| unreachable!()),
232 search_target_calls: 0,
233 bisection_iters: 0,
234 iter1_nanos: 0,
235 later_nanos: 0,
236 stats: SearchStats::default(),
237 }
238 }
239
240 pub(crate) const fn set_node_types(&mut self, node_type_store: [i32; DDS_HANDS]) {
246 self.node_type_store = node_type_store;
247 }
248
249 pub(crate) fn set_strain(&mut self, strain: Strain) {
252 let trump = dds_trump_from_strain(strain);
253 self.trump = trump;
254 self.moves.set_trump(trump);
255 }
256
257 pub(crate) fn reset_best_moves(&mut self) {
260 for d in 0..MAX_DEPTH {
261 self.best_move[d] = MoveType::default();
262 self.best_move_tt[d] = MoveType::default();
263 }
264 }
265
266 pub(crate) fn set_deal(&mut self, pos: &mut Pos, tt: &mut TransTable) {
277 for s in 0..DDS_SUITS {
279 pos.aggr[s] = 0;
280 for h in 0..DDS_HANDS {
281 pos.aggr[s] |= pos.rank_in_suit[h][s];
282 }
283 }
284 for h in 0..DDS_HANDS {
285 for s in 0..DDS_SUITS {
286 pos.length[h][s] = pos.rank_in_suit[h][s].count_ones() as u8;
287 }
288 }
289 for h in 0..DDS_HANDS {
290 pos.hand_dist[h] = (i32::from(pos.length[h][0]) << 8)
291 | (i32::from(pos.length[h][1]) << 4)
292 | i32::from(pos.length[h][2]);
293 }
294
295 let mut hand_lookup = [[0i32; 15]; DDS_SUITS];
297 for (s, suit_lookup) in hand_lookup.iter_mut().enumerate() {
298 for r in (2..=14).rev() {
299 suit_lookup[r] = 0;
300 for h in 0..DDS_HANDS {
301 if pos.rank_in_suit[h][s] & BIT_MAP_RANK[r] != 0 {
302 suit_lookup[r] = h as i32;
303 break;
304 }
305 }
306 }
307 }
308
309 tt.init(&hand_lookup);
310
311 for ord in 1..=13usize {
314 for s in 0..DDS_SUITS {
315 self.rel[0].abs_rank[ord][s].hand = -1;
316 self.rel[0].abs_rank[ord][s].rank = 0;
317 }
318 }
319
320 let mut top_bit_rank: u32 = 1;
321 let mut top_bit_no: usize = 2;
322 for aggr in 1u32..8192 {
323 if aggr >= (top_bit_rank << 1) {
324 top_bit_rank <<= 1;
325 top_bit_no += 1;
326 }
327 self.rel[aggr as usize] = self.rel[(aggr ^ top_bit_rank) as usize];
328
329 let weight = aggr.count_ones() as usize;
330 for c in (2..=weight).rev() {
331 for s in 0..DDS_SUITS {
332 let prev_hand = self.rel[aggr as usize].abs_rank[c - 1][s].hand;
333 let prev_rank = self.rel[aggr as usize].abs_rank[c - 1][s].rank;
334 self.rel[aggr as usize].abs_rank[c][s].hand = prev_hand;
335 self.rel[aggr as usize].abs_rank[c][s].rank = prev_rank;
336 }
337 }
338 for (s, suit_lookup) in hand_lookup.iter().enumerate() {
339 self.rel[aggr as usize].abs_rank[1][s].hand = suit_lookup[top_bit_no];
340 self.rel[aggr as usize].abs_rank[1][s].rank = top_bit_no as i32;
341 }
342 }
343
344 for s in 0..DDS_SUITS {
349 let a = pos.aggr[s] as usize;
350 pos.winner[s] = HighCard {
351 rank: self.rel[a].abs_rank[1][s].rank,
352 hand: self.rel[a].abs_rank[1][s].hand,
353 };
354 pos.second_best[s] = HighCard {
355 rank: self.rel[a].abs_rank[2][s].rank,
356 hand: self.rel[a].abs_rank[2][s].hand,
357 };
358 }
359 }
360
361 #[inline]
367 const fn make0(&mut self, pos: &mut Pos, depth: i32, mply: &MoveType) {
368 let depth_u = depth as usize;
369 let h = pos.first[depth_u] as usize;
370 let s = mply.suit as usize;
371 let r = mply.rank as usize;
372
373 pos.first[depth_u - 1] = pos.first[depth_u];
374 pos.move_history[depth_u] = *mply;
375
376 pos.rank_in_suit[h][s] &= !BIT_MAP_RANK[r];
377 pos.aggr[s] ^= BIT_MAP_RANK[r];
378 pos.hand_dist[h] -= HAND_DELTA[s];
379 pos.length[h][s] = pos.length[h][s].saturating_sub(1);
380 }
381
382 #[inline]
384 const fn make1(&mut self, pos: &mut Pos, depth: i32, mply: &MoveType) {
385 let depth_u = depth as usize;
386 let first_hand = pos.first[depth_u];
387 pos.first[depth_u - 1] = first_hand;
388
389 let h = ((first_hand + 1) & 3) as usize;
390 let s = mply.suit as usize;
391 let r = mply.rank as usize;
392
393 pos.rank_in_suit[h][s] &= !BIT_MAP_RANK[r];
394 pos.aggr[s] ^= BIT_MAP_RANK[r];
395 pos.hand_dist[h] -= HAND_DELTA[s];
396 pos.length[h][s] = pos.length[h][s].saturating_sub(1);
397 }
398
399 #[inline]
401 const fn make2(&mut self, pos: &mut Pos, depth: i32, mply: &MoveType) {
402 let depth_u = depth as usize;
403 let first_hand = pos.first[depth_u];
404 pos.first[depth_u - 1] = first_hand;
405
406 let h = ((first_hand + 2) & 3) as usize;
407 let s = mply.suit as usize;
408 let r = mply.rank as usize;
409
410 pos.rank_in_suit[h][s] &= !BIT_MAP_RANK[r];
411 pos.aggr[s] ^= BIT_MAP_RANK[r];
412 pos.hand_dist[h] -= HAND_DELTA[s];
413 pos.length[h][s] = pos.length[h][s].saturating_sub(1);
414 }
415
416 #[inline]
420 fn make3(
421 &mut self,
422 pos: &mut Pos,
423 trick_cards: &mut [u16; DDS_SUITS],
424 depth: i32,
425 mply: &MoveType,
426 ) {
427 let depth_u = depth as usize;
428 let first_hand = pos.first[depth_u];
429 let trick = ((depth + 3) >> 2) as usize;
430
431 let data = self.moves.get_trick_data((depth + 3) >> 2);
432
433 pos.first[depth_u - 1] = (first_hand + data.rel_winner) & 3;
434
435 let h = ((first_hand + 3) & 3) as usize;
436
437 trick_cards.fill(0);
438 let ss = data.best_suit as usize;
439 if data.play_count[ss] >= 2 {
440 let rr = data.best_rank as usize;
441 trick_cards[ss] = BIT_MAP_RANK[rr] | (data.best_sequence as u16);
442 }
443
444 let r = mply.rank as usize;
445 let s = mply.suit as usize;
446 pos.rank_in_suit[h][s] &= !BIT_MAP_RANK[r];
447 pos.aggr[s] ^= BIT_MAP_RANK[r];
448 pos.hand_dist[h] -= HAND_DELTA[s];
449 pos.length[h][s] = pos.length[h][s].saturating_sub(1);
450
451 let wp = &mut self.winners[trick];
453 wp.number = 0;
454 for st in 0..DDS_SUITS {
455 if data.play_count[st] != 0 {
456 let n = wp.number as usize;
457 wp.winner[n].suit = st as i32;
458 wp.winner[n].winner_rank = pos.winner[st].rank;
459 wp.winner[n].winner_hand = pos.winner[st].hand;
460 wp.winner[n].second_rank = pos.second_best[st].rank;
461 wp.winner[n].second_hand = pos.second_best[st].hand;
462 wp.number += 1;
463
464 let aggr = pos.aggr[st] as usize;
465 pos.winner[st].rank = self.rel[aggr].abs_rank[1][st].rank;
466 pos.winner[st].hand = self.rel[aggr].abs_rank[1][st].hand;
467 pos.second_best[st].rank = self.rel[aggr].abs_rank[2][st].rank;
468 pos.second_best[st].hand = self.rel[aggr].abs_rank[2][st].hand;
469 }
470 }
471 }
472
473 #[inline]
477 fn undo0(&self, pos: &mut Pos, depth: i32, mply: &MoveType) {
478 let depth_u = depth as usize;
479 let trick = ((depth + 3) >> 2) as usize;
480 let h = ((pos.first[depth_u] + 3) & 3) as usize;
481 let s = mply.suit as usize;
482 let r = mply.rank as usize;
483
484 pos.rank_in_suit[h][s] |= BIT_MAP_RANK[r];
485 pos.aggr[s] |= BIT_MAP_RANK[r];
486 pos.hand_dist[h] += HAND_DELTA[s];
487 pos.length[h][s] = pos.length[h][s].saturating_add(1);
488
489 let wp = &self.winners[trick];
490 for n in 0..(wp.number as usize) {
491 let st = wp.winner[n].suit as usize;
492 pos.winner[st].rank = wp.winner[n].winner_rank;
493 pos.winner[st].hand = wp.winner[n].winner_hand;
494 pos.second_best[st].rank = wp.winner[n].second_rank;
495 pos.second_best[st].hand = wp.winner[n].second_hand;
496 }
497 }
498
499 #[inline]
501 const fn undo1(&self, pos: &mut Pos, depth: i32, mply: &MoveType) {
502 let depth_u = depth as usize;
503 let h = pos.first[depth_u] as usize;
504 let s = mply.suit as usize;
505 let r = mply.rank as usize;
506
507 pos.rank_in_suit[h][s] |= BIT_MAP_RANK[r];
508 pos.aggr[s] |= BIT_MAP_RANK[r];
509 pos.hand_dist[h] += HAND_DELTA[s];
510 pos.length[h][s] = pos.length[h][s].saturating_add(1);
511 }
512
513 #[inline]
515 const fn undo2(&self, pos: &mut Pos, depth: i32, mply: &MoveType) {
516 let depth_u = depth as usize;
517 let h = ((pos.first[depth_u] + 1) & 3) as usize;
518 let s = mply.suit as usize;
519 let r = mply.rank as usize;
520
521 pos.rank_in_suit[h][s] |= BIT_MAP_RANK[r];
522 pos.aggr[s] |= BIT_MAP_RANK[r];
523 pos.hand_dist[h] += HAND_DELTA[s];
524 pos.length[h][s] = pos.length[h][s].saturating_add(1);
525 }
526
527 #[inline]
529 const fn undo3(&self, pos: &mut Pos, depth: i32, mply: &MoveType) {
530 let depth_u = depth as usize;
531 let h = ((pos.first[depth_u] + 2) & 3) as usize;
532 let s = mply.suit as usize;
533 let r = mply.rank as usize;
534
535 pos.rank_in_suit[h][s] |= BIT_MAP_RANK[r];
536 pos.aggr[s] |= BIT_MAP_RANK[r];
537 pos.hand_dist[h] += HAND_DELTA[s];
538 pos.length[h][s] = pos.length[h][s].saturating_add(1);
539 }
540
541 #[inline]
549 fn evaluate(&self, pos: &Pos, eval_win_ranks: &mut [u16; DDS_SUITS]) -> i32 {
550 let trump = self.trump;
551 let first_hand = pos.first[0] as usize;
552
553 eval_win_ranks.fill(0);
554
555 let mut hmax: usize = 0;
556 let mut rmax: u16 = 0;
557 let mut count = 0;
558
559 if trump != DDS_NOTRUMP {
560 for h in 0..DDS_HANDS {
561 if pos.rank_in_suit[h][trump as usize] != 0 {
562 count += 1;
563 }
564 if pos.rank_in_suit[h][trump as usize] > rmax {
565 hmax = h;
566 rmax = pos.rank_in_suit[h][trump as usize];
567 }
568 }
569
570 if rmax > 0 {
571 if count >= 2 {
572 eval_win_ranks[trump as usize] = rmax;
573 }
574 if self.node_type_store[hmax] == MAXNODE {
575 return pos.tricks_max + 1;
576 }
577 return pos.tricks_max;
578 }
579 }
580
581 let mut k = 0usize;
583 while k < DDS_SUITS {
584 if pos.rank_in_suit[first_hand][k] != 0 {
585 break;
586 }
587 k += 1;
588 }
589 debug_assert!(k < DDS_SUITS, "Evaluate: 1st hand has no cards");
590
591 for h in 0..DDS_HANDS {
592 if pos.rank_in_suit[h][k] != 0 {
593 count += 1;
594 }
595 if pos.rank_in_suit[h][k] > rmax {
596 hmax = h;
597 rmax = pos.rank_in_suit[h][k];
598 }
599 }
600
601 if count >= 2 {
602 eval_win_ranks[k] = rmax;
603 }
604
605 if self.node_type_store[hmax] == MAXNODE {
606 pos.tricks_max + 1
607 } else {
608 pos.tricks_max
609 }
610 }
611
612 #[inline]
618 pub(crate) fn ab_search_0(
619 &mut self,
620 pos: &mut Pos,
621 tt: &mut TransTable,
622 target: i32,
623 depth: i32,
624 ) -> bool {
625 let trump = self.trump;
626 let depth_u = depth as usize;
627 let hand = pos.first[depth_u];
628 let tricks = depth >> 2;
629
630 pos.win_ranks[depth_u] = [0; DDS_SUITS];
631 stat!(self.stats.node0_entries += 1;);
632
633 if depth >= 20
634 && (tricks as usize) < 12
635 && let Some(value) = self.tt_lookup(pos, tt, target, depth, tricks, hand)
636 {
637 stat!(self.stats.exit_tt_early += 1;);
638 return value;
639 }
640
641 if pos.tricks_max >= target {
642 stat!(self.stats.exit_trivial += 1;);
643 return true;
644 }
645 if pos.tricks_max + tricks + 1 < target {
646 stat!(self.stats.exit_trivial += 1;);
647 return false;
648 }
649 if depth == 0 {
650 let mut ev_win = [0u16; DDS_SUITS];
651 let value_tricks = self.evaluate(pos, &mut ev_win);
652 let value = value_tricks >= target;
653 pos.win_ranks[depth_u].copy_from_slice(&ev_win);
654 stat!(self.stats.exit_leaf += 1;);
655 return value;
656 }
657
658 let mut res = false;
660 let qtricks = quick_tricks(
661 pos,
662 hand,
663 depth,
664 target,
665 trump,
666 &mut res,
667 &self.node_type_store,
668 );
669 let hand_u = hand as usize;
670
671 if self.node_type_store[hand_u] == MAXNODE {
672 if res {
673 stat!(self.stats.exit_quick += 1;);
674 return qtricks != 0;
675 }
676 if !later_tricks_min(pos, hand, depth, target, trump, &self.node_type_store) {
677 stat!(self.stats.exit_later += 1;);
678 return false;
679 }
680 } else {
681 if res {
682 stat!(self.stats.exit_quick += 1;);
683 return qtricks == 0;
684 }
685 if later_tricks_max(pos, hand, depth, target, trump, &self.node_type_store) {
686 stat!(self.stats.exit_later += 1;);
687 return true;
688 }
689 }
690
691 if depth < 20
693 && (tricks as usize) < 12
694 && let Some(value) = self.tt_lookup(pos, tt, target, depth, tricks, hand)
695 {
696 stat!(self.stats.exit_tt_late += 1;);
697 return value;
698 }
699
700 let success = self.node_type_store[hand_u] == MAXNODE;
702 let mut value = !success;
703 stat!(self.stats.reached_moveloop0 += 1;);
704
705 self.lowest_win[depth_u] = [0; DDS_SUITS];
706 let bm = self.best_move[depth_u];
707 let bmtt = self.best_move_tt[depth_u];
708 self.moves
709 .move_gen_0(tricks, pos, &bm, &bmtt, self.rel.as_ref());
710
711 pos.win_ranks[depth_u] = [0; DDS_SUITS];
712
713 let mut chosen_move = MoveType::default();
714 let mut cutoff = false;
715 #[cfg(feature = "profiling")]
716 let mut move_index = 0u32;
717 loop {
718 let win_arr = pos.win_ranks[depth_u];
719 let mply = self.moves.make_next(tricks, 0, &win_arr);
720 let Some(mply) = mply else { break };
721 stat!(move_index += 1;);
722
723 self.make0(pos, depth, &mply);
724 value = self.ab_search_1(pos, tt, target, depth - 1);
725 self.undo1(pos, depth, &mply);
726
727 let prev = pos.win_ranks[depth_u - 1];
728 if value == success {
729 pos.win_ranks[depth_u] = prev;
730 chosen_move = mply;
731 cutoff = true;
732 break;
733 }
734 let cur = &mut pos.win_ranks[depth_u];
735 for ss in 0..DDS_SUITS {
736 cur[ss] |= prev[ss];
737 }
738 }
739
740 if cutoff {
741 self.best_move[depth_u] = chosen_move;
742 }
743 #[cfg(feature = "profiling")]
744 if cutoff {
745 self.stats.note_cutoff(move_index);
746 } else {
747 self.stats.allnode_nodes += 1;
748 }
749
750 if (tricks as usize) < 12 {
752 let mut first = NodeCards::default();
753 if value {
754 if self.node_type_store[0] == MAXNODE {
755 first.ubound = (tricks + 1) as i8;
756 first.lbound = (target - pos.tricks_max) as i8;
757 } else {
758 first.ubound = (tricks + 1 - target + pos.tricks_max) as i8;
759 first.lbound = 0;
760 }
761 } else if self.node_type_store[0] == MAXNODE {
762 first.ubound = (target - pos.tricks_max - 1) as i8;
763 first.lbound = 0;
764 } else {
765 first.ubound = (tricks + 1) as i8;
766 first.lbound = (tricks + 1 - target + pos.tricks_max + 1) as i8;
767 }
768 first.best_move_suit = self.best_move[depth_u].suit as u8;
769 first.best_move_rank = self.best_move[depth_u].rank as u8;
770
771 let flag = (self.node_type_store[hand_u] == MAXNODE && value)
772 || (self.node_type_store[hand_u] == MINNODE && !value);
773
774 let aggr_u32 = [
776 u32::from(pos.aggr[0]),
777 u32::from(pos.aggr[1]),
778 u32::from(pos.aggr[2]),
779 u32::from(pos.aggr[3]),
780 ];
781 tt.add(tricks, hand, &aggr_u32, pos.win_ranks[depth_u], first, flag);
782 stat!(self.stats.tt_stores += 1;);
783 }
784
785 value
786 }
787
788 #[inline]
792 fn tt_lookup(
793 &mut self,
794 pos: &mut Pos,
795 tt: &mut TransTable,
796 target: i32,
797 depth: i32,
798 tricks: i32,
799 hand: i32,
800 ) -> Option<bool> {
801 let depth_u = depth as usize;
802 let limit = if self.node_type_store[0] == MAXNODE {
803 target - pos.tricks_max - 1
804 } else {
805 tricks - (target - pos.tricks_max - 1)
806 };
807 let aggr_u32 = [
808 u32::from(pos.aggr[0]),
809 u32::from(pos.aggr[1]),
810 u32::from(pos.aggr[2]),
811 u32::from(pos.aggr[3]),
812 ];
813 let mut lower_flag = false;
814 stat!(self.stats.tt_lookups += 1;);
815 let cards = tt.lookup(
816 tricks,
817 hand,
818 &aggr_u32,
819 &pos.hand_dist,
820 limit,
821 &mut lower_flag,
822 )?;
823 stat!(self.stats.tt_hits += 1;);
824 let lw = cards.least_win;
825 let bm_suit = i32::from(cards.best_move_suit);
826 let bm_rank = i32::from(cards.best_move_rank);
827
828 for ss in 0..DDS_SUITS {
829 pos.win_ranks[depth_u][ss] = WIN_RANKS[pos.aggr[ss] as usize][lw[ss] as usize];
830 }
831 if bm_rank != 0 {
832 self.best_move_tt[depth_u].suit = bm_suit;
833 self.best_move_tt[depth_u].rank = bm_rank;
834 }
835 let score = if self.node_type_store[0] == MAXNODE {
836 lower_flag
837 } else {
838 !lower_flag
839 };
840 Some(score)
841 }
842
843 #[inline]
845 pub(crate) fn ab_search_1(
846 &mut self,
847 pos: &mut Pos,
848 tt: &mut TransTable,
849 target: i32,
850 depth: i32,
851 ) -> bool {
852 let trump = self.trump;
853 let depth_u = depth as usize;
854 let hand = (pos.first[depth_u] + 1) & 3;
855 let success = self.node_type_store[hand as usize] == MAXNODE;
856 let mut value = !success;
857 let tricks = (depth + 3) >> 2;
858
859 if quick_tricks_second_hand(
860 pos,
861 hand,
862 depth,
863 target,
864 trump,
865 &self.node_type_store,
866 self.ini_depth,
867 ) {
868 return success;
869 }
870
871 self.lowest_win[depth_u] = [0; DDS_SUITS];
872 self.moves.move_gen_123(tricks, 1, pos);
873 if depth == self.ini_depth {
874 let fm = self.forbidden_moves;
875 self.moves.purge(tricks, 1, &fm);
876 }
877
878 pos.win_ranks[depth_u] = [0; DDS_SUITS];
879
880 let mut chosen_move = MoveType::default();
881 let mut cutoff = false;
882 #[cfg(feature = "profiling")]
883 let mut move_index = 0u32;
884 loop {
885 let win_arr = pos.win_ranks[depth_u];
886 let mply = self.moves.make_next(tricks, 1, &win_arr);
887 let Some(mply) = mply else { break };
888 stat!(move_index += 1;);
889
890 self.make1(pos, depth, &mply);
891 value = self.ab_search_2(pos, tt, target, depth - 1);
892 self.undo2(pos, depth, &mply);
893
894 let prev = pos.win_ranks[depth_u - 1];
895 if value == success {
896 pos.win_ranks[depth_u] = prev;
897 chosen_move = mply;
898 cutoff = true;
899 break;
900 }
901 let cur = &mut pos.win_ranks[depth_u];
902 for ss in 0..DDS_SUITS {
903 cur[ss] |= prev[ss];
904 }
905 }
906 if cutoff {
907 self.best_move[depth_u] = chosen_move;
908 }
909 #[cfg(feature = "profiling")]
910 if cutoff {
911 self.stats.note_cutoff(move_index);
912 } else {
913 self.stats.allnode_nodes += 1;
914 }
915 value
916 }
917
918 #[inline]
920 pub(crate) fn ab_search_2(
921 &mut self,
922 pos: &mut Pos,
923 tt: &mut TransTable,
924 target: i32,
925 depth: i32,
926 ) -> bool {
927 let depth_u = depth as usize;
928 let hand = (pos.first[depth_u] + 2) & 3;
929 let success = self.node_type_store[hand as usize] == MAXNODE;
930 let mut value = !success;
931 let tricks = (depth + 3) >> 2;
932
933 self.lowest_win[depth_u] = [0; DDS_SUITS];
934 self.moves.move_gen_123(tricks, 2, pos);
935 if depth == self.ini_depth {
936 let fm = self.forbidden_moves;
937 self.moves.purge(tricks, 2, &fm);
938 }
939
940 pos.win_ranks[depth_u] = [0; DDS_SUITS];
941
942 let mut chosen_move = MoveType::default();
943 let mut cutoff = false;
944 #[cfg(feature = "profiling")]
945 let mut move_index = 0u32;
946 loop {
947 let win_arr = pos.win_ranks[depth_u];
948 let mply = self.moves.make_next(tricks, 2, &win_arr);
949 let Some(mply) = mply else { break };
950 stat!(move_index += 1;);
951
952 self.make2(pos, depth, &mply);
953 value = self.ab_search_3(pos, tt, target, depth - 1);
954 self.undo3(pos, depth, &mply);
955
956 let prev = pos.win_ranks[depth_u - 1];
957 if value == success {
958 pos.win_ranks[depth_u] = prev;
959 chosen_move = mply;
960 cutoff = true;
961 break;
962 }
963 let cur = &mut pos.win_ranks[depth_u];
964 for ss in 0..DDS_SUITS {
965 cur[ss] |= prev[ss];
966 }
967 }
968 if cutoff {
969 self.best_move[depth_u] = chosen_move;
970 }
971 #[cfg(feature = "profiling")]
972 if cutoff {
973 self.stats.note_cutoff(move_index);
974 } else {
975 self.stats.allnode_nodes += 1;
976 }
977 value
978 }
979
980 #[inline]
985 pub(crate) fn ab_search_3(
986 &mut self,
987 pos: &mut Pos,
988 tt: &mut TransTable,
989 target: i32,
990 depth: i32,
991 ) -> bool {
992 let depth_u = depth as usize;
993 let hand = (pos.first[depth_u] + 3) & 3;
994 let success = self.node_type_store[hand as usize] == MAXNODE;
995 let mut value = !success;
996 let tricks = (depth + 3) >> 2;
997
998 self.lowest_win[depth_u] = [0; DDS_SUITS];
999 self.moves.move_gen_123(tricks, 3, pos);
1000 if depth == self.ini_depth {
1001 let fm = self.forbidden_moves;
1002 self.moves.purge(tricks, 3, &fm);
1003 }
1004
1005 pos.win_ranks[depth_u] = [0; DDS_SUITS];
1006
1007 let mut make_win_rank = [0u16; DDS_SUITS];
1008 let mut chosen_move = MoveType::default();
1009 let mut cutoff = false;
1010 #[cfg(feature = "profiling")]
1011 let mut move_index = 0u32;
1012
1013 loop {
1014 let win_arr = pos.win_ranks[depth_u];
1015 let mply = self.moves.make_next(tricks, 3, &win_arr);
1016 let Some(mply) = mply else { break };
1017 stat!(move_index += 1;);
1018
1019 self.make3(pos, &mut make_win_rank, depth, &mply);
1020
1021 let new_lead = pos.first[depth_u - 1] as usize;
1022 let incremented = self.node_type_store[new_lead] == MAXNODE;
1023 if incremented {
1024 pos.tricks_max += 1;
1025 }
1026
1027 value = self.ab_search_0(pos, tt, target, depth - 1);
1028
1029 self.undo0(pos, depth, &mply);
1030
1031 if incremented {
1032 pos.tricks_max -= 1;
1033 }
1034
1035 let prev = pos.win_ranks[depth_u - 1];
1036 if value == success {
1037 let cur = &mut pos.win_ranks[depth_u];
1038 for ss in 0..DDS_SUITS {
1039 cur[ss] = prev[ss] | make_win_rank[ss];
1040 }
1041 chosen_move = mply;
1042 cutoff = true;
1043 break;
1044 }
1045 let cur = &mut pos.win_ranks[depth_u];
1046 for ss in 0..DDS_SUITS {
1047 cur[ss] |= prev[ss] | make_win_rank[ss];
1048 }
1049 }
1050 if cutoff {
1051 self.best_move[depth_u] = chosen_move;
1052 }
1053 #[cfg(feature = "profiling")]
1054 if cutoff {
1055 self.stats.note_cutoff(move_index);
1056 } else {
1057 self.stats.allnode_nodes += 1;
1058 }
1059 value
1060 }
1061
1062 pub(crate) fn search_target(
1074 &mut self,
1075 pos: &mut Pos,
1076 tt: &mut TransTable,
1077 ini_depth: i32,
1078 ) -> i32 {
1079 self.search_target_calls += 1;
1080 self.ini_depth = ini_depth;
1081
1082 let total_cards: i32 = (0..DDS_HANDS)
1087 .map(|h| {
1088 (0..DDS_SUITS)
1089 .map(|s| i32::from(pos.length[h][s]))
1090 .sum::<i32>()
1091 })
1092 .sum();
1093 if total_cards == 0 {
1094 return 0;
1095 }
1096
1097 let start_trick = (ini_depth + 3) >> 2;
1100 self.moves.init_removed_ranks(start_trick, pos);
1101
1102 let lead_hand = pos.first[ini_depth as usize];
1103 self.moves.reinit(start_trick, lead_hand);
1104
1105 let mut lowerbound = 0i32;
1106 let mut upperbound = (ini_depth + 4) >> 2;
1107 if upperbound <= 0 {
1108 return 0;
1109 }
1110
1111 let mut iter_idx = 0u32;
1112 while lowerbound < upperbound {
1113 self.bisection_iters += 1;
1114 iter_idx += 1;
1115 let target = (lowerbound + upperbound + 1) / 2;
1116 self.reset_best_moves();
1117 let t0 = std::time::Instant::now();
1118 let val = self.ab_search_0(pos, tt, target, ini_depth);
1119 let dt = t0.elapsed().as_nanos();
1120 if iter_idx == 1 {
1121 self.iter1_nanos += dt;
1122 } else {
1123 self.later_nanos += dt;
1124 }
1125 if val {
1126 lowerbound = target;
1127 } else {
1128 upperbound = target - 1;
1129 }
1130 }
1131
1132 lowerbound
1133 }
1134}
1135
1136#[cfg(test)]
1141mod tests {
1142 use super::*;
1143 use crate::lookup::BIT_MAP_RANK;
1144
1145 fn build_pos(rank_in_suit: [[u16; 4]; 4]) -> Pos {
1148 let mut p = Pos {
1149 rank_in_suit,
1150 ..Pos::default()
1151 };
1152 for (s, aggr) in p.aggr.iter_mut().enumerate() {
1153 *aggr = (0..4).fold(0u16, |a, h| a | rank_in_suit[h][s]);
1154 }
1155 for (h, length_row) in p.length.iter_mut().enumerate() {
1156 for (s, length) in length_row.iter_mut().enumerate() {
1157 *length = rank_in_suit[h][s].count_ones() as u8;
1158 }
1159 }
1160 p
1161 }
1162
1163 fn card_count(p: &Pos) -> i32 {
1165 p.length.iter().flatten().map(|&n| i32::from(n)).sum()
1166 }
1167
1168 #[test]
1169 fn empty_position_returns_zero() {
1170 let mut pos = Pos::default();
1171 let mut tt = TransTable::new();
1172 let mut eng = Engine::new(Strain::Notrump);
1173 eng.set_deal(&mut pos, &mut tt);
1174
1175 let tricks = eng.search_target(&mut pos, &mut tt, 0);
1177 assert_eq!(tricks, 0);
1178 }
1179
1180 #[test]
1181 fn one_winner_returns_one() {
1182 let mut rank = [[0u16; 4]; 4];
1185 rank[0][0] = BIT_MAP_RANK[14]; rank[1][1] = BIT_MAP_RANK[2]; rank[2][2] = BIT_MAP_RANK[2]; rank[3][3] = BIT_MAP_RANK[2]; let mut pos = build_pos(rank);
1190
1191 let cc = card_count(&pos);
1192 assert_eq!(cc, 4, "should be exactly 4 cards");
1193 let ini_depth = cc - 4; pos.first[ini_depth as usize] = 0;
1197
1198 let mut tt = TransTable::new();
1199 let mut eng = Engine::new(Strain::Notrump);
1200 eng.set_node_types([MAXNODE, MINNODE, MAXNODE, MINNODE]);
1202 eng.set_deal(&mut pos, &mut tt);
1203
1204 let tricks = eng.search_target(&mut pos, &mut tt, ini_depth);
1205 assert_eq!(tricks, 1, "MAX should win 1 trick with the A");
1206 }
1207
1208 #[test]
1209 fn one_loser_returns_zero() {
1210 let mut rank = [[0u16; 4]; 4];
1213 rank[0][0] = BIT_MAP_RANK[2]; rank[1][1] = BIT_MAP_RANK[14]; rank[2][2] = BIT_MAP_RANK[2]; rank[3][3] = BIT_MAP_RANK[2]; let mut pos = build_pos(rank);
1218
1219 let cc = card_count(&pos);
1220 assert_eq!(cc, 4);
1221 let ini_depth = cc - 4; pos.first[ini_depth as usize] = 1;
1225
1226 let mut tt = TransTable::new();
1227 let mut eng = Engine::new(Strain::Notrump);
1228 eng.set_node_types([MAXNODE, MINNODE, MAXNODE, MINNODE]);
1232 eng.set_deal(&mut pos, &mut tt);
1233
1234 let tricks = eng.search_target(&mut pos, &mut tt, ini_depth);
1235 assert_eq!(tricks, 0, "MAX should win 0 tricks (MIN owns the A)");
1236 }
1237
1238 #[test]
1239 fn small_real_position() {
1240 let mut rank = [[0u16; 4]; 4];
1250 rank[0][0] = BIT_MAP_RANK[14];
1252 rank[0][1] = BIT_MAP_RANK[2];
1253 rank[0][2] = BIT_MAP_RANK[2];
1254 rank[0][3] = BIT_MAP_RANK[2];
1255 rank[1][0] = BIT_MAP_RANK[3];
1257 rank[1][1] = BIT_MAP_RANK[14];
1258 rank[1][2] = BIT_MAP_RANK[3];
1259 rank[1][3] = BIT_MAP_RANK[3];
1260 rank[2][0] = BIT_MAP_RANK[4];
1262 rank[2][1] = BIT_MAP_RANK[4];
1263 rank[2][2] = BIT_MAP_RANK[14];
1264 rank[2][3] = BIT_MAP_RANK[4];
1265 rank[3][0] = BIT_MAP_RANK[5];
1267 rank[3][1] = BIT_MAP_RANK[5];
1268 rank[3][2] = BIT_MAP_RANK[5];
1269 rank[3][3] = BIT_MAP_RANK[14];
1270
1271 let mut pos = build_pos(rank);
1272 let cc = card_count(&pos);
1273 assert_eq!(cc, 16);
1274 let ini_depth = cc - 4; pos.first[ini_depth as usize] = 0;
1278
1279 let mut tt = TransTable::new();
1280 let mut eng = Engine::new(Strain::Notrump);
1281 eng.set_node_types([MAXNODE, MINNODE, MAXNODE, MINNODE]);
1282 eng.set_deal(&mut pos, &mut tt);
1283
1284 let tricks = eng.search_target(&mut pos, &mut tt, ini_depth);
1285 assert_eq!(
1286 tricks, 2,
1287 "NS should win exactly 2 tricks (the two aces they hold)"
1288 );
1289 }
1290}