use super::super::constraint::{
Cons, Constraint, balanced, described, has_stopper, hcp, len, min_level_is, points, stopper_in,
stopper_in_their_suits, suit_hcp, support, they_bid, top_honors,
};
use super::super::context::Context;
use super::super::fallback::{
Fallback, FirstIs, OvercallAtMost, ReplaceNext, SuffixIs, described_guard, described_rewrite,
guard, rewriter,
};
use super::super::trie::{Classifier, classifier};
use super::super::{Alert, Competitive, Rules};
use super::notrump::{
PUPPET, complete_transfer, notrump_minors, notrump_responses, smolen_at_three,
smolen_completion, stayman_answers, transfer_super_accept,
};
use super::weak_twos;
use super::{call, fallback_all_seats};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Hand, Strain, Suit};
use std::cell::Cell;
use std::sync::Arc;
const CUE_RAISE: Alert = Alert("comp:cue-raise");
const NEGATIVE_DOUBLE: Alert = Alert("comp:negative-double");
const SUPPORT_DOUBLE: Alert = Alert("comp:support-double");
const LEBENSOHL_RELAY: Alert = Alert("comp:lebensohl-relay");
const LEBENSOHL_CUE: Alert = Alert("comp:lebensohl-cue");
const LEBENSOHL_TRANSFER: Alert = Alert("comp:lebensohl-transfer");
const STAYMAN: Alert = Alert("comp:stayman");
const SMOLEN: Alert = Alert("comp:smolen");
const LEAPING_MICHAELS: Alert = Alert("comp:leaping-michaels");
const UVU_CUE: Alert = Alert("comp:uvu-cue");
const UVU_SPLINTER: Alert = Alert("comp:uvu-splinter");
const STAYMAN_REDOUBLE: Alert = Alert("comp:stayman-redouble");
const TRANSFER_REDOUBLE: Alert = Alert("comp:transfer-redouble");
const UVU_MAJOR_RAISE: Alert = Alert("comp:uvu-major-raise");
const UVU_MAJOR_FOURTH: Alert = Alert("comp:uvu-major-fourth");
const WEAK_TWO_XX: Alert = Alert("comp:weak-two-xx");
const CONTESTED_OGUST: Alert = Alert("comp:ogust");
const CACHALOT_X: Alert = Alert("comp:cachalot-x");
const CACHALOT_TRANSFER: Alert = Alert("comp:cachalot-transfer");
const CACHALOT_TAKEOUT: Alert = Alert("comp:cachalot-takeout");
const CACHALOT_THREE: Alert = Alert("comp:cachalot-three");
const JORDAN: Alert = Alert("comp:jordan");
const VALUE_REDOUBLE: Alert = Alert("comp:value-redouble");
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LebensohlStyle {
Off,
Plain,
Transfer,
}
thread_local! {
static LEBENSOHL_STYLE: Cell<LebensohlStyle> = const { Cell::new(LebensohlStyle::Transfer) };
}
pub fn set_lebensohl_style(style: LebensohlStyle) {
LEBENSOHL_STYLE.with(|cell| cell.set(style));
}
pub fn set_lebensohl(on: bool) {
set_lebensohl_style(if on {
LebensohlStyle::Plain
} else {
LebensohlStyle::Off
});
}
fn lebensohl_style() -> LebensohlStyle {
LEBENSOHL_STYLE.with(Cell::get)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DoubleStyle {
Takeout,
Penalty,
PenaltyLight,
#[default]
Optional,
}
thread_local! {
static DOUBLE_STYLE: Cell<DoubleStyle> = const { Cell::new(DoubleStyle::Optional) };
}
pub fn set_double_style(style: DoubleStyle) {
DOUBLE_STYLE.with(|cell| cell.set(style));
}
fn double_style() -> DoubleStyle {
DOUBLE_STYLE.with(Cell::get)
}
thread_local! {
static PENALTY_DOUBLE_LEAVE_IN: Cell<bool> = const { Cell::new(true) };
}
pub fn set_penalty_double_leave_in(on: bool) {
PENALTY_DOUBLE_LEAVE_IN.with(|cell| cell.set(on));
}
fn penalty_double_leave_in() -> bool {
PENALTY_DOUBLE_LEAVE_IN.with(Cell::get)
}
fn opener_leaves_in_penalty_double() -> Rules {
Rules::new().rule(Call::Pass, 1.5, hcp(0..))
}
fn opener_cooperates_optional(over: Suit) -> Rules {
let mut rules = Rules::new().rule(Call::Pass, 1.5, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit == over {
continue;
}
let strain = Strain::from(suit);
for level in 2..=3 {
rules = rules.rule(
Bid::new(level, strain),
1.6,
min_level_is(level, strain) & len(over, ..=2) & len(suit, 5..),
);
}
}
rules
}
thread_local! {
static UVU: Cell<bool> = const { Cell::new(true) };
static UVU_X_FLOOR: Cell<u8> = const { Cell::new(9) };
static UVU_CUE_FLOOR: Cell<u8> = const { Cell::new(8) };
static UVU_NATURAL_FLOOR: Cell<u8> = const { Cell::new(6) };
}
pub fn set_uvu(on: bool) {
UVU.with(|cell| cell.set(on));
}
fn uvu() -> bool {
UVU.with(Cell::get)
}
pub fn set_uvu_x_floor(floor: u8) {
UVU_X_FLOOR.with(|cell| cell.set(floor));
}
fn uvu_x_floor() -> u8 {
UVU_X_FLOOR.with(Cell::get)
}
pub fn set_uvu_cue_floor(floor: u8) {
UVU_CUE_FLOOR.with(|cell| cell.set(floor));
}
fn uvu_cue_floor() -> u8 {
UVU_CUE_FLOOR.with(Cell::get)
}
pub fn set_uvu_natural_floor(floor: u8) {
UVU_NATURAL_FLOOR.with(|cell| cell.set(floor));
}
fn uvu_natural_floor() -> u8 {
UVU_NATURAL_FLOOR.with(Cell::get)
}
thread_local! {
static DOUBLE_OVERRIDE: Cell<Option<(usize, usize, u8)>> = const { Cell::new(None) };
}
pub fn set_double_override(spec: Option<(usize, usize, u8)>) {
DOUBLE_OVERRIDE.with(|cell| cell.set(spec));
}
fn responder_double(rules: Rules, over: Suit) -> Rules {
if let Some((lo, hi, floor)) = DOUBLE_OVERRIDE.with(Cell::get) {
return rules.rule(Call::Double, 1.55, len(over, lo..=hi) & hcp(floor..));
}
match double_style() {
DoubleStyle::Takeout => rules.rule(Call::Double, 1.55, len(over, ..=3) & hcp(8..)),
DoubleStyle::Penalty => rules.rule(Call::Double, 1.55, len(over, 4..) & hcp(9..)),
DoubleStyle::PenaltyLight => rules.rule(Call::Double, 1.55, len(over, 4..) & hcp(7..)),
DoubleStyle::Optional => rules.rule(Call::Double, 1.55, len(over, 2..=3) & hcp(8..)),
}
}
thread_local! {
static PENALTY_PASS: Cell<Option<(usize, u8, bool)>> =
const { Cell::new(Some((4, 4, true))) };
}
pub fn set_penalty_pass(spec: Option<(usize, u8, bool)>) {
PENALTY_PASS.with(|cell| cell.set(spec));
}
fn penalty_pass() -> Option<(usize, u8, bool)> {
PENALTY_PASS.with(Cell::get)
}
thread_local! {
static DIRECT_3NT_STOPPER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_direct_3nt_stopper(on: bool) {
DIRECT_3NT_STOPPER.with(|cell| cell.set(on));
}
fn direct_3nt_stopper() -> bool {
DIRECT_3NT_STOPPER.with(Cell::get)
}
thread_local! {
static TRAP_PASS: Cell<bool> = const { Cell::new(true) };
}
pub fn set_trap_pass(on: bool) {
TRAP_PASS.with(|cell| cell.set(on));
}
fn trap_pass() -> bool {
TRAP_PASS.with(Cell::get)
}
thread_local! {
static CUE_RAISE_ANSWER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_cue_raise_answer(on: bool) {
CUE_RAISE_ANSWER.with(|cell| cell.set(on));
}
fn cue_raise_answer() -> bool {
CUE_RAISE_ANSWER.with(Cell::get)
}
thread_local! {
static CUE_MINOR_RAISE_ANSWER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_cue_minor_raise_answer(on: bool) {
CUE_MINOR_RAISE_ANSWER.with(|cell| cell.set(on));
}
fn cue_minor_raise_answer() -> bool {
CUE_MINOR_RAISE_ANSWER.with(Cell::get)
}
thread_local! {
static UVU_OVER_MAJORS: Cell<bool> = const { Cell::new(true) };
}
pub fn set_uvu_over_majors(on: bool) {
UVU_OVER_MAJORS.with(|cell| cell.set(on));
}
pub(crate) fn uvu_over_majors() -> bool {
UVU_OVER_MAJORS.with(Cell::get)
}
thread_local! {
static WEAK_TWO_COMPETITION: Cell<bool> = const { Cell::new(false) };
static STRONG_TWO_COMPETITION: Cell<bool> = const { Cell::new(true) };
}
pub fn set_weak_two_competition(on: bool) {
WEAK_TWO_COMPETITION.with(|cell| cell.set(on));
}
fn weak_two_competition() -> bool {
WEAK_TWO_COMPETITION.with(Cell::get)
}
pub fn set_strong_two_competition(on: bool) {
STRONG_TWO_COMPETITION.with(|cell| cell.set(on));
}
fn strong_two_competition() -> bool {
STRONG_TWO_COMPETITION.with(Cell::get)
}
thread_local! {
static MAJOR_SUPPORT_DOUBLE: Cell<bool> = const { Cell::new(true) };
}
pub fn set_major_support_double(on: bool) {
MAJOR_SUPPORT_DOUBLE.with(|cell| cell.set(on));
}
fn major_support_double() -> bool {
MAJOR_SUPPORT_DOUBLE.with(Cell::get)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NegativeDoubleShape {
BothMajors,
Modern,
Cachalot,
Sputnik,
}
thread_local! {
static NEGATIVE_DOUBLE_SHAPE: Cell<NegativeDoubleShape> =
const { Cell::new(NegativeDoubleShape::BothMajors) };
static FREE_BIDS: Cell<bool> = const { Cell::new(false) };
static FREE_BID_FLOOR: Cell<u8> = const { Cell::new(6) };
}
pub fn set_negative_double_shape(shape: NegativeDoubleShape) {
NEGATIVE_DOUBLE_SHAPE.with(|cell| cell.set(shape));
}
fn negative_double_shape() -> NegativeDoubleShape {
NEGATIVE_DOUBLE_SHAPE.with(Cell::get)
}
pub fn set_free_bids(on: bool) {
FREE_BIDS.with(|cell| cell.set(on));
}
fn free_bids_engaged() -> bool {
FREE_BIDS.with(Cell::get) || negative_double_shape() != NegativeDoubleShape::BothMajors
}
pub fn set_free_bid_floor(min: u8) {
FREE_BID_FLOOR.with(|cell| cell.set(min));
}
fn free_bid_floor() -> u8 {
FREE_BID_FLOOR.with(Cell::get)
}
thread_local! {
static HIGH_OVERCALL_RESPONSES: Cell<bool> = const { Cell::new(false) };
}
pub fn set_high_overcall_responses(on: bool) {
HIGH_OVERCALL_RESPONSES.with(|cell| cell.set(on));
}
fn high_overcall_responses() -> bool {
HIGH_OVERCALL_RESPONSES.with(Cell::get)
}
thread_local! {
static JORDAN_TRUSCOTT: Cell<bool> = const { Cell::new(true) };
}
pub fn set_jordan_truscott(on: bool) {
JORDAN_TRUSCOTT.with(|cell| cell.set(on));
}
fn jordan_truscott() -> bool {
JORDAN_TRUSCOTT.with(Cell::get)
}
thread_local! {
static SPLINTER_DOUBLED: Cell<bool> = const { Cell::new(true) };
}
pub fn set_splinter_doubled(on: bool) {
SPLINTER_DOUBLED.with(|cell| cell.set(on));
}
fn splinter_doubled() -> bool {
SPLINTER_DOUBLED.with(Cell::get)
}
fn author_direct_3nt(rules: Rules, weight: f32, over: Suit) -> Rules {
let nt = Bid::new(3, Strain::Notrump);
match (direct_3nt_stopper(), trap_pass()) {
(true, true) => rules.rule(
nt,
weight,
points(10..) & stopper_in(over) & suit_hcp(over, ..=4),
),
(true, false) => rules.rule(nt, weight, points(10..) & stopper_in(over)),
(false, true) => rules.rule(nt, weight, points(10..) & suit_hcp(over, ..=4)),
(false, false) => rules.rule(nt, weight, points(10..)),
}
}
thread_local! {
static DELAYED_CUE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_delayed_cue(on: bool) {
DELAYED_CUE.with(|cell| cell.set(on));
}
pub(super) fn delayed_cue() -> bool {
DELAYED_CUE.with(Cell::get)
}
thread_local! {
static COMPETITION_OVER_STAYMAN: Cell<bool> = const { Cell::new(true) };
}
pub fn set_competition_over_stayman(on: bool) {
COMPETITION_OVER_STAYMAN.with(|cell| cell.set(on));
}
fn competition_over_stayman() -> bool {
COMPETITION_OVER_STAYMAN.with(Cell::get)
}
thread_local! {
static COMPETITION_OVER_TRANSFER: Cell<bool> = const { Cell::new(false) };
}
pub fn set_competition_over_transfer(on: bool) {
COMPETITION_OVER_TRANSFER.with(|cell| cell.set(on));
}
fn competition_over_transfer() -> bool {
COMPETITION_OVER_TRANSFER.with(Cell::get)
}
thread_local! {
static COMPETITION_OVER_MINOR_TRANSFER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_competition_over_minor_transfer(on: bool) {
COMPETITION_OVER_MINOR_TRANSFER.with(|cell| cell.set(on));
}
fn competition_over_minor_transfer() -> bool {
COMPETITION_OVER_MINOR_TRANSFER.with(Cell::get)
}
thread_local! {
static COMPETITION_OVER_DIAMOND_TRANSFER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_competition_over_diamond_transfer(on: bool) {
COMPETITION_OVER_DIAMOND_TRANSFER.with(|cell| cell.set(on));
}
fn competition_over_diamond_transfer() -> bool {
COMPETITION_OVER_DIAMOND_TRANSFER.with(Cell::get)
}
thread_local! {
static NATURAL_FLOOR: Cell<(u8, u8)> = const { Cell::new((5, 0)) };
}
pub fn set_natural_floor(hcp_floor: u8, points_floor: u8) {
NATURAL_FLOOR.with(|cell| cell.set((hcp_floor, points_floor)));
}
fn natural_floor_on() -> bool {
let (hcp, points) = NATURAL_FLOOR.with(Cell::get);
hcp > 0 || points > 0
}
fn natural_floor_hcp() -> u8 {
NATURAL_FLOOR.with(Cell::get).0
}
fn natural_floor_pts() -> u8 {
NATURAL_FLOOR.with(Cell::get).1
}
thread_local! {
static DEFENSE_2D_MULTI: Cell<bool> = const { Cell::new(false) };
}
pub fn set_defense_to_2d_multi(on: bool) {
DEFENSE_2D_MULTI.with(|cell| cell.set(on));
}
fn defense_2d_multi() -> bool {
DEFENSE_2D_MULTI.with(Cell::get)
}
pub(super) fn unbid_major(over: Suit) -> Option<Suit> {
match over {
Suit::Hearts => Some(Suit::Spades),
Suit::Spades => Some(Suit::Hearts),
_ => None,
}
}
fn lebensohl_relay_shape(over: Suit) -> Cons<impl Constraint + Clone> {
let five = |s: Suit| len(s, 5..);
let any5 = match over {
Suit::Clubs => five(Suit::Diamonds) | five(Suit::Hearts) | five(Suit::Spades),
Suit::Diamonds => five(Suit::Clubs) | five(Suit::Hearts) | five(Suit::Spades),
Suit::Hearts => five(Suit::Clubs) | five(Suit::Diamonds) | five(Suit::Spades),
Suit::Spades => five(Suit::Clubs) | five(Suit::Diamonds) | five(Suit::Hearts),
};
any5 & hcp(6..)
}
fn over_their_overcall(opening: Suit) -> Rules {
let o = opening;
let o_strain = Strain::from(o);
let is_major = matches!(o, Suit::Hearts | Suit::Spades);
let raise_min: usize = if is_major { 3 } else { 5 };
let jump_min: usize = if is_major { 4 } else { 5 };
let other_major = match o {
Suit::Hearts => Suit::Spades,
_ => Suit::Hearts,
};
let mut rules = Rules::new();
for t in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if t == o {
continue;
}
let t_strain = Strain::from(t);
for lvl in 2u8..=3 {
rules = rules
.rule(
Bid::new(lvl, t_strain),
2.0,
they_bid(t_strain)
& min_level_is(lvl, t_strain)
& support(raise_min..)
& points(10..),
)
.alert(CUE_RAISE);
}
}
rules = rules.rule(
Bid::new(3, o_strain),
1.6,
min_level_is(2, o_strain) & support(jump_min..) & points(..=9),
);
rules = rules.rule(
Bid::new(3, o_strain),
1.3,
min_level_is(3, o_strain) & support(raise_min..) & points(6..=9),
);
rules = rules.rule(
Bid::new(2, o_strain),
1.5,
min_level_is(2, o_strain) & support(raise_min..) & points(6..=9),
);
let shape = negative_double_shape();
rules = if is_major {
rules
.rule(Call::Double, 1.0, len(other_major, 4..) & hcp(8..))
.alert(NEGATIVE_DOUBLE)
} else {
match shape {
NegativeDoubleShape::BothMajors => rules
.rule(
Call::Double,
1.0,
len(Suit::Hearts, 4..) & len(Suit::Spades, 4..) & hcp(8..),
)
.alert(NEGATIVE_DOUBLE),
NegativeDoubleShape::Modern => rules
.rule(
Call::Double,
1.0,
min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, 4..)
& len(Suit::Spades, 4..)
& hcp(6..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
they_bid(Strain::Hearts)
& min_level_is(1, Strain::Spades)
& len(Suit::Spades, 4..=4)
& hcp(6..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
they_bid(Strain::Spades) & len(Suit::Hearts, 4..) & hcp(8..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
(they_bid(Strain::Clubs) | they_bid(Strain::Diamonds))
& !min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, 4..)
& len(Suit::Spades, 4..)
& hcp(8..),
)
.alert(NEGATIVE_DOUBLE),
NegativeDoubleShape::Cachalot => rules
.rule(
Call::Double,
1.0,
min_level_is(1, Strain::Hearts) & len(Suit::Hearts, 4..) & hcp(6..),
)
.alert(CACHALOT_X)
.rule(
Call::Double,
1.0,
they_bid(Strain::Hearts)
& min_level_is(1, Strain::Spades)
& len(Suit::Spades, 4..)
& hcp(6..),
)
.alert(CACHALOT_X)
.rule(
Call::Double,
1.0,
they_bid(Strain::Spades) & len(Suit::Hearts, 4..) & hcp(8..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
(they_bid(Strain::Clubs) | they_bid(Strain::Diamonds))
& !min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, 4..)
& len(Suit::Spades, 4..)
& hcp(8..),
)
.alert(NEGATIVE_DOUBLE),
NegativeDoubleShape::Sputnik => rules
.rule(
Call::Double,
1.0,
min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, ..=3)
& len(Suit::Spades, ..=3)
& hcp(7..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
they_bid(Strain::Hearts)
& min_level_is(1, Strain::Spades)
& len(Suit::Spades, ..=3)
& hcp(7..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
they_bid(Strain::Spades) & len(Suit::Hearts, 4..) & hcp(8..),
)
.alert(NEGATIVE_DOUBLE)
.rule(
Call::Double,
1.0,
(they_bid(Strain::Clubs) | they_bid(Strain::Diamonds))
& !min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, 4..)
& len(Suit::Spades, 4..)
& hcp(8..),
)
.alert(NEGATIVE_DOUBLE),
}
};
if !is_major && shape == NegativeDoubleShape::Cachalot {
rules = rules
.rule(
Bid::new(1, Strain::Hearts),
1.45,
min_level_is(1, Strain::Hearts)
& len(Suit::Spades, 4..)
& len(Suit::Hearts, ..=3)
& hcp(6..),
)
.alert(CACHALOT_TRANSFER)
.rule(
Bid::new(1, Strain::Spades),
0.85,
min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, ..=3)
& len(Suit::Spades, ..=3)
& hcp(8..),
)
.alert(CACHALOT_TAKEOUT)
.rule(
Bid::new(1, Strain::Spades),
0.85,
they_bid(Strain::Hearts)
& min_level_is(1, Strain::Spades)
& len(Suit::Spades, ..=3)
& hcp(8..),
)
.alert(CACHALOT_TAKEOUT);
}
if !is_major && shape == NegativeDoubleShape::Sputnik {
rules = rules
.rule(
Bid::new(1, Strain::Hearts),
1.45,
min_level_is(1, Strain::Hearts)
& len(Suit::Hearts, 4..)
& points(free_bid_floor()..),
)
.rule(
Bid::new(1, Strain::Spades),
1.45,
min_level_is(1, Strain::Spades)
& len(Suit::Spades, 4..)
& points(free_bid_floor()..),
);
}
if free_bids_engaged() {
let rotate = !is_major
&& matches!(
shape,
NegativeDoubleShape::Cachalot | NegativeDoubleShape::Sputnik
);
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if x == o {
continue;
}
let xs = Strain::from(x);
if !(rotate && matches!(x, Suit::Hearts | Suit::Spades)) {
rules = rules.rule(
Bid::new(1, xs),
1.45,
min_level_is(1, xs) & len(x, 5..) & points(free_bid_floor()..) & !they_bid(xs),
);
}
rules = rules.rule(
Bid::new(2, xs),
1.45,
min_level_is(2, xs) & len(x, 5..) & points(10..) & !they_bid(xs),
);
}
rules = rules
.rule(
Bid::new(1, Strain::Notrump),
0.9,
min_level_is(1, Strain::Notrump)
& hcp(free_bid_floor()..=10)
& stopper_in_their_suits(),
)
.rule(
Bid::new(2, Strain::Notrump),
0.95,
min_level_is(2, Strain::Notrump) & hcp(11..=12) & stopper_in_their_suits(),
);
}
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if x == o {
continue;
}
let x_strain = Strain::from(x);
for lvl in 2u8..=3 {
rules = rules.rule(
Bid::new(lvl, x_strain),
1.1,
min_level_is(lvl - 1, x_strain) & len(x, 6..) & points(2..=5) & !they_bid(x_strain),
);
}
}
rules.rule(Call::Pass, 0.0, hcp(0..))
}
fn support_rules(major: Suit) -> Rules {
let m = Strain::from(major);
Rules::new()
.rule(Call::Double, 1.5, support(3..=3))
.alert(SUPPORT_DOUBLE)
.rule(Bid::new(2, m), 1.4, support(4..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn answer_neg_double_of_minor(opening_major: Suit) -> Rules {
let m = Strain::from(opening_major);
let other = if opening_major == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
let other_strain = Strain::from(other);
Rules::new()
.rule(Bid::new(2, other_strain), 1.0, len(other, 3..))
.rule(Bid::new(2, m), 0.5, len(opening_major, 5..))
}
fn answer_cue_raise(major: Suit) -> Rules {
let trump = Strain::from(major);
Rules::new()
.rule(Bid::new(4, trump), 1.0, points(13..))
.rule(Bid::new(3, trump), 0.0, hcp(0..))
}
fn answer_cue_minor_raise(minor: Suit) -> Rules {
let trump = Strain::from(minor);
Rules::new()
.rule(
Bid::new(3, Strain::Notrump),
1.0,
points(14..) & stopper_in_their_suits(),
)
.rule(Bid::new(3, trump), 0.5, min_level_is(3, trump))
.rule(Bid::new(4, trump), 0.5, min_level_is(4, trump))
}
fn uvu_major_responder(major: Suit) -> Rules {
let m = Strain::from(major);
let om = unbid_major(major).expect("a major opening has an unbid major");
Rules::new()
.rule(
Bid::new(3, Strain::Clubs),
2.0,
len(major, 3..) & points(10..),
)
.alert(UVU_MAJOR_RAISE)
.rule(
Bid::new(3, Strain::Diamonds),
1.9,
len(om, 5..) & points(13..),
)
.alert(UVU_MAJOR_FOURTH)
.rule(
Bid::new(3, Strain::Notrump),
1.5,
points(13..) & stopper_in(Suit::Clubs) & stopper_in(Suit::Diamonds),
)
.rule(
Call::Double,
1.4,
hcp(10..)
& (len(Suit::Clubs, 4..)
| suit_hcp(Suit::Clubs, 4..)
| len(Suit::Diamonds, 4..)
| suit_hcp(Suit::Diamonds, 4..)),
)
.rule(Bid::new(3, m), 1.3, len(major, 3..) & points(6..=9))
.rule(Bid::new(4, m), 1.25, len(major, 4..) & points(..=9))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn michaels_cue_responder(major: Suit) -> Rules {
let m = Strain::from(major);
let om_cue = if major == Suit::Hearts {
Bid::new(2, Strain::Spades)
} else {
Bid::new(3, Strain::Hearts)
};
Rules::new()
.rule(om_cue, 2.0, len(major, 3..) & points(10..))
.alert(UVU_MAJOR_RAISE)
.rule(Call::Double, 1.6, hcp(10..))
.rule(Bid::new(3, m), 1.3, len(major, 3..) & points(6..=9))
.rule(Bid::new(4, m), 1.25, len(major, 4..) & points(..=9))
.rule(
Bid::new(3, Strain::Clubs),
1.1,
len(Suit::Clubs, 6..) & points(2..=9),
)
.rule(
Bid::new(3, Strain::Diamonds),
1.1,
len(Suit::Diamonds, 6..) & points(2..=9),
)
.rule(Call::Pass, 0.0, hcp(0..))
}
fn uvu_fourth_suit_answer(major: Suit) -> Rules {
let m = Strain::from(major);
let om = unbid_major(major).expect("a major opening has an unbid major");
let om_strain = Strain::from(om);
Rules::new()
.rule(Bid::new(4, om_strain), 1.5, len(om, 3..))
.rule(
Bid::new(3, Strain::Notrump),
1.2,
stopper_in(Suit::Clubs) & stopper_in(Suit::Diamonds),
)
.rule(Bid::new(4, m), 1.0, len(major, 6..))
.rule(Bid::new(3, Strain::Notrump), 0.2, hcp(0..))
}
fn cachalot_answer(opening: Suit, over: Suit, shown: Suit) -> Rules {
let m = Strain::from(shown);
let mut rules = Rules::new()
.rule(Bid::new(2, m), 1.3, len(shown, 4..))
.rule(Bid::new(1, m), 1.2, len(shown, 3..=3))
.alert(CACHALOT_THREE);
if shown == Suit::Hearts {
rules = rules.rule(Bid::new(1, Strain::Spades), 1.1, len(Suit::Spades, 4..));
}
rules
.rule(Bid::new(1, Strain::Notrump), 1.0, stopper_in(over))
.rule(Bid::new(2, Strain::from(opening)), 0.9, len(opening, 5..))
.rule(Bid::new(1, Strain::Notrump), 0.2, hcp(0..))
}
fn cachalot_takeout_answer(opening: Suit, over: Suit) -> Rules {
let o = Strain::from(opening);
Rules::new()
.rule(Bid::new(1, Strain::Notrump), 1.0, stopper_in(over))
.rule(Bid::new(2, o), 0.9, len(opening, 5..))
.rule(Bid::new(2, o), 0.2, hcp(0..))
}
fn doubled_opening_responder(opening: Suit) -> Rules {
let o = opening;
let o_strain = Strain::from(o);
let is_major = matches!(o, Suit::Hearts | Suit::Spades);
let jordan_min: usize = if is_major { 4 } else { 5 };
let raise_min: usize = if is_major { 3 } else { 5 };
let xx_max: usize = if is_major { 3 } else { 4 };
let mut rules = Rules::new()
.rule(
Bid::new(2, Strain::Notrump),
2.0,
len(o, jordan_min..) & points(10..),
)
.alert(JORDAN)
.rule(Call::Redouble, 1.6, hcp(10..) & len(o, ..=xx_max))
.alert(VALUE_REDOUBLE)
.rule(
Bid::new(3, o_strain),
1.5,
len(o, jordan_min..) & points(..=9),
)
.rule(
Bid::new(2, o_strain),
1.4,
len(o, raise_min..) & points(6..=9),
);
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if x == o {
continue;
}
let xs = Strain::from(x);
rules = rules
.rule(
Bid::new(1, xs),
1.3,
min_level_is(1, xs) & len(x, 4..) & points(6..),
)
.rule(
Bid::new(2, xs),
1.2,
min_level_is(2, xs) & len(x, 5..) & points(6..=9),
);
}
rules
.rule(Bid::new(1, Strain::Notrump), 1.1, hcp(6..=9))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn answer_preemptive_raise(opening: Suit) -> Rules {
let o_strain = Strain::from(opening);
let game = if matches!(opening, Suit::Hearts | Suit::Spades) {
Rules::new().rule(Bid::new(4, o_strain), 0.9, points(17..))
} else {
Rules::new().rule(Bid::new(5, o_strain), 0.9, points(19..))
};
game.rule(Call::Pass, 0.0, hcp(0..))
}
fn answer_weak_new_suit(x: Suit) -> Rules {
Rules::new()
.rule(
Bid::new(3, Strain::from(x)),
0.9,
len(x, 4..) & points(15..),
)
.rule(Call::Pass, 0.3, hcp(0..))
}
fn over_their_high_overcall(opening: Suit) -> Rules {
let o = opening;
let o_strain = Strain::from(o);
let is_major = matches!(o, Suit::Hearts | Suit::Spades);
let raise_min: usize = if is_major { 3 } else { 5 };
let mut rules = Rules::new().rule(
Bid::new(3, Strain::Notrump),
1.7,
points(13..) & stopper_in_their_suits(),
);
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if x == o {
continue;
}
let xs = Strain::from(x);
rules = rules.rule(
Bid::new(3, xs),
1.45,
min_level_is(3, xs) & !they_bid(xs) & len(x, 5..) & points(13..),
);
}
rules = if is_major {
let om = unbid_major(o).expect("a major opening has an unbid major");
let om_strain = Strain::from(om);
rules
.rule(
Call::Double,
1.0,
len(om, 4..) & hcp(10..) & !they_bid(om_strain),
)
.alert(NEGATIVE_DOUBLE)
} else {
rules
.rule(
Call::Double,
1.0,
(len(Suit::Hearts, 4..) | len(Suit::Spades, 4..)) & hcp(10..),
)
.alert(NEGATIVE_DOUBLE)
};
rules = rules.rule(
Bid::new(3, o_strain),
1.3,
min_level_is(3, o_strain) & len(o, raise_min..) & points(6..),
);
rules = if is_major {
rules.rule(Bid::new(4, o_strain), 1.25, len(o, 4..) & points(11..))
} else {
rules.rule(Bid::new(4, o_strain), 1.25, len(o, 5..) & points(..=9))
};
rules.rule(Call::Pass, 0.0, hcp(0..))
}
fn answer_high_neg_double(opening: Suit) -> Rules {
let o_strain = Strain::from(opening);
let mut rules = Rules::new();
for m in [Suit::Hearts, Suit::Spades] {
if m == opening {
continue;
}
let ms = Strain::from(m);
rules = rules
.rule(
Bid::new(3, ms),
1.2,
min_level_is(3, ms) & len(m, 4..) & !they_bid(ms),
)
.rule(
Bid::new(4, ms),
1.1,
min_level_is(4, ms) & len(m, 4..) & !they_bid(ms),
);
}
rules = rules
.rule(Bid::new(3, Strain::Notrump), 1.0, stopper_in_their_suits())
.rule(
Bid::new(3, o_strain),
0.9,
min_level_is(3, o_strain) & len(opening, 6..),
)
.rule(
Bid::new(4, o_strain),
0.85,
min_level_is(4, o_strain) & len(opening, 6..),
);
for m in [Suit::Hearts, Suit::Spades] {
if m == opening {
continue;
}
let ms = Strain::from(m);
rules = rules
.rule(
Bid::new(3, ms),
0.3,
min_level_is(3, ms) & len(m, 3..) & !they_bid(ms),
)
.rule(
Bid::new(4, ms),
0.25,
min_level_is(4, ms) & len(m, 3..) & !they_bid(ms),
);
}
rules.rule(Bid::new(3, Strain::Notrump), 0.15, hcp(0..))
}
fn weak_two_doubled_responder(our: Suit) -> Rules {
weak_twos::responses(our)
.rule(Call::Redouble, 1.8, hcp(13..))
.alert(WEAK_TWO_XX)
}
fn weak_two_overcalled_responder(our: Suit) -> Rules {
let trump = Strain::from(our);
Rules::new()
.rule(
Bid::new(2, Strain::Notrump),
2.0,
min_level_is(2, Strain::Notrump) & len(our, 2..) & points(14..),
)
.alert(CONTESTED_OGUST)
.rule(Call::Double, 1.6, hcp(11..))
.rule(
Bid::new(3, trump),
1.3,
min_level_is(3, trump) & len(our, 3..),
)
.rule(Bid::new(4, trump), 1.25, len(our, 4..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn strong_two_overcalled_responder() -> Rules {
let mut rules = Rules::new()
.rule(
Bid::new(2, Strain::Notrump),
1.3,
min_level_is(2, Strain::Notrump) & hcp(8..) & balanced() & stopper_in_their_suits(),
)
.rule(
Bid::new(3, Strain::Notrump),
1.3,
min_level_is(3, Strain::Notrump) & hcp(8..) & balanced() & stopper_in_their_suits(),
)
.rule(Call::Double, 1.2, hcp(6..))
.rule(Call::Pass, 0.5, hcp(0..));
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(x);
for level in 2..=3u8 {
rules = rules.rule(
Bid::new(level, strain),
1.5,
min_level_is(level, strain) & len(x, 5..) & top_honors(x, 2..) & points(8..),
);
}
}
rules
}
fn strong_two_reopening() -> Rules {
let mut rules = Rules::new()
.rule(
Bid::new(2, Strain::Notrump),
1.2,
min_level_is(2, Strain::Notrump) & balanced() & stopper_in_their_suits(),
)
.rule(
Bid::new(3, Strain::Notrump),
1.2,
min_level_is(3, Strain::Notrump) & balanced() & stopper_in_their_suits(),
)
.rule(Call::Double, 0.4, hcp(0..));
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(x);
for level in 2..=3u8 {
rules = rules.rule(
Bid::new(level, strain),
1.0,
min_level_is(level, strain) & len(x, 5..),
);
}
}
rules
}
pub(super) fn lebensohl_responder(over: Suit) -> Rules {
let mut rules = Rules::new();
for s in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if s == over {
continue;
}
let strain = Strain::from(s);
rules = rules.rule(Bid::new(3, strain), 1.8, len(s, 5..) & points(10..));
}
let cue = Bid::new(3, Strain::from(over));
rules = match unbid_major(over) {
Some(major) => rules
.rule(cue, 1.75, len(major, 4..) & points(10..))
.alert(LEBENSOHL_CUE),
None => rules
.rule(
cue,
1.75,
(len(Suit::Hearts, 4..) | len(Suit::Spades, 4..)) & points(10..),
)
.alert(LEBENSOHL_CUE),
};
rules = author_direct_3nt(rules, 1.7, over);
rules = responder_double(rules, over);
for s in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if s == over {
continue;
}
let strain = Strain::from(s);
rules = rules.rule(
Bid::new(2, strain),
1.5,
min_level_is(2, strain)
& len(s, 5..)
& points(..=9)
& hcp(natural_floor_hcp()..)
& points(natural_floor_pts()..),
);
}
let long_suit = lebensohl_relay_shape(over);
rules = rules
.rule(Bid::new(2, Strain::Notrump), 1.4, points(..=9) & long_suit)
.alert(LEBENSOHL_RELAY);
rules.rule(Call::Pass, 0.0, hcp(0..))
}
fn multi_responder() -> Rules {
let over = Suit::Diamonds; let mut rules = Rules::new();
rules = rules.rule(Call::Double, 1.55, points(8..));
for s in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(s);
rules = rules.rule(Bid::new(3, strain), 1.8, len(s, 5..) & points(10..));
}
rules = author_direct_3nt(rules, 1.7, over);
for s in [Suit::Hearts, Suit::Spades] {
let strain = Strain::from(s);
rules = rules.rule(
Bid::new(2, strain),
1.5,
len(s, 5..) & points(..=9) & hcp(natural_floor_hcp()..) & points(natural_floor_pts()..),
);
}
let long_suit = lebensohl_relay_shape(over);
rules = rules
.rule(Bid::new(2, Strain::Notrump), 1.4, points(..=9) & long_suit)
.alert(LEBENSOHL_RELAY);
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn complete_lebensohl_relay() -> Rules {
Rules::new().rule(Bid::new(3, Strain::Clubs), 1.0, hcp(0..))
}
pub(super) fn lebensohl_relay_rebid(over: Suit) -> Rules {
let mut rules = Rules::new();
for s in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if s == over {
continue;
}
let strain = Strain::from(s);
rules = rules.rule(
Bid::new(3, strain),
1.0,
min_level_is(3, strain) & len(s, 5..),
);
}
if let (true, Some(major)) = (delayed_cue(), unbid_major(over)) {
rules = rules
.rule(
Bid::new(3, Strain::from(over)),
1.5,
points(10..) & stopper_in(over) & len(major, 4..) & len(major, ..5),
)
.alert(LEBENSOHL_CUE);
}
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn lebensohl_signoff_raise(signoff: Suit, resp_floor: u8) -> Rules {
let game = Bid::new(4, Strain::from(signoff));
let base = 23u8.saturating_sub(resp_floor); Rules::new()
.rule(
game,
1.0,
(len(signoff, 3..=3) & points(base..))
| (len(signoff, 4..=4) & points(base.saturating_sub(1)..))
| (len(signoff, 5..) & points(base.saturating_sub(2)..)),
)
.rule(Call::Pass, 0.0, hcp(0..))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Competitive4333 {
Allow,
Suppress,
SuppressWithStopper,
}
thread_local! {
static COMPETITIVE_4333: Cell<Competitive4333> =
const { Cell::new(Competitive4333::Suppress) };
}
pub fn set_competitive_4333(mode: Competitive4333) {
COMPETITIVE_4333.with(|cell| cell.set(mode));
}
fn competitive_4333() -> Competitive4333 {
COMPETITIVE_4333.with(Cell::get)
}
fn competitive_4333_ok(over: Suit, gate: bool) -> Cons<impl Constraint + Clone> {
let mode = if gate {
competitive_4333()
} else {
Competitive4333::Allow
};
described(
"not a flat 4-3-3-3 diverted to 3NT",
move |hand: Hand, _: &Context<'_>| {
let flat = Suit::ASC
.into_iter()
.all(|suit| (3..=4).contains(&hand[suit].len()));
!match mode {
Competitive4333::Allow => false,
Competitive4333::Suppress => flat,
Competitive4333::SuppressWithStopper => flat && has_stopper(hand[over]),
}
},
)
}
pub(super) fn transfer_target(bid_suit: Suit, over: Suit) -> Option<Suit> {
if bid_suit == over {
return None; }
[Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades]
.into_iter()
.find(|&s| (s as u8) > (bid_suit as u8) && s != over)
}
pub(super) fn transfer_lebensohl_responder(over: Suit, gate_4333: bool) -> Rules {
let mut rules = Rules::new();
for bid_suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(bid_suit);
if bid_suit == over {
let cue = Bid::new(3, strain);
let split = delayed_cue() && unbid_major(over).is_some();
rules = match (over, split) {
(Suit::Hearts, true) => rules
.rule(
cue,
1.7,
len(Suit::Spades, 4..)
& points(10..)
& !stopper_in(over)
& competitive_4333_ok(over, gate_4333),
)
.alert(LEBENSOHL_CUE),
(Suit::Spades, true) => rules
.rule(
cue,
1.7,
len(Suit::Hearts, 4..)
& points(10..)
& !stopper_in(over)
& competitive_4333_ok(over, gate_4333),
)
.alert(LEBENSOHL_CUE),
(Suit::Hearts, false) => rules
.rule(
cue,
1.7,
len(Suit::Spades, 4..)
& points(10..)
& competitive_4333_ok(over, gate_4333),
)
.alert(LEBENSOHL_CUE),
(Suit::Spades, false) => rules
.rule(
cue,
1.7,
len(Suit::Hearts, 4..)
& points(10..)
& competitive_4333_ok(over, gate_4333),
)
.alert(LEBENSOHL_CUE),
_ => rules
.rule(
cue,
1.7,
(len(Suit::Hearts, 4..) | len(Suit::Spades, 4..))
& points(10..)
& competitive_4333_ok(over, gate_4333),
)
.alert(LEBENSOHL_CUE),
};
} else if let Some(target) = transfer_target(bid_suit, over) {
let weight = if matches!(target, Suit::Hearts | Suit::Spades) {
1.8
} else {
1.45
};
rules = rules
.rule(Bid::new(3, strain), weight, len(target, 5..) & points(9..))
.alert(LEBENSOHL_TRANSFER);
} else if over != Suit::Clubs {
rules = rules
.rule(
Bid::new(3, strain),
1.45,
len(Suit::Clubs, 6..) & points(10..),
)
.alert(LEBENSOHL_TRANSFER);
}
}
rules = author_direct_3nt(rules, 1.5, over);
if let (true, Some(major)) = (delayed_cue(), unbid_major(over)) {
rules = rules
.rule(
Bid::new(2, Strain::Notrump),
1.6,
points(10..) & stopper_in(over) & len(major, 4..) & len(major, ..5),
)
.alert(LEBENSOHL_RELAY);
}
rules = responder_double(rules, over);
for s in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if s == over {
continue;
}
let strain = Strain::from(s);
rules = rules.rule(
Bid::new(2, strain),
1.4,
min_level_is(2, strain)
& len(s, 5..)
& points(..=8)
& hcp(natural_floor_hcp()..)
& points(natural_floor_pts()..),
);
}
let long_suit = lebensohl_relay_shape(over);
rules = rules
.rule(Bid::new(2, Strain::Notrump), 1.35, points(..=8) & long_suit)
.alert(LEBENSOHL_RELAY);
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn transfer_completion(target: Suit, over: Suit) -> Rules {
let t = Strain::from(target);
let mut rules = Rules::new();
if matches!(target, Suit::Hearts | Suit::Spades) {
rules = rules.rule(Bid::new(4, t), 1.6, len(target, 3..)).rule(
Bid::new(3, Strain::Notrump),
1.4,
len(target, ..3),
);
} else {
rules = rules
.rule(Bid::new(3, Strain::Notrump), 1.5, stopper_in(over))
.rule(Bid::new(3, t), 1.3, len(target, 3..));
}
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn cue_stayman_answer(over: Suit) -> Rules {
let mut rules = Rules::new();
for major in [Suit::Hearts, Suit::Spades] {
if major == over {
continue;
}
let m = Strain::from(major);
rules = rules
.rule(Bid::new(3, m), 1.6, len(major, 4..) & min_level_is(3, m))
.rule(Bid::new(4, m), 1.5, len(major, 4..) & min_level_is(4, m));
}
rules.rule(Bid::new(3, Strain::Notrump), 1.3, hcp(0..))
}
pub(super) fn cue_stayman_answer_no_stopper(over: Suit) -> Rules {
let mut rules = Rules::new();
for major in [Suit::Hearts, Suit::Spades] {
if major == over {
continue;
}
let m = Strain::from(major);
rules = rules
.rule(Bid::new(3, m), 1.6, len(major, 4..) & min_level_is(3, m))
.rule(Bid::new(4, m), 1.5, len(major, 4..) & min_level_is(4, m));
}
rules = rules.rule(Bid::new(3, Strain::Notrump), 1.45, stopper_in(over));
for minor in [Suit::Clubs, Suit::Diamonds] {
let m = Strain::from(minor);
rules = rules.rule(Bid::new(4, m), 1.2, len(minor, 4..) & min_level_is(4, m));
}
rules.rule(Bid::new(3, Strain::Notrump), 1.0, hcp(0..))
}
pub(super) fn transfer_stayman_2d_responder(gate_4333: bool) -> Rules {
let mut rules = Rules::new();
rules = rules
.rule(
Bid::new(3, Strain::Clubs),
1.85,
(len(Suit::Hearts, 4..=4) | len(Suit::Spades, 4..=4))
& points(10..)
& competitive_4333_ok(Suit::Diamonds, gate_4333),
)
.alert(STAYMAN);
rules = rules
.rule(
Bid::new(3, Strain::Diamonds),
1.8,
len(Suit::Hearts, 5..) & points(9..),
)
.alert(LEBENSOHL_TRANSFER)
.rule(
Bid::new(3, Strain::Hearts),
1.8,
len(Suit::Spades, 5..) & points(9..),
)
.alert(LEBENSOHL_TRANSFER);
rules = rules
.rule(
Bid::new(3, Strain::Spades),
1.45,
len(Suit::Clubs, 6..) & points(10..),
)
.alert(LEBENSOHL_TRANSFER);
rules = rules
.rule(
Bid::new(4, Strain::Diamonds),
2.0,
len(Suit::Hearts, 5..) & len(Suit::Spades, 5..) & points(10..),
)
.alert(LEAPING_MICHAELS)
.rule(
Bid::new(4, Strain::Clubs),
2.0,
len(Suit::Clubs, 5..)
& (len(Suit::Hearts, 5..) | len(Suit::Spades, 5..))
& points(10..),
)
.alert(LEAPING_MICHAELS);
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.5,
points(10..) & stopper_in(Suit::Diamonds),
);
rules = responder_double(rules, Suit::Diamonds);
for s in [Suit::Hearts, Suit::Spades] {
let strain = Strain::from(s);
rules = rules.rule(
Bid::new(2, strain),
1.4,
min_level_is(2, strain)
& len(s, 5..)
& points(..=8)
& hcp(natural_floor_hcp()..)
& points(natural_floor_pts()..),
);
}
let long_suit = lebensohl_relay_shape(Suit::Diamonds);
rules = rules
.rule(Bid::new(2, Strain::Notrump), 1.35, points(..=8) & long_suit)
.alert(LEBENSOHL_RELAY);
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn stayman_2d_answer() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Hearts), 1.6, len(Suit::Hearts, 4..))
.rule(
Bid::new(3, Strain::Spades),
1.55,
len(Suit::Spades, 4..) & len(Suit::Hearts, ..4),
)
.rule(Bid::new(3, Strain::Diamonds), 0.5, hcp(0..))
}
pub(super) fn stayman_2d_fit_rebid(major: Suit) -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::from(major)), 1.4, len(major, 4..))
.rule(Bid::new(3, Strain::Notrump), 0.5, hcp(0..))
}
pub(super) fn clubs_transfer_completion(over: Suit) -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.4, stopper_in(over))
.rule(Bid::new(5, Strain::Clubs), 0.5, hcp(0..))
}
pub(super) fn lm_2d_both_majors_advance() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Spades), 1.6, len(Suit::Spades, 4..))
.rule(Bid::new(4, Strain::Hearts), 1.55, len(Suit::Hearts, 4..))
.rule(Bid::new(4, Strain::Spades), 1.5, len(Suit::Spades, 3..))
.rule(Bid::new(4, Strain::Hearts), 1.0, hcp(0..))
}
pub(super) fn lm_2d_clubs_ask() -> Rules {
Rules::new().rule(Bid::new(4, Strain::Diamonds), 1.4, hcp(0..))
}
pub(super) fn lm_2d_clubs_major() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Hearts), 1.5, len(Suit::Hearts, 5..))
.rule(Bid::new(4, Strain::Spades), 1.5, len(Suit::Spades, 5..))
.rule(Bid::new(5, Strain::Clubs), 0.5, hcp(0..))
}
pub(super) fn uvu_responder() -> Rules {
let x_floor = uvu_x_floor();
let cue_floor = uvu_cue_floor();
let weak = cue_floor.saturating_sub(1); let both_majors_55 = len(Suit::Spades, 5..) & len(Suit::Hearts, 5..);
let mut rules = Rules::new();
rules = rules
.rule(
Bid::new(4, Strain::Clubs),
2.0,
both_majors_55.clone() & len(Suit::Clubs, ..=1) & points(10..),
)
.alert(UVU_SPLINTER)
.rule(
Bid::new(4, Strain::Diamonds),
2.0,
both_majors_55 & len(Suit::Diamonds, ..=1) & points(10..),
)
.alert(UVU_SPLINTER);
rules = rules
.rule(
Bid::new(3, Strain::Clubs),
1.85,
((len(Suit::Spades, 5..) & len(Suit::Hearts, ..=4))
| len(Suit::Spades, 4..=4)
| (len(Suit::Hearts, 4..=4) & len(Suit::Spades, ..=3)))
& points(cue_floor..),
)
.alert(UVU_CUE)
.rule(
Bid::new(3, Strain::Diamonds),
1.8,
len(Suit::Hearts, 5..) & len(Suit::Spades, ..=3) & points(cue_floor..),
)
.alert(UVU_CUE);
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.5,
points(10..) & stopper_in(Suit::Clubs) & stopper_in(Suit::Diamonds),
);
rules = rules.rule(
Call::Double,
1.4,
hcp(x_floor..)
& (len(Suit::Clubs, 4..)
| suit_hcp(Suit::Clubs, 4..)
| len(Suit::Diamonds, 4..)
| suit_hcp(Suit::Diamonds, 4..)),
);
let nat = usize::from(uvu_natural_floor());
rules = rules
.rule(
Bid::new(3, Strain::Hearts),
1.3,
len(Suit::Hearts, nat..) & points(..=weak),
)
.rule(
Bid::new(3, Strain::Spades),
1.3,
len(Suit::Spades, nat..) & points(..=weak),
);
rules.rule(Call::Pass, 0.0, hcp(0..))
}
pub(super) fn uvu_smolen() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Hearts), 1.5, len(Suit::Spades, 5..))
.alert(SMOLEN)
.rule(Bid::new(3, Strain::Spades), 1.5, len(Suit::Hearts, 5..))
.alert(SMOLEN)
.rule(Bid::new(3, Strain::Notrump), 0.5, hcp(0..))
}
pub(super) fn uvu_rebid_over_3h() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Hearts), 1.5, len(Suit::Hearts, 4..))
.rule(Bid::new(3, Strain::Spades), 1.4, len(Suit::Spades, 5..))
.rule(Bid::new(3, Strain::Notrump), 0.5, hcp(0..))
}
fn stayman_doubled_opener() -> Rules {
Rules::new()
.rule(
Call::Redouble,
1.0,
len(Suit::Clubs, 5..) & suit_hcp(Suit::Clubs, 5..),
)
.rule(
Bid::new(2, Strain::Hearts),
1.0,
len(Suit::Hearts, 4..) & stopper_in(Suit::Clubs),
)
.rule(
Bid::new(2, Strain::Spades),
1.0,
len(Suit::Spades, 4..) & len(Suit::Hearts, ..4) & stopper_in(Suit::Clubs),
)
.rule(
Bid::new(2, Strain::Diamonds),
0.5,
len(Suit::Hearts, ..4) & len(Suit::Spades, ..4) & stopper_in(Suit::Clubs),
)
.rule(Call::Pass, 0.25, !stopper_in(Suit::Clubs))
}
fn stayman_redouble_reask() -> Rules {
Rules::new()
.rule(
Call::Redouble,
1.0,
len(Suit::Hearts, 4..) | len(Suit::Spades, 4..),
)
.alert(STAYMAN_REDOUBLE)
.rule(Call::Pass, 0.1, hcp(0..))
}
fn stayman_overcalled_opener(over: Suit) -> Rules {
let mut rules = Rules::new();
if (Suit::Hearts as u8) > (over as u8) {
rules = rules.rule(Bid::new(2, Strain::Hearts), 1.0, len(Suit::Hearts, 4..));
}
if (Suit::Spades as u8) > (over as u8) {
rules = rules.rule(
Bid::new(2, Strain::Spades),
1.0,
len(Suit::Spades, 4..) & len(Suit::Hearts, ..4),
);
}
rules
.rule(Call::Double, 0.6, len(over, 4..))
.rule(Call::Pass, 0.2, hcp(0..))
}
fn transfer_doubled_opener(major: Suit, bid: Suit) -> Rules {
let strain = Strain::from(major);
let mut rules = Rules::new();
if transfer_super_accept() {
rules = rules.rule(Bid::new(3, strain), 1.5, len(major, 4..) & hcp(17..));
}
rules
.rule(Bid::new(2, strain), 1.0, len(major, 3..))
.rule(Call::Redouble, 0.6, len(bid, 5..) & suit_hcp(bid, 5..))
.rule(Call::Pass, 0.25, len(major, ..3))
}
fn transfer_pass_reask(major: Suit) -> Rules {
Rules::new()
.rule(Call::Redouble, 1.0, len(major, 5..))
.alert(TRANSFER_REDOUBLE)
.rule(Call::Pass, 0.1, hcp(0..))
}
fn transfer_overcalled_opener(major: Suit, over_suit: Suit, over_level: u8) -> Rules {
let strain = Strain::from(major);
let lvl = if strain > Strain::from(over_suit) {
over_level
} else {
over_level + 1
};
Rules::new()
.rule(
Bid::new(lvl, strain),
1.0,
min_level_is(lvl, strain) & len(major, 4..),
)
.rule(Call::Double, 0.6, len(over_suit, 4..))
.rule(Call::Pass, 0.2, hcp(0..))
}
fn minor_doubled_opener() -> Rules {
Rules::new()
.rule(
Bid::new(3, Strain::Clubs),
1.0,
hcp(17..) & stopper_in(Suit::Spades),
)
.rule(Bid::new(2, Strain::Notrump), 0.9, stopper_in(Suit::Spades))
.rule(Call::Redouble, 0.8, hcp(17..))
.rule(Call::Pass, 0.25, hcp(0..))
}
fn minor_no_stopper_rebid() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Clubs), 0.8, len(Suit::Clubs, 6..))
.rule(Call::Pass, 0.1, hcp(0..))
}
fn minor_overcalled_high() -> Rules {
Rules::new()
.rule(
Bid::new(3, Strain::Notrump),
1.0,
hcp(17..) & stopper_in(Suit::Spades),
)
.rule(Call::Double, 0.7, hcp(17..))
.rule(Call::Pass, 0.2, hcp(0..))
}
fn minor_overcalled_low(over: Suit) -> Rules {
Rules::new()
.rule(Call::Double, 0.6, len(over, 4..))
.rule(Call::Pass, 0.2, hcp(0..))
}
fn diamond_doubled_opener() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Diamonds), 1.0, len(Suit::Diamonds, 3..))
.rule(
Bid::new(3, Strain::Clubs),
0.7,
len(Suit::Diamonds, ..3) & len(Suit::Clubs, 4..),
)
.rule(Call::Redouble, 0.6, hcp(17..))
.rule(Call::Pass, 0.25, hcp(0..))
}
fn diamond_no_fit_rebid() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Diamonds), 0.8, len(Suit::Diamonds, 5..))
.rule(Call::Pass, 0.1, hcp(0..))
}
fn diamond_overcalled_low() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Diamonds), 1.0, len(Suit::Diamonds, 3..))
.rule(Call::Double, 0.6, len(Suit::Clubs, 4..))
.rule(Call::Pass, 0.2, hcp(0..))
}
fn diamond_overcalled_high(over: Suit) -> Rules {
Rules::new()
.rule(
Bid::new(3, Strain::Notrump),
1.0,
hcp(17..) & stopper_in(over),
)
.rule(Call::Double, 0.6, len(over, 4..))
.rule(Call::Pass, 0.2, hcp(0..))
}
#[must_use]
pub fn competition() -> Competitive {
let mut book = Competitive::new();
for opening in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let opening_call = call(1, Strain::from(opening));
fallback_all_seats(
&mut book,
&[opening_call],
3,
Arc::new(OvercallAtMost(Bid::new(2, Strain::Spades))),
Fallback::classify(over_their_overcall(opening)),
);
fallback_all_seats(
&mut book,
&[opening_call],
3,
Arc::new(FirstIs(Call::Double)),
Fallback::rebase(ReplaceNext(Call::Pass)),
);
}
if splinter_doubled() {
for major in [Suit::Hearts, Suit::Spades] {
let m_strain = Strain::from(major);
let splinter_suits: &[Suit] = if major == Suit::Hearts {
&[Suit::Spades, Suit::Clubs, Suit::Diamonds]
} else {
&[Suit::Clubs, Suit::Diamonds, Suit::Hearts]
};
for &x in splinter_suits {
let (level, strain) = super::responses::splinter_bid(major, x);
let suffix = [call(1, m_strain), Call::Pass, call(level, strain)];
fallback_all_seats(
&mut book,
&suffix,
3,
Arc::new(FirstIs(Call::Double)),
Fallback::rebase(ReplaceNext(Call::Pass)),
);
}
}
}
let mut support_pairs = vec![
(Suit::Clubs, Suit::Hearts),
(Suit::Clubs, Suit::Spades),
(Suit::Diamonds, Suit::Hearts),
(Suit::Diamonds, Suit::Spades),
];
if major_support_double() {
support_pairs.push((Suit::Hearts, Suit::Spades));
}
for (opening, major) in support_pairs {
let suffix = [
call(1, Strain::from(opening)),
Call::Pass,
call(1, Strain::from(major)),
];
let just_below = if major == Suit::Hearts {
Bid::new(2, Strain::Diamonds)
} else {
Bid::new(2, Strain::Hearts)
};
fallback_all_seats(
&mut book,
&suffix,
3,
Arc::new(OvercallAtMost(just_below)),
Fallback::classify(support_rules(major)),
);
fallback_all_seats(
&mut book,
&suffix,
3,
Arc::new(SuffixIs(vec![Call::Double])),
Fallback::classify({
let m = Strain::from(major);
Rules::new()
.rule(Call::Redouble, 1.5, support(3..=3))
.alert(SUPPORT_DOUBLE)
.rule(Bid::new(2, m), 1.4, support(4..))
.rule(Call::Pass, 0.0, hcp(0..))
}),
);
}
for major in [Suit::Hearts, Suit::Spades] {
fallback_all_seats(
&mut book,
&[call(1, Strain::from(major))],
3,
Arc::new(described_guard(
"2♣/2♦ X -",
guard(|_: &Context<'_>, suffix: &[Call]| {
matches!(
suffix,
[Call::Bid(b), Call::Double, Call::Pass]
if b.level.get() == 2
&& (b.strain == Strain::Clubs || b.strain == Strain::Diamonds)
)
}),
)),
Fallback::classify(answer_neg_double_of_minor(major)),
);
}
if cue_raise_answer() {
for major in [Suit::Hearts, Suit::Spades] {
let trump = Strain::from(major);
fallback_all_seats(
&mut book,
&[call(1, trump)],
3,
Arc::new(described_guard(
"(overcall ≤2♠) cue -",
guard(move |_: &Context<'_>, suffix: &[Call]| {
matches!(
suffix,
[Call::Bid(ovc), Call::Bid(cue), Call::Pass]
if cue.strain == ovc.strain
&& cue > ovc
&& *ovc <= Bid::new(2, Strain::Spades)
&& ovc.strain != trump
)
}),
)),
Fallback::classify(answer_cue_raise(major)),
);
}
}
if cue_minor_raise_answer() {
for minor in [Suit::Clubs, Suit::Diamonds] {
let trump = Strain::from(minor);
fallback_all_seats(
&mut book,
&[call(1, trump)],
3,
Arc::new(described_guard(
"(overcall) (cue ≤3♠) -",
guard(move |_: &Context<'_>, suffix: &[Call]| {
matches!(
suffix,
[Call::Bid(ovc), Call::Bid(cue), Call::Pass]
if cue.strain == ovc.strain
&& cue > ovc
&& *cue <= Bid::new(3, Strain::Spades)
&& ovc.strain != trump
)
}),
)),
Fallback::classify(answer_cue_minor_raise(minor)),
);
}
}
if uvu_over_majors() {
for major in [Suit::Hearts, Suit::Spades] {
let trump = Strain::from(major);
let open = call(1, trump);
let unusual = call(2, Strain::Notrump);
let michaels = call(2, trump);
let om_cue = if major == Suit::Hearts {
call(2, Strain::Spades)
} else {
call(3, Strain::Hearts)
};
fallback_all_seats(
&mut book,
&[open, unusual],
3,
Arc::new(SuffixIs(vec![])),
Fallback::classify(uvu_major_responder(major)),
);
fallback_all_seats(
&mut book,
&[open, unusual],
3,
Arc::new(SuffixIs(vec![call(3, Strain::Clubs), Call::Pass])),
Fallback::classify(answer_cue_raise(major)),
);
fallback_all_seats(
&mut book,
&[open, unusual],
3,
Arc::new(SuffixIs(vec![call(3, Strain::Diamonds), Call::Pass])),
Fallback::classify(uvu_fourth_suit_answer(major)),
);
fallback_all_seats(
&mut book,
&[open, michaels],
3,
Arc::new(SuffixIs(vec![])),
Fallback::classify(michaels_cue_responder(major)),
);
fallback_all_seats(
&mut book,
&[open, michaels],
3,
Arc::new(SuffixIs(vec![om_cue, Call::Pass])),
Fallback::classify(answer_cue_raise(major)),
);
}
}
if jordan_truscott() {
for opening in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let o_strain = Strain::from(opening);
let key = [call(1, o_strain), Call::Double];
fallback_all_seats(
&mut book,
&key,
3,
Arc::new(SuffixIs(vec![])),
Fallback::classify(doubled_opening_responder(opening)),
);
fallback_all_seats(
&mut book,
&key,
3,
Arc::new(SuffixIs(vec![call(2, Strain::Notrump), Call::Pass])),
Fallback::classify(match opening {
Suit::Hearts | Suit::Spades => answer_cue_raise(opening),
minor => answer_cue_minor_raise(minor),
}),
);
fallback_all_seats(
&mut book,
&key,
3,
Arc::new(SuffixIs(vec![call(3, o_strain), Call::Pass])),
Fallback::classify(answer_preemptive_raise(opening)),
);
for x in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
if Strain::from(x) >= o_strain {
continue;
}
fallback_all_seats(
&mut book,
&key,
3,
Arc::new(SuffixIs(vec![call(2, Strain::from(x)), Call::Pass])),
Fallback::classify(answer_weak_new_suit(x)),
);
}
}
}
if high_overcall_responses() {
for opening in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let o_strain = Strain::from(opening);
let open = call(1, o_strain);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(described_guard(
"(2NT < overcall ≤3♠)",
guard(move |_: &Context<'_>, s: &[Call]| {
matches!(s, [Call::Bid(b)]
if *b > Bid::new(2, Strain::Notrump)
&& *b <= Bid::new(3, Strain::Spades)
&& b.strain.is_suit()
&& b.strain != o_strain)
}),
)),
Fallback::classify(over_their_high_overcall(opening)),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(described_guard(
"(2NT < overcall ≤3♠) X -",
guard(move |_: &Context<'_>, s: &[Call]| {
matches!(s, [Call::Bid(b), Call::Double, Call::Pass]
if *b > Bid::new(2, Strain::Notrump)
&& *b <= Bid::new(3, Strain::Spades)
&& b.strain.is_suit()
&& b.strain != o_strain)
}),
)),
Fallback::classify(answer_high_neg_double(opening)),
);
}
}
if negative_double_shape() == NegativeDoubleShape::Cachalot {
let one_heart = call(1, Strain::Hearts);
let one_spade = call(1, Strain::Spades);
let clubs = call(1, Strain::Clubs);
let d_ovc = call(1, Strain::Diamonds);
fallback_all_seats(
&mut book,
&[clubs, d_ovc],
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass])),
Fallback::classify(cachalot_answer(Suit::Clubs, Suit::Diamonds, Suit::Hearts)),
);
fallback_all_seats(
&mut book,
&[clubs, d_ovc],
3,
Arc::new(SuffixIs(vec![one_heart, Call::Pass])),
Fallback::classify(cachalot_answer(Suit::Clubs, Suit::Diamonds, Suit::Spades)),
);
fallback_all_seats(
&mut book,
&[clubs, d_ovc],
3,
Arc::new(SuffixIs(vec![one_spade, Call::Pass])),
Fallback::classify(cachalot_takeout_answer(Suit::Clubs, Suit::Diamonds)),
);
for opening in [Suit::Clubs, Suit::Diamonds] {
let open = call(1, Strain::from(opening));
fallback_all_seats(
&mut book,
&[open, one_heart],
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass])),
Fallback::classify(cachalot_answer(opening, Suit::Hearts, Suit::Spades)),
);
fallback_all_seats(
&mut book,
&[open, one_heart],
3,
Arc::new(SuffixIs(vec![one_spade, Call::Pass])),
Fallback::classify(cachalot_takeout_answer(opening, Suit::Hearts)),
);
}
}
if negative_double_shape() == NegativeDoubleShape::Sputnik {
let one_heart = call(1, Strain::Hearts);
fallback_all_seats(
&mut book,
&[call(1, Strain::Clubs), call(1, Strain::Diamonds)],
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass])),
Fallback::classify(cachalot_takeout_answer(Suit::Clubs, Suit::Diamonds)),
);
for opening in [Suit::Clubs, Suit::Diamonds] {
fallback_all_seats(
&mut book,
&[call(1, Strain::from(opening)), one_heart],
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass])),
Fallback::classify(cachalot_takeout_answer(opening, Suit::Hearts)),
);
}
}
if weak_two_competition() {
for our in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let trump = Strain::from(our);
let open = call(2, trump);
let two_nt = call(2, Strain::Notrump);
fallback_all_seats(
&mut book,
&[open, Call::Double],
3,
Arc::new(SuffixIs(vec![])),
Fallback::classify(weak_two_doubled_responder(our)),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(FirstIs(Call::Double)),
Fallback::rebase(ReplaceNext(Call::Pass)),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(OvercallAtMost(Bid::new(3, Strain::Spades))),
Fallback::classify(weak_two_overcalled_responder(our)),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(described_guard(
"(overcall <2NT) 2NT …",
guard(move |_: &Context<'_>, s: &[Call]| {
matches!(s.first(), Some(&Call::Bid(b)) if b < Bid::new(2, Strain::Notrump))
&& s.get(1) == Some(&two_nt)
}),
)),
Fallback::rebase(ReplaceNext(Call::Pass)),
);
}
}
if strong_two_competition() {
let open = call(2, Strain::Clubs);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(FirstIs(Call::Double)),
Fallback::rebase(ReplaceNext(Call::Pass)),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(described_guard(
"(overcall)",
guard(|_: &Context<'_>, s: &[Call]| matches!(s, [Call::Bid(_)])),
)),
Fallback::classify(strong_two_overcalled_responder()),
);
fallback_all_seats(
&mut book,
&[open],
3,
Arc::new(described_guard(
"(overcall) - -",
guard(|_: &Context<'_>, s: &[Call]| {
matches!(s, [Call::Bid(_), Call::Pass, Call::Pass])
}),
)),
Fallback::classify(strong_two_reopening()),
);
}
let style = lebensohl_style();
if style != LebensohlStyle::Off {
let one_nt = call(1, Strain::Notrump);
let two_nt = call(2, Strain::Notrump);
let three_clubs = call(3, Strain::Clubs);
let two_clubs = call(2, Strain::Clubs);
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(FirstIs(two_clubs)),
Fallback::rebase(described_rewrite(
"systems on: their 2♣ is treated as a pass; X asks as the stolen 2♣ Stayman",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth) != Some(&two_clubs) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth] = Call::Pass; if auction.get(depth + 1) == Some(&Call::Double) {
rewritten[depth + 1] = two_clubs; }
Some(rewritten)
}),
)),
);
let responses = notrump_responses();
fallback_all_seats(
&mut book,
&[one_nt, two_clubs],
3,
Arc::new(SuffixIs(vec![])),
Fallback::classify(classifier(move |hand: Hand, context: &Context<'_>| {
let mut logits = responses.classify(hand, context);
let stayman = *logits.0.get(two_clubs);
*logits.0.get_mut(two_clubs) = f32::NEG_INFINITY; *logits.0.get_mut(Call::Double) = stayman; logits
})),
);
if let Some((min_len, min_hcp, over_major)) = penalty_pass() {
let pass_logit = if over_major { 1.5 } else { 0.75 };
let answers = stayman_answers().rule(
Call::Pass,
pass_logit,
len(Suit::Clubs, min_len..) & suit_hcp(Suit::Clubs, min_hcp..),
);
fallback_all_seats(
&mut book,
&[one_nt, two_clubs],
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass])),
Fallback::classify(answers),
);
}
for over in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let overcall = call(2, Strain::from(over));
let responder = match style {
_ if over == Suit::Diamonds && defense_2d_multi() => multi_responder(),
LebensohlStyle::Transfer if over == Suit::Diamonds => {
transfer_stayman_2d_responder(true)
}
LebensohlStyle::Transfer => transfer_lebensohl_responder(over, true),
_ => lebensohl_responder(over),
};
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall])),
Fallback::classify(responder),
);
let opener_reply = match double_style() {
DoubleStyle::Penalty | DoubleStyle::PenaltyLight => {
Some(opener_leaves_in_penalty_double())
}
DoubleStyle::Optional => Some(opener_cooperates_optional(over)),
DoubleStyle::Takeout => None,
};
if let (true, Some(reply)) = (penalty_double_leave_in(), opener_reply) {
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall, Call::Double, Call::Pass])),
Fallback::classify(reply),
);
}
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall, two_nt, Call::Pass])),
Fallback::classify(complete_lebensohl_relay()),
);
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![
overcall,
two_nt,
Call::Pass,
three_clubs,
Call::Pass,
])),
Fallback::classify(lebensohl_relay_rebid(over)),
);
for signoff in [Suit::Hearts, Suit::Spades] {
if (signoff as u8) >= (over as u8) {
continue;
}
let three_m = call(3, Strain::from(signoff));
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![
overcall,
two_nt,
Call::Pass,
three_clubs,
Call::Pass,
three_m,
Call::Pass,
])),
Fallback::classify(lebensohl_signoff_raise(signoff, 6)),
);
}
if natural_floor_on() {
for signoff in [Suit::Hearts, Suit::Spades] {
if (signoff as u8) <= (over as u8) {
continue; }
let two_m = call(2, Strain::from(signoff));
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall, two_m, Call::Pass])),
Fallback::classify(lebensohl_signoff_raise(signoff, natural_floor_hcp())),
);
}
}
if style == LebensohlStyle::Plain {
let cue = call(3, Strain::from(over));
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall, cue, Call::Pass])),
Fallback::classify(cue_stayman_answer(over)),
);
}
if style == LebensohlStyle::Transfer && over != Suit::Diamonds {
for bid_suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let resp = call(3, Strain::from(bid_suit));
let reply = if bid_suit == over {
cue_stayman_answer(over)
} else if let Some(target) = transfer_target(bid_suit, over) {
transfer_completion(target, over)
} else if over != Suit::Clubs {
clubs_transfer_completion(over) } else {
continue; };
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall, resp, Call::Pass])),
Fallback::classify(reply),
);
}
}
if style == LebensohlStyle::Transfer && unbid_major(over).is_some() {
let cue = call(3, Strain::from(over));
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![
overcall,
two_nt,
Call::Pass,
three_clubs,
Call::Pass,
cue,
Call::Pass,
])),
Fallback::classify(cue_stayman_answer(over)),
);
}
if style == LebensohlStyle::Transfer && over == Suit::Diamonds {
let p = Call::Pass;
let c3 = call(3, Strain::Clubs);
let d3 = call(3, Strain::Diamonds);
let h3 = call(3, Strain::Hearts);
let s3 = call(3, Strain::Spades);
let c4 = call(4, Strain::Clubs);
let d4 = call(4, Strain::Diamonds);
let nodes: Vec<(Vec<Call>, Rules)> = vec![
(vec![overcall, c3, p], stayman_2d_answer()),
(vec![overcall, c3, p, d3, p], smolen_at_three()),
(
vec![overcall, c3, p, d3, p, h3, p],
smolen_completion(Suit::Spades),
),
(
vec![overcall, c3, p, d3, p, s3, p],
smolen_completion(Suit::Hearts),
),
(
vec![overcall, c3, p, h3, p],
stayman_2d_fit_rebid(Suit::Hearts),
),
(
vec![overcall, c3, p, s3, p],
stayman_2d_fit_rebid(Suit::Spades),
),
(
vec![overcall, d3, p],
transfer_completion(Suit::Hearts, over),
),
(
vec![overcall, h3, p],
transfer_completion(Suit::Spades, over),
),
(vec![overcall, s3, p], clubs_transfer_completion(over)),
(vec![overcall, d4, p], lm_2d_both_majors_advance()),
(vec![overcall, c4, p], lm_2d_clubs_ask()),
(vec![overcall, c4, p, d4, p], lm_2d_clubs_major()),
];
for (suffix, rules) in nodes {
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(suffix)),
Fallback::classify(rules),
);
}
}
}
}
if competition_over_stayman() {
let stayman = [call(1, Strain::Notrump), Call::Pass, call(2, Strain::Clubs)];
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(SuffixIs(vec![Call::Double])),
Fallback::classify(stayman_doubled_opener()),
);
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(described_guard(
"X (bid) …",
guard(|_: &Context<'_>, s: &[Call]| {
s.first() == Some(&Call::Double) && matches!(s.get(1), Some(Call::Bid(_)))
}),
)),
Fallback::rebase(described_rewrite(
"systems on: their X is stripped to a pass",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth) != Some(&Call::Double) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth] = Call::Pass; Some(rewritten)
}),
)),
);
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass, Call::Pass])),
Fallback::classify(stayman_redouble_reask()),
);
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(SuffixIs(vec![
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
Call::Pass,
])),
Fallback::classify(stayman_answers()),
);
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(described_guard(
"- 2♦/2♥/2♠ X …",
guard(|_: &Context<'_>, s: &[Call]| {
s.first() == Some(&Call::Pass)
&& matches!(
s.get(1),
Some(Call::Bid(b))
if b.level.get() == 2
&& matches!(
b.strain,
Strain::Diamonds | Strain::Hearts | Strain::Spades
)
)
&& s.get(2) == Some(&Call::Double)
}),
)),
Fallback::rebase(described_rewrite(
"systems on: their X is stripped to a pass",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth + 2) != Some(&Call::Double) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth + 2] = Call::Pass; Some(rewritten)
}),
)),
);
for over in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let overcall = call(2, Strain::from(over));
fallback_all_seats(
&mut book,
&stayman,
3,
Arc::new(SuffixIs(vec![overcall])),
Fallback::classify(stayman_overcalled_opener(over)),
);
}
}
if competition_over_transfer() {
for (resp, major) in [(Suit::Diamonds, Suit::Hearts), (Suit::Hearts, Suit::Spades)] {
let transfer = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::from(resp)),
];
fallback_all_seats(
&mut book,
&transfer,
3,
Arc::new(SuffixIs(vec![Call::Double])),
Fallback::classify(transfer_doubled_opener(major, resp)),
);
fallback_all_seats(
&mut book,
&transfer,
3,
Arc::new(described_guard(
"X (bid) …",
guard(|_: &Context<'_>, s: &[Call]| {
s.first() == Some(&Call::Double) && matches!(s.get(1), Some(Call::Bid(_)))
}),
)),
Fallback::rebase(described_rewrite(
"systems on: their X is stripped to a pass",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth) != Some(&Call::Double) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth] = Call::Pass; Some(rewritten)
}),
)),
);
fallback_all_seats(
&mut book,
&transfer,
3,
Arc::new(SuffixIs(vec![Call::Double, Call::Pass, Call::Pass])),
Fallback::classify(transfer_pass_reask(major)),
);
fallback_all_seats(
&mut book,
&transfer,
3,
Arc::new(SuffixIs(vec![
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
Call::Pass,
])),
Fallback::classify(complete_transfer(major)),
);
let overcalls: &[(Suit, u8)] = match resp {
Suit::Diamonds => &[(Suit::Spades, 2), (Suit::Clubs, 3), (Suit::Diamonds, 3)],
_ => &[(Suit::Clubs, 3), (Suit::Diamonds, 3)],
};
for &(over_suit, over_level) in overcalls {
let overcall = call(over_level, Strain::from(over_suit));
fallback_all_seats(
&mut book,
&transfer,
3,
Arc::new(SuffixIs(vec![overcall])),
Fallback::classify(transfer_overcalled_opener(major, over_suit, over_level)),
);
}
}
}
if competition_over_minor_transfer() && notrump_minors() == PUPPET {
let two_spade = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
];
fallback_all_seats(
&mut book,
&two_spade,
3,
Arc::new(SuffixIs(vec![Call::Double])),
Fallback::classify(minor_doubled_opener()),
);
fallback_all_seats(
&mut book,
&two_spade,
3,
Arc::new(described_guard(
"X (bid) …",
guard(|_: &Context<'_>, s: &[Call]| {
s.first() == Some(&Call::Double) && matches!(s.get(1), Some(Call::Bid(_)))
}),
)),
Fallback::rebase(described_rewrite(
"systems on: their X is stripped to a pass",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth) != Some(&Call::Double) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth] = Call::Pass; Some(rewritten)
}),
)),
);
for deny in [
[Call::Double, Call::Pass, Call::Pass],
[Call::Double, Call::Redouble, Call::Pass],
] {
fallback_all_seats(
&mut book,
&two_spade,
3,
Arc::new(SuffixIs(deny.to_vec())),
Fallback::classify(minor_no_stopper_rebid()),
);
}
let overcalls: [(Call, Rules); 5] = [
(call(2, Strain::Notrump), minor_overcalled_high()),
(call(3, Strain::Clubs), minor_overcalled_high()),
(
call(3, Strain::Diamonds),
minor_overcalled_low(Suit::Diamonds),
),
(call(3, Strain::Hearts), minor_overcalled_low(Suit::Hearts)),
(call(3, Strain::Spades), minor_overcalled_low(Suit::Spades)),
];
for (over, rules) in overcalls {
fallback_all_seats(
&mut book,
&two_spade,
3,
Arc::new(SuffixIs(vec![over])),
Fallback::classify(rules),
);
}
}
if competition_over_diamond_transfer() && notrump_minors() == PUPPET {
let two_nt = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
];
fallback_all_seats(
&mut book,
&two_nt,
3,
Arc::new(SuffixIs(vec![Call::Double])),
Fallback::classify(diamond_doubled_opener()),
);
fallback_all_seats(
&mut book,
&two_nt,
3,
Arc::new(described_guard(
"X (bid) …",
guard(|_: &Context<'_>, s: &[Call]| {
s.first() == Some(&Call::Double) && matches!(s.get(1), Some(Call::Bid(_)))
}),
)),
Fallback::rebase(described_rewrite(
"systems on: their X is stripped to a pass",
rewriter(move |auction: &[Call], depth: usize| {
if auction.get(depth) != Some(&Call::Double) {
return None;
}
let mut rewritten = auction.to_vec();
rewritten[depth] = Call::Pass; Some(rewritten)
}),
)),
);
for deny in [
[Call::Double, Call::Pass, Call::Pass],
[Call::Double, Call::Redouble, Call::Pass],
] {
fallback_all_seats(
&mut book,
&two_nt,
3,
Arc::new(SuffixIs(deny.to_vec())),
Fallback::classify(diamond_no_fit_rebid()),
);
}
let overcalls: [(Call, Rules); 4] = [
(call(3, Strain::Clubs), diamond_overcalled_low()),
(
call(3, Strain::Diamonds),
diamond_overcalled_high(Suit::Diamonds),
),
(
call(3, Strain::Hearts),
diamond_overcalled_high(Suit::Hearts),
),
(
call(3, Strain::Spades),
diamond_overcalled_high(Suit::Spades),
),
];
for (over, rules) in overcalls {
fallback_all_seats(
&mut book,
&two_nt,
3,
Arc::new(SuffixIs(vec![over])),
Fallback::classify(rules),
);
}
}
if uvu() {
let one_nt = call(1, Strain::Notrump);
let p = Call::Pass;
let overcall = call(2, Strain::Notrump);
let c3 = call(3, Strain::Clubs);
let d3 = call(3, Strain::Diamonds);
let h3 = call(3, Strain::Hearts);
let s3 = call(3, Strain::Spades);
let c4 = call(4, Strain::Clubs);
let d4 = call(4, Strain::Diamonds);
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(vec![overcall])),
Fallback::classify(uvu_responder()),
);
let nodes: Vec<(Vec<Call>, Rules)> = vec![
(vec![overcall, c3, p], stayman_2d_answer()),
(vec![overcall, c3, p, d3, p], uvu_smolen()),
(
vec![overcall, c3, p, d3, p, h3, p],
smolen_completion(Suit::Spades),
),
(
vec![overcall, c3, p, d3, p, s3, p],
smolen_completion(Suit::Hearts),
),
(vec![overcall, c3, p, h3, p], uvu_rebid_over_3h()),
(
vec![overcall, c3, p, s3, p],
stayman_2d_fit_rebid(Suit::Spades),
),
(vec![overcall, d3, p], smolen_completion(Suit::Hearts)),
(vec![overcall, c4, p], lm_2d_both_majors_advance()),
(vec![overcall, d4, p], lm_2d_both_majors_advance()),
];
for (suffix, rules) in nodes {
fallback_all_seats(
&mut book,
&[one_nt],
3,
Arc::new(SuffixIs(suffix)),
Fallback::classify(rules),
);
}
}
book
}
#[cfg(test)]
mod tests {
use crate::bidding::Family;
use crate::bidding::american::american;
use contract_bridge::auction::{Call, RelativeVulnerability};
use contract_bridge::{Bid, Hand, Strain};
const fn call(level: u8, strain: Strain) -> Call {
Call::Bid(Bid::new(level, strain))
}
fn best_call(auction: &[Call], hand: &str) -> (Call, bool) {
let hand: Hand = hand.parse().expect("valid test hand");
let (logits, prov) = american()
.against(Family::NATURAL)
.classify_with_provenance(hand, RelativeVulnerability::NONE, auction)
.expect("a legal auction classifies");
let best = (&logits.0)
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty");
(best, prov.depth == 0 && prov.fallback.is_some())
}
fn bid(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_lebensohl_style(super::LebensohlStyle::Plain);
best_call(auction, hand)
}
fn bid_transfer(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_lebensohl_style(super::LebensohlStyle::Transfer);
best_call(auction, hand)
}
fn bid_uvu(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_uvu(true);
super::set_uvu_x_floor(9);
super::set_uvu_cue_floor(8);
best_call(auction, hand)
}
fn bid_xfer(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_competition_over_transfer(true);
crate::bidding::american::set_transfer_super_accept(true);
let result = best_call(auction, hand);
super::set_competition_over_transfer(false);
crate::bidding::american::set_transfer_super_accept(false);
result
}
fn bid_minor(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_competition_over_minor_transfer(true);
let result = best_call(auction, hand);
super::set_competition_over_minor_transfer(true);
result
}
fn bid_diamond(auction: &[Call], hand: &str) -> (Call, bool) {
super::set_competition_over_diamond_transfer(true);
let result = best_call(auction, hand);
super::set_competition_over_diamond_transfer(true);
result
}
#[test]
fn minor_doubled_opener_shows_min_with_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
Call::Double,
];
let (c, floored) = bid_minor(&auction, "KJ2.A32.K432.Q32");
assert_eq!(c, call(2, Strain::Notrump));
assert!(!floored, "the coded answer must come from the book");
}
#[test]
fn minor_doubled_opener_jumps_max_with_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
Call::Double,
];
let (c, floored) = bid_minor(&auction, "KQ2.AQ2.KJ32.A32");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the coded max answer must come from the book");
}
#[test]
fn minor_doubled_opener_passes_min_no_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
Call::Double,
];
let (c, floored) = bid_minor(&auction, "432.AQ2.KQ32.K32");
assert_eq!(c, Call::Pass);
assert!(!floored, "the no-stopper pass must come from the book");
}
#[test]
fn minor_doubled_opener_redoubles_max_no_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
Call::Double,
];
let (c, _) = bid_minor(&auction, "432.AKQ.AQJ2.K32");
assert_eq!(c, Call::Redouble);
}
#[test]
fn minor_no_stopper_responder_signs_off_in_clubs() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
Call::Double,
Call::Pass,
Call::Pass,
];
let (c, floored) = bid_minor(&auction, "32.43.32.KJ98765");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the club sign-off must come from the book");
}
#[test]
fn minor_overcalled_high_bids_game_with_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
call(2, Strain::Notrump),
];
let (c, floored) = bid_minor(&auction, "KQ2.AQ2.KJ32.A32");
assert_eq!(c, call(3, Strain::Notrump));
assert!(!floored, "the coded game must come from the book");
}
#[test]
fn minor_overcalled_low_is_systems_off() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
call(3, Strain::Diamonds),
];
let (c, _) = bid_minor(&auction, "K32.K32.AQ32.A32");
assert_eq!(c, Call::Double);
}
#[test]
fn diamond_doubled_opener_completes_with_a_fit() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
Call::Double,
];
let (c, floored) = bid_diamond(&auction, "Axx.Kxx.Qxx.AKxx");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the contested completion must come from the book");
}
#[test]
fn diamond_doubled_opener_bids_natural_clubs() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
Call::Double,
];
let (c, floored) = bid_diamond(&auction, "AQx.Kxx.xx.AQxx");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the natural 3♣ must come from the book");
}
#[test]
fn diamond_doubled_opener_redoubles_max_no_fit() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
Call::Double,
];
let (c, floored) = bid_diamond(&auction, "AKxx.AQxx.Jx.Axx");
assert_eq!(c, Call::Redouble);
assert!(!floored, "the values redouble must come from the book");
}
#[test]
fn diamond_no_fit_responder_signs_off_in_diamonds() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
Call::Double,
Call::Pass,
Call::Pass,
];
let (c, floored) = bid_diamond(&auction, "xx.xx.KJxxxx.xxx");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the signoff must come from the book");
}
#[test]
fn diamond_overcalled_low_still_completes() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
call(3, Strain::Clubs),
];
let (c, floored) = bid_diamond(&auction, "Axx.Kxx.Qxx.AKxx");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the completion over 3♣ must come from the book");
}
#[test]
fn diamond_overcalled_high_three_notrump_with_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
call(3, Strain::Hearts),
];
let (c, floored) = bid_diamond(&auction, "AQx.KJx.Qx.AKxxx");
assert_eq!(c, call(3, Strain::Notrump));
assert!(!floored, "the 3NT must come from the book");
}
#[test]
fn diamond_competition_disabled_falls_to_floor() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
Call::Double,
];
super::set_competition_over_diamond_transfer(false);
let (_, floored) = best_call(&auction, "Axx.Kxx.Qxx.AKxx");
super::set_competition_over_diamond_transfer(true); assert!(floored, "with the toggle off opener falls to the floor");
}
#[test]
fn stayman_doubled_opener_bids_major_with_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
];
let (c, floored) = best_call(&auction, "A32.KQ32.A32.K32");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the coded answer must come from the book");
}
#[test]
fn stayman_doubled_opener_passes_without_stopper() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
];
let (c, floored) = best_call(&auction, "AQ2.KQ32.AQ32.32");
assert_eq!(c, Call::Pass);
assert!(!floored, "the stopper-denying pass must come from the book");
}
#[test]
fn stayman_doubled_opener_redoubles_with_clubs() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
];
let (c, _) = best_call(&auction, "A2.K32.A32.KQ876");
assert_eq!(c, Call::Redouble);
}
#[test]
fn stayman_doubled_reask_is_forcing() {
let reask = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
Call::Pass,
Call::Pass,
];
let (c, floored) = best_call(&reask, "KQ32.A32.A32.432");
assert_eq!(c, Call::Redouble);
assert!(!floored, "the re-ask must come from the book");
let answer = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
Call::Pass,
];
let (c, floored) = best_call(&answer, "AQ32.K32.KQ2.432");
assert_eq!(c, call(2, Strain::Spades));
assert!(!floored, "the forced re-answer must come from the book");
}
#[test]
fn stayman_overcalled_opener_bids_major() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
call(2, Strain::Diamonds),
];
let (c, floored) = best_call(&auction, "A32.KQ32.K32.A32");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the natural major must come from the book");
}
#[test]
fn stayman_overcalled_opener_doubles_their_suit() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Clubs),
call(2, Strain::Diamonds),
];
let (c, _) = best_call(&auction, "K32.K32.AQ32.A32");
assert_eq!(c, Call::Double);
}
#[test]
fn defense_to_their_stayman_doubles_clubs() {
crate::bidding::american::set_stayman_defense(true);
let auction = [call(1, Strain::Notrump), Call::Pass, call(2, Strain::Clubs)];
let (c, floored) = best_call(&auction, "A2.K32.A32.KQ876");
crate::bidding::american::set_stayman_defense(false); assert_eq!(c, Call::Double);
assert!(
!floored,
"the lead-directing X must come from the defense book"
);
}
#[test]
fn transfer_super_accept_uncontested() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = bid_xfer(&auction, "A2.KQ32.KQ32.K32");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "the super-accept must come from the book");
}
#[test]
fn transfer_doubled_opener_completes_with_support() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
];
let (c, floored) = bid_xfer(&auction, "KQ2.K32.KQ32.Q32");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the completion must come from the book");
}
#[test]
fn transfer_doubled_opener_super_accepts() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
];
let (c, _) = bid_xfer(&auction, "A2.KQ32.KQ32.K32");
assert_eq!(c, call(3, Strain::Hearts));
}
#[test]
fn transfer_doubled_opener_passes_with_doubleton() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
];
let (c, floored) = bid_xfer(&auction, "KQ32.K2.KQ32.Q32");
assert_eq!(c, Call::Pass);
assert!(!floored, "the declining pass must come from the book");
}
#[test]
fn transfer_doubled_opener_redoubles_with_the_transfer_suit() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
];
let (c, _) = bid_xfer(&auction, "Q43.K2.AKQ32.Q32");
assert_eq!(c, Call::Redouble);
}
#[test]
fn transfer_doubled_reask_is_forcing() {
let reask = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
Call::Pass,
];
let (c, floored) = bid_xfer(&reask, "K2.QJ432.K32.432");
assert_eq!(c, Call::Redouble);
assert!(!floored, "the re-ask must come from the book");
let answer = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
Call::Pass,
];
let (c, floored) = bid_xfer(&answer, "AQ32.K32.KQ2.432");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the forced completion must come from the book");
}
#[test]
fn transfer_overcalled_opener_super_accepts() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
call(2, Strain::Spades),
];
let (c, floored) = bid_xfer(&auction, "K2.KQ32.AQ32.K32");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "the natural super-accept must come from the book");
}
#[test]
fn opener_answers_cue_raise_instead_of_passing() {
let auction = [
call(1, Strain::Spades),
call(2, Strain::Clubs),
call(3, Strain::Clubs),
Call::Pass,
];
let (c, floored) = best_call(&auction, "QT743.KQ7.832.A9");
assert_eq!(c, call(3, Strain::Spades));
assert!(
!floored,
"opener's answer must come from the cue-raise book"
);
}
#[test]
fn michaels_cue_of_our_major_is_not_a_cue_raise() {
let auction = [
call(1, Strain::Spades),
call(2, Strain::Spades),
call(3, Strain::Spades),
Call::Pass,
];
let (c, _) = best_call(&auction, "AKQT98.Q.AQT73.Q");
assert_ne!(
c,
call(4, Strain::Notrump),
"a natural spade raise must not be answered as a cue-raise"
);
}
#[test]
fn opener_answers_minor_cue_raise() {
let auction = [
call(1, Strain::Diamonds),
call(2, Strain::Clubs),
call(3, Strain::Clubs),
Call::Pass,
];
let (c, floored) = best_call(&auction, "K43.Q43.AJ632.Q5");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the minor sign-off must come from the book");
let (c, floored) = best_call(&auction, "A54.Q43.AKJ32.K5");
assert_eq!(c, call(3, Strain::Notrump));
assert!(!floored, "the 3NT accept must come from the book");
}
#[test]
fn minor_cue_raise_decline_jumps_when_3m_is_below_the_cue() {
let auction = [
call(1, Strain::Clubs),
call(2, Strain::Diamonds),
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = best_call(&auction, "A32.K43.43.KQ432");
assert_eq!(c, call(4, Strain::Clubs));
assert!(!floored, "the 4♣ decline must come from the book");
}
#[test]
fn defense_to_their_transfer_doubles_the_bid_suit() {
crate::bidding::american::set_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
];
let (c, floored) = best_call(&auction, "K2.A32.KQ1054.432");
crate::bidding::american::set_transfer_defense(false); assert_eq!(c, Call::Double);
assert!(
!floored,
"the lead-directing X must come from the defense book"
);
}
#[test]
fn defense_to_their_transfer_cues_michaels() {
crate::bidding::american::set_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
];
let (c, floored) = best_call(&auction, "AQ1054.3.KJ1054.32");
crate::bidding::american::set_transfer_defense(false); assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the Michaels cue must come from the defense book");
}
#[test]
fn defense_to_their_minor_transfer_doubles_spades() {
crate::bidding::american::set_minor_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
];
let (c, floored) = best_call(&auction, "KQJ54.A32.432.32");
crate::bidding::american::set_minor_transfer_defense(false); assert_eq!(c, Call::Double);
assert!(
!floored,
"the lead-directing X must come from the defense book"
);
}
#[test]
fn defense_to_their_minor_transfer_cues_top_and_bottom() {
crate::bidding::american::set_minor_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
];
let (c, floored) = best_call(&auction, "KQ1054.3.KJ1054.32");
crate::bidding::american::set_minor_transfer_defense(false); assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the top-and-bottom cue must come from the book");
}
#[test]
fn defense_to_their_diamond_transfer_doubles_diamonds() {
crate::bidding::american::set_diamond_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
];
let (c, floored) = best_call(&auction, "A32.32.KQJ54.432");
crate::bidding::american::set_diamond_transfer_defense(false); assert_eq!(c, Call::Double);
assert!(
!floored,
"the lead-directing X must come from the defense book"
);
}
#[test]
fn defense_to_their_diamond_transfer_cues_both_majors() {
crate::bidding::american::set_diamond_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Notrump),
];
let (c, floored) = best_call(&auction, "KQ1054.KJ1054.3.32");
crate::bidding::american::set_diamond_transfer_defense(false); assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the both-majors cue must come from the book");
}
#[test]
fn defense_to_their_minor_transfer_two_notrump_is_reds() {
crate::bidding::american::set_minor_transfer_defense(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Spades),
];
let (c, floored) = best_call(&auction, "3.KQ1054.KJ1054.32");
crate::bidding::american::set_minor_transfer_defense(false); assert_eq!(c, call(2, Strain::Notrump));
assert!(!floored, "the red two-suiter must come from the book");
}
#[test]
fn uvu_three_clubs_is_stayman() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Notrump)];
let (c, floored) = bid_uvu(&auction, "AQ32.KJ32.A2.432");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the cue must come from the book");
}
#[test]
fn uvu_three_diamonds_shows_hearts() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Notrump)];
let (c, _) = bid_uvu(&auction, "K3.KQ976.A32.432");
assert_eq!(c, call(3, Strain::Diamonds));
}
#[test]
fn uvu_splinter_with_five_five() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Notrump)];
let (c, _) = bid_uvu(&auction, "AQ876.KJ987.32.A");
assert_eq!(c, call(4, Strain::Clubs));
}
#[test]
fn uvu_penalty_double_on_values() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Notrump)];
let (c, floored) = bid_uvu(&auction, "KJ2.AQ2.J532.532");
assert_eq!(c, Call::Double);
assert!(!floored, "the penalty X must come from the book");
}
#[test]
fn uvu_smolen_shows_the_five_card_spade() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Notrump),
call(3, Strain::Clubs),
Call::Pass,
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = bid_uvu(&auction, "AQ876.K32.A32.32");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "Smolen must come from the book");
}
#[test]
fn uvu_disabled_falls_to_floor() {
super::set_uvu(false);
let auction = [call(1, Strain::Notrump), call(2, Strain::Notrump)];
let (_, floored) = best_call(&auction, "AQ32.KJ32.A2.432");
super::set_uvu(true); assert!(floored, "without the toggle the auction is unauthored");
}
#[test]
fn uvu_encircling_doubles_the_runout() {
super::set_uvu(true);
crate::bidding::instinct::set_uvu_encircle(true);
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Notrump),
Call::Double,
call(3, Strain::Clubs),
Call::Pass,
Call::Pass,
];
let (c, _) = best_call(&auction, "K54.84.732.KQJT9");
crate::bidding::instinct::set_uvu_encircle(false); assert_eq!(c, Call::Double, "encircle the 3♣ runout with a club stack");
}
#[test]
fn transfer_smolen_three_clubs_is_stayman() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid_transfer(&auction, "AQ32.KJ32.A2.432");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "Stayman must come from the book");
}
#[test]
fn transfer_smolen_opener_answers_stayman() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
call(3, Strain::Clubs),
Call::Pass,
];
let (c, floored) = bid_transfer(&auction, "K2.AQ54.A32.Q432");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "the Stayman answer must come from the book");
}
#[test]
fn transfer_smolen_three_diamonds_is_the_heart_transfer() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid_transfer(&auction, "K3.KQ976.A32.432");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the heart transfer must come from the book");
let opener = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, _) = bid_transfer(&opener, "AQ5.A432.KQ4.J32");
assert_eq!(c, call(4, Strain::Hearts));
}
#[test]
fn transfer_smolen_routes_five_four_to_stayman_not_a_transfer() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, _) = bid_transfer(&auction, "AKJ54.Q432.K2.32");
assert_eq!(c, call(3, Strain::Clubs));
}
#[test]
fn transfer_smolen_jumps_smolen_after_the_denial() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
call(3, Strain::Clubs),
Call::Pass,
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = bid_transfer(&auction, "AKJ54.Q432.K2.32");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "Smolen must come from the book");
let mut full = auction.to_vec();
full.push(call(3, Strain::Hearts));
full.push(Call::Pass);
let (c, _) = bid_transfer(&full, "Q32.A65.AQ43.K32");
assert_eq!(c, call(4, Strain::Spades));
}
#[test]
fn transfer_smolen_leaping_michaels_both_majors() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid_transfer(&auction, "KQ954.AJ876.2.32");
assert_eq!(c, call(4, Strain::Diamonds));
assert!(!floored, "Leaping Michaels must come from the book");
let opener = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
call(4, Strain::Diamonds),
Call::Pass,
];
let (c, _) = bid_transfer(&opener, "A32.K43.AQ32.Q42");
assert_eq!(c, call(4, Strain::Spades));
}
#[test]
fn transfer_smolen_keeps_cohen_over_a_major_overcall() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Hearts)];
let (c, floored) = bid_transfer(&auction, "AKQ65.43.K32.J32");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the Cohen transfer must come from the book");
}
#[test]
fn lebensohl_forcing_three_level_is_a_book_node() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid(&auction, "KQT95.A43.32.J32");
assert_eq!(c, call(3, Strain::Spades));
assert!(!floored, "the forcing 3-level bid must come from the book");
}
#[test]
fn lebensohl_weak_long_suit_relays_then_completes() {
let responder = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid(&responder, "J2.43.32.KQ9876");
assert_eq!(c, call(2, Strain::Notrump));
assert!(!floored, "the Lebensohl relay must come from the book");
let opener = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
call(2, Strain::Notrump),
Call::Pass,
];
let (completion, _) = bid(&opener, "AQ32.KQ5.AQ4.A32");
assert_eq!(completion, call(3, Strain::Clubs));
}
#[test]
fn lebensohl_weak_bids_natural_two_level() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid(&auction, "K2.QJ976.432.432");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the natural 2-level bid must come from the book");
}
#[test]
fn lebensohl_cue_is_stayman() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Hearts)];
let (c, floored) = bid(&auction, "AQ32.K43.A32.K32");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "the cue must come from the book");
let opener = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
call(3, Strain::Hearts),
Call::Pass,
];
let (a, floored) = bid(&opener, "KJ54.A32.K43.Q32");
assert_eq!(a, call(3, Strain::Spades));
assert!(!floored, "the Stayman answer must come from the book");
}
#[test]
fn lebensohl_five_card_suit_relays_then_signs_off_at_the_three_level() {
let responder = [call(1, Strain::Notrump), call(2, Strain::Spades)];
let (c, floored) = bid(&responder, "32.KQJ32.432.432");
assert_eq!(c, call(2, Strain::Notrump));
assert!(!floored, "the relay must come from the book");
let after_3c = [
call(1, Strain::Notrump),
call(2, Strain::Spades),
call(2, Strain::Notrump),
Call::Pass,
call(3, Strain::Clubs),
Call::Pass,
];
let (c, floored) = bid(&after_3c, "32.KQJ32.432.432");
assert_eq!(c, call(3, Strain::Hearts));
assert!(!floored, "the 3-level sign-off must come from the book");
}
#[test]
fn lebensohl_maximum_raises_weak_signoff_to_game() {
let after_signoff = [
call(1, Strain::Notrump),
call(2, Strain::Spades),
call(2, Strain::Notrump),
Call::Pass,
call(3, Strain::Clubs),
Call::Pass,
call(3, Strain::Hearts),
Call::Pass,
];
let (c, floored) = bid(&after_signoff, "AK32.K43.A43.K32");
assert_eq!(c, call(4, Strain::Hearts));
assert!(!floored, "the game raise must come from the book");
let (c, _) = bid(&after_signoff, "AK32.K43.KQ3.432");
assert_eq!(c, Call::Pass, "a minimum passes the weak sign-off");
}
#[test]
fn transfer_lebensohl_shows_spades_through_their_hearts() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Hearts)];
let (c, floored) = bid_transfer(&auction, "AKQ65.43.K32.J32");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the transfer must come from the book");
}
#[test]
fn transfer_lebensohl_opener_bids_game_not_a_partscore() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, _) = bid_transfer(&auction, "AK5.KQ52.A43.432");
assert_eq!(c, call(4, Strain::Spades));
}
#[test]
fn transfer_lebensohl_cue_is_stayman() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
call(3, Strain::Hearts),
Call::Pass,
];
let (c, floored) = bid_transfer(&auction, "AQ32.K43.A32.K32");
assert_eq!(c, call(3, Strain::Spades));
assert!(!floored, "the Stayman answer must come from the book");
}
#[test]
fn transfer_lebensohl_keeps_the_penalty_double() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) =
bid_transfer_dbl(super::DoubleStyle::Penalty, &auction, "K2.K43.J932.Q432");
assert_eq!(c, Call::Double);
assert!(!floored, "the penalty double must come from the book");
}
fn bid_transfer_dbl(style: super::DoubleStyle, auction: &[Call], hand: &str) -> (Call, bool) {
super::set_lebensohl_style(super::LebensohlStyle::Transfer);
super::set_double_style(style);
let result = best_call(auction, hand);
super::set_double_style(super::DoubleStyle::default());
result
}
#[test]
fn takeout_authored_double() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) =
bid_transfer_dbl(super::DoubleStyle::Takeout, &auction, "K432.K432.32.Q43");
assert_eq!(c, Call::Double);
assert!(
!floored,
"the authored takeout double must come from the book"
);
}
#[test]
fn optional_double_two_three_cards() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) =
bid_transfer_dbl(super::DoubleStyle::Optional, &auction, "K43.K43.432.Q43");
assert_eq!(c, Call::Double);
assert!(!floored, "the optional double must come from the book");
let (c, _) = bid_transfer_dbl(super::DoubleStyle::Optional, &auction, "K432.K432.2.Q432");
assert_ne!(
c,
Call::Double,
"short-in-their-suit must not make an optional double"
);
}
#[test]
fn opener_pulls_a_takeout_double() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
];
let (c, floored) =
bid_transfer_dbl(super::DoubleStyle::Takeout, &auction, "AQ2.AQ2.A32.Q432");
assert_eq!(c, call(3, Strain::Notrump));
assert!(floored, "opener's pull comes from the instinct floor");
let (c, _) = bid_transfer_dbl(super::DoubleStyle::Takeout, &auction, "K32.A32.AKQ2.J32");
assert_eq!(c, Call::Pass, "a trump stack converts to penalty");
}
#[test]
fn transfer_lebensohl_weak_bids_natural_two_level() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Diamonds)];
let (c, floored) = bid_transfer(&auction, "K2.QJ976.432.432");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the natural 2-level bid must come from the book");
}
#[test]
fn transfer_lebensohl_top_step_is_a_clubs_transfer() {
let hand = "32.543.32.AKQJ86";
for (over, top) in [
(Strain::Diamonds, Strain::Spades),
(Strain::Hearts, Strain::Spades),
(Strain::Spades, Strain::Hearts),
] {
let auction = [call(1, Strain::Notrump), call(2, over)];
let (c, floored) = bid_transfer_dbl(super::DoubleStyle::Penalty, &auction, hand);
assert_eq!(c, call(3, top), "top step → clubs over (2{over:?})");
assert!(!floored, "the clubs transfer must come from the book");
}
}
#[test]
fn transfer_lebensohl_traps_a_too_good_stopper() {
let auction = [call(1, Strain::Notrump), call(2, Strain::Hearts)];
let (trap, _) = bid_transfer_dbl(super::DoubleStyle::Takeout, &auction, "K32.AQ86.KJ5.J32");
assert_eq!(
trap,
Call::Pass,
"a too-good stopper (6 HCP in hearts) traps"
);
let (bid, _) = bid_transfer_dbl(super::DoubleStyle::Takeout, &auction, "K32.A964.KJ5.Q32");
assert_eq!(
bid,
call(3, Strain::Notrump),
"an adequate stopper (4 HCP in hearts) still bids 3NT"
);
}
#[test]
fn transfer_lebensohl_top_step_opener_completes_at_game() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
call(3, Strain::Spades),
Call::Pass,
];
let (c, floored) = bid_transfer(&auction, "A432.KQ5.A32.432");
assert_eq!(c, call(3, Strain::Notrump), "stopper → 3NT");
assert!(!floored, "the completion must come from the book");
let (c, _) = bid_transfer(&auction, "A432.543.AKQ.432");
assert_eq!(c, call(5, Strain::Clubs), "no stopper → 5♣");
}
#[test]
fn opener_leaves_in_responder_penalty_double_when_penalty_style() {
use super::{DoubleStyle, set_double_style, set_penalty_double_leave_in};
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
Call::Double,
Call::Pass,
];
super::set_lebensohl_style(super::LebensohlStyle::Plain);
set_double_style(DoubleStyle::Penalty);
set_penalty_double_leave_in(true);
let (c_on, floored_on) = best_call(&auction, "AQ5.J42.KQ3.K842"); assert_eq!(c_on, Call::Pass, "penalty double left in");
assert!(
!floored_on,
"the leave-in must be a book node, not the floor"
);
set_penalty_double_leave_in(false);
let (c_off, floored_off) = best_call(&auction, "AQ5.J42.KQ3.K842");
assert!(
floored_off,
"off → the node is gone, opener falls to the floor"
);
assert_ne!(
c_off,
Call::Pass,
"the floor advances the double instead of sitting"
);
set_penalty_double_leave_in(true);
set_double_style(DoubleStyle::Penalty);
}
#[test]
fn opener_cooperates_with_responder_optional_double() {
use super::{DoubleStyle, set_double_style, set_penalty_double_leave_in};
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
Call::Double,
Call::Pass,
];
super::set_lebensohl_style(super::LebensohlStyle::Plain);
set_double_style(DoubleStyle::Optional);
set_penalty_double_leave_in(true);
let (fit, floored) = best_call(&auction, "AK5.Q93.KJ54.Q5");
assert_eq!(fit, Call::Pass, "a three-card fit stands");
assert!(!floored, "the cooperation must be an authored node");
let (run, _) = best_call(&auction, "A52.93.KJ5.AKQ76");
assert_eq!(
run,
call(3, Strain::Clubs),
"a doubleton runs to the five-card suit"
);
let (stuck, _) = best_call(&auction, "A52.93.KJ54.AKQ6");
assert_eq!(stuck, Call::Pass, "a doubleton with no suit stands");
set_double_style(DoubleStyle::Penalty); }
#[test]
fn uvu_major_cues_split_raise_and_fourth_suit() {
super::set_uvu_over_majors(true);
let auction = [call(1, Strain::Hearts), call(2, Strain::Notrump)];
let (raise, floored) = best_call(&auction, "K52.QJ5.A964.Q32");
assert_eq!(raise, call(3, Strain::Clubs), "the cheap cue raises");
assert!(!floored, "an authored node, not the floor");
let (fourth, _) = best_call(&auction, "AQJ54.K5.965.A43");
assert_eq!(fourth, call(3, Strain::Diamonds), "the second cue forces");
super::set_uvu_over_majors(true);
}
#[test]
fn michaels_cue_of_our_major_gets_a_structure() {
super::set_uvu_over_majors(true);
let auction = [call(1, Strain::Spades), call(2, Strain::Spades)];
let (cue, floored) = best_call(&auction, "KQ5.A54.96432.Q2");
assert_eq!(cue, call(3, Strain::Hearts), "the known-suit cue raises");
assert!(!floored, "an authored node, not the floor");
let (raise, _) = best_call(&auction, "Q542.95.9643.KQ3");
assert_eq!(raise, call(3, Strain::Spades), "the natural raise survives");
super::set_uvu_over_majors(true);
}
#[test]
fn opener_answers_the_uvu_major_cue() {
super::set_uvu_over_majors(true);
let auction = [
call(1, Strain::Hearts),
call(2, Strain::Notrump),
call(3, Strain::Clubs),
Call::Pass,
];
let (decline, floored) = best_call(&auction, "965.AQJ54.K54.32");
assert_eq!(decline, call(3, Strain::Hearts), "a minimum signs off");
assert!(!floored, "an authored node, not the floor");
let (accept, _) = best_call(&auction, "65.AKQ54.KJ54.A2");
assert_eq!(accept, call(4, Strain::Hearts), "a maximum accepts");
super::set_uvu_over_majors(true);
}
#[test]
fn opener_answers_the_uvu_fourth_suit_force() {
super::set_uvu_over_majors(true);
let auction = [
call(1, Strain::Hearts),
call(2, Strain::Notrump),
call(3, Strain::Diamonds),
Call::Pass,
];
let (game, floored) = best_call(&auction, "K65.AQJ54.K54.32");
assert_eq!(game, call(4, Strain::Spades), "raise the game force");
assert!(!floored, "an authored node, not the floor");
super::set_uvu_over_majors(true);
}
#[test]
fn weak_two_doubled_gets_business_redouble_and_systems_on() {
super::set_weak_two_competition(true);
let auction = [call(2, Strain::Spades), Call::Double];
let (xx, floored) = best_call(&auction, "A.K654.A964.KQ32");
assert_eq!(xx, Call::Redouble, "business redouble on values");
assert!(!floored, "an authored node, not the floor");
let (raise, _) = best_call(&auction, "954.Q542.964.432");
assert_eq!(raise, call(3, Strain::Spades), "the raise stays preemptive");
let ogust = [
call(2, Strain::Hearts),
Call::Double,
call(2, Strain::Notrump),
Call::Pass,
];
let (answer, _) = best_call(&ogust, "54.KQ9654.96.432");
assert_eq!(answer, call(3, Strain::Diamonds), "Ogust survives their X");
super::set_weak_two_competition(false);
}
#[test]
fn weak_two_overcalled_double_is_values_and_ogust_survives() {
super::set_weak_two_competition(true);
let auction = [call(2, Strain::Hearts), call(2, Strain::Spades)];
let (double, floored) = best_call(&auction, "KJ54.Q5.A964.Q32");
assert_eq!(double, Call::Double, "values double");
assert!(!floored, "an authored node, not the floor");
let (ask, _) = best_call(&auction, "AK54.Q5.A964.K32");
assert_eq!(ask, call(2, Strain::Notrump), "Ogust survives the overcall");
let answered = [
call(2, Strain::Hearts),
call(2, Strain::Spades),
call(2, Strain::Notrump),
Call::Pass,
];
let (answer, _) = best_call(&answered, "54.KQ9654.96.432");
assert_eq!(answer, call(3, Strain::Diamonds), "min points, good suit");
super::set_weak_two_competition(false);
}
#[test]
fn strong_two_contested_stays_strong() {
super::set_strong_two_competition(true);
let doubled = [call(2, Strain::Clubs), Call::Double];
let (negative, floored) = best_call(&doubled, "9542.Q54.964.432");
assert_eq!(negative, call(2, Strain::Hearts), "systems on over their X");
assert!(!floored, "the rebase resolves to the authored tree");
let overcalled = [call(2, Strain::Clubs), call(2, Strain::Spades)];
let (positive, _) = best_call(&overcalled, "54.AQ542.964.Q32");
assert_eq!(positive, call(3, Strain::Hearts), "natural positive");
let (waiting, _) = best_call(&overcalled, "954.Q542.964.432");
assert_eq!(waiting, Call::Pass, "the waiting pass");
let reopen = [
call(2, Strain::Clubs),
call(2, Strain::Spades),
Call::Pass,
Call::Pass,
];
let (rebid, _) = best_call(&reopen, "AQ2.AKQ5.KQ54.A2");
assert_eq!(rebid, call(2, Strain::Notrump), "opener never sells out");
super::set_strong_two_competition(true);
}
#[test]
fn major_support_double_shows_three_spades() {
super::set_major_support_double(true);
let auction = [
call(1, Strain::Hearts),
Call::Pass,
call(1, Strain::Spades),
call(2, Strain::Clubs),
];
let (support, floored) = best_call(&auction, "K32.AQ542.A95.32");
assert_eq!(support, Call::Double, "exactly three = support double");
assert!(!floored, "an authored node, not the floor");
super::set_major_support_double(true);
}
#[test]
fn modern_negative_double_is_exactly_four_over_one_heart() {
super::set_negative_double_shape(super::NegativeDoubleShape::Modern);
let auction = [call(1, Strain::Diamonds), call(1, Strain::Hearts)];
let (free, floored) = best_call(&auction, "AQ542.95.964.Q32");
assert_eq!(free, call(1, Strain::Spades), "five spades bid the suit");
assert!(!floored, "an authored node, not the floor");
let (neg, _) = best_call(&auction, "AQ54.95.9642.Q32");
assert_eq!(neg, Call::Double, "exactly four doubles");
super::set_negative_double_shape(super::NegativeDoubleShape::BothMajors);
}
#[test]
fn free_bids_fill_the_natural_gaps() {
super::set_free_bids(true);
let auction = [call(1, Strain::Spades), call(2, Strain::Diamonds)];
let (two_hearts, floored) = best_call(&auction, "K5.AQ542.964.Q32");
assert_eq!(two_hearts, call(2, Strain::Hearts), "the 2-level free bid");
assert!(!floored, "an authored node, not the floor");
let one_nt_auction = [call(1, Strain::Hearts), call(1, Strain::Spades)];
let (one_nt, _) = best_call(&one_nt_auction, "K52.95.KJ64.QJ32");
assert_eq!(one_nt, call(1, Strain::Notrump), "the natural 1NT");
super::set_free_bids(false);
}
#[test]
fn free_bid_floor_gates_the_marginal_hand() {
super::set_free_bids(true);
let auction = [call(1, Strain::Clubs), call(1, Strain::Diamonds)];
let hand = "T32.KJ542.94.Q32";
let (bid_at_6, _) = best_call(&auction, hand);
assert_eq!(
bid_at_6,
call(1, Strain::Hearts),
"the 1♥ free bid at floor 6"
);
super::set_free_bid_floor(8);
let (bid_at_8, _) = best_call(&auction, hand);
assert_ne!(
bid_at_8,
call(1, Strain::Hearts),
"floor 8 rejects the 6-count"
);
super::set_free_bid_floor(6);
super::set_free_bids(false);
}
#[test]
fn cachalot_rotates_the_one_level() {
super::set_negative_double_shape(super::NegativeDoubleShape::Cachalot);
let auction = [call(1, Strain::Clubs), call(1, Strain::Diamonds)];
let (x, floored) = best_call(&auction, "K52.QJ54.964.Q32");
assert_eq!(x, Call::Double, "X shows the adjacent major");
assert!(!floored, "an authored node, not the floor");
let (transfer, _) = best_call(&auction, "QJ54.K52.964.Q32");
assert_eq!(transfer, call(1, Strain::Hearts), "1♥ shows spades");
let (takeout, _) = best_call(&auction, "K52.Q54.964.QJ32");
assert_eq!(takeout, call(1, Strain::Spades), "1♠ is the takeout hand");
let complete = [
call(1, Strain::Clubs),
call(1, Strain::Diamonds),
Call::Double,
Call::Pass,
];
let (three, _) = best_call(&complete, "AQ54.K52.96.QJ32");
assert_eq!(three, call(1, Strain::Hearts), "exactly three completes");
let (four, _) = best_call(&complete, "AQ5.K542.96.QJ32");
assert_eq!(four, call(2, Strain::Hearts), "four raises");
super::set_negative_double_shape(super::NegativeDoubleShape::BothMajors);
}
#[test]
fn sputnik_negative_double_is_the_residual() {
super::set_negative_double_shape(super::NegativeDoubleShape::Sputnik);
let auction = [call(1, Strain::Clubs), call(1, Strain::Diamonds)];
let (spades, floored) = best_call(&auction, "KJ54.952.964.Q32");
assert_eq!(spades, call(1, Strain::Spades), "four spades bid the suit");
assert!(!floored, "an authored node, not the floor");
let (neg, _) = best_call(&auction, "K52.Q54.J964.Q32");
assert_eq!(
neg,
Call::Double,
"≤3 in both majors is the residual double"
);
super::set_negative_double_shape(super::NegativeDoubleShape::BothMajors);
}
#[test]
fn high_overcalls_get_a_structure() {
super::set_high_overcall_responses(true);
let auction = [call(1, Strain::Spades), call(3, Strain::Diamonds)];
let (neg, floored) = best_call(&auction, "K5.KQ54.965.A432");
assert_eq!(neg, Call::Double, "the 3-level negative double");
assert!(!floored, "an authored node, not the floor");
let (game, _) = best_call(&auction, "K5.KQ54.A65.A432");
assert_eq!(game, call(3, Strain::Notrump), "3NT with a stopper");
let answer = [
call(1, Strain::Spades),
call(3, Strain::Diamonds),
Call::Double,
Call::Pass,
];
let (major, _) = best_call(&answer, "AQ542.KJ54.96.32");
assert_eq!(major, call(3, Strain::Hearts), "four hearts answer 3♥");
super::set_high_overcall_responses(false);
}
#[test]
fn jordan_truscott_over_their_double() {
super::set_jordan_truscott(true);
let auction = [call(1, Strain::Spades), Call::Double];
let (jordan, floored) = best_call(&auction, "Q542.A5.K964.Q32");
assert_eq!(jordan, call(2, Strain::Notrump), "Jordan/Truscott");
assert!(!floored, "an authored node, not the floor");
let (xx, _) = best_call(&auction, "K2.A54.K964.Q532");
assert_eq!(xx, Call::Redouble, "the value redouble");
let (preempt, _) = best_call(&auction, "Q542.9.96432.Q32");
assert_eq!(preempt, call(3, Strain::Spades), "preemptive jump raise");
let weak = [
call(1, Strain::Spades),
Call::Double,
call(2, Strain::Clubs),
Call::Pass,
];
let (pass, weak_floored) = best_call(&weak, "AQ542.K54.96.432");
assert_eq!(pass, Call::Pass, "the weak new suit is dropped");
assert!(!weak_floored, "an authored node, not the floor");
let answer = [
call(1, Strain::Spades),
Call::Double,
call(2, Strain::Notrump),
Call::Pass,
];
let (accept, _) = best_call(&answer, "AKQ54.K54.96.A32");
assert_eq!(accept, call(4, Strain::Spades), "a maximum accepts");
let (decline, _) = best_call(&answer, "AQ542.954.96.A32");
assert_eq!(decline, call(3, Strain::Spades), "a minimum declines");
super::set_jordan_truscott(true);
}
#[test]
fn competitive_fallbacks_are_renderable() {
use crate::bidding::fallback::Fallback;
let book = super::competition();
let all = book.0.fallbacks();
assert!(
all.len() > 30,
"the competitive book has {} guarded entries — the walk is broken",
all.len()
);
for (auction, guard, fallback) in &all {
let key = contract_bridge::auction::display_calls(auction).to_string();
assert!(
guard.describe().is_some(),
"unlabeled guard at [{key}] — wrap it in described_guard"
);
if let Fallback::Rebase(rewrite) = fallback {
assert!(
rewrite.describe().is_some(),
"opaque rebase at [{key}] — wrap it in described_rewrite"
);
}
}
let (_, guard, fallback) = all
.iter()
.find(|(auction, ..)| auction.as_ref() == [Call::Bid(Bid::new(1, Strain::Spades))])
.expect("a guarded entry at [1♠]");
assert_eq!(guard.describe().as_deref(), Some("(overcall ≤2♠)"));
let Fallback::Classify(classifier) = fallback else {
panic!("the direct-seat package is a classifier");
};
let rules = classifier.as_rules().expect("an authored Rules table");
assert!(
rules.rules().iter().any(|rule| rule.call() == Call::Double),
"the negative double renders"
);
}
}