use super::super::constraint::{
Cons, Constraint, and, balanced, described, hcp, len, min_level_is, or, passed_hand, points,
short_in_their_suits, stopper_in_their_suits, suit_hcp, takeout_double_shape_ok, top_honors,
unbid_support,
};
use super::super::context::Context;
use super::super::{Alert, Defensive, Rules, Trie};
use super::competition::{
LebensohlStyle, clubs_transfer_completion, complete_lebensohl_relay, cue_stayman_answer,
cue_stayman_answer_no_stopper, delayed_cue, lebensohl_relay_rebid, lebensohl_responder,
lm_2d_both_majors_advance, lm_2d_clubs_ask, lm_2d_clubs_major, stayman_2d_answer,
stayman_2d_fit_rebid, transfer_completion, transfer_lebensohl_responder,
transfer_stayman_2d_responder, transfer_target,
};
use super::notrump::{smolen_at_three, smolen_completion};
use super::{call, insert_all_seats};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Hand, Strain, Suit};
use std::cell::Cell;
thread_local! {
static ADVANCE_SOHL: Cell<LebensohlStyle> = const { Cell::new(LebensohlStyle::Transfer) };
}
pub fn set_advance_sohl_style(style: LebensohlStyle) {
ADVANCE_SOHL.with(|cell| cell.set(style));
}
fn advance_sohl_style() -> LebensohlStyle {
ADVANCE_SOHL.with(Cell::get)
}
thread_local! {
static LEAPING_MICHAELS: Cell<bool> = const { Cell::new(true) };
}
pub fn set_leaping_michaels(on: bool) {
LEAPING_MICHAELS.with(|cell| cell.set(on));
}
pub(crate) fn leaping_michaels_enabled() -> bool {
LEAPING_MICHAELS.with(Cell::get)
}
thread_local! {
static LANDY: Cell<Option<(u8, u8)>> = const { Cell::new(None) };
}
pub fn set_landy(range: Option<(u8, u8)>) {
LANDY.with(|cell| cell.set(range));
if let Some((lo, hi)) = range {
set_woolsey_points(lo, hi);
}
}
pub(crate) fn landy_range() -> Option<(u8, u8)> {
LANDY.with(Cell::get)
}
thread_local! {
static DOUBLED_LANDY_ESCAPE: Cell<(usize, usize)> = const { Cell::new((6, 2)) };
}
pub fn set_doubled_landy_escape(gate: (usize, usize)) {
DOUBLED_LANDY_ESCAPE.with(|cell| cell.set(gate));
}
fn doubled_landy_escape() -> (usize, usize) {
DOUBLED_LANDY_ESCAPE.with(Cell::get)
}
thread_local! {
static UNUSUAL_NT: Cell<Option<(u8, u8)>> = const { Cell::new(Some((8, 13))) };
}
pub fn set_unusual_notrump_defense(range: Option<(u8, u8)>) {
UNUSUAL_NT.with(|cell| cell.set(range));
}
pub(crate) fn unusual_notrump_range() -> Option<(u8, u8)> {
UNUSUAL_NT.with(Cell::get)
}
thread_local! {
static LANDY_HCP: Cell<bool> = const { Cell::new(false) };
}
pub fn set_landy_hcp(on: bool) {
LANDY_HCP.with(|cell| cell.set(on));
}
fn landy_use_hcp() -> bool {
LANDY_HCP.with(Cell::get)
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum NotrumpDefense {
#[default]
Natural,
DirectDont,
Meckwell,
Woolsey,
DirectLandy,
AlwaysPass,
Off,
}
thread_local! {
static NOTRUMP_DEFENSE: Cell<NotrumpDefense> = const { Cell::new(NotrumpDefense::Natural) };
static DIRECT_LANDY_FOUR_FOUR: Cell<bool> = const { Cell::new(false) };
}
pub fn set_notrump_defense(system: NotrumpDefense) {
NOTRUMP_DEFENSE.with(|cell| cell.set(system));
}
pub(crate) fn notrump_defense() -> NotrumpDefense {
NOTRUMP_DEFENSE.with(Cell::get)
}
thread_local! {
static NOTRUMP_BALANCING: Cell<bool> = const { Cell::new(false) };
}
pub fn set_natural_defense(on: bool) {
if on {
set_notrump_defense(NotrumpDefense::Natural);
} else if notrump_defense() == NotrumpDefense::Natural {
set_notrump_defense(NotrumpDefense::Off);
}
}
pub(crate) fn natural_defense_enabled() -> bool {
notrump_defense() == NotrumpDefense::Natural
}
pub fn set_notrump_balancing(on: bool) {
NOTRUMP_BALANCING.with(|cell| cell.set(on));
}
fn notrump_balancing_enabled() -> bool {
NOTRUMP_BALANCING.with(Cell::get)
}
pub fn set_direct_dont(on: bool) {
if on {
set_notrump_defense(NotrumpDefense::DirectDont);
} else if notrump_defense() == NotrumpDefense::DirectDont {
set_notrump_defense(NotrumpDefense::Natural);
}
}
pub(crate) fn direct_dont_enabled() -> bool {
notrump_defense() == NotrumpDefense::DirectDont
}
thread_local! {
static MECKWELL_MINOR_MAJOR_44: Cell<bool> = const { Cell::new(false) };
static MECKWELL_X_FOUR_FOUR: Cell<bool> = const { Cell::new(true) };
static MECKWELL_X_FLOOR: Cell<u8> = const { Cell::new(0) };
}
pub fn set_meckwell(on: bool) {
if on {
set_notrump_defense(NotrumpDefense::Meckwell);
} else if notrump_defense() == NotrumpDefense::Meckwell {
set_notrump_defense(NotrumpDefense::Natural);
}
}
pub(crate) fn meckwell_enabled() -> bool {
notrump_defense() == NotrumpDefense::Meckwell
}
pub fn set_meckwell_minor_major_44(on: bool) {
MECKWELL_MINOR_MAJOR_44.with(|cell| cell.set(on));
}
fn meckwell_minor_major_44() -> bool {
MECKWELL_MINOR_MAJOR_44.with(Cell::get)
}
pub fn set_meckwell_x_four_four(on: bool) {
MECKWELL_X_FOUR_FOUR.with(|cell| cell.set(on));
}
fn meckwell_x_four_four() -> bool {
MECKWELL_X_FOUR_FOUR.with(Cell::get)
}
pub fn set_meckwell_x_floor(floor: u8) {
MECKWELL_X_FLOOR.with(|cell| cell.set(floor));
}
fn meckwell_x_floor() -> u8 {
match MECKWELL_X_FLOOR.with(Cell::get) {
0 => natural_overcall_points().0,
floor => floor,
}
}
thread_local! {
static STAYMAN_DEFENSE: Cell<bool> = const { Cell::new(false) };
static STAYMAN_DEF_OVERCALL: Cell<(usize, u8)> = const { Cell::new((6, 14)) };
}
pub fn set_stayman_defense(on: bool) {
STAYMAN_DEFENSE.with(|cell| cell.set(on));
}
pub fn set_stayman_defense_overcall(min_len: usize, points_floor: u8) {
STAYMAN_DEF_OVERCALL.with(|cell| cell.set((min_len, points_floor)));
}
fn stayman_defense_overcall() -> (usize, u8) {
STAYMAN_DEF_OVERCALL.with(Cell::get)
}
fn stayman_defense_enabled() -> bool {
STAYMAN_DEFENSE.with(Cell::get)
}
thread_local! {
static TRANSFER_DEFENSE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_transfer_defense(on: bool) {
TRANSFER_DEFENSE.with(|cell| cell.set(on));
}
fn transfer_defense_enabled() -> bool {
TRANSFER_DEFENSE.with(Cell::get)
}
thread_local! {
static MINOR_TRANSFER_DEFENSE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_minor_transfer_defense(on: bool) {
MINOR_TRANSFER_DEFENSE.with(|cell| cell.set(on));
}
fn minor_transfer_defense_enabled() -> bool {
MINOR_TRANSFER_DEFENSE.with(Cell::get)
}
thread_local! {
static DIAMOND_TRANSFER_DEFENSE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_diamond_transfer_defense(on: bool) {
DIAMOND_TRANSFER_DEFENSE.with(|cell| cell.set(on));
}
fn diamond_transfer_defense_enabled() -> bool {
DIAMOND_TRANSFER_DEFENSE.with(Cell::get)
}
thread_local! {
static DIRECT_DONT_ONE_SUITER_MIN: Cell<u8> = const { Cell::new(5) };
static DIRECT_DONT_FOUR_FOUR: Cell<bool> = const { Cell::new(true) };
static DIRECT_DONT_X_FLOOR: Cell<u8> = const { Cell::new(0) };
}
pub fn set_direct_dont_one_suiter_min(min: u8) {
DIRECT_DONT_ONE_SUITER_MIN.with(|cell| cell.set(min));
}
fn direct_dont_one_suiter_min() -> usize {
DIRECT_DONT_ONE_SUITER_MIN.with(Cell::get) as usize
}
pub fn set_direct_dont_four_four(on: bool) {
DIRECT_DONT_FOUR_FOUR.with(|cell| cell.set(on));
}
pub fn set_direct_dont_x_floor(floor: u8) {
DIRECT_DONT_X_FLOOR.with(|cell| cell.set(floor));
}
fn direct_dont_x_floor() -> u8 {
match DIRECT_DONT_X_FLOOR.with(Cell::get) {
0 => natural_overcall_points().0,
floor => floor,
}
}
fn direct_dont_four_four() -> bool {
DIRECT_DONT_FOUR_FOUR.with(Cell::get)
}
thread_local! {
static DIRECT_LANDY_DOUBLE_FLOOR: Cell<u8> = const { Cell::new(15) };
static DIRECT_LANDY_PENALTY_PASS: Cell<bool> = const { Cell::new(false) };
}
pub fn set_direct_landy_double(shape: Option<bool>) {
match shape {
Some(four_four) => {
set_notrump_defense(NotrumpDefense::DirectLandy);
DIRECT_LANDY_FOUR_FOUR.with(|cell| cell.set(four_four));
}
None if notrump_defense() == NotrumpDefense::DirectLandy => {
set_notrump_defense(NotrumpDefense::Natural);
}
None => {}
}
}
pub(crate) fn direct_landy_double() -> Option<bool> {
(notrump_defense() == NotrumpDefense::DirectLandy)
.then(|| DIRECT_LANDY_FOUR_FOUR.with(Cell::get))
}
pub fn set_direct_landy_double_floor(floor: u8) {
DIRECT_LANDY_DOUBLE_FLOOR.with(|cell| cell.set(floor));
}
fn direct_landy_double_floor() -> u8 {
DIRECT_LANDY_DOUBLE_FLOOR.with(Cell::get)
}
pub fn set_direct_landy_penalty_pass(on: bool) {
DIRECT_LANDY_PENALTY_PASS.with(|cell| cell.set(on));
}
fn direct_landy_penalty_pass() -> bool {
DIRECT_LANDY_PENALTY_PASS.with(Cell::get)
}
thread_local! {
static WOOLSEY_POINTS: Cell<(u8, u8)> = const { Cell::new((8, 19)) };
static WOOLSEY_DOUBLE_FLOOR: Cell<u8> = const { Cell::new(12) };
}
pub fn set_woolsey(on: bool) {
if on {
set_notrump_defense(NotrumpDefense::Woolsey);
} else if notrump_defense() == NotrumpDefense::Woolsey {
set_notrump_defense(NotrumpDefense::Natural);
}
}
pub(crate) fn woolsey_enabled() -> bool {
notrump_defense() == NotrumpDefense::Woolsey
}
pub fn set_woolsey_points(lo: u8, hi: u8) {
WOOLSEY_POINTS.with(|cell| cell.set((lo, hi)));
}
pub(crate) fn woolsey_points() -> (u8, u8) {
WOOLSEY_POINTS.with(Cell::get)
}
pub fn set_woolsey_double_floor(floor: u8) {
WOOLSEY_DOUBLE_FLOOR.with(|cell| cell.set(floor));
}
pub(crate) fn woolsey_double_floor() -> u8 {
WOOLSEY_DOUBLE_FLOOR.with(Cell::get)
}
fn woolsey_multi() -> Cons<impl Constraint + Clone> {
or([Suit::Hearts, Suit::Spades], 6..) & and([Suit::Clubs, Suit::Diamonds], ..=4)
}
fn woolsey_muiderberg(major: Suit) -> Cons<impl Constraint + Clone> {
let other = if major == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
len(major, 5..=5) & len(other, ..=3) & or([Suit::Clubs, Suit::Diamonds], 4..)
}
fn woolsey_double_shape() -> Cons<impl Constraint + Clone> {
((len(Suit::Hearts, 4..=4) & len(Suit::Spades, ..=3))
| (len(Suit::Spades, 4..=4) & len(Suit::Hearts, ..=3)))
& or([Suit::Clubs, Suit::Diamonds], 5..=6)
}
fn both_majors_x_advance(lo: u8) -> Rules {
let base = landy_advances(lo);
if direct_landy_penalty_pass() {
let penalty = 22u8.saturating_sub(lo);
base.rule(
Call::Pass,
1.25,
len(Suit::Hearts, ..=2) & len(Suit::Spades, ..=2) & points(penalty..),
)
} else {
base
}
}
fn both_majors_shape(four_four: bool) -> Cons<impl Constraint + Clone> {
let longer = if four_four { 4 } else { 5 };
and([Suit::Hearts, Suit::Spades], 4..) & or([Suit::Hearts, Suit::Spades], longer..)
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum DoubleShape {
#[default]
Balanced,
SemiBalanced,
Any,
}
thread_local! {
static NATURAL_DOUBLE_SHAPE: Cell<DoubleShape> = const { Cell::new(DoubleShape::Balanced) };
static NATURAL_DOUBLE_FLOOR: Cell<u8> = const { Cell::new(15) };
static NATURAL_DOUBLE_WEIGHT: Cell<f32> = const { Cell::new(1.3) };
static NATURAL_OVERCALL_POINTS: Cell<(u8, u8)> = const { Cell::new((8, 14)) };
}
pub fn set_natural_double_shape(shape: DoubleShape) {
NATURAL_DOUBLE_SHAPE.with(|cell| cell.set(shape));
}
fn natural_double_shape() -> DoubleShape {
NATURAL_DOUBLE_SHAPE.with(Cell::get)
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum TakeoutSupport {
Off,
Lenient,
#[default]
Strict,
}
thread_local! {
static TAKEOUT_SUPPORT: Cell<TakeoutSupport> = const { Cell::new(TakeoutSupport::Strict) };
static OVERCALL_DISCIPLINE: Cell<bool> = const { Cell::new(true) };
static PASSED_HAND_OVERCALL: Cell<bool> = const { Cell::new(false) };
static TWO_LEVEL_MINOR_OVERCALL_TIGHT: Cell<bool> = const { Cell::new(false) };
static NT_OVERCALL_NO_MAJOR: Cell<bool> = const { Cell::new(false) };
static NT_OVERCALL_SYSTEMS_ON: Cell<bool> = const { Cell::new(true) };
static NT_OVERCALL_GLADIATOR: Cell<bool> = const { Cell::new(false) };
}
pub fn set_takeout_support(gate: TakeoutSupport) {
TAKEOUT_SUPPORT.with(|cell| cell.set(gate));
}
fn takeout_support() -> TakeoutSupport {
TAKEOUT_SUPPORT.with(Cell::get)
}
pub fn set_overcall_discipline(on: bool) {
OVERCALL_DISCIPLINE.with(|cell| cell.set(on));
}
pub fn set_passed_hand_overcall(on: bool) {
PASSED_HAND_OVERCALL.with(|cell| cell.set(on));
}
fn passed_hand_overcall() -> bool {
PASSED_HAND_OVERCALL.with(Cell::get)
}
pub fn set_two_level_minor_overcall_tight(on: bool) {
TWO_LEVEL_MINOR_OVERCALL_TIGHT.with(|cell| cell.set(on));
}
fn two_level_minor_overcall_tight() -> bool {
TWO_LEVEL_MINOR_OVERCALL_TIGHT.with(Cell::get)
}
pub fn set_nt_overcall_no_major(on: bool) {
NT_OVERCALL_NO_MAJOR.with(|cell| cell.set(on));
}
fn nt_overcall_no_major() -> bool {
NT_OVERCALL_NO_MAJOR.with(Cell::get)
}
pub fn set_nt_overcall_systems_on(on: bool) {
NT_OVERCALL_SYSTEMS_ON.with(|cell| cell.set(on));
}
pub(crate) fn nt_overcall_systems_on() -> bool {
NT_OVERCALL_SYSTEMS_ON.with(Cell::get)
}
pub fn set_nt_overcall_gladiator(on: bool) {
NT_OVERCALL_GLADIATOR.with(|cell| cell.set(on));
}
pub(crate) fn nt_overcall_gladiator() -> bool {
NT_OVERCALL_GLADIATOR.with(Cell::get)
}
fn overcall_discipline() -> bool {
OVERCALL_DISCIPLINE.with(Cell::get)
}
pub fn set_natural_double_floor(floor: u8) {
NATURAL_DOUBLE_FLOOR.with(|cell| cell.set(floor));
}
pub(crate) fn natural_double_floor() -> u8 {
NATURAL_DOUBLE_FLOOR.with(Cell::get)
}
pub fn set_natural_double_weight(weight: f32) {
NATURAL_DOUBLE_WEIGHT.with(|cell| cell.set(weight));
}
fn natural_double_weight() -> f32 {
NATURAL_DOUBLE_WEIGHT.with(Cell::get)
}
pub fn set_natural_overcall_points(lo: u8, hi: u8) {
NATURAL_OVERCALL_POINTS.with(|cell| cell.set((lo, hi)));
}
pub(crate) fn natural_overcall_points() -> (u8, u8) {
NATURAL_OVERCALL_POINTS.with(Cell::get)
}
fn semi_balanced() -> Cons<impl Constraint + Clone> {
balanced()
| described("5422/6322/7222", |hand: Hand, _: &Context<'_>| {
let mut lengths = Suit::ASC.map(|suit| hand[suit].len());
lengths.sort_unstable();
matches!(lengths, [2, 2, 4, 5] | [2, 2, 3, 6] | [2, 2, 2, 7])
})
}
pub fn set_always_pass_defense(on: bool) {
if on {
set_notrump_defense(NotrumpDefense::AlwaysPass);
} else if notrump_defense() == NotrumpDefense::AlwaysPass {
set_notrump_defense(NotrumpDefense::Natural);
}
}
thread_local! {
static RESPONSIVE_TAKEOUT: Cell<bool> = const { Cell::new(true) };
static RESPONSIVE_OVERCALL: Cell<bool> = const { Cell::new(false) };
static RICH_ADVANCE_DOUBLE: Cell<bool> = const { Cell::new(false) };
static ADVANCE_RUBENS: Cell<bool> = const { Cell::new(false) };
}
pub fn set_responsive_takeout(on: bool) {
RESPONSIVE_TAKEOUT.with(|cell| cell.set(on));
}
fn responsive_takeout_enabled() -> bool {
RESPONSIVE_TAKEOUT.with(Cell::get)
}
pub fn set_responsive_overcall(on: bool) {
RESPONSIVE_OVERCALL.with(|cell| cell.set(on));
}
pub fn set_rich_advance_double(on: bool) {
RICH_ADVANCE_DOUBLE.with(|cell| cell.set(on));
}
fn rich_advance_double_enabled() -> bool {
RICH_ADVANCE_DOUBLE.with(Cell::get)
}
pub fn set_advance_rubens(on: bool) {
ADVANCE_RUBENS.with(|cell| cell.set(on));
}
fn advance_rubens_enabled() -> bool {
ADVANCE_RUBENS.with(Cell::get)
}
fn responsive_overcall_enabled() -> bool {
RESPONSIVE_OVERCALL.with(Cell::get)
}
#[must_use]
pub fn defense_to_suit(their_opening: Bid) -> Rules {
let theirs = their_opening.strain;
let t = theirs.suit().expect("their opening is always a suit bid");
let one_nt = Bid::new(1, Strain::Notrump);
let nt_base = hcp(15..=18) & balanced() & stopper_in_their_suits();
let mut rules = if nt_overcall_no_major() {
Rules::new().rule(
one_nt,
1.5,
nt_base & len(Suit::Hearts, ..=4) & len(Suit::Spades, ..=4),
)
} else {
Rules::new().rule(one_nt, 1.5, nt_base)
};
rules = match takeout_support() {
TakeoutSupport::Off => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & takeout_double_shape_ok(),
),
TakeoutSupport::Lenient => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & unbid_support(1) & takeout_double_shape_ok(),
),
TakeoutSupport::Strict => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & unbid_support(0) & takeout_double_shape_ok(),
),
};
rules = rules
.rule(Call::Double, 1.2, points(17..))
.rule(Call::Pass, 0.0, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
if strain != theirs {
let level = if strain > theirs { 1 } else { 2 };
let weight = if level == 1 { 1.4 } else { 1.0 };
let tight_minor = level == 2
&& matches!(suit, Suit::Clubs | Suit::Diamonds)
&& two_level_minor_overcall_tight();
let relax_passed =
overcall_discipline() && level == 2 && passed_hand_overcall() && !tight_minor;
let band = if !overcall_discipline() {
8..=16
} else if level == 1 {
8..=17
} else if tight_minor {
15..=17
} else if relax_passed {
9..=17
} else {
11..=17
};
rules = if relax_passed {
rules.rule(
Bid::new(level, strain),
weight,
len(suit, 5..) & points(band) & (points(11..) | passed_hand()),
)
} else {
rules.rule(
Bid::new(level, strain),
weight,
len(suit, 5..) & points(band),
)
};
}
}
rules = match t {
Suit::Clubs | Suit::Diamonds => rules
.rule(
Bid::new(2, theirs),
2.0,
len(Suit::Hearts, 5..) & len(Suit::Spades, 5..) & points(8..),
)
.alert(MICHAELS),
Suit::Hearts => rules
.rule(
Bid::new(2, theirs),
2.0,
len(Suit::Spades, 5..)
& (len(Suit::Clubs, 5..) | len(Suit::Diamonds, 5..))
& points(8..),
)
.alert(MICHAELS),
Suit::Spades => rules
.rule(
Bid::new(2, theirs),
2.0,
len(Suit::Hearts, 5..)
& (len(Suit::Clubs, 5..) | len(Suit::Diamonds, 5..))
& points(8..),
)
.alert(MICHAELS),
};
match t {
Suit::Clubs => rules
.rule(
Bid::new(2, Strain::Notrump),
1.9,
len(Suit::Diamonds, 5..) & len(Suit::Hearts, 5..) & points(8..),
)
.alert(UNUSUAL),
Suit::Diamonds => rules
.rule(
Bid::new(2, Strain::Notrump),
1.9,
len(Suit::Clubs, 5..) & len(Suit::Hearts, 5..) & points(8..),
)
.alert(UNUSUAL),
Suit::Hearts | Suit::Spades => rules
.rule(
Bid::new(2, Strain::Notrump),
1.9,
len(Suit::Clubs, 5..) & len(Suit::Diamonds, 5..) & points(8..),
)
.alert(UNUSUAL),
}
}
#[must_use]
pub fn defense_to_weak_two(their_opening: Bid) -> Rules {
let theirs = their_opening.strain;
let level = their_opening.level.get();
let mut rules = Rules::new().rule(
Bid::new(2, Strain::Notrump),
1.5,
hcp(15..=18) & balanced() & stopper_in_their_suits(),
);
rules = match takeout_support() {
TakeoutSupport::Off => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & takeout_double_shape_ok(),
),
TakeoutSupport::Lenient => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & unbid_support(1) & takeout_double_shape_ok(),
),
TakeoutSupport::Strict => rules.rule(
Call::Double,
1.3,
hcp(12..) & short_in_their_suits() & unbid_support(0) & takeout_double_shape_ok(),
),
};
rules = rules
.rule(Call::Double, 1.2, points(17..))
.rule(Call::Pass, 0.0, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
if strain != theirs {
let overcall_level = if strain > theirs { level } else { level + 1 };
rules = rules.rule(
Bid::new(overcall_level, strain),
1.0,
len(suit, 5..) & points(10..=16),
);
}
}
if leaping_michaels_enabled() {
let t = theirs.suit().expect("weak two is a suit bid");
let gf = points(14..);
match t {
Suit::Hearts | Suit::Spades => {
let other = if t == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
for minor in [Suit::Clubs, Suit::Diamonds] {
rules = rules
.rule(
Bid::new(4, Strain::from(minor)),
2.0,
len(minor, 5..) & len(other, 5..) & gf.clone(),
)
.alert(LEAPING);
}
}
Suit::Diamonds => {
rules = rules
.rule(
Bid::new(4, Strain::Clubs),
2.0,
len(Suit::Clubs, 5..)
& (len(Suit::Hearts, 5..) | len(Suit::Spades, 5..))
& gf.clone(),
)
.alert(LEAPING)
.rule(
Bid::new(4, Strain::Diamonds),
2.0,
len(Suit::Hearts, 5..) & len(Suit::Spades, 5..) & gf.clone(),
)
.alert(LEAPING);
}
Suit::Clubs => {} }
}
rules
}
fn five_four(a: Suit, b: Suit) -> Cons<impl Constraint + Clone> {
(len(a, 5..) & len(b, 4..)) | (len(a, 4..) & len(b, 5..))
}
fn passed_two_suiter(a: Suit, b: Suit) -> Cons<impl Constraint + Clone> {
five_four(a, b) & len(a, ..=5) & len(b, ..=5)
}
const MICHAELS: Alert = Alert("michaels");
const ADVANCE_CUE: Alert = Alert("advance-cue");
const ADVANCE_TRANSFER: Alert = Alert("advance-transfer");
const UNUSUAL: Alert = Alert("unusual-2nt");
const LEAPING: Alert = Alert("leaping-michaels");
const GLADIATOR_RELAY: Alert = Alert("gladiator:relay");
const GLADIATOR_RELAY_PC: Alert = Alert("gladiator:relay-pc");
const GLADIATOR_STAYMAN: Alert = Alert("gladiator:stayman");
const GLADIATOR_CLUB_TRANSFER: Alert = Alert("gladiator:club-transfer");
const GLADIATOR_SPLINTER: Alert = Alert("gladiator:splinter");
const RESPONSIVE: Alert = Alert("responsive-double");
const WOOLSEY_X: Alert = Alert("1ntd:woolsey-x");
const LANDY_X: Alert = Alert("1ntd:landy-x");
const DONT_X: Alert = Alert("1ntd:dont-x");
const LANDY_2C: Alert = Alert("1ntd:landy-2c");
const WOOLSEY_2C: Alert = Alert("1ntd:woolsey-2c");
const DONT_2C: Alert = Alert("1ntd:dont-2c");
const MULTI_2D: Alert = Alert("1ntd:multi-2d");
const DONT_2D: Alert = Alert("1ntd:dont-2d");
const MUIDERBERG_2H: Alert = Alert("1ntd:muiderberg-2h");
const DONT_2H: Alert = Alert("1ntd:dont-2h");
const MUIDERBERG_2S: Alert = Alert("1ntd:muiderberg-2s");
const UNUSUAL_2NT: Alert = Alert("1ntd:unusual-2nt");
const MECKWELL_X: Alert = Alert("1ntd:meckwell-x");
const MECKWELL_2C: Alert = Alert("1ntd:meckwell-2c");
const MECKWELL_2D: Alert = Alert("1ntd:meckwell-2d");
const STAYMAN_DEFENSE_X: Alert = Alert("staydef:x-clubs");
const TRANSFER_DEFENSE_X: Alert = Alert("xferdef:x-bidsuit");
const TRANSFER_DEFENSE_CUE: Alert = Alert("xferdef:cue-michaels");
const MINOR_TRANSFER_DEFENSE_X: Alert = Alert("minorxferdef:x-spades");
const MINOR_TRANSFER_DEFENSE_2NT: Alert = Alert("minorxferdef:2nt-reds");
const MINOR_TRANSFER_DEFENSE_CUE: Alert = Alert("minorxferdef:cue-top-bottom");
const DIAMOND_TRANSFER_DEFENSE_X: Alert = Alert("diaxferdef:x-diamonds");
const DIAMOND_TRANSFER_DEFENSE_CUE: Alert = Alert("diaxferdef:cue-majors");
fn woolsey_x() -> Rules {
Rules::new().rule(
Call::Double,
1.9,
woolsey_double_shape() & points(woolsey_double_floor()..),
)
}
fn landy_x() -> Rules {
let four_four = direct_landy_double().unwrap_or(false);
Rules::new().rule(
Call::Double,
1.9,
both_majors_shape(four_four) & points(direct_landy_double_floor()..),
)
}
fn defense_to_their_stayman() -> Rules {
let (min_len, floor) = stayman_defense_overcall();
Rules::new()
.rule(
Call::Double,
1.9,
len(Suit::Clubs, 5..) & suit_hcp(Suit::Clubs, 5..) & points(8..),
)
.alert(STAYMAN_DEFENSE_X)
.rule(
Bid::new(2, Strain::Diamonds),
1.8,
len(Suit::Diamonds, min_len..) & points(floor..),
)
.rule(
Bid::new(2, Strain::Hearts),
1.8,
len(Suit::Hearts, min_len..) & points(floor..),
)
.rule(
Bid::new(2, Strain::Spades),
1.8,
len(Suit::Spades, min_len..) & points(floor..),
)
.rule(
Bid::new(3, Strain::Clubs),
2.0,
len(Suit::Clubs, 6..) & points(floor..),
)
.rule(Call::Pass, 0.5, hcp(0..))
}
fn defense_to_their_transfer(bid: Suit, shown_major: Suit) -> Rules {
let (min_len, floor) = (6usize, 14u8);
let other_major = if shown_major == Suit::Spades {
Suit::Hearts
} else {
Suit::Spades
};
let mut rules = Rules::new()
.rule(
Call::Double,
1.9,
len(bid, 5..) & suit_hcp(bid, 5..) & points(8..),
)
.alert(TRANSFER_DEFENSE_X)
.rule(
Bid::new(2, Strain::from(shown_major)),
1.7,
len(other_major, 5..)
& (len(Suit::Clubs, 5..) | len(Suit::Diamonds, 5..))
& points(8..),
)
.alert(TRANSFER_DEFENSE_CUE);
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit == shown_major {
continue;
}
let strain = Strain::from(suit);
let level = if strain > Strain::from(bid) { 2 } else { 3 };
let weight = if suit == bid { 2.0 } else { 1.8 };
rules = rules.rule(
Bid::new(level, strain),
weight,
len(suit, min_len..) & points(floor..),
);
}
rules.rule(Call::Pass, 0.5, hcp(0..))
}
fn defense_to_their_minor_transfer() -> Rules {
Rules::new()
.rule(
Call::Double,
1.9,
len(Suit::Spades, 5..) & suit_hcp(Suit::Spades, 5..) & points(8..),
)
.alert(MINOR_TRANSFER_DEFENSE_X)
.rule(
Bid::new(2, Strain::Notrump),
1.7,
len(Suit::Diamonds, 5..) & len(Suit::Hearts, 5..) & points(8..),
)
.alert(MINOR_TRANSFER_DEFENSE_2NT)
.rule(
Bid::new(3, Strain::Clubs),
2.0,
len(Suit::Spades, 5..) & len(Suit::Diamonds, 5..) & points(8..),
)
.alert(MINOR_TRANSFER_DEFENSE_CUE)
.rule(
Bid::new(3, Strain::Diamonds),
1.8,
len(Suit::Diamonds, 6..) & points(14..),
)
.rule(
Bid::new(3, Strain::Hearts),
1.8,
len(Suit::Hearts, 6..) & points(14..),
)
.rule(Call::Pass, 0.5, hcp(0..))
}
fn defense_to_their_diamond_transfer() -> Rules {
Rules::new()
.rule(
Call::Double,
1.9,
len(Suit::Diamonds, 5..) & suit_hcp(Suit::Diamonds, 5..) & points(8..),
)
.alert(DIAMOND_TRANSFER_DEFENSE_X)
.rule(
Bid::new(3, Strain::Diamonds),
2.0,
len(Suit::Hearts, 5..) & len(Suit::Spades, 5..) & points(8..),
)
.alert(DIAMOND_TRANSFER_DEFENSE_CUE)
.rule(
Bid::new(3, Strain::Clubs),
1.8,
len(Suit::Clubs, 6..) & points(14..),
)
.rule(
Bid::new(3, Strain::Hearts),
1.8,
len(Suit::Hearts, 6..) & points(14..),
)
.rule(
Bid::new(3, Strain::Spades),
1.8,
len(Suit::Spades, 6..) & points(14..),
)
.rule(Call::Pass, 0.5, hcp(0..))
}
fn dont_x() -> Rules {
let lo = direct_dont_x_floor();
let one_min = direct_dont_one_suiter_min();
Rules::new().rule(
Call::Double,
1.9,
dont_one_suiter_direct(one_min) & points(lo..),
)
}
fn landy_2c() -> Rules {
let (lo, hi) = woolsey_points();
let shape = five_four(Suit::Hearts, Suit::Spades);
if landy_use_hcp() {
Rules::new().rule(Bid::new(2, Strain::Clubs), 1.9, shape & hcp(lo..=hi))
} else {
Rules::new().rule(Bid::new(2, Strain::Clubs), 1.9, shape & points(lo..=hi))
}
}
fn woolsey_2c() -> Rules {
let (lo, hi) = woolsey_points();
Rules::new().rule(
Bid::new(2, Strain::Clubs),
1.9,
passed_two_suiter(Suit::Hearts, Suit::Spades) & points(lo..=hi),
)
}
fn dont_2c() -> Rules {
let lo = natural_overcall_points().0;
let ff = direct_dont_four_four();
Rules::new().rule(
Bid::new(2, Strain::Clubs),
2.0,
dont_minor_major(Suit::Clubs, ff) & points(lo..),
)
}
fn multi_2d() -> Rules {
let (lo, hi) = woolsey_points();
Rules::new().rule(
Bid::new(2, Strain::Diamonds),
1.9,
woolsey_multi() & points(lo..=hi),
)
}
fn dont_2d() -> Rules {
let lo = natural_overcall_points().0;
let ff = direct_dont_four_four();
Rules::new().rule(
Bid::new(2, Strain::Diamonds),
2.0,
dont_minor_major(Suit::Diamonds, ff) & points(lo..),
)
}
fn muiderberg(major: Suit) -> Rules {
let (lo, hi) = woolsey_points();
Rules::new().rule(
Bid::new(2, Strain::from(major)),
1.9,
woolsey_muiderberg(major) & points(lo..=hi),
)
}
fn dont_2h() -> Rules {
let lo = natural_overcall_points().0;
let ff = direct_dont_four_four();
Rules::new().rule(
Bid::new(2, Strain::Hearts),
2.0,
dont_both_majors(ff) & points(lo..),
)
}
fn meckwell_x() -> Rules {
let lo = meckwell_x_floor();
Rules::new().rule(
Call::Double,
1.9,
meckwell_double_shape(6, meckwell_x_four_four()) & points(lo..),
)
}
fn meckwell_2c() -> Rules {
let lo = natural_overcall_points().0;
Rules::new().rule(
Bid::new(2, Strain::Clubs),
2.0,
dont_minor_major(Suit::Clubs, meckwell_minor_major_44()) & points(lo..),
)
}
fn meckwell_2d() -> Rules {
let lo = natural_overcall_points().0;
Rules::new().rule(
Bid::new(2, Strain::Diamonds),
2.0,
dont_minor_major(Suit::Diamonds, meckwell_minor_major_44()) & points(lo..),
)
}
fn unusual_2nt() -> Rules {
let (lo, hi) = unusual_notrump_range().unwrap_or((0, 37));
let shape = len(Suit::Clubs, 5..) & len(Suit::Diamonds, 5..);
if landy_use_hcp() {
Rules::new().rule(Bid::new(2, Strain::Notrump), 1.8, shape & hcp(lo..=hi))
} else {
Rules::new().rule(Bid::new(2, Strain::Notrump), 1.8, shape & points(lo..=hi))
}
}
fn chain_natural_overcalls(mut rules: Rules, skip_clubs: bool) -> Rules {
let (oc_lo, oc_hi) = natural_overcall_points();
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit == Suit::Clubs && skip_clubs {
continue;
}
rules = rules.rule(
Bid::new(2, Strain::from(suit)),
1.0,
len(suit, 5..) & points(oc_lo..=oc_hi),
);
}
rules
}
fn chain_natural_base(rules: Rules) -> Rules {
match notrump_defense() {
NotrumpDefense::Woolsey | NotrumpDefense::AlwaysPass => {
rules.rule(Call::Pass, 0.0, hcp(0..))
}
NotrumpDefense::DirectDont => {
let lo = natural_overcall_points().0;
let one_min = direct_dont_one_suiter_min();
rules
.rule(
Bid::new(2, Strain::Spades),
1.0,
len(Suit::Spades, one_min..) & points(lo..),
)
.rule(Call::Pass, 0.0, hcp(0..))
}
NotrumpDefense::Meckwell => {
let lo = natural_overcall_points().0;
rules
.rule(
Bid::new(2, Strain::Hearts),
1.0,
meckwell_natural_major(Suit::Hearts) & points(lo..),
)
.rule(
Bid::new(2, Strain::Spades),
1.0,
meckwell_natural_major(Suit::Spades) & points(lo..),
)
.rule(Call::Pass, 0.0, hcp(0..))
}
NotrumpDefense::DirectLandy => {
chain_natural_overcalls(rules.rule(Call::Pass, 0.0, hcp(0..)), false)
}
NotrumpDefense::Natural => {
let floor = natural_double_floor();
let w = natural_double_weight();
let rules = match natural_double_shape() {
DoubleShape::Balanced => rules.rule(Call::Double, w, hcp(floor..) & balanced()),
DoubleShape::SemiBalanced => {
rules.rule(Call::Double, w, hcp(floor..) & semi_balanced())
}
DoubleShape::Any => rules.rule(Call::Double, w, hcp(floor..)),
};
chain_natural_overcalls(
rules.rule(Call::Pass, 0.0, hcp(0..)),
landy_range().is_some(),
)
}
NotrumpDefense::Off => rules,
}
}
fn active_alerts() -> Vec<Alert> {
let mut alerts = Vec::new();
let system = notrump_defense();
match system {
NotrumpDefense::AlwaysPass => return alerts,
NotrumpDefense::Woolsey => {
alerts.extend([
WOOLSEY_X,
WOOLSEY_2C,
MULTI_2D,
MUIDERBERG_2H,
MUIDERBERG_2S,
]);
}
NotrumpDefense::DirectDont => alerts.extend([DONT_X, DONT_2C, DONT_2D, DONT_2H]),
NotrumpDefense::Meckwell => alerts.extend([MECKWELL_X, MECKWELL_2C, MECKWELL_2D]),
NotrumpDefense::DirectLandy => alerts.push(LANDY_X),
NotrumpDefense::Natural | NotrumpDefense::Off => {}
}
if landy_range().is_some() && matches!(system, NotrumpDefense::Natural | NotrumpDefense::Off) {
alerts.push(LANDY_2C);
}
if unusual_notrump_range().is_some() {
alerts.push(UNUSUAL_2NT);
}
alerts
}
pub fn defense_to_notrump() -> Rules {
let alerts = active_alerts();
chain_natural_base(Rules::new())
.chain(woolsey_x().alert(WOOLSEY_X))
.chain(landy_x().alert(LANDY_X))
.chain(dont_x().alert(DONT_X))
.chain(landy_2c().alert(LANDY_2C))
.chain(woolsey_2c().alert(WOOLSEY_2C))
.chain(dont_2c().alert(DONT_2C))
.chain(multi_2d().alert(MULTI_2D))
.chain(dont_2d().alert(DONT_2D))
.chain(muiderberg(Suit::Hearts).alert(MUIDERBERG_2H))
.chain(dont_2h().alert(DONT_2H))
.chain(muiderberg(Suit::Spades).alert(MUIDERBERG_2S))
.chain(meckwell_x().alert(MECKWELL_X))
.chain(meckwell_2c().alert(MECKWELL_2C))
.chain(meckwell_2d().alert(MECKWELL_2D))
.chain(unusual_2nt().alert(UNUSUAL_2NT))
.gated(move |t| alerts.contains(&t))
}
fn dont_one_suiter_direct(min: usize) -> Cons<impl Constraint + Clone> {
use Suit::{Clubs, Diamonds, Hearts, Spades};
(len(Clubs, min..) & and([Diamonds, Hearts, Spades], ..=3))
| (len(Diamonds, min..) & and([Clubs, Hearts, Spades], ..=3))
| (len(Hearts, min..) & and([Clubs, Diamonds, Spades], ..=3))
}
fn dont_minor_major(minor: Suit, allow_44: bool) -> Cons<impl Constraint + Clone> {
let longer = if allow_44 { 4 } else { 5 };
len(minor, 4..)
& or([Suit::Hearts, Suit::Spades], 4..)
& (len(minor, longer..) | or([Suit::Hearts, Suit::Spades], longer..))
}
fn dont_both_majors(allow_44: bool) -> Cons<impl Constraint + Clone> {
let longer = if allow_44 { 4 } else { 5 };
and([Suit::Hearts, Suit::Spades], 4..) & or([Suit::Hearts, Suit::Spades], longer..)
}
fn meckwell_double_shape(min: usize, four_four: bool) -> Cons<impl Constraint + Clone> {
use Suit::{Clubs, Diamonds, Hearts, Spades};
(len(Clubs, min..) & and([Diamonds, Hearts, Spades], ..=3))
| (len(Diamonds, min..) & and([Clubs, Hearts, Spades], ..=3))
| both_majors_shape(four_four)
}
fn meckwell_natural_major(major: Suit) -> Cons<impl Constraint + Clone> {
let other = if major == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
len(major, 5..) & len(other, ..=3) & and([Suit::Clubs, Suit::Diamonds], ..=3)
}
#[cfg(test)]
mod shape_guards {
use super::*;
use crate::bidding::verify::{accepts, compare, empty_context};
use contract_bridge::Hand;
use rand::SeedableRng;
use rand::rngs::StdRng;
const N: usize = 8000;
fn check(label: &str, candidate: impl Constraint, reference: impl Fn(Hand) -> bool) {
let ctx = empty_context();
let mut rng = StdRng::seed_from_u64(20_260_625);
let report = compare(reference, |h| accepts(&candidate, h, &ctx), &mut rng, N);
assert!(
report.agrees(),
"{label}: {} of {} hands disagree, e.g. {:?}",
report.disagreements.len(),
report.tested,
report.disagreements.first(),
);
assert!(
report.reference_accepts > 0,
"{label}: reference accepts nothing — a vacuous guard",
);
}
#[test]
fn reauthored_shapes_match_intended_spec() {
use Suit::{Clubs, Diamonds, Hearts, Spades};
let ln = |h: Hand, s: Suit| h[s].len();
check("woolsey_multi", woolsey_multi(), |h| {
(ln(h, Hearts) >= 6 || ln(h, Spades) >= 6) && ln(h, Clubs) <= 4 && ln(h, Diamonds) <= 4
});
for major in [Hearts, Spades] {
let other = if major == Hearts { Spades } else { Hearts };
check("woolsey_muiderberg", woolsey_muiderberg(major), move |h| {
ln(h, major) == 5
&& ln(h, other) <= 3
&& (ln(h, Clubs) >= 4 || ln(h, Diamonds) >= 4)
});
}
check("woolsey_double_shape", woolsey_double_shape(), |h| {
let four_major = (ln(h, Hearts) == 4 && ln(h, Spades) <= 3)
|| (ln(h, Spades) == 4 && ln(h, Hearts) <= 3);
four_major && ((5..=6).contains(&ln(h, Clubs)) || (5..=6).contains(&ln(h, Diamonds)))
});
check("both_majors_shape(false)", both_majors_shape(false), |h| {
(ln(h, Hearts) >= 5 && ln(h, Spades) >= 4) || (ln(h, Hearts) >= 4 && ln(h, Spades) >= 5)
});
check("both_majors_shape(true)", both_majors_shape(true), |h| {
ln(h, Hearts) >= 4 && ln(h, Spades) >= 4
});
for min in [5usize, 6] {
check(
"dont_one_suiter_direct",
dont_one_suiter_direct(min),
move |h| {
let one = |long: Suit| {
ln(h, long) >= min
&& [Clubs, Diamonds, Hearts, Spades]
.iter()
.all(|&s| s == long || ln(h, s) <= 3)
};
one(Clubs) || one(Diamonds) || one(Hearts)
},
);
}
for (minor, a44) in [
(Clubs, true),
(Clubs, false),
(Diamonds, true),
(Diamonds, false),
] {
let longer = if a44 { 4 } else { 5 };
check("dont_minor_major", dont_minor_major(minor, a44), move |h| {
let hi = ln(h, Hearts).max(ln(h, Spades));
ln(h, minor) >= 4 && hi >= 4 && (ln(h, minor) >= longer || hi >= longer)
});
}
check("dont_both_majors(true)", dont_both_majors(true), |h| {
ln(h, Hearts) >= 4 && ln(h, Spades) >= 4
});
check("dont_both_majors(false)", dont_both_majors(false), |h| {
(ln(h, Hearts) >= 5 && ln(h, Spades) >= 4) || (ln(h, Hearts) >= 4 && ln(h, Spades) >= 5)
});
for a44 in [true, false] {
let longer = if a44 { 4 } else { 5 };
check(
"meckwell_double_shape",
meckwell_double_shape(6, a44),
move |h| {
let one_minor = (ln(h, Clubs) >= 6
&& ln(h, Diamonds) <= 3
&& ln(h, Hearts) <= 3
&& ln(h, Spades) <= 3)
|| (ln(h, Diamonds) >= 6
&& ln(h, Clubs) <= 3
&& ln(h, Hearts) <= 3
&& ln(h, Spades) <= 3);
let both_majors = ln(h, Hearts) >= 4
&& ln(h, Spades) >= 4
&& (ln(h, Hearts) >= longer || ln(h, Spades) >= longer);
one_minor || both_majors
},
);
}
for major in [Hearts, Spades] {
let other = if major == Hearts { Spades } else { Hearts };
check(
"meckwell_natural_major",
meckwell_natural_major(major),
move |h| {
ln(h, major) >= 5
&& ln(h, other) <= 3
&& ln(h, Clubs) <= 3
&& ln(h, Diamonds) <= 3
},
);
}
}
}
fn landy_advances(lo: u8) -> Rules {
let invite = 20u8.saturating_sub(lo);
let game = 22u8.saturating_sub(lo);
let hearts_longer = described("♥ at least as long as ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() >= h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
let equal_majors = described("equal majors", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() == h[Suit::Spades].len()
});
Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.4,
len(Suit::Hearts, 4..) & points(game..) & hearts_longer.clone(),
)
.rule(
Bid::new(4, Strain::Spades),
1.4,
len(Suit::Spades, 4..) & points(game..) & spades_longer.clone(),
)
.rule(Bid::new(2, Strain::Notrump), 1.2, points(game..))
.rule(
Bid::new(3, Strain::Hearts),
1.1,
len(Suit::Hearts, 4..) & points(invite..game) & hearts_longer.clone(),
)
.rule(
Bid::new(3, Strain::Spades),
1.1,
len(Suit::Spades, 4..) & points(invite..game) & spades_longer.clone(),
)
.rule(
Bid::new(2, Strain::Diamonds),
1.0,
equal_majors & points(..invite),
)
.rule(
Bid::new(2, Strain::Hearts),
0.9,
hearts_longer & points(..invite),
)
.rule(
Bid::new(2, Strain::Spades),
0.9,
spades_longer & points(..invite),
)
}
fn landy_advances_over_double(lo: u8) -> Rules {
let invite = 20u8.saturating_sub(lo);
let game = 22u8.saturating_sub(lo);
let hearts_longer = described("♥ at least as long as ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() >= h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
let equal_majors = described("equal majors", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() == h[Suit::Spades].len()
});
let (min_minor, max_major) = doubled_landy_escape();
let short_majors = len(Suit::Hearts, ..=max_major) & len(Suit::Spades, ..=max_major);
Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.4,
len(Suit::Hearts, 4..) & points(game..) & hearts_longer.clone(),
)
.rule(
Bid::new(4, Strain::Spades),
1.4,
len(Suit::Spades, 4..) & points(game..) & spades_longer.clone(),
)
.rule(Bid::new(2, Strain::Notrump), 1.2, points(game..))
.rule(
Bid::new(3, Strain::Hearts),
1.1,
len(Suit::Hearts, 4..) & points(invite..game) & hearts_longer.clone(),
)
.rule(
Bid::new(3, Strain::Spades),
1.1,
len(Suit::Spades, 4..) & points(invite..game) & spades_longer.clone(),
)
.rule(
Call::Pass,
1.05,
len(Suit::Clubs, min_minor..) & short_majors.clone(),
)
.rule(
Bid::new(2, Strain::Diamonds),
1.0,
len(Suit::Diamonds, min_minor..) & short_majors & points(..game),
)
.rule(Call::Redouble, 0.95, equal_majors & points(..invite))
.rule(
Bid::new(2, Strain::Hearts),
0.9,
hearts_longer & points(..invite),
)
.rule(
Bid::new(2, Strain::Spades),
0.9,
spades_longer & points(..invite),
)
}
fn landy_doubled_2d_rebid() -> Rules {
let hearts_longer = described("♥ at least as long as ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() >= h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
Rules::new()
.rule(
Bid::new(2, Strain::Hearts),
1.0,
len(Suit::Diamonds, ..=1) & hearts_longer,
)
.rule(
Bid::new(2, Strain::Spades),
1.0,
len(Suit::Diamonds, ..=1) & spades_longer,
)
.rule(Call::Pass, 0.0, hcp(0..))
}
fn landy_2d_rebid() -> Rules {
let hearts_longer = described("♥ at least as long as ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() >= h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
Rules::new()
.rule(Bid::new(2, Strain::Hearts), 1.0, hearts_longer)
.rule(Bid::new(2, Strain::Spades), 1.0, spades_longer)
}
fn sit() -> Rules {
Rules::new().rule(Call::Pass, 0.0, hcp(0..))
}
fn both_majors_x_runout(lo: u8) -> Rules {
let game = 22u8.saturating_sub(lo);
let hearts_longer = described("♥ longer than ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() > h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
let short_majors = len(Suit::Hearts, ..=2) & len(Suit::Spades, ..=2);
Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.4,
len(Suit::Hearts, 4..) & points(game..) & hearts_longer.clone(),
)
.rule(
Bid::new(4, Strain::Spades),
1.4,
len(Suit::Spades, 4..) & points(game..) & spades_longer.clone(),
)
.rule(
Bid::new(2, Strain::Clubs),
1.1,
len(Suit::Clubs, 5..) & short_majors.clone(),
)
.rule(
Bid::new(2, Strain::Diamonds),
1.1,
len(Suit::Diamonds, 5..) & short_majors,
)
.rule(Bid::new(2, Strain::Spades), 1.0, spades_longer)
.rule(Bid::new(2, Strain::Hearts), 1.0, hearts_longer)
.rule(Call::Pass, 0.5, hcp(0..))
}
fn passed_dont_x_advance() -> Rules {
Rules::new().rule(Bid::new(2, Strain::Clubs), 1.0, hcp(0..))
}
fn passed_dont_x_rebid() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Diamonds), 1.0, len(Suit::Diamonds, 5..))
.rule(Bid::new(2, Strain::Hearts), 1.0, len(Suit::Hearts, 5..))
.rule(Bid::new(2, Strain::Spades), 1.0, len(Suit::Spades, 5..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn passed_dont_2c_advance() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Diamonds), 1.0, len(Suit::Clubs, ..=2))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn passed_dont_2c_rebid() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Hearts), 1.0, len(Suit::Hearts, 4..))
.rule(Bid::new(2, Strain::Spades), 1.0, len(Suit::Spades, 4..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn passed_dont_2d_advance() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Hearts), 1.0, len(Suit::Diamonds, ..=2))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn passed_dont_2d_rebid() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Spades), 1.0, len(Suit::Spades, 4..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn passed_dont_2h_advance() -> Rules {
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
Rules::new()
.rule(Bid::new(2, Strain::Spades), 1.0, spades_longer)
.rule(Call::Pass, 0.0, hcp(0..))
}
fn meckwell_x_advance() -> Rules {
Rules::new().rule(Bid::new(2, Strain::Clubs), 1.0, hcp(0..))
}
fn meckwell_x_rebid() -> Rules {
Rules::new()
.rule(
Bid::new(2, Strain::Diamonds),
1.0,
len(Suit::Diamonds, 5..) & len(Suit::Hearts, ..=3) & len(Suit::Spades, ..=3),
)
.rule(Bid::new(2, Strain::Hearts), 1.0, len(Suit::Hearts, 4..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn landy_2nt_rebid(lo: u8, hi: u8) -> Rules {
let hi = hi.min(16);
let step = hi.saturating_sub(lo) / 3;
let med = lo + step;
let max = lo + 2 * step;
let five_five = len(Suit::Hearts, 5..) & len(Suit::Spades, 5..);
Rules::new()
.rule(
Bid::new(3, Strain::Hearts),
1.3,
five_five.clone() & points(lo..med),
)
.rule(
Bid::new(3, Strain::Spades),
1.3,
five_five.clone() & points(med..max),
)
.rule(Bid::new(3, Strain::Notrump), 1.3, five_five & points(max..))
.rule(Bid::new(3, Strain::Clubs), 1.2, points(lo..max))
.rule(Bid::new(3, Strain::Diamonds), 1.2, points(max..))
}
fn multi_advances(lo: u8) -> Rules {
let invite = 20u8.saturating_sub(lo);
let game = 22u8.saturating_sub(lo);
Rules::new()
.rule(Bid::new(2, Strain::Notrump), 1.0, points(game..))
.rule(Bid::new(2, Strain::Spades), 0.95, points(invite..game))
.rule(Bid::new(2, Strain::Hearts), 0.9, points(..invite))
}
fn multi_2h_rebid() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Hearts), 1.1, len(Suit::Hearts, 7..))
.rule(Bid::new(3, Strain::Spades), 1.1, len(Suit::Spades, 7..))
.rule(Bid::new(2, Strain::Spades), 1.0, len(Suit::Spades, 6..=6))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn multi_2s_rebid() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Hearts), 1.0, len(Suit::Hearts, 6..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn multi_2nt_rebid() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Hearts), 1.0, len(Suit::Hearts, 6..))
.rule(Bid::new(4, Strain::Spades), 1.0, len(Suit::Spades, 6..))
}
fn muiderberg_advances(major: Suit, lo: u8) -> Rules {
let invite = 20u8.saturating_sub(lo);
let game = 22u8.saturating_sub(lo);
let strain = Strain::from(major);
Rules::new()
.rule(Bid::new(4, strain), 1.2, len(major, 3..) & points(game..))
.rule(
Bid::new(3, strain),
1.1,
(len(major, 4..) & points(..game)) | (len(major, 3..) & points(invite..game)),
)
.rule(
Bid::new(2, Strain::Notrump),
1.0,
len(major, ..=2) & points(invite..),
)
.rule(Call::Pass, 0.0, hcp(0..))
}
fn muiderberg_advances_doubled(major: Suit, lo: u8) -> Rules {
let invite = 20u8.saturating_sub(lo);
let game = 22u8.saturating_sub(lo);
let strain = Strain::from(major);
Rules::new()
.rule(Bid::new(4, strain), 1.2, len(major, 3..) & points(game..))
.rule(
Bid::new(3, strain),
1.1,
(len(major, 4..) & points(..game)) | (len(major, 3..) & points(invite..game)),
)
.rule(Bid::new(2, Strain::Notrump), 0.5, len(major, ..=2))
.rule(Call::Pass, 0.0, len(major, 3..))
}
fn muiderberg_2nt_rebid() -> Rules {
let diamonds_longer = described("♦ at least as long as ♣", |h: Hand, _: &Context<'_>| {
h[Suit::Diamonds].len() >= h[Suit::Clubs].len()
});
Rules::new()
.rule(Bid::new(3, Strain::Diamonds), 1.0, diamonds_longer)
.rule(Bid::new(3, Strain::Clubs), 0.9, hcp(0..))
}
fn woolsey_x_advance(lo: u8) -> Rules {
let game = 22u8.saturating_sub(lo);
Rules::new()
.rule(Bid::new(2, Strain::Spades), 1.11, len(Suit::Spades, 5..))
.rule(Bid::new(2, Strain::Hearts), 1.1, len(Suit::Hearts, 5..))
.rule(Bid::new(2, Strain::Notrump), 1.0, points(game..))
.rule(Bid::new(2, Strain::Clubs), 0.9, hcp(0..))
}
fn woolsey_x_minor_rebid() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Diamonds), 1.0, len(Suit::Diamonds, 5..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn woolsey_x_2nt_rebid() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Hearts), 1.0, len(Suit::Hearts, 4..))
.rule(Bid::new(3, Strain::Spades), 1.0, len(Suit::Spades, 4..))
}
#[must_use]
pub fn advance_double(their_opening: Bid) -> Rules {
let theirs = their_opening.strain;
let t = theirs.suit().expect("their opening is always a suit bid");
let level = their_opening.level.get();
let mut rules = Rules::new()
.rule(Call::Pass, 1.5, len(t, 4..) & top_honors(t, 2..) & hcp(6..))
.rule(
Bid::new(3, Strain::Notrump),
1.3,
hcp(13..) & stopper_in_their_suits(),
)
.rule(Bid::new(level, Strain::Notrump), 0.3, hcp(0..))
.rule(Call::Pass, 0.0, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
if strain == theirs {
continue;
}
let bid_level = if strain > theirs { level } else { level + 1 };
rules = rules.rule(Bid::new(bid_level, strain), 1.0, len(suit, 4..));
if matches!(suit, Suit::Hearts | Suit::Spades) {
rules = rules.rule(Bid::new(4, strain), 1.4, len(suit, 4..) & points(11..));
}
}
rules
}
#[must_use]
fn advance_double_rich(their_opening: Bid) -> Rules {
let theirs = their_opening.strain;
let t = theirs.suit().expect("their opening is always a suit bid");
let level = their_opening.level.get();
let cue = Bid::new(level + 1, theirs);
let mut rules = Rules::new()
.rule(
Call::Pass,
1.6,
len(t, 5..) | (len(t, 4..) & top_honors(t, 2..)),
);
rules = rules.rule(cue, 1.05, hcp(10..)).alert(ADVANCE_CUE);
rules = rules
.rule(
Bid::new(3, Strain::Notrump),
1.45,
hcp(13..=17) & stopper_in_their_suits(),
)
.rule(
Bid::new(level + 1, Strain::Notrump),
1.15,
hcp(11..=12) & balanced() & stopper_in_their_suits(),
)
.rule(
Bid::new(level, Strain::Notrump),
1.1,
hcp(8..=10) & stopper_in_their_suits(),
)
.rule(Call::Pass, 0.0, hcp(0..));
let transfer_majors: Vec<Suit> = if advance_rubens_enabled() {
advance_major_transfers(theirs)
.into_iter()
.map(|(_, target)| target)
.collect()
} else {
Vec::new()
};
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
if strain == theirs {
continue;
}
let bid_level = if strain > theirs { level } else { level + 1 };
rules = rules.rule(Bid::new(bid_level, strain), 1.0, len(suit, 4..));
rules = rules.rule(Bid::new(bid_level, strain), 0.3, len(suit, 3..));
let jump = bid_level + 1;
if matches!(suit, Suit::Hearts | Suit::Spades) {
if jump == 2 {
rules = rules.rule(Bid::new(2, strain), 1.2, hcp(8..=10) & len(suit, 4..));
} else if jump == 3 {
rules = rules.rule(Bid::new(3, strain), 1.25, hcp(10..=12) & len(suit, 5..));
}
}
if matches!(suit, Suit::Hearts | Suit::Spades) {
let bid = Bid::new(4, strain);
rules = if transfer_majors.contains(&suit) {
rules.rule(bid, 1.5, len(suit, 5..) & hcp(0..=10))
} else {
rules.rule(bid, 1.5, len(suit, 5..) & points(11..=15))
};
}
}
if advance_rubens_enabled() {
for (bid, target) in advance_major_transfers(theirs) {
rules = rules
.rule(bid, 1.6, hcp(10..) & len(target, 5..))
.alert(ADVANCE_TRANSFER);
}
}
rules
}
fn advance_major_transfers(theirs: Strain) -> Vec<(Bid, Suit)> {
if theirs == Strain::Spades {
return Vec::new();
}
let mut out = Vec::new();
for target in [Suit::Hearts, Suit::Spades] {
if Strain::from(target) == theirs {
continue;
}
let below = match target {
Suit::Hearts => Suit::Diamonds,
Suit::Spades => Suit::Hearts,
_ => unreachable!("only hearts and spades are majors"),
};
out.push((Bid::new(3, Strain::from(below)), target));
}
out
}
fn complete_advance_transfer(target: Suit) -> Rules {
let strain = Strain::from(target);
Rules::new()
.rule(Bid::new(4, strain), 1.3, len(target, 4..) & points(15..))
.rule(Bid::new(3, strain), 1.0, hcp(0..))
}
fn advance_transfer_rebid(target: Suit) -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::from(target)), 1.0, hcp(12..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn answer_advance_cue(their_opening: Bid) -> Rules {
let theirs = their_opening.strain;
let level = their_opening.level.get();
let mut rules = Rules::new()
.rule(
Bid::new(3, Strain::Notrump),
1.2,
hcp(15..) & stopper_in_their_suits(),
)
.rule(Bid::new(level + 1, Strain::Notrump), 0.2, hcp(0..));
for major in [Suit::Hearts, Suit::Spades] {
let m = Strain::from(major);
if m == theirs {
continue;
}
let cheap = if m > theirs { level + 1 } else { level + 2 };
rules = rules.rule(Bid::new(cheap, m), 1.3, len(major, 4..));
rules = rules.rule(Bid::new(4, m), 1.5, len(major, 4..) & points(15..));
}
rules
}
fn advance_cue_answers(their_opening: Bid) -> Vec<Bid> {
let theirs = their_opening.strain;
let level = their_opening.level.get();
let mut out = vec![Bid::new(level + 1, Strain::Notrump)];
for major in [Suit::Hearts, Suit::Spades] {
let m = Strain::from(major);
if m == theirs {
continue;
}
let cheap = if m > theirs { level + 1 } else { level + 2 };
out.push(Bid::new(cheap, m));
}
out
}
fn advance_cue_rebid(answer: Bid) -> Rules {
let mut rules = Rules::new();
if let Some(s) = answer.strain.suit() {
rules = rules.rule(Bid::new(4, answer.strain), 1.0, len(s, 3..) & hcp(13..));
}
rules
.rule(
Bid::new(3, Strain::Notrump),
0.6,
hcp(13..) & stopper_in_their_suits(),
)
.rule(Bid::new(3, Strain::Notrump), 0.2, hcp(13..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn insert_advance_of_double(d: &mut Defensive, suit: Suit, opening: Bid, style: LebensohlStyle) {
let dbl_p = [Call::Bid(opening), Call::Double, Call::Pass];
if style == LebensohlStyle::Off {
insert_all_seats(d, &dbl_p, 3, advance_double(opening));
return;
}
let advancer = match style {
LebensohlStyle::Transfer if suit == Suit::Diamonds => transfer_stayman_2d_responder(false),
LebensohlStyle::Transfer => transfer_lebensohl_responder(suit, false),
_ => lebensohl_responder(suit),
};
insert_all_seats(d, &dbl_p, 3, advancer);
let two_nt = call(2, Strain::Notrump);
let three_clubs = call(3, Strain::Clubs);
insert_all_seats(
d,
&[
Call::Bid(opening),
Call::Double,
Call::Pass,
two_nt,
Call::Pass,
],
3,
complete_lebensohl_relay(),
);
insert_all_seats(
d,
&[
Call::Bid(opening),
Call::Double,
Call::Pass,
two_nt,
Call::Pass,
three_clubs,
Call::Pass,
],
3,
lebensohl_relay_rebid(suit),
);
if style == LebensohlStyle::Transfer && suit != Suit::Diamonds {
let recognize = matches!(suit, Suit::Hearts | Suit::Spades);
let split = delayed_cue() && recognize;
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 == suit {
if split {
cue_stayman_answer_no_stopper(suit)
} else {
cue_stayman_answer(suit)
}
} else if let Some(target) = transfer_target(bid_suit, suit) {
transfer_completion(target, suit)
} else {
continue; };
insert_all_seats(
d,
&[
Call::Bid(opening),
Call::Double,
Call::Pass,
resp,
Call::Pass,
],
3,
reply,
);
}
if recognize {
let cue = call(3, Strain::from(suit));
insert_all_seats(
d,
&[
Call::Bid(opening),
Call::Double,
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
call(3, Strain::Clubs),
Call::Pass,
cue,
Call::Pass,
],
3,
cue_stayman_answer(suit),
);
}
}
if style == LebensohlStyle::Transfer && suit == 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![c3, p], stayman_2d_answer()),
(vec![c3, p, d3, p], smolen_at_three()),
(vec![c3, p, d3, p, h3, p], smolen_completion(Suit::Spades)),
(vec![c3, p, d3, p, s3, p], smolen_completion(Suit::Hearts)),
(vec![c3, p, h3, p], stayman_2d_fit_rebid(Suit::Hearts)),
(vec![c3, p, s3, p], stayman_2d_fit_rebid(Suit::Spades)),
(vec![d3, p], transfer_completion(Suit::Hearts, suit)),
(vec![h3, p], transfer_completion(Suit::Spades, suit)),
(vec![s3, p], clubs_transfer_completion(suit)),
(vec![d4, p], lm_2d_both_majors_advance()),
(vec![c4, p], lm_2d_clubs_ask()),
(vec![c4, p, d4, p], lm_2d_clubs_major()),
];
for (rest, rules) in nodes {
let prefix: Vec<Call> = dbl_p.iter().copied().chain(rest).collect();
insert_all_seats(d, &prefix, 3, rules);
}
}
}
fn other_major(major: Suit) -> Suit {
debug_assert!(matches!(major, Suit::Hearts | Suit::Spades));
if major == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
}
}
fn gladiator_advances(their_major: Suit) -> Rules {
let o = other_major(their_major);
let m = Strain::from(their_major);
let os = Strain::from(o);
let inv = 8u8;
let game = 10u8;
Rules::new()
.rule(
Bid::new(4, Strain::Clubs),
1.5,
len(o, 5..) & len(Suit::Clubs, 5..) & points(game..),
)
.alert(LEAPING)
.rule(
Bid::new(4, Strain::Diamonds),
1.5,
len(o, 5..) & len(Suit::Diamonds, 5..) & points(game..),
)
.alert(LEAPING)
.rule(
Bid::new(4, m),
1.5,
len(Suit::Diamonds, 5..) & len(Suit::Clubs, 5..) & points(game..),
)
.alert(LEAPING)
.rule(
Bid::new(3, m),
1.45,
len(o, 4..) & len(their_major, ..=1) & points(game..),
)
.alert(GLADIATOR_SPLINTER)
.rule(Bid::new(4, os), 1.35, len(o, 6..) & points(game..))
.rule(Bid::new(2, m), 1.4, len(o, 4..=4) & points(inv..))
.alert(GLADIATOR_STAYMAN)
.rule(
Bid::new(3, Strain::Clubs),
1.3,
len(Suit::Clubs, 5..) & points(game..),
)
.rule(
Bid::new(3, Strain::Diamonds),
1.3,
len(Suit::Diamonds, 5..) & points(game..),
)
.rule(Bid::new(3, os), 1.3, len(o, 5..) & points(game..))
.rule(
Bid::new(3, Strain::Notrump),
1.2,
balanced() & points(game..),
)
.rule(
Bid::new(2, Strain::Notrump),
1.05,
len(Suit::Clubs, 6..) & points(..inv),
)
.alert(GLADIATOR_CLUB_TRANSFER)
.rule(
Bid::new(2, Strain::Diamonds),
1.0,
len(Suit::Diamonds, 5..=5) & points(inv..game),
)
.rule(Bid::new(2, os), 1.0, len(o, 5..=5) & points(inv..game))
.rule(
Bid::new(2, Strain::Clubs),
0.5,
(points(..inv) & (len(Suit::Diamonds, 5..) | len(o, 5..))) | points(inv..game),
)
.alert(GLADIATOR_RELAY)
.rule(Call::Pass, 0.3, hcp(0..))
}
fn gladiator_cue_answer(their_major: Suit) -> Rules {
let o = other_major(their_major);
let m = Strain::from(their_major);
let os = Strain::from(o);
let cheap = if os > m { 2 } else { 3 };
Rules::new()
.rule(Bid::new(cheap, os), 1.4, len(o, 4..) & hcp(15..=16))
.rule(Bid::new(cheap + 1, os), 1.4, len(o, 4..) & hcp(17..=18))
.rule(
Bid::new(2, Strain::Notrump),
1.3,
len(o, ..=3) & hcp(15..=16),
)
.rule(
Bid::new(3, Strain::Notrump),
1.3,
len(o, ..=3) & hcp(17..=18),
)
.rule(Bid::new(3, Strain::Notrump), 1.0, hcp(0..))
}
fn gladiator_relay_rebid() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Diamonds), 1.0, hcp(0..))
.alert(GLADIATOR_RELAY_PC)
}
fn gladiator_relay_continuation(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
let inv = 8u8;
let game = 10u8;
Rules::new()
.rule(
Bid::new(3, Strain::Clubs),
0.95,
len(Suit::Clubs, 6..) & points(inv..game),
)
.rule(
Bid::new(3, Strain::Diamonds),
0.95,
len(Suit::Diamonds, 6..) & points(inv..game),
)
.rule(Bid::new(3, os), 0.95, len(o, 6..) & points(inv..game))
.rule(Bid::new(2, os), 0.9, len(o, 5..) & points(..inv))
.rule(Bid::new(2, Strain::Notrump), 0.85, points(inv..game))
.rule(Call::Pass, 0.5, hcp(0..))
}
fn gladiator_inv_diamond_answer() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.3, hcp(17..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn gladiator_inv_major_answer(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
Rules::new()
.rule(Bid::new(4, os), 1.4, len(o, 3..) & hcp(17..))
.rule(Bid::new(3, Strain::Notrump), 1.2, len(o, ..3) & hcp(17..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn gladiator_club_transfer_rebid() -> Rules {
Rules::new().rule(Bid::new(3, Strain::Clubs), 1.0, hcp(0..))
}
fn gladiator_relay_inv_answer() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.3, hcp(17..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn gladiator_relay_major_answer(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
Rules::new()
.rule(Bid::new(4, os), 1.4, len(o, 3..) & hcp(17..))
.rule(Bid::new(3, Strain::Notrump), 1.2, len(o, ..3) & hcp(17..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn gladiator_gf_major_answer(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
Rules::new().rule(Bid::new(4, os), 1.4, len(o, 3..)).rule(
Bid::new(3, Strain::Notrump),
1.2,
hcp(0..),
)
}
fn gladiator_gf_minor_answer() -> Rules {
Rules::new().rule(Bid::new(3, Strain::Notrump), 1.2, hcp(0..))
}
fn gladiator_cue_min_fit(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
Rules::new()
.rule(Bid::new(4, os), 1.3, points(10..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn gladiator_cue_max_fit_raise(their_major: Suit) -> Rules {
let o = other_major(their_major);
let os = Strain::from(o);
Rules::new().rule(Bid::new(4, os), 1.3, hcp(0..))
}
fn gladiator_cue_min_misfit() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.3, points(10..))
.rule(Call::Pass, 1.0, hcp(0..))
}
fn michaels_advances(t: Suit) -> Rules {
match t {
Suit::Clubs | Suit::Diamonds => {
let hearts_longer = described(
"♥ at least as long as ♠",
|hand: Hand, _: &Context<'_>| hand[Suit::Hearts].len() >= hand[Suit::Spades].len(),
);
let spades_longer = described("♠ longer than ♥", |hand: Hand, _: &Context<'_>| {
hand[Suit::Spades].len() > hand[Suit::Hearts].len()
});
Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.3,
points(10..) & len(Suit::Hearts, 3..) & hearts_longer.clone(),
)
.rule(
Bid::new(4, Strain::Spades),
1.3,
points(10..) & len(Suit::Spades, 3..) & spades_longer.clone(),
)
.rule(Bid::new(2, Strain::Hearts), 1.0, hearts_longer)
.rule(Bid::new(2, Strain::Spades), 1.0, spades_longer)
}
Suit::Hearts => Rules::new()
.rule(
Bid::new(4, Strain::Spades),
1.3,
points(10..) & len(Suit::Spades, 3..),
)
.rule(Bid::new(2, Strain::Spades), 0.5, hcp(0..)),
Suit::Spades => Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.3,
points(10..) & len(Suit::Hearts, 3..),
)
.rule(Bid::new(3, Strain::Hearts), 0.5, hcp(0..)),
}
}
fn leaping_michaels_advances(theirs: Suit, lm: Suit) -> Rules {
match theirs {
Suit::Hearts | Suit::Spades => {
let major = if theirs == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
Rules::new()
.rule(Bid::new(4, Strain::from(major)), 1.3, len(major, 2..))
.rule(Bid::new(5, Strain::from(lm)), 1.2, len(major, 0..=1))
}
Suit::Diamonds => match lm {
Suit::Diamonds => {
let hearts_longer =
described("♥ at least as long as ♠", |h: Hand, _: &Context<'_>| {
h[Suit::Hearts].len() >= h[Suit::Spades].len()
});
let spades_longer = described("♠ longer than ♥", |h: Hand, _: &Context<'_>| {
h[Suit::Spades].len() > h[Suit::Hearts].len()
});
Rules::new()
.rule(Bid::new(4, Strain::Hearts), 1.3, hearts_longer)
.rule(Bid::new(4, Strain::Spades), 1.3, spades_longer)
}
Suit::Clubs => Rules::new()
.rule(
Bid::new(5, Strain::Clubs),
1.2,
len(Suit::Clubs, 3..) & len(Suit::Hearts, 0..=2) & len(Suit::Spades, 0..=2),
)
.rule(Bid::new(4, Strain::Hearts), 1.3, hcp(0..)),
_ => unreachable!("a Leaping Michaels jump is clubs or diamonds"),
},
Suit::Clubs => unreachable!("there is no weak 2♣ opening"),
}
}
fn leaping_michaels_2d_4c_rebid() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Spades), 1.3, len(Suit::Spades, 5..))
.rule(Call::Pass, 1.0, hcp(0..))
}
const fn unusual_suits(t: Suit) -> (Suit, Suit) {
match t {
Suit::Clubs => (Suit::Diamonds, Suit::Hearts),
Suit::Diamonds => (Suit::Clubs, Suit::Hearts),
Suit::Hearts | Suit::Spades => (Suit::Clubs, Suit::Diamonds),
}
}
fn unusual_nt_advances(t: Suit) -> Rules {
let (a, b) = unusual_suits(t);
let a_longer = described(
format!("{a} at least as long as {b}"),
move |hand: Hand, _: &Context<'_>| hand[a].len() >= hand[b].len(),
);
let b_longer = described(
format!("{b} longer than {a}"),
move |hand: Hand, _: &Context<'_>| hand[b].len() > hand[a].len(),
);
Rules::new()
.rule(Bid::new(3, Strain::from(a)), 1.0, a_longer)
.rule(Bid::new(3, Strain::from(b)), 1.0, b_longer)
}
fn responsive_doubles(t: Suit, _raise_lvl: u8) -> Rules {
let mut rules = if matches!(t, Suit::Hearts | Suit::Spades) {
Rules::new()
.rule(
Call::Double,
1.5,
len(Suit::Clubs, 4..) & len(Suit::Diamonds, 4..) & points(8..),
)
.alert(RESPONSIVE)
} else {
Rules::new()
.rule(
Call::Double,
1.5,
len(Suit::Hearts, 4..) & len(Suit::Spades, 4..) & points(8..),
)
.alert(RESPONSIVE)
};
rules = rules.rule(Call::Pass, 0.0, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit == t {
continue;
}
let strain = Strain::from(suit);
for bid_lvl in 2u8..=3 {
rules = rules.rule(
Bid::new(bid_lvl, strain),
1.0,
min_level_is(bid_lvl, strain) & len(suit, 5..) & points(8..),
);
}
}
rules
}
fn responsive_overcall_doubles(open: Suit, overcall: Suit, _raise_lvl: u8) -> Rules {
let mut unbid = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades]
.into_iter()
.filter(|&s| s != open && s != overcall);
let s1 = unbid.next().expect("two suits remain unbid");
let s2 = unbid.next().expect("two suits remain unbid");
Rules::new()
.rule(Call::Double, 1.5, len(s1, 4..) & len(s2, 4..) & points(8..))
.alert(RESPONSIVE)
}
#[must_use]
pub fn defensive() -> Defensive {
let mut d = Defensive::new();
let advance_sohl = advance_sohl_style();
let nt_overcall_book = nt_overcall_systems_on().then(|| {
let mut nt = Trie::new();
super::notrump::register_one_nt(&mut nt);
nt
});
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let theirs = Strain::from(suit);
let opening = Bid::new(1, theirs);
insert_all_seats(&mut d, &[Call::Bid(opening)], 3, defense_to_suit(opening));
let advances = if rich_advance_double_enabled() {
advance_double_rich(opening)
} else {
advance_double(opening)
};
insert_all_seats(
&mut d,
&[Call::Bid(opening), Call::Double, Call::Pass],
3,
advances,
);
if rich_advance_double_enabled() {
let cue = call(2, theirs);
for rho in [Call::Pass, Call::Double] {
let after_cue = [Call::Bid(opening), Call::Double, Call::Pass, cue, rho];
insert_all_seats(&mut d, &after_cue, 3, answer_advance_cue(opening));
for answer in advance_cue_answers(opening) {
for rho2 in [Call::Pass, Call::Double] {
let mut seq = after_cue.to_vec();
seq.push(Call::Bid(answer));
seq.push(rho2);
insert_all_seats(&mut d, &seq, 3, advance_cue_rebid(answer));
}
}
}
if advance_rubens_enabled() {
for (bid, target) in advance_major_transfers(theirs) {
let completion = call(3, Strain::from(target));
for rho in [Call::Pass, Call::Double] {
let after_xfer = [
Call::Bid(opening),
Call::Double,
Call::Pass,
Call::Bid(bid),
rho,
];
insert_all_seats(&mut d, &after_xfer, 3, complete_advance_transfer(target));
let mut rebid = after_xfer.to_vec();
rebid.push(completion);
rebid.push(Call::Pass);
insert_all_seats(&mut d, &rebid, 3, advance_transfer_rebid(target));
}
}
}
}
if matches!(suit, Suit::Hearts | Suit::Spades) && nt_overcall_gladiator() {
let one_nt = call(1, Strain::Notrump);
let base = [Call::Bid(opening), one_nt, Call::Pass];
let p = Call::Pass;
let seq = |tail: &[Call]| -> Vec<Call> {
base.iter().copied().chain(tail.iter().copied()).collect()
};
insert_all_seats(&mut d, &base, 3, gladiator_advances(suit));
let o = other_major(suit);
let os = Strain::from(o);
let cue = call(2, theirs);
insert_all_seats(&mut d, &seq(&[cue, p]), 3, gladiator_cue_answer(suit));
let cheap = if os > theirs { 2 } else { 3 };
insert_all_seats(
&mut d,
&seq(&[cue, p, call(cheap, os), p]),
3,
gladiator_cue_min_fit(suit),
);
insert_all_seats(
&mut d,
&seq(&[cue, p, call(2, Strain::Notrump), p]),
3,
gladiator_cue_min_misfit(),
);
if cheap + 1 < 4 {
insert_all_seats(
&mut d,
&seq(&[cue, p, call(cheap + 1, os), p]),
3,
gladiator_cue_max_fit_raise(suit),
);
}
insert_all_seats(
&mut d,
&seq(&[call(2, Strain::Diamonds), p]),
3,
gladiator_inv_diamond_answer(),
);
insert_all_seats(
&mut d,
&seq(&[call(2, os), p]),
3,
gladiator_inv_major_answer(suit),
);
insert_all_seats(
&mut d,
&seq(&[call(2, Strain::Notrump), p]),
3,
gladiator_club_transfer_rebid(),
);
insert_all_seats(
&mut d,
&seq(&[call(3, Strain::Clubs), p]),
3,
gladiator_gf_minor_answer(),
);
insert_all_seats(
&mut d,
&seq(&[call(3, Strain::Diamonds), p]),
3,
gladiator_gf_minor_answer(),
);
insert_all_seats(
&mut d,
&seq(&[call(3, os), p]),
3,
gladiator_gf_major_answer(suit),
);
insert_all_seats(
&mut d,
&seq(&[call(3, theirs), p]),
3,
gladiator_gf_major_answer(suit),
);
let relay = call(2, Strain::Clubs);
let forced = call(2, Strain::Diamonds);
insert_all_seats(&mut d, &seq(&[relay, p]), 3, gladiator_relay_rebid());
insert_all_seats(
&mut d,
&seq(&[relay, p, forced, p]),
3,
gladiator_relay_continuation(suit),
);
for inv in [
call(2, Strain::Notrump),
call(3, Strain::Clubs),
call(3, Strain::Diamonds),
] {
insert_all_seats(
&mut d,
&seq(&[relay, p, forced, p, inv, p]),
3,
gladiator_relay_inv_answer(),
);
}
insert_all_seats(
&mut d,
&seq(&[relay, p, forced, p, call(3, os), p]),
3,
gladiator_relay_major_answer(suit),
);
} else if let Some(nt) = &nt_overcall_book {
let one_nt = call(1, Strain::Notrump);
for n in 0..=3 {
let prefix: Vec<Call> = core::iter::repeat_n(Call::Pass, n)
.chain([Call::Bid(opening), one_nt])
.collect();
let collisions = d.graft(&prefix, nt, &[one_nt]);
debug_assert!(
collisions.is_empty(),
"1NT-overcall systems-on graft collides at {prefix:?}: {collisions:?}"
);
}
}
let michaels_bid = call(2, theirs);
insert_all_seats(
&mut d,
&[Call::Bid(opening), michaels_bid, Call::Pass],
3,
michaels_advances(suit),
);
let unusual_bid = call(2, Strain::Notrump);
insert_all_seats(
&mut d,
&[Call::Bid(opening), unusual_bid, Call::Pass],
3,
unusual_nt_advances(suit),
);
if responsive_takeout_enabled() {
for raise_lvl in [2u8, 3] {
let raise = call(raise_lvl, theirs);
insert_all_seats(
&mut d,
&[Call::Bid(opening), Call::Double, raise],
3,
responsive_doubles(suit, raise_lvl),
);
}
}
if responsive_overcall_enabled() {
for over in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if over == suit {
continue;
}
let over_lvl = if over > suit { 1 } else { 2 };
let overcall = call(over_lvl, Strain::from(over));
for raise_lvl in [2u8, 3] {
let raise = call(raise_lvl, theirs);
insert_all_seats(
&mut d,
&[Call::Bid(opening), overcall, raise],
3,
responsive_overcall_doubles(suit, over, raise_lvl),
);
}
}
}
}
for suit in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let theirs = Strain::from(suit);
let opening = Bid::new(2, theirs);
insert_all_seats(
&mut d,
&[Call::Bid(opening)],
3,
defense_to_weak_two(opening),
);
insert_advance_of_double(&mut d, suit, opening, advance_sohl);
if leaping_michaels_enabled() {
for lm in [Suit::Clubs, Suit::Diamonds] {
insert_all_seats(
&mut d,
&[Call::Bid(opening), call(4, Strain::from(lm)), Call::Pass],
3,
leaping_michaels_advances(suit, lm),
);
}
if suit == Suit::Diamonds {
insert_all_seats(
&mut d,
&[
Call::Bid(opening),
call(4, Strain::Clubs),
Call::Pass,
call(4, Strain::Hearts),
Call::Pass,
],
3,
leaping_michaels_2d_4c_rebid(),
);
}
}
}
let notrump = call(1, Strain::Notrump);
insert_all_seats(&mut d, &[notrump], 3, defense_to_notrump());
if notrump_balancing_enabled() {
insert_all_seats(
&mut d,
&[notrump, Call::Pass, Call::Pass],
3,
defense_to_notrump(),
);
}
if stayman_defense_enabled() {
insert_all_seats(
&mut d,
&[notrump, Call::Pass, call(2, Strain::Clubs)],
3,
defense_to_their_stayman(),
);
}
if transfer_defense_enabled() {
for (resp, shown) in [(Suit::Diamonds, Suit::Hearts), (Suit::Hearts, Suit::Spades)] {
insert_all_seats(
&mut d,
&[notrump, Call::Pass, call(2, Strain::from(resp))],
3,
defense_to_their_transfer(resp, shown),
);
}
}
if minor_transfer_defense_enabled() {
insert_all_seats(
&mut d,
&[notrump, Call::Pass, call(2, Strain::Spades)],
3,
defense_to_their_minor_transfer(),
);
}
if diamond_transfer_defense_enabled() {
insert_all_seats(
&mut d,
&[notrump, Call::Pass, call(2, Strain::Notrump)],
3,
defense_to_their_diamond_transfer(),
);
}
if landy_range().is_some() || woolsey_enabled() {
let (lo, hi) = woolsey_points();
let landy_2c = call(2, Strain::Clubs);
insert_all_seats(
&mut d,
&[notrump, landy_2c, Call::Pass],
3,
landy_advances(lo),
);
insert_all_seats(
&mut d,
&[
notrump,
landy_2c,
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
],
3,
landy_2d_rebid(),
);
insert_all_seats(
&mut d,
&[
notrump,
landy_2c,
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
],
3,
landy_2nt_rebid(lo, hi),
);
insert_all_seats(
&mut d,
&[notrump, landy_2c, Call::Double],
3,
landy_advances_over_double(lo),
);
insert_all_seats(
&mut d,
&[notrump, landy_2c, Call::Double, Call::Redouble, Call::Pass],
3,
landy_2d_rebid(),
);
insert_all_seats(
&mut d,
&[
notrump,
landy_2c,
Call::Double,
call(2, Strain::Diamonds),
Call::Pass,
],
3,
landy_doubled_2d_rebid(),
);
insert_all_seats(
&mut d,
&[
notrump,
landy_2c,
Call::Double,
call(2, Strain::Notrump),
Call::Pass,
],
3,
landy_2nt_rebid(lo, hi),
);
}
if woolsey_enabled() {
let lo = woolsey_points().0;
let x = Call::Double;
let multi = call(2, Strain::Diamonds);
let hearts = call(2, Strain::Hearts);
let spades = call(2, Strain::Spades);
let clubs = call(2, Strain::Clubs);
let nt2 = call(2, Strain::Notrump);
for rho in [Call::Pass, x] {
insert_all_seats(&mut d, &[notrump, multi, rho], 3, multi_advances(lo));
for after in [Call::Pass, x] {
insert_all_seats(
&mut d,
&[notrump, multi, rho, hearts, after],
3,
multi_2h_rebid(),
);
insert_all_seats(
&mut d,
&[notrump, multi, rho, spades, after],
3,
multi_2s_rebid(),
);
insert_all_seats(
&mut d,
&[notrump, multi, rho, nt2, after],
3,
multi_2nt_rebid(),
);
}
}
for (major, mbid) in [(Suit::Hearts, hearts), (Suit::Spades, spades)] {
insert_all_seats(
&mut d,
&[notrump, mbid, Call::Pass],
3,
muiderberg_advances(major, lo),
);
insert_all_seats(
&mut d,
&[notrump, mbid, x],
3,
muiderberg_advances_doubled(major, lo),
);
for rho in [Call::Pass, x] {
insert_all_seats(
&mut d,
&[notrump, mbid, rho, nt2, Call::Pass],
3,
muiderberg_2nt_rebid(),
);
}
}
let xfloor = woolsey_double_floor();
for adv in [Call::Pass, Call::Redouble] {
insert_all_seats(&mut d, &[notrump, x, adv], 3, woolsey_x_advance(xfloor));
for after in [Call::Pass, x] {
insert_all_seats(
&mut d,
&[notrump, x, adv, clubs, after],
3,
woolsey_x_minor_rebid(),
);
}
insert_all_seats(
&mut d,
&[notrump, x, adv, nt2, Call::Pass],
3,
woolsey_x_2nt_rebid(),
);
}
}
if unusual_notrump_range().is_some() {
insert_all_seats(
&mut d,
&[notrump, call(2, Strain::Notrump), Call::Pass],
3,
unusual_nt_advances(Suit::Spades),
);
insert_all_seats(
&mut d,
&[notrump, call(2, Strain::Notrump), Call::Double],
3,
unusual_nt_advances(Suit::Spades),
);
}
let p = Call::Pass;
let x = Call::Double;
let xx = Call::Redouble;
if direct_dont_enabled() {
let c2 = call(2, Strain::Clubs);
let d2 = call(2, Strain::Diamonds);
let h2 = call(2, Strain::Hearts);
insert_all_seats(&mut d, &[notrump, x, p], 3, passed_dont_x_advance());
insert_all_seats(&mut d, &[notrump, x, p, c2, p], 3, passed_dont_x_rebid());
insert_all_seats(&mut d, &[notrump, c2, p], 3, passed_dont_2c_advance());
insert_all_seats(&mut d, &[notrump, c2, p, d2, p], 3, passed_dont_2c_rebid());
insert_all_seats(&mut d, &[notrump, d2, p], 3, passed_dont_2d_advance());
insert_all_seats(&mut d, &[notrump, d2, p, h2, p], 3, passed_dont_2d_rebid());
insert_all_seats(&mut d, &[notrump, h2, p], 3, passed_dont_2h_advance());
insert_all_seats(&mut d, &[notrump, x, xx], 3, passed_dont_x_advance());
insert_all_seats(&mut d, &[notrump, x, xx, c2, p], 3, passed_dont_x_rebid());
insert_all_seats(&mut d, &[notrump, x, p, c2, x], 3, passed_dont_x_rebid());
insert_all_seats(&mut d, &[notrump, x, xx, c2, x], 3, passed_dont_x_rebid());
}
if meckwell_enabled() {
let c2 = call(2, Strain::Clubs);
let d2 = call(2, Strain::Diamonds);
let h2 = call(2, Strain::Hearts);
insert_all_seats(&mut d, &[notrump, x, p], 3, meckwell_x_advance());
insert_all_seats(&mut d, &[notrump, x, p, c2, p], 3, meckwell_x_rebid());
insert_all_seats(
&mut d,
&[notrump, x, p, c2, p, h2, p],
3,
passed_dont_2h_advance(),
);
insert_all_seats(&mut d, &[notrump, c2, p], 3, passed_dont_2c_advance());
insert_all_seats(&mut d, &[notrump, c2, p, d2, p], 3, passed_dont_2c_rebid());
insert_all_seats(&mut d, &[notrump, d2, p], 3, passed_dont_2d_advance());
insert_all_seats(&mut d, &[notrump, d2, p, h2, p], 3, passed_dont_2d_rebid());
insert_all_seats(&mut d, &[notrump, x, xx], 3, meckwell_x_advance());
insert_all_seats(&mut d, &[notrump, x, xx, c2, p], 3, meckwell_x_rebid());
insert_all_seats(&mut d, &[notrump, x, p, c2, x], 3, meckwell_x_rebid());
insert_all_seats(&mut d, &[notrump, x, xx, c2, x], 3, meckwell_x_rebid());
insert_all_seats(
&mut d,
&[notrump, x, p, c2, p, h2, x],
3,
passed_dont_2h_advance(),
);
}
if direct_landy_double().is_some() {
let (lo, hi) = (direct_landy_double_floor(), 37u8);
let d2 = call(2, Strain::Diamonds);
let nt2 = call(2, Strain::Notrump);
insert_all_seats(&mut d, &[notrump, x, p], 3, both_majors_x_advance(lo));
insert_all_seats(&mut d, &[notrump, x, p, d2, p], 3, landy_2d_rebid());
insert_all_seats(&mut d, &[notrump, x, p, d2, x], 3, landy_2d_rebid());
insert_all_seats(&mut d, &[notrump, x, p, nt2, p], 3, landy_2nt_rebid(lo, hi));
insert_all_seats(&mut d, &[notrump, x, p, nt2, x], 3, landy_2nt_rebid(lo, hi));
insert_all_seats(&mut d, &[notrump, x, xx], 3, both_majors_x_runout(lo));
insert_all_seats(&mut d, &[notrump, x, xx, p, p], 3, landy_2d_rebid());
for m in [call(2, Strain::Hearts), call(2, Strain::Spades)] {
insert_all_seats(&mut d, &[notrump, x, xx, p, p, m, p], 3, sit());
insert_all_seats(&mut d, &[notrump, x, xx, p, p, m, x], 3, sit());
}
for relay in [x, p] {
for m in [call(2, Strain::Hearts), call(2, Strain::Spades)] {
insert_all_seats(&mut d, &[notrump, x, p, d2, relay, m, x, p, p], 3, sit());
}
}
}
d
}
#[cfg(test)]
mod tests {
use crate::bidding::Family;
use crate::bidding::american::{
LebensohlStyle, american, set_advance_sohl_style, set_always_pass_defense, set_direct_dont,
set_direct_landy_double, set_leaping_michaels, set_meckwell, set_unusual_notrump_defense,
set_woolsey, set_woolsey_double_floor, set_woolsey_points,
};
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())
}
#[test]
fn two_level_minor_overcall_tight_gates_the_minimum() {
let over_1s = [call(1, Strain::Spades)];
let minimum = "J2.K2.Q432.AQ876"; let (default_call, floored) = best_call(&over_1s, minimum);
assert_eq!(default_call, call(2, Strain::Clubs), "default overcalls 2♣");
assert!(!floored, "the 2♣ overcall is a book node");
super::set_two_level_minor_overcall_tight(true);
let (tight_call, _) = best_call(&over_1s, minimum);
let (strong_call, _) = best_call(&over_1s, "A2.K2.Q432.AKJ87"); super::set_two_level_minor_overcall_tight(false);
assert_eq!(
tight_call,
Call::Pass,
"tight strands the minimum into Pass"
);
assert_ne!(
strong_call,
Call::Pass,
"a 17-count still competes, not silenced"
);
}
#[test]
fn gladiator_club_three_way() {
super::set_nt_overcall_gladiator(true);
let s = || call(1, Strain::Spades);
let nt = || call(1, Strain::Notrump);
let p = Call::Pass;
let c = |n| call(n, Strain::Clubs);
let d2 = call(2, Strain::Diamonds);
let (weak, _) = best_call(&[s(), nt(), p], "43.72.852.KJ9876"); let (complete, _) = best_call(
&[s(), nt(), p, call(2, Strain::Notrump), p],
"AQ4.KQ4.AK92.65", );
let (gf, _) = best_call(&[s(), nt(), p], "A3.K2.42.AKQ9876"); let (relay, _) = best_call(&[s(), nt(), p], "43.72.K5.KQ9876"); let (pull, _) = best_call(&[s(), nt(), p, c(2), p, d2, p], "43.72.K5.KQ9876");
super::set_nt_overcall_gladiator(false);
assert_eq!(weak, call(2, Strain::Notrump), "weak 6♣ transfers via 2NT");
assert_eq!(complete, c(3), "overcaller completes the club transfer");
assert_eq!(gf, c(3), "game-forcing clubs bid 3♣ directly");
assert_eq!(relay, c(2), "invitational 6♣ starts with the 2♣ relay");
assert_eq!(pull, c(3), "invitational 6♣ pulls to 3♣ over the forced 2♦");
}
#[test]
fn nt_overcall_no_major_routes_five_card_major_to_the_suit() {
let over_1d = [call(1, Strain::Diamonds)];
let five_heart = "32.KQJ82.KQ4.A32"; let (default_call, _) = best_call(&over_1d, five_heart);
assert_eq!(
default_call,
call(1, Strain::Notrump),
"default buries the major in 1NT"
);
super::set_nt_overcall_no_major(true);
let (gated_call, _) = best_call(&over_1d, five_heart);
let (flat_call, _) = best_call(&over_1d, "A32.KQ4.KQ4.J432"); super::set_nt_overcall_no_major(false);
assert_eq!(
gated_call,
call(1, Strain::Hearts),
"5-card major overcalls the suit"
);
assert_eq!(
flat_call,
call(1, Strain::Notrump),
"no 5M still overcalls 1NT"
);
}
#[test]
fn nt_overcall_systems_on_grafts_the_1nt_structure() {
super::set_nt_overcall_systems_on(true);
let d = || call(1, Strain::Diamonds);
let nt = || call(1, Strain::Notrump);
let (stayman, floored) = best_call(
&[d(), nt(), Call::Pass],
"A432.KQ84.32.QJ4", );
let (transfer, _) = best_call(
&[d(), nt(), Call::Pass],
"KQ432.K84.32.QJ4", );
let (answer, _) = best_call(
&[d(), nt(), Call::Pass, call(2, Strain::Clubs), Call::Pass],
"Q3.KJ84.AQ54.KQ2", );
super::set_nt_overcall_systems_on(false);
assert_eq!(stayman, call(2, Strain::Clubs), "advancer bids 2♣ Stayman");
assert!(
!floored,
"the grafted Stayman is a book node, not the floor"
);
assert_eq!(
transfer,
call(2, Strain::Hearts),
"a five-card spade suit transfers"
);
assert_eq!(answer, call(2, Strain::Hearts), "overcaller shows 4 hearts");
}
#[test]
fn gladiator_replaces_the_major_graft() {
super::set_nt_overcall_gladiator(true);
let s = || call(1, Strain::Spades);
let nt = || call(1, Strain::Notrump);
let (cue, floored) = best_call(
&[s(), nt(), Call::Pass],
"K84.KQ84.QJ32.42", );
let (relay, _) = best_call(
&[s(), nt(), Call::Pass],
"432.J8.QJ543.J32", );
let (flat, _) = best_call(
&[s(), nt(), Call::Pass],
"432.J84.J543.J32", );
let (answer, _) = best_call(
&[s(), nt(), Call::Pass, call(2, Strain::Spades), Call::Pass],
"AQ.KQ84.AQ54.J32", );
super::set_nt_overcall_gladiator(false);
assert_eq!(
cue,
call(2, Strain::Spades),
"advancer cues 2♠ = Stayman for hearts"
);
assert!(!floored, "the Gladiator cue is a book node, not the floor");
assert_eq!(
relay,
call(2, Strain::Clubs),
"a weak hand with a 5-card escape suit bids the 2♣ relay"
);
assert_eq!(
flat,
Call::Pass,
"a flat weak hand passes 1NT, not the relay"
);
assert_eq!(
answer,
call(4, Strain::Hearts),
"overcaller jumps to 4♥ with a maximum fit"
);
}
#[test]
fn gladiator_continuations_reach_game() {
super::set_nt_overcall_gladiator(true);
let s = || call(1, Strain::Spades);
let nt = || call(1, Strain::Notrump);
let h = |n| call(n, Strain::Hearts);
let (place, _) = best_call(
&[
s(),
nt(),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
h(3),
Call::Pass,
],
"K84.KQ84.KJ32.42", );
let (raise, floored) = best_call(
&[s(), nt(), Call::Pass, h(3), Call::Pass],
"AQ2.KQ8.AQ54.K93", );
super::set_nt_overcall_gladiator(false);
assert_eq!(place, h(4), "GF advancer raises the min fit to 4♥");
assert!(
!floored,
"the overcaller's raise is a book node, not the floor"
);
assert_eq!(raise, h(4), "overcaller raises the game-forcing 3♥ to 4♥");
}
#[test]
fn landy_range_feeds_the_shared_woolsey_band() {
super::set_landy(Some((9, 16)));
assert_eq!(
super::woolsey_points(),
(9, 16),
"a Landy range sets the shared band"
);
set_woolsey_points(7, 18);
super::set_landy(None);
assert_eq!(
super::woolsey_points(),
(7, 18),
"set_landy(None) leaves the band alone"
);
set_woolsey_points(8, 19);
}
#[test]
fn defense_to_notrump_authors_one_rule_per_call() {
fn reset() {
super::set_woolsey(false);
super::set_direct_dont(false);
super::set_meckwell(false);
super::set_direct_landy_double(None);
super::set_landy(None);
super::set_natural_defense(true);
super::set_always_pass_defense(false);
super::set_unusual_notrump_defense(Some((8, 13)));
}
let configs: [(&str, fn()); 7] = [
("natural+unusual2nt", || {}),
("natural+landy", || super::set_landy(Some((8, 15)))),
("woolsey", || super::set_woolsey(true)),
("dont", || super::set_direct_dont(true)),
("meckwell", || super::set_meckwell(true)),
("direct-landy-x", || {
super::set_direct_landy_double(Some(false))
}),
("always-pass", || super::set_always_pass_defense(true)),
];
for (label, setup) in configs {
reset();
setup();
let calls: Vec<Call> = super::defense_to_notrump()
.rules()
.iter()
.map(|r| r.call())
.collect();
reset();
assert!(
calls.contains(&Call::Pass),
"{label}: the owning Pass is missing",
);
for i in 0..calls.len() {
for j in (i + 1)..calls.len() {
assert!(
calls[i] != calls[j],
"{label}: call {:?} authored by two rules at the [1NT] node",
calls[i],
);
}
}
}
}
fn advance(style: LebensohlStyle, auction: &[Call], hand: &str) -> (Call, bool) {
set_advance_sohl_style(style);
best_call(auction, hand)
}
fn over_2d() -> [Call; 3] {
[call(2, Strain::Diamonds), Call::Double, Call::Pass]
}
#[test]
fn off_keeps_the_flat_advance_no_relay() {
let (c, _) = advance(LebensohlStyle::Off, &over_2d(), "32.43.32.KQ9876");
assert_eq!(c, call(3, Strain::Clubs));
}
#[test]
fn plain_weak_long_suit_relays_then_completes() {
let (c, floored) = advance(LebensohlStyle::Plain, &over_2d(), "J2.43.32.KQ9876");
assert_eq!(c, call(2, Strain::Notrump));
assert!(!floored, "the relay must come from the book");
let relayed = [
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
let (completion, _) = advance(LebensohlStyle::Plain, &relayed, "AKJ2.KQ52.4.A532");
assert_eq!(completion, call(3, Strain::Clubs));
}
#[test]
fn plain_forcing_three_level_is_a_book_node() {
let (c, floored) = advance(LebensohlStyle::Plain, &over_2d(), "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 transfer_shows_spades_through_their_hearts() {
let over_2h = [call(2, Strain::Hearts), Call::Double, Call::Pass];
let (c, floored) = advance(LebensohlStyle::Transfer, &over_2h, "AKQ65.43.K32.J32");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the transfer must come from the book");
}
#[test]
fn transfer_doubler_bids_game_not_partscore() {
let auction = [
call(2, Strain::Hearts),
Call::Double,
Call::Pass,
call(3, Strain::Diamonds),
Call::Pass,
];
let (c, _) = advance(LebensohlStyle::Transfer, &auction, "AK52.4.A432.K432");
assert_eq!(c, call(4, Strain::Spades));
}
#[test]
fn transfer_cue_is_stayman() {
let auction = [
call(2, Strain::Hearts),
Call::Double,
Call::Pass,
call(3, Strain::Hearts),
Call::Pass,
];
let (c, floored) = advance(LebensohlStyle::Transfer, &auction, "AQ32.K32.4.KJ432");
assert_eq!(c, call(3, Strain::Spades));
assert!(!floored, "the Stayman answer must come from the book");
}
#[test]
fn penalty_pass_sits_for_the_double() {
let over_2s = [call(2, Strain::Spades), Call::Double, Call::Pass];
let (c, floored) = advance(LebensohlStyle::Plain, &over_2s, "KQJ95.J32.432.32");
assert_eq!(c, Call::Pass);
assert!(!floored, "the sign-off Pass must come from the book node");
}
#[test]
fn transfer_over_2d_is_three_club_stayman() {
let (c, floored) = advance(LebensohlStyle::Transfer, &over_2d(), "AQ32.KJ32.A2.432");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the Stayman bid must come from the book");
}
#[test]
fn always_pass_defense_passes_over_1nt() {
let over_1nt = [call(1, Strain::Notrump)];
set_always_pass_defense(true);
let (c, floored) = best_call(&over_1nt, "AQ32.KQ3.K32.Q32");
set_always_pass_defense(false);
assert_eq!(c, Call::Pass);
assert!(!floored, "the always-pass must come from the book node");
}
#[test]
fn suppress_flat_4333_takeout_routes_to_pass() {
let over_1d = [call(1, Strain::Diamonds)];
let hand = "AKxx.Qxx.Qxx.Qxx";
crate::bidding::constraint::set_suppress_flat_4333_takeout(false);
let (off, _) = best_call(&over_1d, hand);
assert_eq!(off, Call::Double, "flat 4333 doubles by default (knob off)");
crate::bidding::constraint::set_suppress_flat_4333_takeout(true);
let (on, _) = best_call(&over_1d, hand);
crate::bidding::constraint::set_suppress_flat_4333_takeout(false);
assert_ne!(
on,
Call::Double,
"knob on suppresses the takeout double on a weak flat 4333",
);
}
#[test]
fn suppress_4432_vs_major_routes_to_pass() {
let over_1h = [call(1, Strain::Hearts)];
let hand = "KQxx.xx.xxx.AKxx";
let (off, _) = best_call(&over_1h, hand);
assert_eq!(off, Call::Double, "4432 vs a major doubles by default");
crate::bidding::constraint::set_suppress_4432_vs_minor(true);
let (minor, _) = best_call(&over_1h, hand);
crate::bidding::constraint::set_suppress_4432_vs_minor(false);
assert_eq!(
minor,
Call::Double,
"the vs-minor knob leaves a major opening"
);
crate::bidding::constraint::set_suppress_4432_vs_major(true);
let (on, _) = best_call(&over_1h, hand);
crate::bidding::constraint::set_suppress_4432_vs_major(false);
assert_ne!(on, Call::Double, "vs-major knob suppresses the 4432 double");
}
#[test]
fn suppress_4432_vs_minor_routes_to_pass() {
let over_1c = [call(1, Strain::Clubs)];
let hand = "AKxx.KQxx.xxx.xx";
let (off, _) = best_call(&over_1c, hand);
assert_eq!(off, Call::Double, "4432 vs a minor doubles by default");
crate::bidding::constraint::set_suppress_4432_vs_major(true);
let (major, _) = best_call(&over_1c, hand);
crate::bidding::constraint::set_suppress_4432_vs_major(false);
assert_eq!(
major,
Call::Double,
"the vs-major knob leaves a minor opening"
);
crate::bidding::constraint::set_suppress_4432_vs_minor(true);
let (on, _) = best_call(&over_1c, hand);
crate::bidding::constraint::set_suppress_4432_vs_minor(false);
assert_ne!(on, Call::Double, "vs-minor knob suppresses the 4432 double");
}
#[test]
fn suppress_5332_takeout_bids_the_suit() {
let over_1d = [call(1, Strain::Diamonds)];
let hand = "AQx.KJx.xx.QT9xx";
crate::bidding::constraint::set_suppress_5332_takeout(false);
let (off, _) = best_call(&over_1d, hand);
assert_eq!(off, Call::Double, "5332 doubles with the knob off");
crate::bidding::constraint::set_suppress_5332_takeout(true);
let (on, _) = best_call(&over_1d, hand);
crate::bidding::constraint::set_suppress_5332_takeout(false);
assert_ne!(
on,
Call::Double,
"the default suppresses the 5332 takeout double"
);
}
#[test]
fn rich_advance_double_cues_and_forces() {
let auction = [call(1, Strain::Hearts), Call::Double, Call::Pass];
let force = "AQxx.xxx.AQx.Kxx";
let broke = "xxx.xxxx.xxx.xxx";
super::set_rich_advance_double(true);
let (cued, _) = best_call(&auction, force);
let (forced, _) = best_call(&auction, broke);
super::set_rich_advance_double(false);
let (flat_force, _) = best_call(&auction, force);
assert_eq!(
cued,
call(2, Strain::Hearts),
"a game force with no limited natural home cues opener's suit"
);
assert!(
matches!(forced, Call::Bid(_)),
"a broke advancer bids rather than passing partner's takeout (got {forced:?})"
);
assert_ne!(
flat_force,
call(2, Strain::Hearts),
"the flat floor has no cue-bid advance"
);
}
#[test]
fn advance_cue_rebid_forces_or_invites() {
let auction = [
call(1, Strain::Hearts),
Call::Double,
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let force = "Axx.xxx.AKQx.Qxx";
let invite = "Axx.xxx.AJxx.xxx";
super::set_rich_advance_double(true);
let (driven, _) = best_call(&auction, force);
let (rested, _) = best_call(&auction, invite);
super::set_rich_advance_double(false);
assert_eq!(
driven,
call(4, Strain::Spades),
"a game-forcing advancer raises the cue answer to game"
);
assert_eq!(
rested,
Call::Pass,
"an invitational advancer passes the doubler's minimum"
);
}
#[test]
fn rich_advance_weak_shapely_blasts_game() {
let auction = [call(1, Strain::Spades), Call::Double, Call::Pass];
let weak = "T3.AT9753.5.KQT7";
super::set_rich_advance_double(true);
let (blast, _) = best_call(&auction, weak);
super::set_rich_advance_double(false);
assert_eq!(
blast,
call(4, Strain::Hearts),
"a shapely weak long-major hand blasts the two-way 4M, not a quiet 2-level"
);
}
#[test]
fn rubens_transfer_completes_into_the_major() {
super::set_rich_advance_double(true);
super::set_advance_rubens(true);
let advancer = "KQJ42.xx.KJx.xxx"; for open in [Strain::Clubs, Strain::Diamonds, Strain::Hearts] {
let start = [call(1, open), Call::Double, Call::Pass];
let (xfer, _) = best_call(&start, advancer);
assert_eq!(
xfer,
call(3, Strain::Hearts),
"5-spade INV+ transfers via 3♥ over (1{open:?})"
);
let after = [
call(1, open),
Call::Double,
Call::Pass,
call(3, Strain::Hearts),
Call::Pass,
];
let (complete, floored) = best_call(&after, "AKx.xxx.Axxx.xxx");
assert_eq!(
complete,
call(3, Strain::Spades),
"doubler completes the transfer into spades over (1{open:?})"
);
assert!(
!floored,
"the completion must come from the book, not the floor"
);
}
super::set_advance_rubens(false);
super::set_rich_advance_double(false);
}
fn woolsey(auction: &[Call], hand: &str) -> (Call, bool) {
set_always_pass_defense(false);
set_unusual_notrump_defense(None);
set_woolsey_points(9, 19);
set_woolsey_double_floor(11);
set_woolsey(true);
let result = best_call(auction, hand);
set_woolsey(false);
result
}
#[test]
fn woolsey_direct_seat_routes_every_shape() {
let over_1nt = [call(1, Strain::Notrump)];
let (multi, floored) = woolsey(&over_1nt, "32.KQJ987.A32.32");
assert_eq!(multi, call(2, Strain::Diamonds));
assert!(
!floored,
"the Woolsey overcall must come from the book node"
);
assert_eq!(
woolsey(&over_1nt, "AJ987.KQ32.32.32").0,
call(2, Strain::Clubs)
);
assert_eq!(
woolsey(&over_1nt, "32.AQJ98.K987.2").0,
call(2, Strain::Hearts)
);
assert_eq!(woolsey(&over_1nt, "AKQ8.32.KJ987.32").0, Call::Double);
}
#[test]
fn woolsey_has_no_penalty_double() {
let over_1nt = [call(1, Strain::Notrump)];
let (strong, floored) = woolsey(&over_1nt, "AQ32.KQ3.KQ3.AQ2");
assert_eq!(strong, Call::Pass);
assert!(!floored, "the settling Pass must come from the book node");
assert_eq!(woolsey(&over_1nt, "AKJ32.K32.Q32.32").0, Call::Pass);
}
#[test]
fn woolsey_multi_advance_pass_or_corrects() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = woolsey(&auction, "32.K32.J32.J5432");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "the Multi advance must come from the book node");
}
#[test]
fn woolsey_x_advance_never_sits_for_penalty() {
let auction = [call(1, Strain::Notrump), Call::Double, Call::Pass];
let (relay, floored) = woolsey(&auction, "432.432.432.5432");
assert_eq!(relay, call(2, Strain::Clubs));
assert!(!floored, "the X advance must come from the book node");
assert_eq!(
woolsey(&auction, "KQ982.32.432.432").0,
call(2, Strain::Spades)
);
}
#[test]
fn woolsey_muiderberg_advance_raises_and_asks() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
Call::Pass,
];
let (raise, floored) = woolsey(&auction, "32.K54.AK32.AQ32");
assert_eq!(raise, call(4, Strain::Hearts));
assert!(
!floored,
"the Muiderberg advance must come from the book node"
);
assert_eq!(
woolsey(&auction, "KQJ2.2.K432.Q432").0,
call(2, Strain::Notrump)
);
}
#[test]
fn woolsey_muiderberg_doubled_escapes_a_misfit() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
Call::Double,
];
let (escape, floored) = woolsey(&auction, "Q432.2.J432.J432");
assert_eq!(escape, call(2, Strain::Notrump));
assert!(!floored, "the doubled escape must come from the book node");
assert_eq!(woolsey(&auction, "Q43.K52.J432.432").0, Call::Pass);
}
#[test]
fn woolsey_muiderberg_2nt_names_the_minor() {
let asked = [
call(1, Strain::Notrump),
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(
woolsey(&asked, "2.AKJ32.Q432.32").0,
call(3, Strain::Diamonds)
);
assert_eq!(woolsey(&asked, "2.AKJ32.32.Q432").0, call(3, Strain::Clubs));
}
#[test]
fn transfer_over_2h_is_plain_cohen() {
let over_2h = [call(2, Strain::Hearts), Call::Double, Call::Pass];
let (c, floored) = advance(LebensohlStyle::Transfer, &over_2h, "AKQ65.43.K32.J32");
assert_eq!(c, call(3, Strain::Diamonds));
assert!(!floored, "the transfer must come from the book");
}
fn leaping(on: bool, auction: &[Call], hand: &str) -> (Call, bool) {
set_advance_sohl_style(LebensohlStyle::Off);
set_leaping_michaels(on);
best_call(auction, hand)
}
#[test]
fn leaping_michaels_minor_plus_other_major_over_a_major() {
let over_2h = [call(2, Strain::Hearts)];
let (c, floored) = leaping(true, &over_2h, "AKQ65.4.32.KQJ76");
assert_eq!(c, call(4, Strain::Clubs));
assert!(!floored, "Leaping Michaels must come from the book node");
let (d, _) = leaping(true, &over_2h, "AKQ65.4.KQJ76.32");
assert_eq!(d, call(4, Strain::Diamonds));
}
#[test]
fn leaping_michaels_cue_shows_both_majors_over_2d() {
let over_2d = [call(2, Strain::Diamonds)];
let (c, floored) = leaping(true, &over_2d, "AKQ65.KQJ76.4.32");
assert_eq!(c, call(4, Strain::Diamonds));
assert!(!floored, "Leaping Michaels must come from the book node");
}
#[test]
fn leaping_michaels_advancer_picks_the_major_game() {
let auction = [call(2, Strain::Hearts), call(4, Strain::Clubs), Call::Pass];
let (fit, floored) = leaping(true, &auction, "KQ7.32.J865.A432");
assert_eq!(fit, call(4, Strain::Spades));
assert!(!floored, "the advance must come from the book node");
let (thin, _) = leaping(true, &auction, "K7.QJ32.8654.A32");
assert_eq!(thin, call(4, Strain::Spades));
let (no_fit, _) = leaping(true, &auction, "2.QJ32.J8654.KQ4");
assert_eq!(no_fit, call(5, Strain::Clubs));
}
#[test]
fn leaping_michaels_advancer_picks_longer_major_over_2d_cue() {
let auction = [
call(2, Strain::Diamonds),
call(4, Strain::Diamonds),
Call::Pass,
];
let (c, floored) = leaping(true, &auction, "AQ32.K8.654.9432");
assert_eq!(c, call(4, Strain::Spades));
assert!(!floored, "the advance must come from the book node");
}
#[test]
fn leaping_michaels_2d_4c_pass_or_correct() {
let advance = [
call(2, Strain::Diamonds),
call(4, Strain::Clubs),
Call::Pass,
];
let (relay, _) = leaping(true, &advance, "K32.A87.9654.J32");
assert_eq!(relay, call(4, Strain::Hearts));
let rebid = [
call(2, Strain::Diamonds),
call(4, Strain::Clubs),
Call::Pass,
call(4, Strain::Hearts),
Call::Pass,
];
let (correct, _) = leaping(true, &rebid, "AKQ65.4.32.KQJ76");
assert_eq!(correct, call(4, Strain::Spades));
}
#[test]
fn leaping_michaels_silent_when_disabled() {
let over_2h = [call(2, Strain::Hearts)];
let (c, _) = leaping(false, &over_2h, "AKQ65.4.32.KQJ76");
assert_ne!(c, call(4, Strain::Clubs));
}
fn direct_dont(auction: &[Call], hand: &str) -> (Call, bool) {
let prev = super::direct_dont_enabled();
set_direct_dont(true);
let result = best_call(auction, hand);
set_direct_dont(prev);
result
}
#[test]
fn direct_dont_replaces_the_penalty_double() {
let over_1nt = [call(1, Strain::Notrump)];
let (c, floored) = direct_dont(&over_1nt, "KJ32.32.4.AQ876");
assert_eq!(c, call(2, Strain::Clubs));
assert!(!floored, "DONT 2♣ must come from the book node");
let (c, _) = direct_dont(&over_1nt, "32.KJ32.AQ876.4");
assert_eq!(c, call(2, Strain::Diamonds));
let (c, _) = direct_dont(&over_1nt, "AJ932.K842.32.32");
assert_eq!(c, call(2, Strain::Hearts));
let (c, _) = direct_dont(&over_1nt, "AKJ87.432.32.432");
assert_eq!(c, call(2, Strain::Spades));
let (c, _) = direct_dont(&over_1nt, "432.AKJ87.32.432");
assert_eq!(c, Call::Double);
let (c, _) = direct_dont(&over_1nt, "AKQ2.KQ2.KJ2.432");
assert_eq!(c, Call::Pass);
}
#[test]
fn direct_dont_one_suiter_double_relays_then_names() {
let nt = call(1, Strain::Notrump);
let p = Call::Pass;
let prev = super::direct_dont_enabled();
set_direct_dont(true);
let (relay, floored) = best_call(&[nt, Call::Double, p], "Q32.Q32.Q432.432");
let after_relay = [nt, Call::Double, p, call(2, Strain::Clubs), p];
let (name, _) = best_call(&after_relay, "432.AKJ87.32.432");
let (escape, esc_floored) =
best_call(&[nt, Call::Double, Call::Redouble], "Q32.Q32.Q432.432");
let relay_doubled = [
nt,
Call::Double,
Call::Redouble,
call(2, Strain::Clubs),
Call::Double,
];
let (named, nd_floored) = best_call(&relay_doubled, "432.AKJ87.32.432");
set_direct_dont(prev);
assert_eq!(relay, call(2, Strain::Clubs));
assert!(!floored, "the direct-seat relay must come from the book");
assert_eq!(name, call(2, Strain::Hearts));
assert_eq!(escape, call(2, Strain::Clubs), "must escape 1NTxx, not sit");
assert!(!esc_floored, "the redouble escape must come from the book");
assert_eq!(
named,
call(2, Strain::Hearts),
"must escape 2♣x to the real suit"
);
assert!(
!nd_floored,
"the doubled-relay escape must come from the book"
);
}
fn meckwell(auction: &[Call], hand: &str) -> (Call, bool) {
let prev = super::meckwell_enabled();
set_meckwell(true);
let result = best_call(auction, hand);
set_meckwell(prev);
result
}
#[test]
fn meckwell_overcalls_replace_the_penalty_double() {
let over_1nt = [call(1, Strain::Notrump)];
let (c, floored) = meckwell(&over_1nt, "32.32.432.AKQ876");
assert_eq!(c, Call::Double);
assert!(!floored, "Meckwell X must come from the book node");
let (c, _) = meckwell(&over_1nt, "AJ32.KQ876.32.32");
assert_eq!(c, Call::Double);
let (c, floored) = meckwell(&over_1nt, "KJ32.32.4.AQ876");
assert_eq!(c, call(2, Strain::Clubs));
assert!(!floored, "Meckwell 2♣ must come from the book node");
let (c, _) = meckwell(&over_1nt, "32.KJ32.AQ876.4");
assert_eq!(c, call(2, Strain::Diamonds));
let (c, floored) = meckwell(&over_1nt, "32.AKJ876.432.32");
assert_eq!(c, call(2, Strain::Hearts));
assert!(!floored, "natural 2♥ must come from the book node");
let (c, _) = meckwell(&over_1nt, "AKJ876.32.432.32");
assert_eq!(c, call(2, Strain::Spades));
let (c, _) = meckwell(&over_1nt, "3.3.AJ876.KQ876");
assert_eq!(c, call(2, Strain::Notrump));
let (c, _) = meckwell(&over_1nt, "AKQ2.KQ2.KJ2.432");
assert_eq!(c, Call::Pass);
}
#[test]
fn meckwell_two_way_double_relays_then_names() {
let nt = call(1, Strain::Notrump);
let p = Call::Pass;
let c2 = call(2, Strain::Clubs);
let prev = super::meckwell_enabled();
set_meckwell(true);
let (relay, relay_floored) = best_call(&[nt, Call::Double, p], "Q32.Q32.Q432.432");
let (diamonds, _) = best_call(&[nt, Call::Double, p, c2, p], "32.32.AKQ876.432");
let (majors, majors_floored) = best_call(&[nt, Call::Double, p, c2, p], "AJ32.KQ87.32.32");
let (clubs, _) = best_call(&[nt, Call::Double, p, c2, p], "32.32.432.AKQ876");
let (escape, esc_floored) =
best_call(&[nt, Call::Double, Call::Redouble], "Q32.Q32.Q432.432");
let (named, nd_floored) =
best_call(&[nt, Call::Double, p, c2, Call::Double], "32.32.AKQ876.432");
set_meckwell(prev);
assert_eq!(relay, c2, "advancer relays 2♣ over the two-way X");
assert!(!relay_floored, "the relay must come from the book");
assert_eq!(
diamonds,
call(2, Strain::Diamonds),
"diamond one-suiter names 2♦"
);
assert_eq!(majors, call(2, Strain::Hearts), "both majors shown as 2♥");
assert!(
!majors_floored,
"the both-majors show must come from the book"
);
assert_eq!(clubs, Call::Pass, "club one-suiter passes to play 2♣");
assert_eq!(escape, c2, "must escape 1NTxx with the relay, not sit");
assert!(!esc_floored, "the redouble escape must come from the book");
assert_eq!(
named,
call(2, Strain::Diamonds),
"must escape 2♣x to real diamonds"
);
assert!(
!nd_floored,
"the doubled-relay escape must come from the book"
);
}
#[test]
fn direct_landy_double_shows_both_majors_and_runs_clean() {
let nt = call(1, Strain::Notrump);
let p = Call::Pass;
let x = Call::Double;
let xx = Call::Redouble;
let d2 = call(2, Strain::Diamonds);
let prev = super::direct_landy_double();
let prev_floor = super::direct_landy_double_floor();
set_direct_landy_double(Some(false)); super::set_direct_landy_double_floor(8);
let (dbl, floored) = best_call(&[nt], "AJ32.KQ876.32.32");
let (pass, _) = best_call(&[nt], "AKQ2.KQ2.KJ2.432");
let (relay, relay_floored) = best_call(&[nt, x, p], "Q32.Q43.J432.432");
let (named, named_floored) = best_call(&[nt, x, p, d2, x], "AJ32.KQ876.32.32");
let (ask, ask_floored) = best_call(&[nt, x, xx], "Q32.Q43.J432.432");
let (clubs, _) = best_call(&[nt, x, xx], "32.43.432.AKQ876");
let (named_xx, named_xx_floored) = best_call(&[nt, x, xx, p, p], "AJ32.KQ876.32.32");
let sit_auction = [nt, x, p, d2, x, call(2, Strain::Hearts), x, p, p];
let (settle, settle_floored) = best_call(&sit_auction, "AJ32.KQ876.32.32");
set_direct_landy_double(prev);
super::set_direct_landy_double_floor(prev_floor);
assert_eq!(ask, Call::Pass, "equal majors over XX → Pass = ask back");
assert!(!ask_floored, "the ask-Pass must come from the book");
assert_eq!(
clubs,
call(2, Strain::Clubs),
"long clubs over XX → 2♣ to play"
);
assert_eq!(
named_xx,
call(2, Strain::Hearts),
"doubler names its major after the ask"
);
assert!(!named_xx_floored, "the named major must come from the book");
assert_eq!(
settle,
Call::Pass,
"must sit in our doubled major, not run to 3♦"
);
assert!(!settle_floored, "the settle-Pass must come from the book");
assert_eq!(dbl, Call::Double);
assert!(!floored, "the both-majors X must come from the book node");
assert_eq!(pass, Call::Pass, "no penalty double when it is replaced");
assert_eq!(relay, d2, "weak equal majors relays 2♦");
assert!(!relay_floored, "the relay must come from the book");
assert_eq!(
named,
call(2, Strain::Hearts),
"must pull from the doubled 2♦ relay"
);
assert!(
!named_floored,
"the doubled-relay escape must come from the book"
);
}
#[test]
fn direct_landy_penalty_pass_defends_1ntx() {
let nt = call(1, Strain::Notrump);
let p = Call::Pass;
let x = Call::Double;
let prev = super::direct_landy_double();
let prev_pen = super::direct_landy_penalty_pass();
let prev_floor = super::direct_landy_double_floor();
set_direct_landy_double(Some(false)); super::set_direct_landy_double_floor(8);
let defensive = "AQ.KQ.QJ876.K432"; super::set_direct_landy_penalty_pass(false);
let (forced, _) = best_call(&[nt, x, p], defensive);
super::set_direct_landy_penalty_pass(true);
let (penalty, pen_floored) = best_call(&[nt, x, p], defensive);
let (with_fit, _) = best_call(&[nt, x, p], "QJ32.K.QJ876.K43");
set_direct_landy_double(prev);
super::set_direct_landy_penalty_pass(prev_pen);
super::set_direct_landy_double_floor(prev_floor);
assert_ne!(forced, Call::Pass, "knob off: advancer is forced to bid");
assert_eq!(
penalty,
Call::Pass,
"knob on, no fit + values → pass for penalty"
);
assert!(!pen_floored, "the penalty pass must come from the book");
assert_ne!(
with_fit,
Call::Pass,
"a major fit still bids, never penalty-passes"
);
}
#[test]
fn doubled_unusual_2nt_runs_never_sits() {
let auction = [
call(1, Strain::Notrump),
call(2, Strain::Notrump),
Call::Double,
];
let (c, floored) = best_call(&auction, "432.32.QJ8.T9876");
assert_eq!(c, call(3, Strain::Clubs));
assert!(!floored, "the runout must come from the book");
let (d, _) = best_call(&auction, "432.32.QJ876.T98");
assert_eq!(d, call(3, Strain::Diamonds));
}
}