diff --git a/src/lib.rs b/src/lib.rs index 4cd242a..417e3ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ impl fmt::Display for Card { } #[repr(u8)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Debug)] pub enum Action { Income, ForeignAid, @@ -161,6 +161,9 @@ impl Player { self.cards.draw_first(card); deck.push(card); } + pub fn holds(&self, card: Card) -> bool { + self.cards.iter().find(|&c| *c == card).is_some() + } } impl Default for Player { @@ -219,7 +222,43 @@ impl Game { players_turn_order.skip(1).filter(|p| self.players[*p].is_alive()) } - fn resolution(&mut self, resolution: Resolution, agents: &[&dyn Agent]) -> CoupResult { + fn advance(&mut self) { + let next = self.turn_iterator().next(); + if let Some(next) = next { + self.turn = next; + } + } + + pub fn block_challenge(&mut self, block_challenge: BlockChallenge, agents: &[&dyn Agent]) -> CoupResult { + 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 + let current_player = &mut self.players[self.turn]; + let card = agents[self.turn] + .choose_lost_influence(¤t_player.cards); + current_player.lose(card, &mut self.discard); + Ok(Phase::Done) + } else { + // Player challenged correctly, blocker loses a card + let blocking_player = &mut self.players[block_challenge.blocker]; + let card = agents[block_challenge.blocker] + .choose_lost_influence(&blocking_player.cards); + blocking_player.lose(card, &mut self.discard); + + // Game continues + Ok(Phase::Resolution(Resolution { + action: block_challenge.action, + target: block_challenge.target, + })) + } + } else { + // Player chose not to challenge the block, turn is forfeit + self.advance(); + Ok(Phase::Done) + } + } + + pub 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 { @@ -228,8 +267,10 @@ impl Game { 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); + if target_player.is_alive() { // Target may have died from challenge + 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"), }, @@ -259,29 +300,37 @@ impl Game { _ => return Err("Steal resolution has no target"), } } - let next = self.turn_iterator().next(); - if let Some(next) = next { - self.turn = next; - } + self.advance(); Ok(Phase::Done) } } -enum Phase { +#[derive(PartialEq, Debug)] +pub enum Phase { Action(Action), //ActionChallenge(ActionChallenge), //Block(Block), - //BlockChallenge(BlockChallenge), + BlockChallenge(BlockChallenge), Resolution(Resolution), Done, } +#[derive(PartialEq, Debug)] +pub struct BlockChallenge { + blocker: usize, + block_card: Card, + action: Action, + target: Option, +} + +#[derive(PartialEq, Debug)] pub struct Resolution { action: Action, target: Option, } pub trait Agent : fmt::Debug { + fn should_block_challenge(&self, block_challenge: &BlockChallenge) -> bool; fn exchange(&self, cards: &[Card]) -> [Card; 2]; fn choose_lost_influence(&self, cards: &[Card]) -> Card; } @@ -290,18 +339,102 @@ pub trait Agent : fmt::Debug { mod test { use super::*; + macro_rules! make_dummy_agent { + ($sbc: expr, $e: expr, $cli: expr) => {{ + #[derive(Debug)] + struct DummyAgent; + impl Agent for DummyAgent { + #[allow(unused)] + fn should_block_challenge(&self, block_challenge: &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 + }} + } + + #[test] + fn test_block_challenge() { + let challenge_agent = make_dummy_agent!(true, unimplemented!(), Assassin); + let non_challenge_agent = make_dummy_agent!(false, unimplemented!(), Assassin); + let block_agent = make_dummy_agent!(unimplemented!(), unimplemented!(), Contessa); + let deck = vec![]; + let discard = vec![]; + let players = vec![ + Player { coins: 2, cards: vec![Assassin] }, + Player { coins: 2, cards: vec![Contessa] }, + ]; + let game = Game { + deck, + discard, + players, + turn: 0 + }; + + // Test non-challenge + { + let mut game = game.clone(); + assert_eq!( + game.block_challenge( BlockChallenge { + blocker: 1, + block_card: Contessa, + action: Assassinate, + target: Some(1), + }, &[&non_challenge_agent, &block_agent]), + Ok(Phase::Done) + ); + assert!(game.discard.is_empty()); + } + + // Test failed challenge + { + let mut game = game.clone(); + assert_eq!( + game.block_challenge( BlockChallenge { + blocker: 1, + block_card: Contessa, + action: Assassinate, + target: Some(1), + }, &[&challenge_agent, &block_agent]), + Ok(Phase::Done) + ); + assert!(!game.discard.is_empty()); + } + + // Test successful challenge + { + let mut game = game.clone(); + assert_eq!( + game.block_challenge( BlockChallenge { + blocker: 1, + block_card: Duke, + action: ForeignAid, + target: None, + }, &[&challenge_agent, &block_agent]), + Ok(Phase::Resolution(Resolution { + action: ForeignAid, + target: None, + })) + ); + assert!(!game.discard.is_empty()); + } + + } + + + + #[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 dummy_agent = make_dummy_agent!(unimplemented!(), [Contessa, Duke], Captain); let deck = vec![Contessa, Contessa]; let discard = vec![]; let players = vec![ @@ -321,7 +454,7 @@ mod test { game.resolution(Resolution { action: Income, target: None, - }, &[&DummyAgent, &DummyAgent]).unwrap(); + }, &[&dummy_agent, &dummy_agent]).unwrap(); assert_eq!(game.players[0].coins, 3); } @@ -331,7 +464,7 @@ mod test { game.resolution(Resolution { action: ForeignAid, target: None, - }, &[&DummyAgent, &DummyAgent]).unwrap(); + }, &[&dummy_agent, &dummy_agent]).unwrap(); assert_eq!(game.players[0].coins, 4); } @@ -341,7 +474,7 @@ mod test { game.resolution(Resolution { action: Coup, target: Some(1), - }, &[&DummyAgent, &DummyAgent]).unwrap(); + }, &[&dummy_agent, &dummy_agent]).unwrap(); assert!(game.players[1].cards.is_empty()); assert_eq!(game.discard, vec![Captain]); } @@ -352,7 +485,7 @@ mod test { game.resolution(Resolution { action: Steal, target: Some(1), - }, &[&DummyAgent, &DummyAgent]).unwrap(); + }, &[&dummy_agent, &dummy_agent]).unwrap(); assert_eq!(game.players[0].coins, 3); assert_eq!(game.players[1].coins, 0); } @@ -363,7 +496,7 @@ mod test { game.resolution(Resolution { action: Exchange, target: Some(1), - }, &[&DummyAgent, &DummyAgent]).unwrap(); + }, &[&dummy_agent, &dummy_agent]).unwrap(); game.players[0].cards.sort(); assert_eq!(game.players[0].cards, vec![Assassin, Contessa]); game.deck.sort();