Implement resolution phase
This commit is contained in:
commit
0dfad4ad30
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
75
Cargo.lock
generated
Normal file
75
Cargo.lock
generated
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coup"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.2+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "coup"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "*"
|
374
src/lib.rs
Normal file
374
src/lib.rs
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
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)]
|
||||||
|
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, Income) |
|
||||||
|
(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)]
|
||||||
|
pub enum Action {
|
||||||
|
Income,
|
||||||
|
ForeignAid,
|
||||||
|
Coup,
|
||||||
|
Tax,
|
||||||
|
Assassinate,
|
||||||
|
Exchange,
|
||||||
|
Steal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
pub fn is_targeted(self) -> bool {
|
||||||
|
matches!(self, Coup | Steal | Assassinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn challenger_mode(self) -> ResMode {
|
||||||
|
match self {
|
||||||
|
Income | ForeignAid | Coup => ResMode::None,
|
||||||
|
Assassinate | Steal | Tax | Exchange => ResMode::Anyone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocker_mode(self) -> ResMode {
|
||||||
|
match self {
|
||||||
|
Income | Tax | Exchange | Coup => ResMode::None,
|
||||||
|
Assassinate | Steal => ResMode::Target,
|
||||||
|
ForeignAid => ResMode::Anyone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
pub enum ResMode {
|
||||||
|
None,
|
||||||
|
Target,
|
||||||
|
Anyone,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Player {
|
||||||
|
pub coins: u8,
|
||||||
|
pub cards: Vec<Card>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub fn is_alive(&self) -> bool {
|
||||||
|
!self.cards.is_empty()
|
||||||
|
}
|
||||||
|
pub fn lose(&mut self, card: Card, deck: &mut Vec<Card>) {
|
||||||
|
self.cards.draw_first(card);
|
||||||
|
deck.push(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 resolution(&mut self, resolution: Resolution, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
||||||
|
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) => {
|
||||||
|
let target_player = &self.players[target];
|
||||||
|
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"),
|
||||||
|
},
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let next = self.turn_iterator().next();
|
||||||
|
if let Some(next) = next {
|
||||||
|
self.turn = next;
|
||||||
|
}
|
||||||
|
Ok(Phase::Done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Phase {
|
||||||
|
Action(Action),
|
||||||
|
//ActionChallenge(ActionChallenge),
|
||||||
|
//Block(Block),
|
||||||
|
//BlockChallenge(BlockChallenge),
|
||||||
|
Resolution(Resolution),
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resolution {
|
||||||
|
action: Action,
|
||||||
|
target: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Agent : fmt::Debug {
|
||||||
|
fn exchange(&self, cards: &[Card]) -> [Card; 2];
|
||||||
|
fn choose_lost_influence(&self, cards: &[Card]) -> Card;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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 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();
|
||||||
|
game.resolution(Resolution {
|
||||||
|
action: Income,
|
||||||
|
target: None,
|
||||||
|
}, &[&DummyAgent, &DummyAgent]).unwrap();
|
||||||
|
assert_eq!(game.players[0].coins, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test foreign aid
|
||||||
|
{
|
||||||
|
let mut game = game.clone();
|
||||||
|
game.resolution(Resolution {
|
||||||
|
action: ForeignAid,
|
||||||
|
target: None,
|
||||||
|
}, &[&DummyAgent, &DummyAgent]).unwrap();
|
||||||
|
assert_eq!(game.players[0].coins, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test coup / assassinate
|
||||||
|
{
|
||||||
|
let mut game = game.clone();
|
||||||
|
game.resolution(Resolution {
|
||||||
|
action: Coup,
|
||||||
|
target: Some(1),
|
||||||
|
}, &[&DummyAgent, &DummyAgent]).unwrap();
|
||||||
|
assert!(game.players[1].cards.is_empty());
|
||||||
|
assert_eq!(game.discard, vec![Captain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test steal
|
||||||
|
{
|
||||||
|
let mut game = game.clone();
|
||||||
|
game.resolution(Resolution {
|
||||||
|
action: Steal,
|
||||||
|
target: Some(1),
|
||||||
|
}, &[&DummyAgent, &DummyAgent]).unwrap();
|
||||||
|
assert_eq!(game.players[0].coins, 3);
|
||||||
|
assert_eq!(game.players[1].coins, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test exchange
|
||||||
|
{
|
||||||
|
let mut game = game.clone();
|
||||||
|
game.resolution(Resolution {
|
||||||
|
action: Exchange,
|
||||||
|
target: Some(1),
|
||||||
|
}, &[&DummyAgent, &DummyAgent]).unwrap();
|
||||||
|
game.players[0].cards.sort();
|
||||||
|
assert_eq!(game.players[0].cards, vec![Assassin, Contessa]);
|
||||||
|
game.deck.sort();
|
||||||
|
assert_eq!(game.deck, vec![Duke, Contessa]);
|
||||||
|
assert!(game.discard.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/main.rs
Normal file
5
src/main.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use coup::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user