Add readme, action challenges, admit that I did it totally wrong

This commit is contained in:
Dane Johnson 2022-05-18 17:47:33 -05:00
parent 7801eb0dd1
commit 12d0b86e78
2 changed files with 174 additions and 40 deletions

29
README.md Normal file
View File

@ -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

View File

@ -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<Card>) {
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);
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)
}
}
}
}
pub fn block(&mut self, block: phase::Block, agents: &[&dyn Agent]) -> CoupResult<Phase> {
@ -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<usize>,
}
#[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<Card>;
@ -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;
struct DummyAgent(Card, Option<Card>, bool);
impl Agent for DummyAgent {
#[allow(unused)]
fn choose_block_card(&self, block: &phase::Block) -> Option<Card> {
$cbc
fn should_action_challenge(&self, _action_challenge: &phase::ActionChallenge) -> bool {
self.2
}
#[allow(unused)]
fn should_block_challenge(&self, block_challenge: &phase::BlockChallenge) -> bool {
$sbc
fn choose_block_card(&self, _block: &phase::Block) -> Option<Card> {
self.1
}
#[allow(unused)]
fn exchange(&self, cards: &[Card]) -> [Card; 2] {
$e
fn should_block_challenge(&self, _block_challenge: &phase::BlockChallenge) -> bool {
self.2
}
#[allow(unused)]
fn choose_lost_influence(&self, cards: &[Card]) -> Card {
$cli
fn exchange(&self, _cards: &[Card]) -> [Card; 2] {
[self.0, self.1.unwrap()]
}
fn choose_lost_influence(&self, _cards: &[Card]) -> Card {
self.0
}
}
DummyAgent
}}
#[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());
}
}