Skip to main content

pons_dds/
search.rs

1//! Alpha-beta search.
2//!
3//! Ports
4//! [`ABsearch.cpp`](../../../ddss-sys/vendor/src/ABsearch.cpp). The
5//! [`Engine`] struct holds the per-search state (the vendor's
6//! `ThreadData` minus TT — the TT is owned separately for lifetime
7//! reasons) and exposes the bisection driver
8//! [`Engine::search_target`].
9//!
10//! ## Layout
11//!
12//! Each `Engine` owns:
13//! * [`crate::moves::Moves`] — the move generator and per-trick state.
14//! * `node_type_store[4]` — MAX/MIN classification per seat.
15//! * `rel: Box<[RelRanks; 8192]>` — the per-deal absolute-rank lookup
16//!   (computed in [`Engine::set_deal`]; see vendor `SetDealTables`).
17//! * Scratch arrays for per-depth `bestMove`, `bestMoveTT`, `lowestWin`,
18//!   `forbiddenMoves`, and per-trick `winners` (used by `Make3` /
19//!   `Undo0`).
20//!
21//! The position [`crate::pos::Pos`] and the [`crate::tt::TransTable`]
22//! are passed in as `&mut` borrows; the engine never owns them.
23//!
24//! ## Make/Undo
25//!
26//! [`Engine::make0`] / [`Engine::make1`] / [`Engine::make2`] /
27//! [`Engine::make3`] mirror the vendor's `Make0`..`Make3`, and
28//! [`Engine::undo0`] / [`Engine::undo1`] / [`Engine::undo2`] /
29//! [`Engine::undo3`] mirror the corresponding `Undo*`. The functions
30//! update `Pos` in place; `make3` also snapshots `winner` /
31//! `second_best` into a per-trick `winners` slot so `undo0` can restore
32//! them exactly.
33
34use 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
47/// Run a profiling statement only when the `profiling` feature is on.
48///
49/// Without the feature the body is `#[cfg]`-stripped, so the counter
50/// bumps in the alpha-beta hot path compile to nothing — a normal
51/// release build pays no instrumentation cost.
52macro_rules! stat {
53    ($($body:tt)*) => {{
54        #[cfg(feature = "profiling")]
55        {
56            $($body)*
57        }
58    }};
59}
60
61/// Per-search instrumentation counters. All zero unless the crate is
62/// built with `--features profiling`. Two questions drive the layout —
63/// the same two that govern double-dummy speed:
64///
65/// 1. **TT hit rate** — how often does a transposition-table probe prune
66///    an entire subtree? (`tt_hits / tt_lookups`)
67/// 2. **Move ordering** — when a node beta-cuts, how early in the move
68///    list does the cutoff fire? Perfect ordering cuts on move 1.
69///
70/// The node-0 funnel additionally attributes *why* lead nodes return
71/// without entering the move loop (TT, trivial bounds, quick/later
72/// tricks, leaf evaluation).
73#[derive(Clone, Copy, Debug, Default)]
74pub struct SearchStats {
75    // ---- Node-0 (lead) early-exit funnel ----
76    /// Times `Engine::ab_search_0` was entered.
77    pub node0_entries: u64,
78    /// Returned via the `depth >= 20` TT probe.
79    pub exit_tt_early: u64,
80    /// Returned via the trivial `tricks_max` bound checks.
81    pub exit_trivial: u64,
82    /// Returned at the `depth == 0` leaf evaluation.
83    pub exit_leaf: u64,
84    /// Returned because `quick_tricks` was decisive.
85    pub exit_quick: u64,
86    /// Returned because `later_tricks_*` was decisive.
87    pub exit_later: u64,
88    /// Returned via the `depth < 20` TT probe.
89    pub exit_tt_late: u64,
90    /// Reached the move-generation + search loop.
91    pub reached_moveloop0: u64,
92
93    // ---- TT probe outcomes (both probe sites in node 0) ----
94    /// Total `tt_lookup` calls.
95    pub tt_lookups: u64,
96    /// `tt_lookup` calls that returned a usable bound.
97    pub tt_hits: u64,
98    /// `tt.add` stores from node 0.
99    pub tt_stores: u64,
100
101    // ---- Move ordering (all four node types) ----
102    /// Nodes whose move loop produced a beta/alpha cutoff.
103    pub cutoff_nodes: u64,
104    /// Nodes whose move loop ran to exhaustion without a cutoff.
105    pub allnode_nodes: u64,
106    /// Cutoffs that fired on the very first move tried.
107    pub cutoff_first: u64,
108    /// Sum of 1-based cutoff move indices (for the mean).
109    pub cutoff_index_sum: u64,
110    /// Histogram of 1-based cutoff index; the last bucket is "8+".
111    pub cutoff_hist: [u64; 8],
112}
113
114#[cfg(feature = "profiling")]
115impl SearchStats {
116    /// Record a beta/alpha cutoff that fired on the `move_index`-th move
117    /// (1-based).
118    #[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
130/// Vendor's `handDelta` constant. Used to update `pos.hand_dist[h]`
131/// whenever a card is played or unplayed in suit `s`.
132const HAND_DELTA: [i32; DDS_SUITS] = [256, 16, 1, 0];
133
134/// Snapshot of `pos.winner[suit]` / `pos.second_best[suit]` for the
135/// suits that were played during a trick. Used by [`Engine::make3`] to
136/// stash the values so [`Engine::undo0`] can restore them.
137///
138/// Mirrors the vendor's `WinnerEntryType`.
139#[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/// Per-trick container for [`WinnerEntry`] snapshots. Mirrors
149/// `WinnersType`. Capacity 4 covers the worst case (one entry per suit
150/// played, max 4 distinct suits).
151#[derive(Clone, Copy, Debug, Default)]
152struct Winners {
153    number: i32,
154    winner: [WinnerEntry; 4],
155}
156
157/// Per-search engine state. Owns [`Moves`], holds the per-deal `rel`
158/// table and the per-depth scratch arrays.
159pub 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    // Per-depth best-move arrays — indexed by `depth`.
166    best_move: [MoveType; MAX_DEPTH],
167    best_move_tt: [MoveType; MAX_DEPTH],
168
169    // Per-depth lowest-win cache (vendor's `lowestWin[depth][suit]`).
170    lowest_win: [[u16; DDS_SUITS]; MAX_DEPTH],
171
172    /// User-forbidden moves; consulted by `purge()` at the top level.
173    forbidden_moves: [MoveType; 14],
174
175    /// Per-trick winner snapshot stack (vendor's `winners[13]`).
176    winners: [Winners; 13],
177
178    /// Per-deal relative-rank table. Computed by [`Engine::set_deal`].
179    /// `Box`-allocated to keep the [`Engine`] struct small on the search
180    /// stack.
181    rel: Box<[RelRanks; 8192]>,
182
183    /// Diagnostic counters. Incremented only in the bisection driver
184    /// ([`Engine::search_target`]), not in the inner alpha-beta recursion,
185    /// so the cost is at most a handful of `+= 1` per `search_target` call.
186    /// Used to answer "how many alpha-beta probes does one bisection
187    /// actually need?" — i.e. whether the TT is carrying bounds between
188    /// successive target probes.
189    pub search_target_calls: u64,
190    pub bisection_iters: u64,
191
192    /// Cumulative wall-clock nanoseconds spent in the FIRST bisection
193    /// iteration of every `search_target` call vs in subsequent
194    /// iterations. The ratio answers "are iters 2..N nearly-free thanks
195    /// to TT caching of internal subtrees, or do they cost the same as
196    /// iter 1?" — diagnostic only, not a hot-path counter.
197    pub iter1_nanos: u128,
198    pub later_nanos: u128,
199
200    /// Per-node search instrumentation. Only mutated when the crate is
201    /// built with `--features profiling`; otherwise stays all-zero and
202    /// every bump site is `#[cfg]`-stripped (see [`stat!`]).
203    pub stats: SearchStats,
204}
205
206impl Engine {
207    /// Create a fresh engine for the given strain. Use
208    /// [`Engine::set_deal`] to compute the `rel` table for a specific
209    /// deal before running [`Engine::search_target`].
210    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        // MAX = N+S by default (matches SolverIF when handToPlay = N/S).
216        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    /// Set the MAX/MIN node-type assignment per seat. Pass `MAXNODE` for
241    /// hands that are on the MAX side, `MINNODE` for the others. The
242    /// usual conventions:
243    /// * Declarer is N or S → `[MAX, MIN, MAX, MIN]`.
244    /// * Declarer is E or W → `[MIN, MAX, MIN, MAX]`.
245    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    /// Configure the search strain: updates the [`Moves`] trump and the
250    /// engine's cached `i32` copy used for hot-path indexing.
251    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    /// Reset the per-depth `bestMove` / `bestMoveTT` arrays. Mirrors
258    /// vendor `ResetBestMoves`.
259    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    // ------------------------------------------------------------------
267    // Per-deal setup: rel table, TT init, winner / second_best, hand_dist
268    // ------------------------------------------------------------------
269
270    /// Populate the per-deal `rel` table from `pos.rank_in_suit`,
271    /// initialize `pos.winner` / `pos.second_best` / `pos.length` /
272    /// `pos.aggr` / `pos.hand_dist`, and call `tt.init(handLookup)`.
273    ///
274    /// Mirrors the joint effect of `SetDeal` + `SetDealTables` +
275    /// `InitWinners` in the vendor's `Init.cpp`.
276    pub(crate) fn set_deal(&mut self, pos: &mut Pos, tt: &mut TransTable) {
277        // --- SetDeal: aggr, length, hand_dist ---
278        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        // --- SetDealTables: handLookup + rel + tt.init ---
296        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        // rel[0]: every rank has (hand=-1, rank=0). Default is hand=0,
312        // rank=0 — fix the hand field.
313        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        // --- InitWinners ---
345        // For a position where no cards have been pre-played, we use
346        // pos.aggr directly (matches the vendor's InitWinners with
347        // posPoint.handRelFirst == 0).
348        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    // ------------------------------------------------------------------
362    // Make / Undo
363    // ------------------------------------------------------------------
364
365    /// Apply the leader's card to `pos`. Mirrors vendor `Make0`.
366    #[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    /// Apply hand 1's card. Mirrors vendor `Make1`.
383    #[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    /// Apply hand 2's card. Mirrors vendor `Make2`.
400    #[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    /// Apply hand 3's card, finishing the trick. Mirrors vendor
417    /// `Make3`. Writes `trick_cards[suit]` with the rank-bit(s) that
418    /// just won (used by the caller to propagate `win_ranks`).
419    #[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        // Snapshot the winners that will change.
452        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    /// Undo a [`Engine::make3`] (i.e. restore the just-played leader's
474    /// card and roll back `winner` / `second_best`). Mirrors vendor
475    /// `Undo0`.
476    #[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    /// Undo a [`Engine::make0`] (leader's card). Mirrors vendor `Undo1`.
500    #[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    /// Undo a [`Engine::make1`]. Mirrors vendor `Undo2`.
514    #[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    /// Undo a [`Engine::make2`]. Mirrors vendor `Undo3`.
528    #[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    // ------------------------------------------------------------------
542    // Evaluate (depth == 0 leaf)
543    // ------------------------------------------------------------------
544
545    /// Last-trick evaluation. Mirrors vendor `Evaluate`. Writes the
546    /// resulting win-ranks into `eval_win_ranks` and returns the final
547    /// trick count for the MAX side.
548    #[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        // Highest card in the suit played by the 1st hand.
582        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    // ------------------------------------------------------------------
613    // ABsearch — recursive entry points
614    // ------------------------------------------------------------------
615
616    /// Lead-hand AB search. Mirrors vendor `ABsearch0`.
617    #[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        // ----- QuickTricks ---------------------------------------------------
659        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        // ----- TT lookup (depth < 20 path) -----------------------------------
692        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        // ----- Movegen + loop ------------------------------------------------
701        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        // ----- TT store ------------------------------------------------------
751        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            // The TT API wants `aggr` as u32 and `win_ranks` as u16.
775            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    /// Helper: do a TT lookup and, if successful, return the cached
789    /// boolean outcome (and update `pos.win_ranks[depth]` /
790    /// `best_move_tt[depth]`).
791    #[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    /// Second-hand AB search. Mirrors vendor `ABsearch1`.
844    #[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    /// Third-hand AB search. Mirrors vendor `ABsearch2`.
919    #[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    /// Fourth-hand AB search. Mirrors vendor `ABsearch3`. Resolves the
981    /// trick winner, increments `tricks_max` accordingly, and recurses
982    /// into [`Engine::ab_search_0`] with the trick-winner as the new
983    /// leader.
984    #[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    // ------------------------------------------------------------------
1063    // Bisection driver
1064    // ------------------------------------------------------------------
1065
1066    /// Drive the bisection loop to find the exact tricks MAX achieves
1067    /// from the position. Mirrors the inner bisection of
1068    /// `SolveSameBoard` in `SolverIF.cpp`.
1069    ///
1070    /// `pos.tricks_max` should be 0 at entry. The engine's `ini_depth`
1071    /// field is set to `ini_depth` before the loop runs; callers should
1072    /// set the deal via [`Engine::set_deal`] beforehand.
1073    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        // Edge case: literally no cards in play. The vendor's
1083        // `SolveBoardInternal` short-circuits this case via
1084        // `LastTrickWinner` before ever calling `ABsearch`; do the same
1085        // here so `Evaluate` isn't asked to inspect an empty hand.
1086        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        // Initialize the move generator's per-trick removed-ranks at
1098        // the trick index where the search starts.
1099        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// ----------------------------------------------------------------------
1137// Tests
1138// ----------------------------------------------------------------------
1139
1140#[cfg(test)]
1141mod tests {
1142    use super::*;
1143    use crate::lookup::BIT_MAP_RANK;
1144
1145    /// Build a partial `Pos` from per-hand suit bitmaps; the helper
1146    /// populates `aggr` and `length` automatically.
1147    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    /// Total number of cards held (across all hands and suits).
1164    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        // ini_depth = 0 — no tricks remaining.
1176        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        // MAX (North) holds the A of suit 0. Each other hand has a
1183        // single low card in a distinct suit. North leads → A wins.
1184        let mut rank = [[0u16; 4]; 4];
1185        rank[0][0] = BIT_MAP_RANK[14]; // N: AS
1186        rank[1][1] = BIT_MAP_RANK[2]; // E: 2H
1187        rank[2][2] = BIT_MAP_RANK[2]; // S: 2D
1188        rank[3][3] = BIT_MAP_RANK[2]; // W: 2C
1189        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; // 0
1194
1195        // North leads.
1196        pos.first[ini_depth as usize] = 0;
1197
1198        let mut tt = TransTable::new();
1199        let mut eng = Engine::new(Strain::Notrump);
1200        // NS = MAX, EW = MIN.
1201        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        // MIN (East) holds the AH; everyone else has a single small
1211        // card. East leads → AH wins. MAX gets 0 tricks.
1212        let mut rank = [[0u16; 4]; 4];
1213        rank[0][0] = BIT_MAP_RANK[2]; // N: 2S
1214        rank[1][1] = BIT_MAP_RANK[14]; // E: AH
1215        rank[2][2] = BIT_MAP_RANK[2]; // S: 2D
1216        rank[3][3] = BIT_MAP_RANK[2]; // W: 2C
1217        let mut pos = build_pos(rank);
1218
1219        let cc = card_count(&pos);
1220        assert_eq!(cc, 4);
1221        let ini_depth = cc - 4; // 0
1222
1223        // East leads.
1224        pos.first[ini_depth as usize] = 1;
1225
1226        let mut tt = TransTable::new();
1227        let mut eng = Engine::new(Strain::Notrump);
1228        // East leads — by SolverIF convention, when handToPlay is E or W,
1229        // NS are still MAX (declarer's side). But to make this test
1230        // simpler we leave NS = MAX so East's AH win = MIN win.
1231        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        // 4 tricks per hand, 16 cards total. Each hand holds the A of
1241        // one suit plus 3 small cards in the other suits. With perfect
1242        // play, both sides claim their 2 As: MAX (NS) wins 2 tricks.
1243        //
1244        // Layout (suit indices 0=S, 1=H, 2=D, 3=C):
1245        //   N: AS, 2H, 2D, 2C
1246        //   E: 3S, AH, 3D, 3C
1247        //   S: 4S, 4H, AD, 4C
1248        //   W: 5S, 5H, 5D, AC
1249        let mut rank = [[0u16; 4]; 4];
1250        // N
1251        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        // E
1256        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        // S
1261        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        // W
1266        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; // 12
1275
1276        // North leads.
1277        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}