//! Coup is a game of deception for two to six players. 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)] /// Each card represents the right to perform specific actions or counteractions. 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, ForeignAid) | (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, PartialEq, Debug)] /// The actions a player can perform on their turn. pub enum Action { Income, ForeignAid, Coup, Tax, Assassinate, Exchange, Steal, } impl Action { /// If the action needs a target. pub fn is_targeted(self) -> bool { matches!(self, Coup | Steal | Assassinate) } /// Which players may challenge the action. pub fn challenger_mode(self) -> ResMode { match self { Income | ForeignAid | Coup => ResMode::None, Assassinate | Steal | Tax | Exchange => ResMode::Anyone, } } /// Which players may block the action. pub fn blocker_mode(self) -> ResMode { match self { Income | Tax | Exchange | Coup => ResMode::None, Assassinate | Steal => ResMode::Target, ForeignAid => ResMode::Anyone, } } /// How much the action costs to perform. 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)] /// How the other players may respond to an action. pub enum ResMode { None, Target, Anyone, } #[derive(Clone)] /// The cards and coins a single player possesses. pub struct Player { pub coins: u8, pub cards: Vec, } impl Player { /// If the player still possesses any cards, and thus is still in the game pub fn is_alive(&self) -> bool { !self.cards.is_empty() } fn wins_challenge(&self, action: Action) -> bool { self.cards.iter().any(|c| c.allows_action(action)) } fn lose(&mut self, card: Card, deck: &mut Vec) { self.cards.draw_first(card); deck.push(card); } fn holds(&self, card: Card) -> bool { self.cards.iter().any(|c| *c == 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 { /// Creates a new game, with default [Players](Player) and a shuffled deck of [Cards](card) 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, } } /// If all but one [Players](Player) have lost all influence. 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 advance(&mut self) { let next = self.turn_iterator().next(); if let Some(next) = next { self.turn = next; } } fn player_lose_influence(&mut self, id: usize, agents: &[&dyn Agent]) -> CoupResult<()>{ let card = agents[id].choose_lost_influence(self); let player = &self.players[id]; if player.holds(card) { self.players[id].lose(card, &mut self.discard); Ok(()) } else { Err("Player discarded a card they don't hold") } } pub fn action(&mut self, agents: &[&dyn Agent]) -> CoupResult { let move_ = agents[self.turn].choose_move(self); if move_.action.is_targeted() && move_.target.is_none() { Err("Targeted action with no target") } else if move_.action.coin_cost() > self.players[self.turn].coins { Err("Cannot afford action") } else { Ok(Phase::ActionChallenge(move_)) } // TODO there're definately more cases, find and cover these } pub fn action_challenge(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult { match move_.action.challenger_mode() { ResMode::None => Ok(Phase::Block(move_)), ResMode::Target => unreachable!(), ResMode::Anyone => { let challenger = self.turn_iterator().find(|&id| agents[id].should_action_challenge(self, move_)); if let Some(challenger) = challenger{ let current_player_wins = self.players[self.turn].wins_challenge(move_.action); if current_player_wins { // Challenger loses influence self.player_lose_influence(challenger, agents)?; Ok(Phase::Block(move_)) } else { // Player loses influence self.player_lose_influence(self.turn, agents)?; // Turn is forfeit self.advance(); Ok(Phase::Done) } } else { Ok(Phase::Block(move_)) } } } } pub fn block(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult { match move_.action.blocker_mode() { ResMode::None => Ok(Phase::Resolution(move_)), ResMode::Target => match agents[move_.target.unwrap()].choose_block_card(self, move_) { Some(card) => { if card.blocks_action(move_.action) { Ok(Phase::BlockChallenge(move_, Block { blocker: move_.target.unwrap(), card, })) } else { Err("Card does not block action") } }, None => Ok(Phase::Resolution(move_)), } ResMode::Anyone => { for id in self.turn_iterator() { if let Some(card) = agents[id].choose_block_card(self, move_) { if card.blocks_action(move_.action) { return Ok(Phase::BlockChallenge(move_, Block { blocker: id, card, })) } else { return Err("Card does not block action") } } } Ok(Phase::Resolution(move_)) } } } pub fn block_challenge(&mut self, move_: Move, block: Block, agents: &[&dyn Agent]) -> CoupResult { if agents[self.turn].should_block_challenge(self, move_, block) { if self.players[block.blocker].holds(block.card) { // Player challenged incorrectly, loses influence and turn is forfeit self.player_lose_influence(self.turn, agents)?; self.advance(); Ok(Phase::Done) } else { // Player challenged correctly, blocker loses a card self.player_lose_influence(block.blocker, agents)?; // Game continues Ok(Phase::Resolution(move_)) } } else { // Player chose not to challenge the block, turn is forfeit self.advance(); Ok(Phase::Done) } } pub fn resolution(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult { self.players[self.turn].coins -= move_.action.coin_cost(); match move_.action { Income => self.players[self.turn].coins += 1, ForeignAid => self.players[self.turn].coins += 2, Coup | Assassinate => match move_.target { Some(target) => { // Target may have died from challenge let target_alive = self.players[target].is_alive(); if target_alive { self.player_lose_influence(target, agents)?; } } _ => return Err("Coup/Assassinate resolution has no target"), }, Tax => self.players[self.turn].coins += 3, Exchange => { let drawn = vec![self.deck.pop().unwrap(), self.deck.pop().unwrap()]; let hand = self.players[self.turn].cards.clone(); let mut choices = [drawn, hand].concat(); let discarded = agents[self.turn].exchange(self, &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); } } self.players[self.turn].cards = choices; self.deck.shuffle(); } Steal => match move_.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"), } } self.advance(); Ok(Phase::Done) } } #[derive(PartialEq, Debug, Clone, Copy)] pub struct Move { pub action: Action, pub target: Option, } #[derive(PartialEq, Debug, Clone, Copy)] pub struct Block { pub blocker: usize, pub card: Card, } /// Phase we should move to. /// /// Coup turns have 5 phases, depending on what actions are taken each phase /// some phases might be skipped. #[derive(PartialEq, Debug)] pub enum Phase { ActionChallenge(Move), Block(Move), BlockChallenge(Move, Block), Resolution(Move), Done, } /// An interface to a game to make strategic decisions. pub trait Agent : fmt::Debug { /// What move should the agent take? fn choose_move(&self, game: &Game) -> Move; /// Should the agent challenge the action? fn should_action_challenge(&self, game: &Game, move_: Move) -> bool; /// Which [card](Card) the agent wishes to use to block the current action. /// If the agent does not wish to block, it should return [None] fn choose_block_card(&self, game: &Game, move_: Move) -> Option; /// Should the agent challenge the block? fn should_block_challenge(&self, game: &Game, move_: Move, block: Block) -> bool; /// The [Ambassador]'s exchange. /// Given 3 or 4 [Cards](Card) the agent must return two cards to the deck. fn exchange(&self, game: &Game, cards: &[Card]) -> [Card; 2]; /// The player has lost influence, and must choose a [Card] from their hand /// to discard. fn choose_lost_influence(&self, game: &Game) -> Card; } #[cfg(test)] mod test;