diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c74735 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Coup + +## Coup from a Programmers Perspective + +Coup is played in turns, each players turn consists of phases, many +of which are skipped depending on the cards. + +1. First player chooses an action that they have the coin to perform, and possibly a target for that action (Action Phase) + +2. If the action requires an identity, all players are given an option to challenge the action (Action Challenge Phase). +If there is as challenge and it is successful it ends the first players turn and they lose influence. +If there is an unsuccessful challenge the challenger loses influence, and the turn continues. +If there is no challenge play continues. + +3. If the action is a blockable action and a targeted action then the target may or may not block. +If the action is blockable and there is no target, all players may block (Block Phase). + +4. If there is a block, the first player may choose to challenge the block. (Block Challenge Phase) +If there is a challenge and it is successful, the blocker loses influence and the first player's turn resolves. +If there is a challenge that is unsuccessful, the first player loses influence and the turn ends. +If there is not challenge the turn ends. + +5. The player's action is resolved (Resolution phase) + +## TODO +1. Remove coins as part of the resolution phase +2. Add checks so dead players don't lose influence +3. Assert that no illegal actions can be performed by the agents +4. Re-order phases such that challenges come after counter-actions and can be performed by any player diff --git a/src/lib.rs b/src/lib.rs index 6e2f239..a3ba06b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,10 +168,15 @@ impl Player { pub fn is_alive(&self) -> bool { !self.cards.is_empty() } + fn wins_challenge(&self, action: Action) -> bool { + self.cards.iter().find(|&c| c.allows_action(action)).is_some() + } + 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().find(|&c| *c == card).is_some() } @@ -201,6 +206,7 @@ macro_rules! thrice { } 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(); @@ -217,6 +223,7 @@ impl Game { 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 } @@ -240,11 +247,45 @@ impl Game { } } - fn player_lose_influence(&mut self, id: usize, agents: &[&dyn Agent]) { + fn player_lose_influence(&mut self, id: usize, agents: &[&dyn Agent]) -> CoupResult<()>{ let player = &mut self.players[id]; let card = agents[id] .choose_lost_influence(&player.cards); - player.lose(card, &mut self.discard); + if player.holds(card) { + player.lose(card, &mut self.discard); + Ok(()) + } else { + Err("Player discarded a card they don't hold") + } + } + + pub fn action_challenge(&mut self, action_challenge: phase::ActionChallenge, agents: &[&dyn Agent]) -> CoupResult { + let block = Phase::Block(phase::Block { + action: action_challenge.action, + target: action_challenge.target, + }); + match action_challenge.action.challenger_mode() { + ResMode::None => Ok(block), + ResMode::Target => unreachable!(), + ResMode::Anyone => { + let challenger = self.turn_iterator().find(|&id| agents[id].should_action_challenge(&action_challenge)); + if let Some(challenger) = challenger{ + let current_player_wins = self.players[self.turn].wins_challenge(action_challenge.action); + if current_player_wins { + // Challenger loses influence + self.player_lose_influence(challenger, agents)?; + Ok(block) + } else { + // Player loses influence + self.player_lose_influence(self.turn, agents)?; + // Turn is forfeit + Ok(Phase::Done) + } + } else { + Ok(block) + } + } + } } pub fn block(&mut self, block: phase::Block, agents: &[&dyn Agent]) -> CoupResult { @@ -298,11 +339,11 @@ impl Game { if agents[self.turn].should_block_challenge(&block_challenge) { if self.players[block_challenge.blocker].holds(block_challenge.block_card) { // Player challenged incorrectly, loses influence and turn is forfeit - self.player_lose_influence(self.turn, agents); + self.player_lose_influence(self.turn, agents)?; Ok(Phase::Done) } else { // Player challenged correctly, blocker loses a card - self.player_lose_influence(block_challenge.blocker, agents); + self.player_lose_influence(block_challenge.blocker, agents)?; // Game continues Ok(Phase::Resolution(phase::Resolution { action: block_challenge.action, @@ -327,7 +368,7 @@ impl Game { // Target may have died from challenge let target_alive = self.players[target].is_alive(); if target_alive { - self.player_lose_influence(target, agents); + self.player_lose_influence(target, agents)?; } } _ => return Err("Coup/Assassinate resolution has no target"), @@ -373,13 +414,19 @@ pub mod phase { #[derive(PartialEq, Debug)] pub enum Phase { Action(Action), - //ActionChallenge(ActionChallenge), + ActionChallenge(ActionChallenge), Block(Block), BlockChallenge(BlockChallenge), Resolution(Resolution), Done, } + #[derive(PartialEq, Debug)] + pub struct ActionChallenge { + pub action: Action, + pub target: Option, + } + #[derive(PartialEq, Debug)] pub struct Block { pub action: Action, @@ -405,6 +452,8 @@ use phase::Phase; /// An interface to a game to make strategic decisions. pub trait Agent : fmt::Debug { + /// Should the agent challenge the action? + fn should_action_challenge(&self, action_challenge: &phase::ActionChallenge) -> 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, block: &phase::Block) -> Option; @@ -422,37 +471,92 @@ pub trait Agent : fmt::Debug { mod test { use super::*; - macro_rules! make_dummy_agent { - ($cbc: expr, $sbc: expr, $e: expr, $cli: expr) => {{ - #[derive(Debug)] - struct DummyAgent; - impl Agent for DummyAgent { - #[allow(unused)] - fn choose_block_card(&self, block: &phase::Block) -> Option { - $cbc - } - #[allow(unused)] - fn should_block_challenge(&self, block_challenge: &phase::BlockChallenge) -> bool { - $sbc - } - #[allow(unused)] - fn exchange(&self, cards: &[Card]) -> [Card; 2] { - $e - } - #[allow(unused)] - fn choose_lost_influence(&self, cards: &[Card]) -> Card { - $cli - } - } - DummyAgent - }} + #[derive(Debug)] + struct DummyAgent(Card, Option, bool); + impl Agent for DummyAgent { + fn should_action_challenge(&self, _action_challenge: &phase::ActionChallenge) -> bool { + self.2 + } + fn choose_block_card(&self, _block: &phase::Block) -> Option { + self.1 + } + fn should_block_challenge(&self, _block_challenge: &phase::BlockChallenge) -> bool { + self.2 + } + fn exchange(&self, _cards: &[Card]) -> [Card; 2] { + [self.0, self.1.unwrap()] + } + fn choose_lost_influence(&self, _cards: &[Card]) -> Card { + self.0 + } + } + + #[test] + fn test_action_challenge() { + let challenge_agent = DummyAgent(Contessa, None, true); + let non_challenge_agent = DummyAgent(Duke, None, false); + let deck = vec![]; + let discard = vec![]; + let players = vec![ + Player { coins: 2, cards: vec![Duke, Captain] }, + Player { coins: 2, cards: vec![Contessa] }, + Player { coins: 2, cards: vec![Ambassador] }, + ]; + let game = Game { + deck, + discard, + players, + turn: 0 + }; + + // Test non-challengable + { + let mut game = game.clone(); + assert_eq!( + game.action_challenge(phase::ActionChallenge { + action: Income, + target: None, + }, &[&challenge_agent, &challenge_agent, &challenge_agent]), + Ok(Phase::Block(phase::Block { + action: Income, + target: None + })) + ); + } + + // Test failed challenge + { + let mut game = game.clone(); + assert_eq!( + game.action_challenge(phase::ActionChallenge { + action: Steal, + target: Some(2), + }, &[&challenge_agent, &challenge_agent, &challenge_agent]), + Ok(Phase::Block(phase::Block { + action: Steal, + target: Some(2) + })) + ); + } + + // Test successful challenge + { + let mut game = game.clone(); + assert_eq!( + game.action_challenge(phase::ActionChallenge { + action: Assassinate, + target: Some(2), + }, &[&non_challenge_agent, &challenge_agent, &challenge_agent]), + Ok(Phase::Done), + ); + } } #[test] fn test_block() { - let non_blocking_agent = make_dummy_agent!(None, unimplemented!(), unimplemented!(), unimplemented!()); - let ambassador_block_agent = make_dummy_agent!(Some(Ambassador), unimplemented!(), unimplemented!(), unimplemented!()); - let duke_block_agent = make_dummy_agent!(Some(Duke), unimplemented!(), unimplemented!(), unimplemented!()); + let non_blocking_agent = DummyAgent(Duke, None, false); + let ambassador_block_agent = DummyAgent(Ambassador, Some(Ambassador), false); + let duke_block_agent = DummyAgent(Duke, Some(Duke), false); let deck = vec![]; let discard = vec![]; @@ -564,9 +668,9 @@ mod test { #[test] fn test_block_challenge() { - let challenge_agent = make_dummy_agent!(unimplemented!(), true, unimplemented!(), Assassin); - let non_challenge_agent = make_dummy_agent!(unimplemented!(), false, unimplemented!(), Assassin); - let block_agent = make_dummy_agent!(unimplemented!(), unimplemented!(), unimplemented!(), Contessa); + let challenge_agent = DummyAgent(Assassin, None, true); + let non_challenge_agent = DummyAgent(Assassin, None, false); + let block_agent = DummyAgent(Contessa, None, false); let deck = vec![]; let discard = vec![]; let players = vec![ @@ -632,7 +736,8 @@ mod test { #[test] fn test_resolution() { - let dummy_agent = make_dummy_agent!(unimplemented!(), unimplemented!(), [Contessa, Duke], Captain); + let dummy_agent = DummyAgent(Assassin, Some(Duke), false); + let loser_agent = DummyAgent(Captain, Some(Duke), false); let deck = vec![Contessa, Contessa]; let discard = vec![]; let players = vec![ @@ -672,7 +777,7 @@ mod test { game.resolution(phase::Resolution { action: Coup, target: Some(1), - }, &[&dummy_agent, &dummy_agent]).unwrap(); + }, &[&dummy_agent, &loser_agent]).unwrap(); assert!(game.players[1].cards.is_empty()); assert_eq!(game.discard, vec![Captain]); } @@ -696,9 +801,9 @@ mod test { target: Some(1), }, &[&dummy_agent, &dummy_agent]).unwrap(); game.players[0].cards.sort(); - assert_eq!(game.players[0].cards, vec![Assassin, Contessa]); + assert_eq!(game.players[0].cards, vec![Contessa, Contessa]); game.deck.sort(); - assert_eq!(game.deck, vec![Duke, Contessa]); + assert_eq!(game.deck, vec![Duke, Assassin]); assert!(game.discard.is_empty()); } }