Switch from using dyn traits to channels for io (breaks cli for now)
This commit is contained in:
parent
06352344fb
commit
cab89f5d64
122
Cargo.lock
generated
122
Cargo.lock
generated
@ -12,6 +12,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
name = "coup"
|
name = "coup"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ntest",
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -32,12 +33,81 @@ version = "0.2.125"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntest"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c544e496c816f0a59645c0bb69097e453df203954ae2ed4b3ac4251fad69d44"
|
||||||
|
dependencies = [
|
||||||
|
"ntest_proc_macro_helper",
|
||||||
|
"ntest_test_cases",
|
||||||
|
"ntest_timeout",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntest_proc_macro_helper"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f52e34b414605b77efc95c3f0ecef01df0c324bcc7f68d9a9cb7a7552777e52"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntest_test_cases"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99a81eb400abc87063f829560bc5c5c835177703b83d1cd991960db0b2a00abe"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntest_timeout"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b10db009e117aca57cbfb70ac332348f9a89d09ff7204497c283c0f7a0c96323"
|
||||||
|
dependencies = [
|
||||||
|
"ntest_proc_macro_helper",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -68,6 +138,58 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.137"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.10.2+wasi-snapshot-preview1"
|
||||||
|
@ -6,4 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "*"
|
rand = "*"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
ntest = "*"
|
218
src/lib.rs
218
src/lib.rs
@ -247,44 +247,59 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player_lose_influence(&mut self, id: usize, agents: &[&dyn Agent]) -> CoupResult<()>{
|
fn player_lose_influence(&mut self, id: usize, channels: &Channels) -> CoupResult<()> {
|
||||||
let card = agents[id].choose_lost_influence(self);
|
channels.send(id, Message::LoseInfluence);
|
||||||
|
let card = channels.recv_discard(id)?;
|
||||||
let player = &self.players[id];
|
let player = &self.players[id];
|
||||||
if player.holds(card) {
|
if player.holds(card) {
|
||||||
self.players[id].lose(card, &mut self.discard);
|
self.players[id].lose(card, &mut self.discard);
|
||||||
|
channels.broadcast(Message::Discard(card));
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err("Player discarded a card they don't hold")
|
Err("Player discarded a card they don't hold")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(&mut self, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
pub fn action(&mut self, channels: &Channels) -> CoupResult<Phase> {
|
||||||
let move_ = agents[self.turn].choose_move(self);
|
channels.broadcast(Message::Turn(self.turn));
|
||||||
|
let move_ = channels.recv_move(self.turn)?;
|
||||||
if move_.action.is_targeted() && move_.target.is_none() {
|
if move_.action.is_targeted() && move_.target.is_none() {
|
||||||
Err("Targeted action with no target")
|
Err("Targeted action with no target")
|
||||||
} else if move_.action.coin_cost() > self.players[self.turn].coins {
|
} else if move_.action.coin_cost() > self.players[self.turn].coins {
|
||||||
Err("Cannot afford action")
|
Err("Cannot afford action")
|
||||||
} else {
|
} else {
|
||||||
|
channels.broadcast(Message::Move(move_));
|
||||||
Ok(Phase::ActionChallenge(move_))
|
Ok(Phase::ActionChallenge(move_))
|
||||||
}
|
}
|
||||||
// TODO there're definately more cases, find and cover these
|
// TODO there're definately more cases, find and cover these
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action_challenge(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
pub fn action_challenge(&mut self, move_: Move, channels: &Channels) -> CoupResult<Phase> {
|
||||||
match move_.action.challenger_mode() {
|
match move_.action.challenger_mode() {
|
||||||
ResMode::None => Ok(Phase::Block(move_)),
|
ResMode::None => Ok(Phase::Block(move_)),
|
||||||
ResMode::Target => unreachable!(),
|
ResMode::Target => unreachable!(),
|
||||||
ResMode::Anyone => {
|
ResMode::Anyone => {
|
||||||
let challenger = self.turn_iterator().find(|&id| agents[id].should_action_challenge(self, move_));
|
let wait_timer = Timer::start();
|
||||||
|
let challenger = 'outer: loop {
|
||||||
|
if wait_timer.is_timeout() {
|
||||||
|
break None
|
||||||
|
}
|
||||||
|
for id in self.turn_iterator() {
|
||||||
|
if let Some(()) = channels.try_recv_challenge(id) {
|
||||||
|
break 'outer Some(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(challenger) = challenger{
|
if let Some(challenger) = challenger{
|
||||||
let current_player_wins = self.players[self.turn].wins_challenge(move_.action);
|
let current_player_wins = self.players[self.turn].wins_challenge(move_.action);
|
||||||
if current_player_wins {
|
if current_player_wins {
|
||||||
// Challenger loses influence
|
// Challenger loses influence
|
||||||
self.player_lose_influence(challenger, agents)?;
|
self.player_lose_influence(challenger, channels)?;
|
||||||
Ok(Phase::Block(move_))
|
Ok(Phase::Block(move_))
|
||||||
} else {
|
} else {
|
||||||
// Player loses influence
|
// Player loses influence
|
||||||
self.player_lose_influence(self.turn, agents)?;
|
self.player_lose_influence(self.turn, channels)?;
|
||||||
// Turn is forfeit
|
// Turn is forfeit
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(Phase::Done)
|
Ok(Phase::Done)
|
||||||
@ -296,50 +311,82 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn block(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
pub fn block(&mut self, move_: Move, channels: &Channels) -> CoupResult<Phase> {
|
||||||
match move_.action.blocker_mode() {
|
match move_.action.blocker_mode() {
|
||||||
ResMode::None => Ok(Phase::Resolution(move_)),
|
ResMode::None => Ok(Phase::Resolution(move_)),
|
||||||
ResMode::Target => match agents[move_.target.unwrap()].choose_block_card(self, move_) {
|
ResMode::Target => {
|
||||||
Some(card) => {
|
let wait_timer = Timer::start();
|
||||||
if card.blocks_action(move_.action) {
|
let card = loop {
|
||||||
Ok(Phase::BlockChallenge(move_, Block {
|
if wait_timer.is_timeout() {
|
||||||
blocker: move_.target.unwrap(),
|
break None
|
||||||
card,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Err("Card does not block action")
|
|
||||||
}
|
}
|
||||||
},
|
if let Some(card) = channels.try_recv_block(move_.target.unwrap()) {
|
||||||
None => Ok(Phase::Resolution(move_)),
|
break Some(card)
|
||||||
}
|
}
|
||||||
ResMode::Anyone => {
|
};
|
||||||
for id in self.turn_iterator() {
|
|
||||||
if let Some(card) = agents[id].choose_block_card(self, move_) {
|
match card {
|
||||||
|
Some(card) => {
|
||||||
if card.blocks_action(move_.action) {
|
if card.blocks_action(move_.action) {
|
||||||
return Ok(Phase::BlockChallenge(move_, Block {
|
Ok(Phase::BlockChallenge(move_, Block {
|
||||||
blocker: id,
|
blocker: move_.target.unwrap(),
|
||||||
card,
|
card,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
return Err("Card does not block action")
|
Err("Card does not block action")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Ok(Phase::Resolution(move_)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResMode::Anyone => {
|
||||||
|
let wait_timer = Timer::start();
|
||||||
|
let block = 'outer: loop {
|
||||||
|
if wait_timer.is_timeout() {
|
||||||
|
break None
|
||||||
|
}
|
||||||
|
for id in self.turn_iterator() {
|
||||||
|
if let Some(card) = channels.try_recv_block(id) {
|
||||||
|
break 'outer Some(Block {
|
||||||
|
blocker: id,
|
||||||
|
card,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(block) = block {
|
||||||
|
if block.card.blocks_action(move_.action) {
|
||||||
|
Ok(Phase::BlockChallenge(move_, block))
|
||||||
|
} else {
|
||||||
|
Err("Card does not block action")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Phase::Resolution(move_))
|
||||||
}
|
}
|
||||||
Ok(Phase::Resolution(move_))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn block_challenge(&mut self, move_: Move, block: Block, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
pub fn block_challenge(&mut self, move_: Move, block: Block, channels: &Channels) -> CoupResult<Phase> {
|
||||||
if agents[self.turn].should_block_challenge(self, move_, block) {
|
let wait_timer = Timer::start();
|
||||||
|
let is_challenge = loop {
|
||||||
|
if wait_timer.is_timeout() {
|
||||||
|
break false
|
||||||
|
}
|
||||||
|
if let Some(()) = channels.try_recv_challenge(self.turn) {
|
||||||
|
break true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if is_challenge {
|
||||||
if self.players[block.blocker].holds(block.card) {
|
if self.players[block.blocker].holds(block.card) {
|
||||||
// Player challenged incorrectly, loses influence and turn is forfeit
|
// Player challenged incorrectly, loses influence and turn is forfeit
|
||||||
self.player_lose_influence(self.turn, agents)?;
|
self.player_lose_influence(self.turn, channels)?;
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(Phase::Done)
|
Ok(Phase::Done)
|
||||||
} else {
|
} else {
|
||||||
// Player challenged correctly, blocker loses a card
|
// Player challenged correctly, blocker loses a card
|
||||||
self.player_lose_influence(block.blocker, agents)?;
|
self.player_lose_influence(block.blocker, channels)?;
|
||||||
// Game continues
|
// Game continues
|
||||||
Ok(Phase::Resolution(move_))
|
Ok(Phase::Resolution(move_))
|
||||||
}
|
}
|
||||||
@ -350,7 +397,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolution(&mut self, move_: Move, agents: &[&dyn Agent]) -> CoupResult<Phase> {
|
pub fn resolution(&mut self, move_: Move, channels: &Channels) -> CoupResult<Phase> {
|
||||||
self.players[self.turn].coins -= move_.action.coin_cost();
|
self.players[self.turn].coins -= move_.action.coin_cost();
|
||||||
match move_.action {
|
match move_.action {
|
||||||
Income => self.players[self.turn].coins += 1,
|
Income => self.players[self.turn].coins += 1,
|
||||||
@ -360,7 +407,7 @@ impl Game {
|
|||||||
// Target may have died from challenge
|
// Target may have died from challenge
|
||||||
let target_alive = self.players[target].is_alive();
|
let target_alive = self.players[target].is_alive();
|
||||||
if target_alive {
|
if target_alive {
|
||||||
self.player_lose_influence(target, agents)?;
|
self.player_lose_influence(target, channels)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err("Coup/Assassinate resolution has no target"),
|
_ => return Err("Coup/Assassinate resolution has no target"),
|
||||||
@ -370,7 +417,10 @@ impl Game {
|
|||||||
let drawn = vec![self.deck.pop().unwrap(), self.deck.pop().unwrap()];
|
let drawn = vec![self.deck.pop().unwrap(), self.deck.pop().unwrap()];
|
||||||
let hand = self.players[self.turn].cards.clone();
|
let hand = self.players[self.turn].cards.clone();
|
||||||
let mut choices = [drawn, hand].concat();
|
let mut choices = [drawn, hand].concat();
|
||||||
let discarded = agents[self.turn].exchange(self, &choices);
|
let mut discarded = [Ambassador; 2];
|
||||||
|
for card in &mut discarded {
|
||||||
|
*card = channels.recv_discard(self.turn)?;
|
||||||
|
}
|
||||||
for card in discarded {
|
for card in discarded {
|
||||||
if !choices.draw_first(card) {
|
if !choices.draw_first(card) {
|
||||||
return Err("Exchanged a card that was not in choices");
|
return Err("Exchanged a card that was not in choices");
|
||||||
@ -421,24 +471,88 @@ pub enum Phase {
|
|||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An interface to a game to make strategic decisions.
|
use std::sync::mpsc;
|
||||||
pub trait Agent : fmt::Debug {
|
|
||||||
/// What move should the agent take?
|
|
||||||
fn choose_move(&self, game: &Game) -> Move;
|
#[derive(Default)]
|
||||||
/// Should the agent challenge the action?
|
pub struct Channels {
|
||||||
fn should_action_challenge(&self, game: &Game, move_: Move) -> bool;
|
txs: Vec<mpsc::Sender<Message>>,
|
||||||
/// Which [card](Card) the agent wishes to use to block the current action.
|
rxs: Vec<mpsc::Receiver<Message>>,
|
||||||
/// If the agent does not wish to block, it should return [None]
|
|
||||||
fn choose_block_card(&self, game: &Game, move_: Move) -> Option<Card>;
|
|
||||||
/// Should the agent challenge the block?
|
|
||||||
fn should_block_challenge(&self, game: &Game, move_: Move, block: Block) -> bool;
|
|
||||||
/// The [Ambassador]'s exchange.
|
|
||||||
/// Given 3 or 4 [Cards](Card) the agent must return two cards to the deck.
|
|
||||||
fn exchange(&self, game: &Game, cards: &[Card]) -> [Card; 2];
|
|
||||||
/// The player has lost influence, and must choose a [Card] from their hand
|
|
||||||
/// to discard.
|
|
||||||
fn choose_lost_influence(&self, game: &Game) -> Card;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Channels {
|
||||||
|
pub fn add_channel(&mut self, tx: mpsc::Sender<Message>, rx: mpsc::Receiver<Message>) {
|
||||||
|
self.txs.push(tx);
|
||||||
|
self.rxs.push(rx);
|
||||||
|
}
|
||||||
|
pub fn send(&self, id: usize, msg: Message) {
|
||||||
|
self.txs[id].send(msg).unwrap();
|
||||||
|
}
|
||||||
|
pub fn recv(&self, id: usize) -> Message {
|
||||||
|
self.rxs[id].recv().unwrap()
|
||||||
|
}
|
||||||
|
pub fn try_recv(&self, id: usize) -> Option<Message> {
|
||||||
|
self.rxs[id].try_recv().ok()
|
||||||
|
}
|
||||||
|
pub fn broadcast(&self, msg: Message) {
|
||||||
|
for id in 0..self.rxs.len() {
|
||||||
|
self.send(id, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn recv_move(&self, id: usize) -> CoupResult<Move> {
|
||||||
|
let msg = self.recv(id);
|
||||||
|
match msg {
|
||||||
|
Message::Move(move_) => Ok(move_),
|
||||||
|
_ => Err("Expected move"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn recv_discard(&self, id: usize) -> CoupResult<Card> {
|
||||||
|
let msg = self.recv(id);
|
||||||
|
match msg {
|
||||||
|
Message::Discard(card) => Ok(card),
|
||||||
|
_ => Err("Expected discard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn try_recv_challenge(&self, id: usize) -> Option<()> {
|
||||||
|
let msg = self.try_recv(id);
|
||||||
|
match msg {
|
||||||
|
Some(Message::Challenge) => Some(()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn try_recv_block(&self, id: usize) -> Option<Card> {
|
||||||
|
let msg = self.try_recv(id);
|
||||||
|
match msg {
|
||||||
|
Some(Message::Block(card)) => Some(card),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Message {
|
||||||
|
Turn(usize),
|
||||||
|
LoseInfluence,
|
||||||
|
Move(Move),
|
||||||
|
Challenge,
|
||||||
|
Block(Card),
|
||||||
|
Discard(Card),
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::time::{ Instant, Duration };
|
||||||
|
const WAIT_TIME: Duration = Duration::from_secs(1);
|
||||||
|
struct Timer {
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
impl Timer {
|
||||||
|
fn start() -> Self {
|
||||||
|
Timer { start: Instant::now() }
|
||||||
|
}
|
||||||
|
fn is_timeout(&self) -> bool {
|
||||||
|
Instant::now() > self.start + WAIT_TIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
32
src/main.rs
32
src/main.rs
@ -1,22 +1,22 @@
|
|||||||
use coup::*;
|
use coup::*;
|
||||||
|
|
||||||
mod cli;
|
// mod cli;
|
||||||
use cli::HumanAgent;
|
// use cli::HumanAgent;
|
||||||
|
|
||||||
fn main() -> CoupResult<()> {
|
fn main() -> CoupResult<()> {
|
||||||
let mut game = Game::new(2);
|
let mut _game = Game::new(2);
|
||||||
let agents: &[&dyn Agent] = &[&HumanAgent::new(0), &HumanAgent::new(1)];
|
|
||||||
while !game.is_game_over() {
|
// while !game.is_game_over() {
|
||||||
let mut phase = game.action(agents)?;
|
// let mut phase = game.action(agents)?;
|
||||||
while phase != Phase::Done {
|
// while phase != Phase::Done {
|
||||||
phase = match phase {
|
// phase = match phase {
|
||||||
Phase::ActionChallenge(move_) => game.action_challenge(move_, agents)?,
|
// Phase::ActionChallenge(move_) => game.action_challenge(move_, agents)?,
|
||||||
Phase::Block(move_) => game.block(move_, agents)?,
|
// Phase::Block(move_) => game.block(move_, agents)?,
|
||||||
Phase::BlockChallenge(move_, block) => game.block_challenge(move_, block, agents)?,
|
// Phase::BlockChallenge(move_, block) => game.block_challenge(move_, block, agents)?,
|
||||||
Phase::Resolution(move_) => game.resolution(move_, agents)?,
|
// Phase::Resolution(move_) => game.resolution(move_, agents)?,
|
||||||
Phase::Done => unreachable!(),
|
// Phase::Done => unreachable!(),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
145
src/test.rs
145
src/test.rs
@ -1,32 +1,26 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
use ntest::timeout;
|
||||||
struct DummyAgent(Card, Option<Card>, bool);
|
|
||||||
impl Agent for DummyAgent {
|
use std::sync::mpsc;
|
||||||
fn choose_move(&self, _game: &Game) -> Move {
|
|
||||||
unimplemented!();
|
fn make_channels(nplayers: usize) -> (Vec<mpsc::Sender<Message>>, Vec<mpsc::Receiver<Message>>, Channels) {
|
||||||
|
let mut txs = Vec::new();
|
||||||
|
let mut rxs = Vec::new();
|
||||||
|
let mut channels = Channels::default();
|
||||||
|
for _ in 0..nplayers {
|
||||||
|
let (ptx, grx) = mpsc::channel();
|
||||||
|
let (gtx, prx) = mpsc::channel();
|
||||||
|
txs.push(ptx);
|
||||||
|
rxs.push(prx);
|
||||||
|
channels.add_channel(gtx, grx);
|
||||||
}
|
}
|
||||||
fn should_action_challenge(&self, _game: &Game, _move: Move) -> bool {
|
(txs, rxs, channels)
|
||||||
self.2
|
|
||||||
}
|
|
||||||
fn choose_block_card(&self, _game: &Game, _move: Move) -> Option<Card> {
|
|
||||||
self.1
|
|
||||||
}
|
|
||||||
fn should_block_challenge(&self, _game: &Game, _move: Move, _block: Block) -> bool {
|
|
||||||
self.2
|
|
||||||
}
|
|
||||||
fn exchange(&self, _game: &Game, _cards: &[Card]) -> [Card; 2] {
|
|
||||||
[self.0, self.1.unwrap()]
|
|
||||||
}
|
|
||||||
fn choose_lost_influence(&self, _game: &Game) -> Card {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[timeout(1100)]
|
||||||
fn test_action_challenge() {
|
fn test_action_challenge() {
|
||||||
let challenge_agent = DummyAgent(Contessa, None, true);
|
|
||||||
let non_challenge_agent = DummyAgent(Duke, None, false);
|
|
||||||
let deck = vec![];
|
let deck = vec![];
|
||||||
let discard = vec![];
|
let discard = vec![];
|
||||||
let players = vec![
|
let players = vec![
|
||||||
@ -44,52 +38,75 @@ fn test_action_challenge() {
|
|||||||
// Test non-challengable
|
// Test non-challengable
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(3);
|
||||||
|
assert_eq!(
|
||||||
|
game.action_challenge(Move {
|
||||||
|
action: Assassinate,
|
||||||
|
target: Some(2),
|
||||||
|
}, &channels),
|
||||||
|
Ok(Phase::Block(Move {
|
||||||
|
action: Assassinate,
|
||||||
|
target: Some(2),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test passed challenge
|
||||||
|
{
|
||||||
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.action_challenge(Move {
|
game.action_challenge(Move {
|
||||||
action: Income,
|
action: Income,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&challenge_agent, &challenge_agent, &challenge_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Block(Move {
|
Ok(Phase::Block(Move {
|
||||||
action: Income,
|
action: Income,
|
||||||
target: None
|
target: None
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Test failed challenge
|
// Test failed challenge
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, rxs, channels) = make_channels(3);
|
||||||
|
txs[2].send(Message::Challenge).unwrap();
|
||||||
|
txs[2].send(Message::Discard(Ambassador)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.action_challenge(Move {
|
game.action_challenge(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(2),
|
target: Some(2),
|
||||||
}, &[&challenge_agent, &challenge_agent, &challenge_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Block(Move {
|
Ok(Phase::Block(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(2)
|
target: Some(2)
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
assert_eq!(rxs[2].recv(), Ok(Message::LoseInfluence));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test successful challenge
|
// Test successful challenge
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, rxs, channels) = make_channels(3);
|
||||||
|
txs[2].send(Message::Challenge).unwrap();
|
||||||
|
txs[0].send(Message::Discard(Duke)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.action_challenge(Move {
|
game.action_challenge(Move {
|
||||||
action: Assassinate,
|
action: Assassinate,
|
||||||
target: Some(2),
|
target: Some(2),
|
||||||
}, &[&non_challenge_agent, &challenge_agent, &challenge_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Done),
|
Ok(Phase::Done),
|
||||||
);
|
);
|
||||||
|
assert_eq!(rxs[0].recv(), Ok(Message::LoseInfluence));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[timeout(2100)]
|
||||||
fn test_block() {
|
fn test_block() {
|
||||||
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 deck = vec![];
|
||||||
let discard = vec![];
|
let discard = vec![];
|
||||||
let players = vec![
|
let players = vec![
|
||||||
@ -107,12 +124,13 @@ fn test_block() {
|
|||||||
|
|
||||||
// Test unblockable
|
// Test unblockable
|
||||||
{
|
{
|
||||||
|
let (_txs, _rxs, channels) = make_channels(3);
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: Income,
|
action: Income,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&non_blocking_agent, &ambassador_block_agent, &ambassador_block_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Resolution(Move {
|
Ok(Phase::Resolution(Move {
|
||||||
action: Income,
|
action: Income,
|
||||||
target: None
|
target: None
|
||||||
@ -123,11 +141,13 @@ fn test_block() {
|
|||||||
// Test target blocked
|
// Test target blocked
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, _rxs, channels) = make_channels(3);
|
||||||
|
txs[2].send(Message::Block(Ambassador)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(2),
|
target: Some(2),
|
||||||
}, &[&non_blocking_agent, &ambassador_block_agent, &ambassador_block_agent]),
|
}, &channels),
|
||||||
Ok(Phase::BlockChallenge(
|
Ok(Phase::BlockChallenge(
|
||||||
Move {
|
Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
@ -141,14 +161,15 @@ fn test_block() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test target non-blocked
|
// Test target passed block
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(2),
|
target: Some(2),
|
||||||
}, &[&non_blocking_agent, &ambassador_block_agent, &non_blocking_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Resolution(Move {
|
Ok(Phase::Resolution(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(2),
|
target: Some(2),
|
||||||
@ -159,11 +180,13 @@ fn test_block() {
|
|||||||
// Test anyone blocked
|
// Test anyone blocked
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, _rxs, channels) = make_channels(3);
|
||||||
|
txs[2].send(Message::Block(Duke)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&non_blocking_agent, &non_blocking_agent, &duke_block_agent]),
|
}, &channels),
|
||||||
Ok(Phase::BlockChallenge(
|
Ok(Phase::BlockChallenge(
|
||||||
Move {
|
Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
@ -180,11 +203,12 @@ fn test_block() {
|
|||||||
// Test no one blocked
|
// Test no one blocked
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&non_blocking_agent, &non_blocking_agent, &non_blocking_agent]),
|
}, &channels),
|
||||||
Ok(Phase::Resolution(Move {
|
Ok(Phase::Resolution(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
@ -195,21 +219,21 @@ fn test_block() {
|
|||||||
// Test bad block card
|
// Test bad block card
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, _rxs, channels) = make_channels(3);
|
||||||
|
txs[2].send(Message::Block(Contessa)).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
game.block(Move {
|
game.block(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&non_blocking_agent, &ambassador_block_agent, &non_blocking_agent])
|
}, &channels)
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[timeout(1100)]
|
||||||
fn test_block_challenge() {
|
fn test_block_challenge() {
|
||||||
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 deck = vec![];
|
||||||
let discard = vec![];
|
let discard = vec![];
|
||||||
let players = vec![
|
let players = vec![
|
||||||
@ -226,6 +250,7 @@ fn test_block_challenge() {
|
|||||||
// Test non-challenge
|
// Test non-challenge
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block_challenge(
|
game.block_challenge(
|
||||||
Move {
|
Move {
|
||||||
@ -236,7 +261,7 @@ fn test_block_challenge() {
|
|||||||
blocker: 1,
|
blocker: 1,
|
||||||
card: Contessa,
|
card: Contessa,
|
||||||
},
|
},
|
||||||
&[&non_challenge_agent, &block_agent]
|
&channels
|
||||||
),
|
),
|
||||||
Ok(Phase::Done)
|
Ok(Phase::Done)
|
||||||
);
|
);
|
||||||
@ -246,6 +271,9 @@ fn test_block_challenge() {
|
|||||||
// Test failed challenge
|
// Test failed challenge
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, rxs, channels) = make_channels(2);
|
||||||
|
txs[0].send(Message::Challenge).unwrap();
|
||||||
|
txs[0].send(Message::Discard(Assassin)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block_challenge(
|
game.block_challenge(
|
||||||
Move {
|
Move {
|
||||||
@ -256,16 +284,20 @@ fn test_block_challenge() {
|
|||||||
blocker: 1,
|
blocker: 1,
|
||||||
card: Contessa,
|
card: Contessa,
|
||||||
},
|
},
|
||||||
&[&challenge_agent, &block_agent]
|
&channels
|
||||||
),
|
),
|
||||||
Ok(Phase::Done)
|
Ok(Phase::Done)
|
||||||
);
|
);
|
||||||
|
assert_eq!(rxs[0].recv(), Ok(Message::LoseInfluence));
|
||||||
assert!(!game.discard.is_empty());
|
assert!(!game.discard.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test successful challenge
|
// Test successful challenge
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, rxs, channels) = make_channels(2);
|
||||||
|
txs[0].send(Message::Challenge).unwrap();
|
||||||
|
txs[1].send(Message::Discard(Contessa)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.block_challenge(
|
game.block_challenge(
|
||||||
Move {
|
Move {
|
||||||
@ -276,21 +308,21 @@ fn test_block_challenge() {
|
|||||||
blocker: 1,
|
blocker: 1,
|
||||||
card: Duke,
|
card: Duke,
|
||||||
},
|
},
|
||||||
&[&challenge_agent, &block_agent]
|
&channels,
|
||||||
),
|
),
|
||||||
Ok(Phase::Resolution(Move {
|
Ok(Phase::Resolution(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
assert_eq!(rxs[1].recv(), Ok(Message::LoseInfluence));
|
||||||
assert!(!game.discard.is_empty());
|
assert!(!game.discard.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[timeout(100)]
|
||||||
fn test_resolution() {
|
fn test_resolution() {
|
||||||
let dummy_agent = DummyAgent(Assassin, Some(Duke), false);
|
|
||||||
let loser_agent = DummyAgent(Captain, Some(Duke), false);
|
|
||||||
let deck = vec![Contessa, Contessa];
|
let deck = vec![Contessa, Contessa];
|
||||||
let discard = vec![];
|
let discard = vec![];
|
||||||
let players = vec![
|
let players = vec![
|
||||||
@ -307,31 +339,36 @@ fn test_resolution() {
|
|||||||
// Test income
|
// Test income
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(2);
|
||||||
game.resolution(Move {
|
game.resolution(Move {
|
||||||
action: Income,
|
action: Income,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
}, &channels).unwrap();
|
||||||
assert_eq!(game.players[0].coins, 8);
|
assert_eq!(game.players[0].coins, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test foreign aid
|
// Test foreign aid
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(2);
|
||||||
game.resolution(Move {
|
game.resolution(Move {
|
||||||
action: ForeignAid,
|
action: ForeignAid,
|
||||||
target: None,
|
target: None,
|
||||||
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
}, &channels).unwrap();
|
||||||
assert_eq!(game.players[0].coins, 9);
|
assert_eq!(game.players[0].coins, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test coup / assassinate
|
// Test coup / assassinate
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, rxs, channels) = make_channels(2);
|
||||||
|
txs[1].send(Message::Discard(Captain)).unwrap();
|
||||||
game.resolution(Move {
|
game.resolution(Move {
|
||||||
action: Coup,
|
action: Coup,
|
||||||
target: Some(1),
|
target: Some(1),
|
||||||
}, &[&dummy_agent, &loser_agent]).unwrap();
|
}, &channels).unwrap();
|
||||||
assert!(game.players[1].cards.is_empty());
|
assert!(game.players[1].cards.is_empty());
|
||||||
|
assert_eq!(rxs[1].recv(), Ok(Message::LoseInfluence));
|
||||||
assert_eq!(game.discard, vec![Captain]);
|
assert_eq!(game.discard, vec![Captain]);
|
||||||
assert_eq!(game.players[0].coins, 0);
|
assert_eq!(game.players[0].coins, 0);
|
||||||
}
|
}
|
||||||
@ -339,10 +376,11 @@ fn test_resolution() {
|
|||||||
// Test steal
|
// Test steal
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (_txs, _rxs, channels) = make_channels(2);
|
||||||
game.resolution(Move {
|
game.resolution(Move {
|
||||||
action: Steal,
|
action: Steal,
|
||||||
target: Some(1),
|
target: Some(1),
|
||||||
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
}, &channels).unwrap();
|
||||||
assert_eq!(game.players[0].coins, 8);
|
assert_eq!(game.players[0].coins, 8);
|
||||||
assert_eq!(game.players[1].coins, 0);
|
assert_eq!(game.players[1].coins, 0);
|
||||||
}
|
}
|
||||||
@ -350,14 +388,17 @@ fn test_resolution() {
|
|||||||
// Test exchange
|
// Test exchange
|
||||||
{
|
{
|
||||||
let mut game = game.clone();
|
let mut game = game.clone();
|
||||||
|
let (txs, _rxs, channels) = make_channels(2);
|
||||||
|
txs[0].send(Message::Discard(Duke)).unwrap();
|
||||||
|
txs[0].send(Message::Discard(Contessa)).unwrap();
|
||||||
game.resolution(Move {
|
game.resolution(Move {
|
||||||
action: Exchange,
|
action: Exchange,
|
||||||
target: Some(1),
|
target: Some(1),
|
||||||
}, &[&dummy_agent, &dummy_agent]).unwrap();
|
}, &channels).unwrap();
|
||||||
game.players[0].cards.sort();
|
game.players[0].cards.sort();
|
||||||
assert_eq!(game.players[0].cards, vec![Contessa, Contessa]);
|
assert_eq!(game.players[0].cards, vec![Assassin, Contessa]);
|
||||||
game.deck.sort();
|
game.deck.sort();
|
||||||
assert_eq!(game.deck, vec![Duke, Assassin]);
|
assert_eq!(game.deck, vec![Duke, Contessa]);
|
||||||
assert!(game.discard.is_empty());
|
assert!(game.discard.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user