use arrayvec::ArrayVec;
use dds_bridge::solver::*;
use dds_bridge::{Builder, Card, Contract, Hand, Holding, Penalty, Rank, Seat, Strain, Suit};
use semver::Version;
#[test]
fn solve_four_13_card_straight_flushes() {
const DEAL: Builder = Builder::new()
.north(Hand::new(
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
))
.east(Hand::new(
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
))
.south(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
))
.west(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
));
const SOLUTION: TrickCountTable = TrickCountTable([
TrickCountRow::new(13, 0, 13, 0),
TrickCountRow::new(0, 13, 0, 13),
TrickCountRow::new(13, 0, 13, 0),
TrickCountRow::new(0, 13, 0, 13),
TrickCountRow::new(0, 0, 0, 0),
]);
const CONTRACT: Contract = Contract::new(7, Strain::Spades, Penalty::Undoubled);
const CONTRACTS: [ParContract; 2] = [
ParContract {
contract: CONTRACT,
declarer: Seat::East,
overtricks: 0,
},
ParContract {
contract: CONTRACT,
declarer: Seat::West,
overtricks: 0,
},
];
let ns = Par {
score: -2210,
contracts: CONTRACTS.to_vec(),
};
let ew = Par {
score: 2210,
contracts: CONTRACTS.to_vec(),
};
assert_eq!(
Solver::lock().solve_deal(DEAL.build_full().unwrap()),
SOLUTION
);
let pars = calculate_pars(SOLUTION, Vulnerability::all());
assert!(pars[0].equivalent(&ns));
assert!(pars[1].equivalent(&ew));
}
#[test]
fn solve_par_5_tricks() {
const AKQJ: Holding = Holding::from_bits_truncate(0xF << 11);
const T987: Holding = Holding::from_bits_truncate(0xF << 7);
const XXXX: Holding = Holding::from_bits_truncate(0xF << 3);
const X: Holding = Holding::from_bits_truncate(1 << 2);
const DEAL: Builder = Builder::new()
.north(Hand::new(T987, XXXX, X, AKQJ))
.east(Hand::new(X, AKQJ, T987, XXXX))
.south(Hand::new(XXXX, T987, AKQJ, X))
.west(Hand::new(AKQJ, X, XXXX, T987));
const SOLUTION: TrickCountTable = TrickCountTable([TrickCountRow::new(5, 5, 5, 5); 5]);
const PAR: Par = Par {
score: 0,
contracts: Vec::new(),
};
assert_eq!(
Solver::lock().solve_deal(DEAL.build_full().unwrap()),
SOLUTION
);
let pars = calculate_pars(SOLUTION, Vulnerability::all());
assert!(pars[0].equivalent(&PAR));
assert!(pars[1].equivalent(&PAR));
}
#[test]
fn solve_everyone_makes_1nt() {
const A54: Holding = Holding::from_bits_truncate(0b100_0000_0011_0000);
const QJ32: Holding = Holding::from_bits_truncate(0b001_1000_0000_1100);
const K976: Holding = Holding::from_bits_truncate(0b010_0010_1100_0000);
const T8: Holding = Holding::from_bits_truncate(0b000_0101_0000_0000);
const DEAL: Builder = Builder::new()
.north(Hand::new(A54, QJ32, K976, T8))
.east(Hand::new(T8, A54, QJ32, K976))
.south(Hand::new(K976, T8, A54, QJ32))
.west(Hand::new(QJ32, K976, T8, A54));
const SUIT: TrickCountRow = TrickCountRow::new(6, 6, 6, 6);
const NT: TrickCountRow = TrickCountRow::new(7, 7, 7, 7);
const SOLUTION: TrickCountTable = TrickCountTable([SUIT, SUIT, SUIT, SUIT, NT]);
const CONTRACT: Contract = Contract::new(1, Strain::Notrump, Penalty::Undoubled);
assert_eq!(
Solver::lock().solve_deal(DEAL.build_full().unwrap()),
SOLUTION
);
let ns = Par {
score: 90,
contracts: vec![
ParContract {
contract: CONTRACT,
declarer: Seat::North,
overtricks: 0,
},
ParContract {
contract: CONTRACT,
declarer: Seat::South,
overtricks: 0,
},
],
};
let ew = Par {
score: 90,
contracts: vec![
ParContract {
contract: CONTRACT,
declarer: Seat::East,
overtricks: 0,
},
ParContract {
contract: CONTRACT,
declarer: Seat::West,
overtricks: 0,
},
],
};
let pars = calculate_pars(SOLUTION, Vulnerability::all());
assert!(pars[0].equivalent(&ns));
assert!(pars[1].equivalent(&ew));
}
#[test]
fn solve_board_score_matches_dd_table() {
const A54: Holding = Holding::from_bits_truncate(0b100_0000_0011_0000);
const QJ32: Holding = Holding::from_bits_truncate(0b001_1000_0000_1100);
const K976: Holding = Holding::from_bits_truncate(0b010_0010_1100_0000);
const T8: Holding = Holding::from_bits_truncate(0b000_0101_0000_0000);
const DEAL: Builder = Builder::new()
.north(Hand::new(A54, QJ32, K976, T8))
.east(Hand::new(T8, A54, QJ32, K976))
.south(Hand::new(K976, T8, A54, QJ32))
.west(Hand::new(QJ32, K976, T8, A54));
let solver = Solver::lock();
let tricks = solver.solve_deal(DEAL.build_full().unwrap());
let found = solver.solve_board(Objective {
board: Board::try_new(
DEAL.build_partial().unwrap(),
CurrentTrick::new(Strain::Notrump, Seat::North),
)
.unwrap(),
target: Target::Any(None),
});
core::mem::drop(solver);
let expected = 13 - u8::from(tricks[Strain::Notrump].get(Seat::North.rho()));
assert!(!found.plays.is_empty());
assert_eq!(u8::from(found.plays[0].score), expected);
}
#[test]
fn solve_boards_matches_solve_board() {
const A54: Holding = Holding::from_bits_truncate(0b100_0000_0011_0000);
const QJ32: Holding = Holding::from_bits_truncate(0b001_1000_0000_1100);
const K976: Holding = Holding::from_bits_truncate(0b010_0010_1100_0000);
const T8: Holding = Holding::from_bits_truncate(0b000_0101_0000_0000);
const DEAL: Builder = Builder::new()
.north(Hand::new(A54, QJ32, K976, T8))
.east(Hand::new(T8, A54, QJ32, K976))
.south(Hand::new(K976, T8, A54, QJ32))
.west(Hand::new(QJ32, K976, T8, A54));
let solver = Solver::lock();
let obj = Objective {
board: Board::try_new(
DEAL.build_partial().unwrap(),
CurrentTrick::new(Strain::Notrump, Seat::North),
)
.unwrap(),
target: Target::Any(None),
};
let single = solver.solve_board(obj.clone());
let batch = solver.solve_boards(&[obj]);
core::mem::drop(solver);
assert_eq!(batch.len(), 1);
assert_eq!(batch[0].plays, single.plays);
}
#[test]
fn solve_deals_crosses_chunk_boundary() {
const DEAL: Builder = Builder::new()
.north(Hand::new(
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
))
.east(Hand::new(
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
))
.south(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
))
.west(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
));
let solver = Solver::lock();
let expected = solver.solve_deal(DEAL.build_full().unwrap());
let deals = [DEAL.build_full().unwrap(); 41];
let tables = solver.solve_deals(&deals, NonEmptyStrainFlags::ALL);
core::mem::drop(solver);
assert_eq!(tables.len(), deals.len());
assert!(tables.iter().all(|&t| t == expected));
}
#[test]
fn analyse_play_empty_trace_complements_solve_board() {
const A54: Holding = Holding::from_bits_truncate(0b100_0000_0011_0000);
const QJ32: Holding = Holding::from_bits_truncate(0b001_1000_0000_1100);
const K976: Holding = Holding::from_bits_truncate(0b010_0010_1100_0000);
const T8: Holding = Holding::from_bits_truncate(0b000_0101_0000_0000);
const DEAL: Builder = Builder::new()
.north(Hand::new(A54, QJ32, K976, T8))
.east(Hand::new(T8, A54, QJ32, K976))
.south(Hand::new(K976, T8, A54, QJ32))
.west(Hand::new(QJ32, K976, T8, A54));
let board = Board::try_new(
DEAL.build_partial().unwrap(),
CurrentTrick::new(Strain::Notrump, Seat::North),
)
.unwrap();
let solver = Solver::lock();
let found = solver.solve_board(Objective {
board: board.clone(),
target: Target::Any(None),
});
let analysis = solver.analyse_play(PlayTrace {
board,
cards: ArrayVec::new(),
});
core::mem::drop(solver);
assert_eq!(analysis.tricks.len(), 1);
assert_eq!(
u8::from(analysis.tricks[0]) + u8::from(found.plays[0].score),
13,
);
}
#[test]
fn analyse_play_optimal_card_preserves_dd_value() {
const A54: Holding = Holding::from_bits_truncate(0b100_0000_0011_0000);
const QJ32: Holding = Holding::from_bits_truncate(0b001_1000_0000_1100);
const K976: Holding = Holding::from_bits_truncate(0b010_0010_1100_0000);
const T8: Holding = Holding::from_bits_truncate(0b000_0101_0000_0000);
const DEAL: Builder = Builder::new()
.north(Hand::new(A54, QJ32, K976, T8))
.east(Hand::new(T8, A54, QJ32, K976))
.south(Hand::new(K976, T8, A54, QJ32))
.west(Hand::new(QJ32, K976, T8, A54));
let board = Board::try_new(
DEAL.build_partial().unwrap(),
CurrentTrick::new(Strain::Notrump, Seat::North),
)
.unwrap();
let solver = Solver::lock();
let found = solver.solve_board(Objective {
board: board.clone(),
target: Target::Any(None),
});
let best = found.plays[0];
let mut cards = ArrayVec::new();
cards.push(best.card);
let analysis = solver.analyse_play(PlayTrace { board, cards });
core::mem::drop(solver);
assert_eq!(analysis.tricks.len(), 2);
assert_eq!(analysis.tricks[0], analysis.tricks[1]);
assert_eq!(u8::from(analysis.tricks[0]) + u8::from(best.score), 13,);
}
#[test]
fn analyse_play_straight_flush_declarer_takes_zero() {
const DEAL: Builder = Builder::new()
.north(Hand::new(
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
))
.east(Hand::new(
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
Holding::EMPTY,
))
.south(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
Holding::EMPTY,
))
.west(Hand::new(
Holding::EMPTY,
Holding::EMPTY,
Holding::EMPTY,
Holding::ALL,
));
let mut cards = ArrayVec::<Card, 52>::new();
cards.push(Card {
suit: Suit::Clubs,
rank: Rank::A,
});
let analysis = Solver::lock().analyse_play(PlayTrace {
board: Board::try_new(
DEAL.build_partial().unwrap(),
CurrentTrick::new(Strain::Notrump, Seat::North),
)
.unwrap(),
cards,
});
assert_eq!(analysis.tricks.len(), 2);
assert!(analysis.tricks.iter().all(|&t| u8::from(t) == 0));
}
#[test]
fn system_info_version_is_2_9_0() {
let info = Solver::lock().system_info();
assert_eq!(info.version(), Version::new(2, 9, 0));
}
#[test]
fn system_info_platform_matches_os() {
let platform = match () {
() if cfg!(target_os = "linux") => Platform::Linux,
() if cfg!(target_os = "macos") => Platform::Apple,
() if cfg!(target_os = "cygwin") => Platform::Cygwin,
() if cfg!(target_os = "windows") => Platform::Windows,
() => return, };
let info = Solver::lock().system_info();
assert_eq!(info.platform(), platform);
}
#[test]
fn system_info_num_bits_matches_target() {
let num_bits: u32 = match () {
() if cfg!(target_pointer_width = "64") => 64,
() if cfg!(target_pointer_width = "32") => 32,
() if cfg!(target_pointer_width = "16") => 16,
() => return, };
let info = Solver::lock().system_info();
assert_eq!(info.num_bits(), num_bits);
}
#[test]
fn system_info_compiler_is_known() {
let info = Solver::lock().system_info();
assert!(!matches!(info.compiler(), Compiler::Unknown(_)));
}
#[test]
fn system_info_threading_is_stl() {
let info = Solver::lock().system_info();
assert_eq!(info.threading(), Threading::STL);
}
#[test]
fn system_info_num_cores_is_positive() {
let info = Solver::lock().system_info();
assert!(info.num_cores() > 0);
}
#[test]
fn system_info_num_threads_is_positive() {
let info = Solver::lock().system_info();
assert!(info.num_threads() > 0);
}
#[test]
fn system_info_thread_sizes_is_nonempty() {
let info = Solver::lock().system_info();
assert!(!info.thread_sizes().is_empty());
}
#[test]
fn system_info_system_string_is_nonempty() {
let info = Solver::lock().system_info();
assert!(!info.system_string().is_empty());
}
#[test]
fn system_info_display_matches_system_string() {
let info = Solver::lock().system_info();
assert_eq!(info.to_string(), info.system_string());
}
#[test]
fn trick_count_try_new_rejects_out_of_range() {
assert_eq!(TrickCount::try_new(14), Err(InvalidTrickCount));
assert_eq!(TrickCount::try_new(255), Err(InvalidTrickCount));
assert!(TrickCount::try_new(0).is_ok());
assert!(TrickCount::try_new(13).is_ok());
}
#[test]
fn trick_count_row_try_new_rejects_out_of_range() {
assert_eq!(TrickCountRow::try_new(14, 0, 0, 0), Err(InvalidTrickCount));
assert_eq!(TrickCountRow::try_new(0, 14, 0, 0), Err(InvalidTrickCount));
assert_eq!(TrickCountRow::try_new(0, 0, 14, 0), Err(InvalidTrickCount));
assert_eq!(TrickCountRow::try_new(0, 0, 0, 14), Err(InvalidTrickCount));
assert!(TrickCountRow::try_new(13, 13, 13, 13).is_ok());
assert!(TrickCountRow::try_new(0, 0, 0, 0).is_ok());
}
fn subset_from(
north: impl IntoIterator<Item = Card>,
east: impl IntoIterator<Item = Card>,
south: impl IntoIterator<Item = Card>,
west: impl IntoIterator<Item = Card>,
) -> dds_bridge::PartialDeal {
Builder::new()
.north(Hand::from_iter(north))
.east(Hand::from_iter(east))
.south(Hand::from_iter(south))
.west(Hand::from_iter(west))
.build_partial()
.unwrap()
}
const fn c(suit: Suit, rank: u8) -> Card {
Card {
suit,
rank: Rank::new(rank),
}
}
#[test]
fn board_try_new_detects_revoke_on_second_card() {
let remaining = subset_from(
[c(Suit::Hearts, 3), c(Suit::Hearts, 4), c(Suit::Hearts, 5)],
[c(Suit::Spades, 13), c(Suit::Hearts, 6), c(Suit::Hearts, 7)],
[
c(Suit::Diamonds, 2),
c(Suit::Diamonds, 3),
c(Suit::Diamonds, 4),
c(Suit::Diamonds, 5),
],
[
c(Suit::Clubs, 2),
c(Suit::Clubs, 3),
c(Suit::Clubs, 4),
c(Suit::Clubs, 5),
],
);
let played = [c(Suit::Spades, 14), c(Suit::Hearts, 2)];
assert_eq!(
Board::try_new(
remaining,
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &played).unwrap(),
),
Err(BoardError::Revoke {
position: RevokePosition::Second
})
);
}
#[test]
fn board_try_new_accepts_non_revoke_discard() {
let remaining = subset_from(
[c(Suit::Hearts, 3), c(Suit::Hearts, 4), c(Suit::Hearts, 5)],
[c(Suit::Hearts, 6), c(Suit::Hearts, 7), c(Suit::Hearts, 8)],
[
c(Suit::Diamonds, 2),
c(Suit::Diamonds, 3),
c(Suit::Diamonds, 4),
c(Suit::Diamonds, 5),
],
[
c(Suit::Clubs, 2),
c(Suit::Clubs, 3),
c(Suit::Clubs, 4),
c(Suit::Clubs, 5),
],
);
let played = [c(Suit::Spades, 14), c(Suit::Hearts, 2)];
assert!(
Board::try_new(
remaining,
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &played).unwrap(),
)
.is_ok()
);
}
#[test]
fn board_try_new_detects_revoke_on_third_card() {
let remaining = subset_from(
[c(Suit::Hearts, 4), c(Suit::Hearts, 5), c(Suit::Hearts, 6)],
[c(Suit::Clubs, 14), c(Suit::Clubs, 13), c(Suit::Clubs, 12)],
[c(Suit::Spades, 12), c(Suit::Clubs, 11), c(Suit::Clubs, 10)],
[
c(Suit::Diamonds, 2),
c(Suit::Diamonds, 3),
c(Suit::Diamonds, 4),
c(Suit::Diamonds, 5),
],
);
let played = [c(Suit::Spades, 14), c(Suit::Spades, 2), c(Suit::Hearts, 3)];
assert_eq!(
Board::try_new(
remaining,
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &played).unwrap(),
),
Err(BoardError::Revoke {
position: RevokePosition::Third
})
);
}
#[test]
fn board_try_new_empty_and_single_card_tricks_cannot_revoke() {
let full = subset_from(
[
c(Suit::Spades, 14),
c(Suit::Hearts, 3),
c(Suit::Hearts, 4),
c(Suit::Hearts, 5),
],
[
c(Suit::Spades, 13),
c(Suit::Hearts, 6),
c(Suit::Hearts, 7),
c(Suit::Hearts, 8),
],
[
c(Suit::Diamonds, 2),
c(Suit::Diamonds, 3),
c(Suit::Diamonds, 4),
c(Suit::Diamonds, 5),
],
[
c(Suit::Clubs, 2),
c(Suit::Clubs, 3),
c(Suit::Clubs, 4),
c(Suit::Clubs, 5),
],
);
assert!(Board::try_new(full, CurrentTrick::new(Strain::Notrump, Seat::North),).is_ok());
let after_lead = subset_from(
[c(Suit::Hearts, 3), c(Suit::Hearts, 4), c(Suit::Hearts, 5)],
[
c(Suit::Spades, 13),
c(Suit::Hearts, 6),
c(Suit::Hearts, 7),
c(Suit::Hearts, 8),
],
[
c(Suit::Diamonds, 2),
c(Suit::Diamonds, 3),
c(Suit::Diamonds, 4),
c(Suit::Diamonds, 5),
],
[
c(Suit::Clubs, 2),
c(Suit::Clubs, 3),
c(Suit::Clubs, 4),
c(Suit::Clubs, 5),
],
);
assert!(
Board::try_new(
after_lead,
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &[c(Suit::Spades, 14)]).unwrap(),
)
.is_ok()
);
}
#[test]
fn current_trick_from_slice_rejects_overlong() {
let played = [
c(Suit::Spades, 14),
c(Suit::Spades, 13),
c(Suit::Spades, 12),
c(Suit::Spades, 11),
];
assert_eq!(
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &played),
Err(CurrentTrickError::TooManyPlayed),
);
}
#[test]
fn current_trick_from_slice_rejects_duplicate() {
let played = [c(Suit::Spades, 14), c(Suit::Spades, 14)];
assert_eq!(
CurrentTrick::from_slice(Strain::Notrump, Seat::North, &played),
Err(CurrentTrickError::DuplicatePlayedCard),
);
}
#[test]
fn current_trick_try_push_refuses_fourth_card() {
let mut trick = CurrentTrick::from_slice(
Strain::Notrump,
Seat::North,
&[
c(Suit::Spades, 14),
c(Suit::Spades, 13),
c(Suit::Spades, 12),
],
)
.unwrap();
assert_eq!(
trick.try_push(c(Suit::Spades, 11)),
Err(CurrentTrickError::TooManyPlayed),
);
assert_eq!(trick.len(), 3);
}