diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-09-05 17:04:06 -0400 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-09-05 17:04:06 -0400 |
commit | 1bead88ffe47ef193a8b2adc36b167967d8450ef (patch) | |
tree | 0e5cf889f31df5da24905329fb784d848595a968 /webapp/src | |
parent | da1f6c622739d6ca0963c247280efa1993b79b41 (diff) |
Add half-baked bidding logic
Diffstat (limited to 'webapp/src')
-rw-r--r-- | webapp/src/bridge_engine.rs | 217 | ||||
-rw-r--r-- | webapp/src/main.rs | 8 |
2 files changed, 187 insertions, 38 deletions
diff --git a/webapp/src/bridge_engine.rs b/webapp/src/bridge_engine.rs index b76ddfd..a30591c 100644 --- a/webapp/src/bridge_engine.rs +++ b/webapp/src/bridge_engine.rs @@ -1,8 +1,9 @@ use crate::card::Card; use crate::card::Suit; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use log::debug; use regex::Regex; +use std::cmp::Ordering; use std::fmt; use std::str::FromStr; use strum::IntoEnumIterator; @@ -34,19 +35,19 @@ pub struct Trick { } #[derive(PartialEq, Eq, Debug)] -pub struct Turn { +pub struct PlayTurn { in_progress: Trick, } #[derive(PartialEq, Eq, Debug)] pub enum PlayResult { - InProgress(Turn), + InProgress(PlayTurn), Trick(Trick), } -impl Turn { - pub fn new(p: Player) -> Turn { - Turn { +impl PlayTurn { + pub fn new(p: Player) -> PlayTurn { + PlayTurn { in_progress: Trick { leader: p, cards_played: Vec::with_capacity(4), @@ -54,7 +55,7 @@ impl Turn { } } - pub fn play(mut self: Turn, card: Card) -> PlayResult { + pub fn play(mut self: PlayTurn, card: Card) -> PlayResult { self.in_progress.cards_played.push(card); if self.in_progress.cards_played.len() >= 4 { return PlayResult::Trick(self.in_progress); @@ -104,52 +105,91 @@ impl FromStr for ContractLevel { } #[derive(PartialEq, Eq, Clone, Copy)] -pub struct Bid { +pub enum Bid { + Pass, + Double, + Redouble, + Raise(Raise), +} + +impl Bid { + pub fn as_raise(&self) -> Option<Raise> { + match self { + Bid::Raise(raise) => Some(*raise), + _ => None, + } + } +} + +impl fmt::Display for Bid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + Bid::Pass => write!(f, "Pass"), + Bid::Double => write!(f, "Double"), + Bid::Redouble => write!(f, "Redouble"), + Bid::Raise(x) => write!(f, "{}", x), + } + } +} + +impl fmt::Debug for Bid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + Bid::Pass => write!(f, "Pass"), + Bid::Double => write!(f, "Double"), + Bid::Redouble => write!(f, "Redouble"), + Bid::Raise(x) => write!(f, "Raise({})", x), + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct Raise { pub level: ContractLevel, pub suit: Option<Suit>, } -impl Bid { - pub fn all_bids() -> Vec<Bid> { +impl Raise { + pub fn all_raises() -> Vec<Raise> { let mut result = Vec::with_capacity(7 * 5); for level in ContractLevel::iter() { for suit in Suit::iter() { - result.push(Bid { + result.push(Raise { level, suit: Some(suit), }); } - result.push(Bid { level, suit: None }); + result.push(Raise { level, suit: None }); } result } } -impl PartialOrd<Self> for Bid { - fn partial_cmp(&self, o: &Self) -> std::option::Option<std::cmp::Ordering> { +impl PartialOrd<Self> for Raise { + fn partial_cmp(&self, o: &Self) -> Option<Ordering> { if self.level != o.level { return self.level.partial_cmp(&o.level); } if self.suit != o.suit { if self.suit == None { - return Some(std::cmp::Ordering::Greater); + return Some(Ordering::Greater); } if o.suit == None { - return Some(std::cmp::Ordering::Less); + return Some(Ordering::Less); } return self.suit.partial_cmp(&o.suit); } - return Some(std::cmp::Ordering::Equal); + return Some(Ordering::Equal); } } -impl Ord for Bid { +impl Ord for Raise { fn cmp(&self, o: &Self) -> std::cmp::Ordering { self.partial_cmp(o).unwrap() } } -impl fmt::Display for Bid { +impl fmt::Display for Raise { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( f, @@ -161,61 +201,170 @@ impl fmt::Display for Bid { } } -impl fmt::Debug for Bid { +impl fmt::Debug for Raise { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", self) } } -impl FromStr for Bid { +impl FromStr for Raise { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { lazy_static::lazy_static! { static ref RE: Regex = Regex::new(r#"\s*(.[0-9]*)\s*(.*)"#).unwrap(); }; - let caps = RE.captures(s).ok_or(anyhow!("invalid bid: {}", s))?; + let caps = RE.captures(s).ok_or(anyhow!("invalid raise: {}", s))?; debug!("caps: {:?}", caps); let level = caps[1].parse()?; let suit = match &caps[2] { "NT" => None, x => Some(x.parse()?), }; - Ok(Bid { level, suit }) + Ok(Raise { level, suit }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ContractModifier { + None, + Doubled, + Redoubled, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Contract { + highest_bid: Option<Raise>, + modifier: ContractModifier, +} + +#[derive(Debug)] +pub struct Bidding { + pub dealer: Player, + pub bids: Vec<Bid>, +} + +impl Bidding { + pub fn new(dealer: Player) -> Bidding { + Bidding { + dealer, + bids: vec![], + } + } + + fn highest_bid(&self) -> Option<Raise> { + for bid in self.bids.iter().rev() { + if let Some(raise) = bid.as_raise() { + return Some(raise); + } + } + None + } + + fn passed_out(&self) -> bool { + let mut passes = 0; + for b in self.bids.iter().rev().take(3) { + if b == &Bid::Pass { + passes += 1 + } + } + passes == 3 + } + + pub fn bid(mut self, bid: Bid) -> Result<BiddingResult, anyhow::Error> { + // TODO: Need logic for double and redouble here. + let highest_bid = self.highest_bid(); + if bid.as_raise().is_some() && bid.as_raise() <= self.highest_bid() { + bail!( + "bid too low: {:?} <= {:?}", + bid.as_raise(), + self.highest_bid() + ); + } + self.bids.push(bid); + if self.passed_out() { + Ok(BiddingResult::Contract( + Contract { + highest_bid, + modifier: ContractModifier::None, + }, + self, + )) + } else { + Ok(BiddingResult::InProgress(self)) + } } } +#[derive(Debug)] +pub enum BiddingResult { + InProgress(Bidding), + Contract(Contract, Bidding), +} + #[cfg(test)] mod tests { use super::*; use log::info; + fn as_bidding(r: BiddingResult) -> Bidding { + match r { + BiddingResult::InProgress(bidding) => bidding, + _ => panic!("expected BiddingResult::InProgress(): {:?}", r), + } + } + + fn as_contract(r: BiddingResult) -> Contract { + match r { + BiddingResult::Contract(contract, _) => contract, + _ => panic!("expected BiddingResult::Contract(): {:?}", r), + } + } + + #[test] + fn bidding() { + crate::tests::test_setup(); + let bidding = Bidding::new(Player::South); + let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); + let bidding = as_bidding(bidding.bid(Bid::Raise("1♦".parse().unwrap())).unwrap()); + let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); + let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); + let contract = as_contract(bidding.bid(Bid::Pass).unwrap()); + assert_eq!( + Contract { + highest_bid: Some("1♦".parse().unwrap()), + modifier: ContractModifier::None + }, + contract + ); + } + #[test] fn bid_conversion() { crate::tests::test_setup(); - let bid1d = Bid { + let bid1d = Raise { level: ContractLevel::One, suit: Some(Suit::Diamond), }; assert_eq!("1♢", format!("{}", bid1d)); assert_eq!("1♢", format!("{:?}", bid1d)); - assert_eq!(bid1d, Bid::from_str("1D").unwrap()); + assert_eq!(bid1d, Raise::from_str("1D").unwrap()); - let mut checked_bids = 0; - for bid in Bid::all_bids() { - assert_eq!(bid, Bid::from_str(format!("{}", bid).as_str()).unwrap()); - checked_bids += 1; + let mut checked_raises = 0; + for bid in Raise::all_raises() { + assert_eq!(bid, Raise::from_str(format!("{}", bid).as_str()).unwrap()); + checked_raises += 1; } - assert_eq!(checked_bids, 35); + assert_eq!(checked_raises, 35); } #[test] fn bid_ord() { - let bid = |s| Bid::from_str(s).unwrap(); + let bid = |s| Raise::from_str(s).unwrap(); assert!(bid("2♦") < bid("3♦")); assert!(bid("3♦") < bid("3♥")); assert!(bid("1♠") < bid("2♣")); assert!(bid("1♠") < bid("1NT")); - for bid in Bid::all_bids() { + for bid in Raise::all_raises() { assert_eq!(bid, bid); } } @@ -244,7 +393,7 @@ mod tests { ); } - fn as_turn(p: PlayResult) -> Turn { + fn as_turn(p: PlayResult) -> PlayTurn { if let PlayResult::InProgress(t) = p { t } else { @@ -262,7 +411,7 @@ mod tests { #[test] fn play_turn() { - let turn = Turn::new(Player::South); + let turn = PlayTurn::new(Player::South); let turn = as_turn(turn.play("♣4".parse().unwrap())); let turn = as_turn(turn.play("♥A".parse().unwrap())); let turn = as_turn(turn.play("♣4".parse().unwrap())); diff --git a/webapp/src/main.rs b/webapp/src/main.rs index b8ac4bb..4316d84 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -4,7 +4,7 @@ use log::{debug, error, info, warn}; use yew::prelude::*; pub mod bridge_engine; pub mod card; -use bridge_engine::Bid; +use bridge_engine::Raise; extern crate wee_alloc; // Use `wee_alloc` as the global allocator. @@ -50,7 +50,7 @@ pub fn app() -> Html { html! { <> <p>{ "Bids" }</p> - <BiddingBox lower_limit={"1H".parse::<Bid>().unwrap()} /> + <BiddingBox lower_limit={"1H".parse::<Raise>().unwrap()} /> <p>{ "North" }</p> <Hand ..(*north).clone() /> <p>{ "West" }</p> @@ -141,7 +141,7 @@ pub fn bid_css_class(suit: Option<Suit>) -> &'static str { #[function_component(BiddingBox)] pub fn bidding_box(props: &BiddingBoxProps) -> Html { - let bids: Html = Bid::all_bids() + let bids: Html = Raise::all_raises() .iter() .map(|bid| { let mut class = if bid < &props.lower_limit { @@ -168,7 +168,7 @@ pub fn bidding_box(props: &BiddingBoxProps) -> Html { #[derive(PartialEq, Properties, Clone)] pub struct BiddingBoxProps { #[prop_or("1♣".parse().unwrap())] - lower_limit: Bid, + lower_limit: Raise, } #[cfg(test)] |