2022-05-18 14:04:39 -05:00
|
|
|
//! Coup is a game of deception for two to six players.
|
|
|
|
|
2022-05-18 11:14:27 -05:00
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
|
|
|
|
use std::fmt;
|
|
|
|
|
|
|
|
use Action::*;
|
|
|
|
use Card::*;
|
|
|
|
|
|
|
|
pub type CoupResult<T> = Result<T, &'static str>;
|
|
|
|
|
|
|
|
#[repr(u8)]
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
2022-05-18 14:04:39 -05:00
|
|
|
/// Each card represents the right to perform specific actions or counteractions.
|
2022-05-18 11:14:27 -05:00
|
|
|
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),
|
2022-05-18 12:27:31 -05:00
|
|
|
(Duke, ForeignAid) |
|
2022-05-18 11:14:27 -05:00
|
|
|
(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<Card> {
|
|
|
|
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)]
|
2022-05-18 14:04:39 -05:00
|
|
|
/// The actions a player can perform on their turn.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub enum Action {
|
|
|
|
Income,
|
|
|
|
ForeignAid,
|
|
|
|
Coup,
|
|
|
|
Tax,
|
|
|
|
Assassinate,
|
|
|
|
Exchange,
|
|
|
|
Steal,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Action {
|
2022-05-18 14:04:39 -05:00
|
|
|
/// If the action needs a target.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub fn is_targeted(self) -> bool {
|
|
|
|
matches!(self, Coup | Steal | Assassinate)
|
|
|
|
}
|
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
/// Which players may challenge the action.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub fn challenger_mode(self) -> ResMode {
|
|
|
|
match self {
|
|
|
|
Income | ForeignAid | Coup => ResMode::None,
|
|
|
|
Assassinate | Steal | Tax | Exchange => ResMode::Anyone,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
/// Which players may block the action.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub fn blocker_mode(self) -> ResMode {
|
|
|
|
match self {
|
|
|
|
Income | Tax | Exchange | Coup => ResMode::None,
|
|
|
|
Assassinate | Steal => ResMode::Target,
|
|
|
|
ForeignAid => ResMode::Anyone,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
/// How much the action costs to perform.
|
2022-05-18 11:14:27 -05:00
|
|
|
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)]
|
2022-05-18 14:04:39 -05:00
|
|
|
/// How the other players may respond to an action.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub enum ResMode {
|
|
|
|
None,
|
|
|
|
Target,
|
|
|
|
Anyone,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2022-05-18 14:04:39 -05:00
|
|
|
/// The cards and coins a single player possesses.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub struct Player {
|
|
|
|
pub coins: u8,
|
|
|
|
pub cards: Vec<Card>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Player {
|
2022-05-18 14:04:39 -05:00
|
|
|
/// If the player still possesses any cards, and thus is still in the game
|
2022-05-18 11:14:27 -05:00
|
|
|
pub fn is_alive(&self) -> bool {
|
|
|
|
!self.cards.is_empty()
|
|
|
|
}
|
2022-05-18 17:47:33 -05:00
|
|
|
fn wins_challenge(&self, action: Action) -> bool {
|
|
|
|
self.cards.iter().find(|&c| c.allows_action(action)).is_some()
|
|
|
|
}
|
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
fn lose(&mut self, card: Card, deck: &mut Vec<Card>) {
|
2022-05-18 11:14:27 -05:00
|
|
|
self.cards.draw_first(card);
|
|
|
|
deck.push(card);
|
|
|
|
}
|
2022-05-18 17:47:33 -05:00
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
fn holds(&self, card: Card) -> bool {
|
2022-05-18 11:14:27 -05:00
|
|
|
self.cards.iter().find(|&c| *c == card).is_some()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Player {
|
|
|
|
fn default() -> Self {
|
|
|
|
Player {
|
|
|
|
cards: Vec::new(),
|
|
|
|
coins: 2,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Game {
|
|
|
|
pub players: Vec<Player>,
|
|
|
|
pub deck: Vec<Card>,
|
|
|
|
pub discard: Vec<Card>,
|
|
|
|
pub turn: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! thrice {
|
|
|
|
($($e:expr),*) => {
|
|
|
|
vec![$($e, $e, $e),*]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Game {
|
2022-05-18 17:47:33 -05:00
|
|
|
/// Creates a new game, with default [Players](Player) and a shuffled deck of [Cards](card)
|
2022-05-18 11:14:27 -05:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
2022-05-18 17:47:33 -05:00
|
|
|
/// If all but one [Players](Player) have lost all influence.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub fn is_game_over(&self) -> bool {
|
|
|
|
self.players.iter().filter(|p| p.is_alive()).count() == 1
|
|
|
|
}
|
|
|
|
|
|
|
|
fn turn_iterator(&self) -> impl Iterator<Item = usize> + '_ {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 17:47:33 -05:00
|
|
|
fn player_lose_influence(&mut self, id: usize, agents: &[&dyn Agent]) -> CoupResult<()>{
|
2022-05-18 11:14:27 -05:00
|
|
|
let player = &mut self.players[id];
|
|
|
|
let card = agents[id]
|
|
|
|
.choose_lost_influence(&player.cards);
|
2022-05-18 17:47:33 -05:00
|
|
|
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<Phase> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
}
|
|
|
|
|
2022-05-18 12:19:12 -05:00
|
|
|
pub fn block(&mut self, block: phase::Block, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
|
|
|
match block.action.blocker_mode() {
|
|
|
|
ResMode::None => Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: block.action,
|
|
|
|
target: block.target,
|
|
|
|
})),
|
|
|
|
ResMode::Target => match agents[block.target.unwrap()].choose_block_card(&block) {
|
2022-05-18 12:27:31 -05:00
|
|
|
Some(card) => {
|
|
|
|
if card.blocks_action(block.action) {
|
|
|
|
Ok(Phase::BlockChallenge(phase::BlockChallenge {
|
|
|
|
blocker: block.target.unwrap(),
|
|
|
|
block_card: card,
|
|
|
|
action: block.action,
|
|
|
|
target: block.target,
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
Err("Card does not block action")
|
|
|
|
}
|
|
|
|
},
|
2022-05-18 12:19:12 -05:00
|
|
|
None => Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: block.action,
|
|
|
|
target: block.target,
|
|
|
|
})),
|
|
|
|
}
|
|
|
|
ResMode::Anyone => {
|
|
|
|
for id in self.turn_iterator() {
|
|
|
|
if let Some(card) = agents[id].choose_block_card(&block) {
|
2022-05-18 12:27:31 -05:00
|
|
|
if card.blocks_action(block.action) {
|
|
|
|
return Ok(Phase::BlockChallenge(phase::BlockChallenge {
|
|
|
|
blocker: id,
|
|
|
|
block_card: card,
|
|
|
|
action: block.action,
|
|
|
|
target: block.target,
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
return Err("Card does not block action")
|
|
|
|
}
|
2022-05-18 12:19:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: block.action,
|
|
|
|
target: block.target,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
pub fn block_challenge(&mut self, block_challenge: phase::BlockChallenge, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
2022-05-18 11:14:27 -05:00
|
|
|
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
|
2022-05-18 17:47:33 -05:00
|
|
|
self.player_lose_influence(self.turn, agents)?;
|
2022-05-18 11:14:27 -05:00
|
|
|
Ok(Phase::Done)
|
|
|
|
} else {
|
|
|
|
// Player challenged correctly, blocker loses a card
|
2022-05-18 17:47:33 -05:00
|
|
|
self.player_lose_influence(block_challenge.blocker, agents)?;
|
2022-05-18 11:14:27 -05:00
|
|
|
// Game continues
|
2022-05-18 11:25:39 -05:00
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: block_challenge.action,
|
|
|
|
target: block_challenge.target,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Player chose not to challenge the block, turn is forfeit
|
|
|
|
self.advance();
|
|
|
|
Ok(Phase::Done)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
pub fn resolution(&mut self, resolution: phase::Resolution, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
2022-05-18 11:14:27 -05:00
|
|
|
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) => {
|
|
|
|
// Target may have died from challenge
|
|
|
|
let target_alive = self.players[target].is_alive();
|
|
|
|
if target_alive {
|
2022-05-18 17:47:33 -05:00
|
|
|
self.player_lose_influence(target, agents)?;
|
2022-05-18 11:14:27 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => 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"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.advance();
|
|
|
|
Ok(Phase::Done)
|
|
|
|
}
|
|
|
|
}
|
2022-05-18 14:04:39 -05:00
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
pub mod phase {
|
2022-05-18 14:04:39 -05:00
|
|
|
//! Structures relating to game phases.
|
|
|
|
//!
|
|
|
|
//! Coup turns have 5 phases, depending on what actions are taken each phase
|
|
|
|
//! some phases might be skipped. These structures marshal information
|
|
|
|
//! between the phases.
|
2022-05-18 11:25:39 -05:00
|
|
|
use super::{ Card, Action };
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub enum Phase {
|
|
|
|
Action(Action),
|
2022-05-18 17:47:33 -05:00
|
|
|
ActionChallenge(ActionChallenge),
|
2022-05-18 11:25:39 -05:00
|
|
|
Block(Block),
|
|
|
|
BlockChallenge(BlockChallenge),
|
|
|
|
Resolution(Resolution),
|
|
|
|
Done,
|
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
|
2022-05-18 17:47:33 -05:00
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub struct ActionChallenge {
|
|
|
|
pub action: Action,
|
|
|
|
pub target: Option<usize>,
|
|
|
|
}
|
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub struct Block {
|
2022-05-18 12:19:12 -05:00
|
|
|
pub action: Action,
|
|
|
|
pub target: Option<usize>,
|
2022-05-18 11:25:39 -05:00
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub struct BlockChallenge {
|
|
|
|
pub blocker: usize,
|
|
|
|
pub block_card: Card,
|
|
|
|
pub action: Action,
|
|
|
|
pub target: Option<usize>,
|
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub struct Resolution {
|
|
|
|
pub action: Action,
|
|
|
|
pub target: Option<usize>,
|
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
}
|
|
|
|
|
2022-05-18 11:25:39 -05:00
|
|
|
use phase::Phase;
|
2022-05-18 11:14:27 -05:00
|
|
|
|
2022-05-18 14:04:39 -05:00
|
|
|
/// An interface to a game to make strategic decisions.
|
2022-05-18 11:14:27 -05:00
|
|
|
pub trait Agent : fmt::Debug {
|
2022-05-18 17:47:33 -05:00
|
|
|
/// Should the agent challenge the action?
|
|
|
|
fn should_action_challenge(&self, action_challenge: &phase::ActionChallenge) -> bool;
|
2022-05-18 14:04:39 -05:00
|
|
|
/// 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]
|
2022-05-18 12:19:12 -05:00
|
|
|
fn choose_block_card(&self, block: &phase::Block) -> Option<Card>;
|
2022-05-18 14:04:39 -05:00
|
|
|
/// Should the agent challenge the block?
|
2022-05-18 11:25:39 -05:00
|
|
|
fn should_block_challenge(&self, block_challenge: &phase::BlockChallenge) -> bool;
|
2022-05-18 14:04:39 -05:00
|
|
|
/// The [Ambassador]'s exchange.
|
|
|
|
/// Given 3 or 4 [Cards](Card) the agent must return two cards to the deck.
|
2022-05-18 11:14:27 -05:00
|
|
|
fn exchange(&self, cards: &[Card]) -> [Card; 2];
|
2022-05-18 14:04:39 -05:00
|
|
|
/// The player has lost influence, and must choose a [Card] from their hand
|
|
|
|
/// to discard.
|
2022-05-18 11:14:27 -05:00
|
|
|
fn choose_lost_influence(&self, cards: &[Card]) -> Card;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
2022-05-18 17:47:33 -05:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct DummyAgent(Card, Option<Card>, 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<Card> {
|
|
|
|
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),
|
|
|
|
);
|
|
|
|
}
|
2022-05-18 11:14:27 -05:00
|
|
|
}
|
|
|
|
|
2022-05-18 12:19:12 -05:00
|
|
|
#[test]
|
|
|
|
fn test_block() {
|
2022-05-18 17:47:33 -05:00
|
|
|
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);
|
2022-05-18 12:19:12 -05:00
|
|
|
|
|
|
|
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 unblockable
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert_eq!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: Income,
|
|
|
|
target: None,
|
|
|
|
}, &[&non_blocking_agent, &ambassador_block_agent, &ambassador_block_agent]),
|
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: Income,
|
|
|
|
target: None
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test target blocked
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert_eq!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: Steal,
|
|
|
|
target: Some(2),
|
|
|
|
}, &[&non_blocking_agent, &ambassador_block_agent, &ambassador_block_agent]),
|
|
|
|
Ok(Phase::BlockChallenge(phase::BlockChallenge {
|
|
|
|
blocker: 2,
|
|
|
|
block_card: Ambassador,
|
|
|
|
action: Steal,
|
|
|
|
target: Some(2),
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test target non-blocked
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert_eq!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: Steal,
|
|
|
|
target: Some(2),
|
|
|
|
}, &[&non_blocking_agent, &ambassador_block_agent, &non_blocking_agent]),
|
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: Steal,
|
|
|
|
target: Some(2),
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test anyone blocked
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert_eq!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}, &[&non_blocking_agent, &non_blocking_agent, &duke_block_agent]),
|
|
|
|
Ok(Phase::BlockChallenge(phase::BlockChallenge {
|
|
|
|
blocker: 2,
|
|
|
|
block_card: Duke,
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test no one blocked
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert_eq!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}, &[&non_blocking_agent, &non_blocking_agent, &non_blocking_agent]),
|
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
2022-05-18 12:27:31 -05:00
|
|
|
|
|
|
|
// Test bad block card
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
|
|
|
assert!(
|
|
|
|
game.block(phase::Block {
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}, &[&non_blocking_agent, &ambassador_block_agent, &non_blocking_agent])
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-05-18 12:19:12 -05:00
|
|
|
}
|
|
|
|
|
2022-05-18 11:14:27 -05:00
|
|
|
#[test]
|
|
|
|
fn test_block_challenge() {
|
2022-05-18 17:47:33 -05:00
|
|
|
let challenge_agent = DummyAgent(Assassin, None, true);
|
|
|
|
let non_challenge_agent = DummyAgent(Assassin, None, false);
|
|
|
|
let block_agent = DummyAgent(Contessa, None, false);
|
2022-05-18 11:14:27 -05:00
|
|
|
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!(
|
2022-05-18 11:25:39 -05:00
|
|
|
game.block_challenge( phase::BlockChallenge {
|
2022-05-18 11:14:27 -05:00
|
|
|
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!(
|
2022-05-18 11:25:39 -05:00
|
|
|
game.block_challenge( phase::BlockChallenge {
|
2022-05-18 11:14:27 -05:00
|
|
|
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!(
|
2022-05-18 11:25:39 -05:00
|
|
|
game.block_challenge( phase::BlockChallenge {
|
2022-05-18 11:14:27 -05:00
|
|
|
blocker: 1,
|
|
|
|
block_card: Duke,
|
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}, &[&challenge_agent, &block_agent]),
|
2022-05-18 11:25:39 -05:00
|
|
|
Ok(Phase::Resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
assert!(!game.discard.is_empty());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_resolution() {
|
2022-05-18 17:47:33 -05:00
|
|
|
let dummy_agent = DummyAgent(Assassin, Some(Duke), false);
|
|
|
|
let loser_agent = DummyAgent(Captain, Some(Duke), false);
|
2022-05-18 11:14:27 -05:00
|
|
|
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();
|
2022-05-18 11:25:39 -05:00
|
|
|
game.resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: Income,
|
|
|
|
target: None,
|
|
|
|
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
|
|
|
assert_eq!(game.players[0].coins, 3);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test foreign aid
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
2022-05-18 11:25:39 -05:00
|
|
|
game.resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: ForeignAid,
|
|
|
|
target: None,
|
|
|
|
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
|
|
|
assert_eq!(game.players[0].coins, 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test coup / assassinate
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
2022-05-18 11:25:39 -05:00
|
|
|
game.resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: Coup,
|
|
|
|
target: Some(1),
|
2022-05-18 17:47:33 -05:00
|
|
|
}, &[&dummy_agent, &loser_agent]).unwrap();
|
2022-05-18 11:14:27 -05:00
|
|
|
assert!(game.players[1].cards.is_empty());
|
|
|
|
assert_eq!(game.discard, vec![Captain]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test steal
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
2022-05-18 11:25:39 -05:00
|
|
|
game.resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: Steal,
|
|
|
|
target: Some(1),
|
|
|
|
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
|
|
|
assert_eq!(game.players[0].coins, 3);
|
|
|
|
assert_eq!(game.players[1].coins, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test exchange
|
|
|
|
{
|
|
|
|
let mut game = game.clone();
|
2022-05-18 11:25:39 -05:00
|
|
|
game.resolution(phase::Resolution {
|
2022-05-18 11:14:27 -05:00
|
|
|
action: Exchange,
|
|
|
|
target: Some(1),
|
|
|
|
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
|
|
|
game.players[0].cards.sort();
|
2022-05-18 17:47:33 -05:00
|
|
|
assert_eq!(game.players[0].cards, vec![Contessa, Contessa]);
|
2022-05-18 11:14:27 -05:00
|
|
|
game.deck.sort();
|
2022-05-18 17:47:33 -05:00
|
|
|
assert_eq!(game.deck, vec![Duke, Assassin]);
|
2022-05-18 11:14:27 -05:00
|
|
|
assert!(game.discard.is_empty());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|