commit 0dfad4ad3021f40245a980e653cdefd8a9289335 Author: Dane Johnson Date: Tue May 17 16:10:56 2022 -0500 Implement resolution phase diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a70388e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "coup" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5384d8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "coup" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "*" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4cd242a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,374 @@ +use rand::seq::SliceRandom; + +use std::fmt; + +use Action::*; +use Card::*; + +pub type CoupResult = Result; + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Card { + Duke, + Assassin, + Captain, + Ambassador, + Contessa, +} + +impl Card { + pub fn allows_action(self, action: Action) -> bool { + matches!((self, action), + (_, Income) | + (_, ForeignAid) | + (Duke, Tax) | + (Assassin, Assassinate) | + (Ambassador, Exchange) | + (Captain, Steal)) + } + pub fn blocks_action(self, action: Action) -> bool { + matches!((self, action), + (Duke, Income) | + (Captain, Steal) | + (Ambassador, Steal) | + (Contessa, Assassinate)) + } +} + +trait Stack { + fn draw(&mut self, player: &mut Player) -> CoupResult<()>; + fn draw_first(&mut self, card: Card) -> bool; + fn shuffle(&mut self); +} + +impl Stack for Vec { + fn draw(&mut self, player: &mut Player) -> CoupResult<()> { + match self.pop() { + Some(card) => { + player.cards.push(card); + Ok(()) + }, + None => Err("Tried to draw from an empty deck!"), + } + } + + fn draw_first(&mut self, card: Card) -> bool { + match self.iter().position(|c| *c == card) { + Some(i) => { + self.remove(i); + true + }, + None => false, + } + } + + fn shuffle(&mut self) { + let deck = self.as_mut_slice(); + let mut rng = rand::thread_rng(); + deck.shuffle(&mut rng); + } +} + +impl fmt::Display for Card { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Duke => "Duke", + Assassin => "Assassin", + Captain => "Captain", + Ambassador => "Ambassador", + Contessa => "Contessa", + }; + write!(f, "{}", name) + } +} + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum Action { + Income, + ForeignAid, + Coup, + Tax, + Assassinate, + Exchange, + Steal, +} + +impl Action { + pub fn is_targeted(self) -> bool { + matches!(self, Coup | Steal | Assassinate) + } + + pub fn challenger_mode(self) -> ResMode { + match self { + Income | ForeignAid | Coup => ResMode::None, + Assassinate | Steal | Tax | Exchange => ResMode::Anyone, + } + } + + pub fn blocker_mode(self) -> ResMode { + match self { + Income | Tax | Exchange | Coup => ResMode::None, + Assassinate | Steal => ResMode::Target, + ForeignAid => ResMode::Anyone, + } + } + + pub fn coin_cost(self) -> u8 { + match self { + Assassinate => 3, + Coup => 7, + _ => 0, + } + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Income => "Income", + ForeignAid => "Foreign Aid", + Coup => "Coup", + Tax => "Tax", + Assassinate => "Assassinate", + Exchange => "Exchange", + Steal => "Steal", + }; + write!(f, "{}", name) + } +} + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum ResMode { + None, + Target, + Anyone, +} + +#[derive(Clone)] +pub struct Player { + pub coins: u8, + pub cards: Vec, +} + +impl Player { + pub fn is_alive(&self) -> bool { + !self.cards.is_empty() + } + pub fn lose(&mut self, card: Card, deck: &mut Vec) { + self.cards.draw_first(card); + deck.push(card); + } +} + +impl Default for Player { + fn default() -> Self { + Player { + cards: Vec::new(), + coins: 2, + } + } +} + +#[derive(Clone)] +pub struct Game { + pub players: Vec, + pub deck: Vec, + pub discard: Vec, + pub turn: usize, +} + +macro_rules! thrice { + ($($e:expr),*) => { + vec![$($e, $e, $e),*] + } +} + +impl Game { + pub fn new(num_players: usize) -> Self { + let mut deck = thrice![Duke, Assassin, Captain, Ambassador, Contessa]; + deck.shuffle(); + let mut players = Vec::new(); + players.resize_with(num_players, Player::default); + for player in &mut players { + deck.draw(player).unwrap(); + deck.draw(player).unwrap(); + } + Game { + deck, + players, + discard: Vec::new(), + turn: 0, + } + } + pub fn is_game_over(&self) -> bool { + self.players.iter().filter(|p| p.is_alive()).count() == 1 + } + + fn turn_iterator(&self) -> impl Iterator + '_ { + let players_turn_order = std::iter::successors(Some(self.turn), |p| { + let next = (p + 1) % self.players.len(); + if next == self.turn { + None + } else { + Some(next) + } + }); + players_turn_order.skip(1).filter(|p| self.players[*p].is_alive()) + } + + fn resolution(&mut self, resolution: Resolution, agents: &[&dyn Agent]) -> CoupResult { + let current_player = &mut self.players.get_mut(self.turn).unwrap(); + let current_agent = agents[self.turn]; + match resolution.action { + Income => current_player.coins += 1, + ForeignAid => current_player.coins += 2, + Coup | Assassinate => match resolution.target { + Some(target) => { + let target_player = &self.players[target]; + let card = agents[target].choose_lost_influence(&target_player.cards); + self.players[target].lose(card, &mut self.discard); + } + _ => return Err("Coup/Assassinate resolution has no target"), + }, + Tax => current_player.coins += 3, + Exchange => { + let drawn = vec![self.deck.pop().unwrap(), self.deck.pop().unwrap()]; + let hand = current_player.cards.clone(); + let mut choices = [drawn, hand].concat(); + let discarded = current_agent.exchange(&choices); + for card in discarded { + if !choices.draw_first(card) { + return Err("Exchanged a card that was not in choices"); + } else { + self.deck.push(card); + } + } + current_player.cards = choices; + self.deck.shuffle(); + } + Steal => match resolution.target { + Some(target) => { + let val = self.players[target].coins.min(2); + self.players[self.turn].coins += val; + self.players[target].coins -= val; + + } + _ => return Err("Steal resolution has no target"), + } + } + let next = self.turn_iterator().next(); + if let Some(next) = next { + self.turn = next; + } + Ok(Phase::Done) + } +} + +enum Phase { + Action(Action), + //ActionChallenge(ActionChallenge), + //Block(Block), + //BlockChallenge(BlockChallenge), + Resolution(Resolution), + Done, +} + +pub struct Resolution { + action: Action, + target: Option, +} + +pub trait Agent : fmt::Debug { + fn exchange(&self, cards: &[Card]) -> [Card; 2]; + fn choose_lost_influence(&self, cards: &[Card]) -> Card; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_resolution() { + #[derive(Debug)] + struct DummyAgent; + impl Agent for DummyAgent { + fn exchange(&self, cards: &[Card]) -> [Card; 2] { + [Contessa, Duke] + } + fn choose_lost_influence(&self, cards: &[Card]) -> Card { + Captain + } + } + let deck = vec![Contessa, Contessa]; + let discard = vec![]; + let players = vec![ + Player { coins: 2, cards: vec![Duke, Assassin] }, + Player { coins: 1, cards: vec![Captain] }, + ]; + let game = Game { + deck, + discard, + players, + turn: 0, + }; + + // Test income + { + let mut game = game.clone(); + game.resolution(Resolution { + action: Income, + target: None, + }, &[&DummyAgent, &DummyAgent]).unwrap(); + assert_eq!(game.players[0].coins, 3); + } + + // Test foreign aid + { + let mut game = game.clone(); + game.resolution(Resolution { + action: ForeignAid, + target: None, + }, &[&DummyAgent, &DummyAgent]).unwrap(); + assert_eq!(game.players[0].coins, 4); + } + + // Test coup / assassinate + { + let mut game = game.clone(); + game.resolution(Resolution { + action: Coup, + target: Some(1), + }, &[&DummyAgent, &DummyAgent]).unwrap(); + assert!(game.players[1].cards.is_empty()); + assert_eq!(game.discard, vec![Captain]); + } + + // Test steal + { + let mut game = game.clone(); + game.resolution(Resolution { + action: Steal, + target: Some(1), + }, &[&DummyAgent, &DummyAgent]).unwrap(); + assert_eq!(game.players[0].coins, 3); + assert_eq!(game.players[1].coins, 0); + } + + // Test exchange + { + let mut game = game.clone(); + game.resolution(Resolution { + action: Exchange, + target: Some(1), + }, &[&DummyAgent, &DummyAgent]).unwrap(); + game.players[0].cards.sort(); + assert_eq!(game.players[0].cards, vec![Assassin, Contessa]); + game.deck.sort(); + assert_eq!(game.deck, vec![Duke, Contessa]); + assert!(game.discard.is_empty()); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8344815 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +use coup::*; + +fn main() { + println!("Hello, world!"); +}