summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-09-04 19:42:57 -0400
committerKjetil Orbekk <kj@orbekk.com>2022-09-04 19:44:53 -0400
commitab68d26c6b16f973642d378900f5e6ac2be221b7 (patch)
tree357206052643d3ee0b065fd4ee6cb8af928e11f9
parentb7180143745c5ebe571936b0116cca403d082e8d (diff)
Add turn/trick representation
Includes convenience test and parsing utilities
-rw-r--r--Cargo.lock119
-rw-r--r--webapp/.env1
-rw-r--r--webapp/Cargo.toml4
-rw-r--r--webapp/src/bridge_engine.rs108
-rw-r--r--webapp/src/card.rs86
-rw-r--r--webapp/src/main.rs9
6 files changed, 327 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d8609d6..f9d3453 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,26 @@
version = 3
[[package]]
+name = "aho-corasick"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -37,6 +57,25 @@ dependencies = [
]
[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "env_logger"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
name = "getrandom"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -171,6 +210,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -217,6 +271,12 @@ dependencies = [
]
[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
name = "once_cell"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -301,6 +361,23 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
name = "rustversion"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -389,6 +466,15 @@ dependencies = [
]
[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -518,6 +604,8 @@ name = "webapp"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
+ "dotenv",
+ "env_logger",
"getrandom",
"log",
"rand",
@@ -528,6 +616,37 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "yew"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/webapp/.env b/webapp/.env
new file mode 100644
index 0000000..edf7f1a
--- /dev/null
+++ b/webapp/.env
@@ -0,0 +1 @@
+RUST_LOG=webapp,bridge
diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml
index 78a5863..15a880a 100644
--- a/webapp/Cargo.toml
+++ b/webapp/Cargo.toml
@@ -12,3 +12,7 @@ strum = "0.24"
strum_macros = "0.24"
rand = "0.8.4"
getrandom = { version = "0.2.7", features = ["js"] }
+
+[dev-dependencies]
+env_logger = "0.8.4"
+dotenv = "0.15"
diff --git a/webapp/src/bridge_engine.rs b/webapp/src/bridge_engine.rs
new file mode 100644
index 0000000..48c0fd2
--- /dev/null
+++ b/webapp/src/bridge_engine.rs
@@ -0,0 +1,108 @@
+use crate::card;
+
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
+pub enum Player {
+ North,
+ West,
+ South,
+ East,
+}
+
+impl Player {
+ pub fn next(&self) -> Self {
+ match self {
+ Self::North => Self::West,
+ Self::West => Self::South,
+ Self::South => Self::East,
+ Self::East => Self::North,
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub struct Trick {
+ pub leader: Player,
+ pub cards_played: Vec<card::Card>,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub struct Turn {
+ in_progress: Trick,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum PlayResult {
+ InProgress(Turn),
+ Trick(Trick),
+}
+
+impl Turn {
+ pub fn new(p: Player) -> Turn {
+ Turn {
+ in_progress: Trick {
+ leader: p,
+ cards_played: Vec::with_capacity(4),
+ },
+ }
+ }
+
+ pub fn play(mut self: Turn, card: card::Card) -> PlayResult {
+ self.in_progress.cards_played.push(card);
+ if self.in_progress.cards_played.len() >= 4 {
+ return PlayResult::Trick(self.in_progress);
+ }
+ PlayResult::InProgress(self)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn next_player() {
+ let next_players = vec![Player::North, Player::West, Player::South, Player::East]
+ .iter()
+ .map(Player::next)
+ .collect::<Vec<_>>();
+ assert_eq!(
+ next_players,
+ vec![Player::West, Player::South, Player::East, Player::North]
+ );
+ }
+
+ fn as_turn(p: PlayResult) -> Turn {
+ if let PlayResult::InProgress(t) = p {
+ t
+ } else {
+ panic!("expected PlayResult::InProgress(): {:?}", p);
+ }
+ }
+
+ fn as_trick(p: PlayResult) -> Trick {
+ if let PlayResult::Trick(t) = p {
+ t
+ } else {
+ panic!("expected PlayResult::Trick(): {:?}", p);
+ }
+ }
+
+ #[test]
+ fn play_turn() {
+ let turn = Turn::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()));
+ let trick = as_trick(turn.play("♣A".parse().unwrap()));
+ assert_eq!(
+ trick,
+ Trick {
+ leader: Player::South,
+ cards_played: ["♣4", "♥A", "♣4", "♣A"]
+ .into_iter()
+ .map(|c| c.parse().unwrap())
+ .collect()
+ }
+ );
+ }
+}
diff --git a/webapp/src/card.rs b/webapp/src/card.rs
index 4fda0a7..174676f 100644
--- a/webapp/src/card.rs
+++ b/webapp/src/card.rs
@@ -40,6 +40,33 @@ impl fmt::Display for Suit {
}
}
+#[derive(Debug, PartialEq, Eq)]
+pub enum ParseError {
+ InvalidSuit(String),
+ InvalidRank(String),
+ MissingParts(String),
+}
+
+impl std::str::FromStr for Suit {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> std::result::Result<Self, ParseError> {
+ match s.trim() {
+ "♣" => Ok(Suit::Club),
+ "C" => Ok(Suit::Club),
+ "♢" => Ok(Suit::Diamond),
+ "♦" => Ok(Suit::Diamond),
+ "D" => Ok(Suit::Diamond),
+ "♡" => Ok(Suit::Heart),
+ "♥" => Ok(Suit::Heart),
+ "H" => Ok(Suit::Heart),
+ "♠" => Ok(Suit::Spade),
+ "S" => Ok(Suit::Spade),
+ _ => Err(ParseError::InvalidSuit(s.to_string())),
+ }
+ }
+}
+
impl fmt::Debug for Suit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", self)
@@ -72,6 +99,30 @@ impl fmt::Debug for Rank {
}
}
+impl std::str::FromStr for Rank {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> std::result::Result<Self, ParseError> {
+ match s.trim().to_ascii_uppercase().as_str() {
+ "A" => Ok(Rank::Ace),
+ "K" => Ok(Rank::King),
+ "Q" => Ok(Rank::Queen),
+ "J" => Ok(Rank::Jack),
+ "10" => Ok(Rank::Ten),
+ "T" => Ok(Rank::Ten),
+ "9" => Ok(Rank::Nine),
+ "8" => Ok(Rank::Eight),
+ "7" => Ok(Rank::Seven),
+ "6" => Ok(Rank::Six),
+ "5" => Ok(Rank::Five),
+ "4" => Ok(Rank::Four),
+ "3" => Ok(Rank::Three),
+ "2" => Ok(Rank::Two),
+ _ => Err(ParseError::InvalidRank(s.to_string())),
+ }
+ }
+}
+
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct Card(pub Suit, pub Rank);
@@ -88,6 +139,22 @@ impl fmt::Debug for Card {
}
}
+impl std::str::FromStr for Card {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> std::result::Result<Self, ParseError> {
+ let stripped = s.replace(" ", "");
+ let mut chars = stripped.chars();
+ let suit = chars
+ .next()
+ .ok_or(ParseError::MissingParts(s.to_string()))?
+ .to_string()
+ .parse()?;
+ let rank = chars.collect::<String>().parse()?;
+ Ok(Card(suit, rank))
+ }
+}
+
fn make_deck() -> Vec<Card> {
let mut result = vec![];
for suit in Suit::iter() {
@@ -98,6 +165,25 @@ fn make_deck() -> Vec<Card> {
result
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use log::info;
+
+ #[test]
+ fn string_conversion() {
+ crate::tests::test_setup();
+ info!("deck: {:?}", make_deck());
+ assert_eq!(
+ make_deck(),
+ make_deck()
+ .iter()
+ .map(|card| format!("{}", card).parse().unwrap())
+ .collect::<Vec<Card>>(),
+ );
+ }
+}
+
pub fn shuffle_deck<R>(rng: &mut R) -> (Vec<Card>, Vec<Card>, Vec<Card>, Vec<Card>)
where
R: Rng,
diff --git a/webapp/src/main.rs b/webapp/src/main.rs
index a99ea94..633f560 100644
--- a/webapp/src/main.rs
+++ b/webapp/src/main.rs
@@ -3,6 +3,7 @@ use crate::card::{Rank, Suit};
use log::{debug, error, info, warn};
use yew::prelude::*;
pub mod card;
+pub mod bridge_engine;
fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
@@ -118,3 +119,11 @@ impl From<card::Card> for CardProps {
CardProps { suit, rank }
}
}
+
+#[cfg(test)]
+mod tests {
+ pub fn test_setup() {
+ dotenv::dotenv().ok();
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+}