summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-09-05 17:04:06 -0400
committerKjetil Orbekk <kj@orbekk.com>2022-09-05 17:04:06 -0400
commit1bead88ffe47ef193a8b2adc36b167967d8450ef (patch)
tree0e5cf889f31df5da24905329fb784d848595a968
parentda1f6c622739d6ca0963c247280efa1993b79b41 (diff)
Add half-baked bidding logic
-rw-r--r--webapp/src/bridge_engine.rs217
-rw-r--r--webapp/src/main.rs8
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)]