diff --git a/src/cards.rs b/src/card.rs similarity index 69% rename from src/cards.rs rename to src/card.rs index f14ee0c..d98afbe 100644 --- a/src/cards.rs +++ b/src/card.rs @@ -2,62 +2,6 @@ pub use super::Game; use serde::{Serialize, Serializer}; use std::fmt; -pub fn copper() -> Card { - Card { - name: "Copper".into(), - cost: 0, - types: vec![CardType::Treasure(1)], - } -} - -pub fn silver() -> Card { - Card { - name: "Silver".into(), - cost: 3, - types: vec![CardType::Treasure(2)], - } -} - -pub fn gold() -> Card { - Card { - name: "Gold".into(), - cost: 6, - types: vec![CardType::Treasure(3)], - } -} - -pub fn estate() -> Card { - Card { - name: "Estate".into(), - cost: 2, - types: vec![CardType::Victory(1)], - } -} - -pub fn duchy() -> Card { - Card { - name: "Duchy".into(), - cost: 5, - types: vec![CardType::Victory(3)], - } -} - -pub fn province() -> Card { - Card { - name: "Province".into(), - cost: 8, - types: vec![CardType::Victory(6)], - } -} - -pub fn curse() -> Card { - Card { - name: "Curse".into(), - cost: 0, - types: vec![CardType::Curse], - } -} - macro_rules! draw { ($g:ident, $e:expr) => { $g.players[$g.active_player].draw($e) @@ -93,6 +37,7 @@ where pub enum CardType { #[serde(serialize_with = "serialize_card_type")] Action(fn(&mut Game)), + Attack, Curse, #[serde(serialize_with = "serialize_card_type")] Reaction(fn(&mut Game)), @@ -102,7 +47,7 @@ pub enum CardType { #[derive(Clone)] pub struct Card { - pub name: String, + pub name: &'static str, pub cost: u32, pub types: Vec, } diff --git a/src/main.rs b/src/main.rs index 8c01438..a2d6d71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ #[macro_use] -mod cards; +mod card; +mod sets; use async_std::{prelude::*, sync::RwLock}; -use cards::*; +use card::*; use itertools::Itertools; use rand::{seq::SliceRandom, thread_rng, Rng}; use serde::{Deserialize, Serialize}; @@ -157,16 +158,15 @@ impl Default for GameSetup { fn default() -> GameSetup { GameSetup { deck: vec![ - copper(), - copper(), - copper(), - copper(), - copper(), - copper(), - copper(), - estate(), - estate(), - estate(), + sets::base::copper(), + sets::base::copper(), + sets::base::copper(), + sets::base::copper(), + sets::base::copper(), + sets::base::copper(), + sets::base::estate(), + sets::base::estate(), + sets::base::estate(), ], } } @@ -176,6 +176,7 @@ impl Default for GameSetup { #[derive(Clone, Serialize)] enum ResolvingPlayer { ActivePlayer, + AllNonActivePlayers, } #[derive(Deserialize)] @@ -190,14 +191,8 @@ enum ResolveReply { #[derive(Clone, Serialize)] #[serde(tag = "type")] enum ResolveRequest { - ChooseHandCardsToDiscard { - player: ResolvingPlayer, - filter: CardFilter, - }, - GainCard { - player: ResolvingPlayer, - filter: CardFilter, - }, + ChooseHandCardsToDiscard { filter: CardFilter }, + GainCard { filter: CardFilter }, } #[derive(Clone)] @@ -207,7 +202,18 @@ enum Effect { OnCardPlayed(fn(&mut Game, &Card) -> bool), /// Effect that blocks further processing of the game state until /// some user input is received that allows to fully resolve the effect - Resolving(String, ResolveRequest, ResolvingEffect), + Resolving { + card: String, + player: ResolvingPlayer, + request: ResolveRequest, + effect: ResolvingEffectHandler, + }, +} + +#[derive(Default)] +struct EffectState { + resolved: bool, + players_responded: Vec, } #[derive(Clone, Serialize)] @@ -218,7 +224,8 @@ enum CardFilter { Type(CardType), } -type ResolvingEffect = fn(&mut Game, &ResolveReply); +type ResolvingEffectHandler = + fn(&mut Game, &ResolveReply, usize, &ResolveRequest, &mut EffectState); pub struct Game { effects: Vec, @@ -233,7 +240,13 @@ pub struct Game { /// Any effect from a card that requires further input from players /// and blocks the game until fully resolved. - resolving_effect: Option<(String, ResolveRequest, ResolvingEffect)>, + resolving_effect: Option<( + String, + ResolveRequest, + ResolvingEffectHandler, + ResolvingPlayer, + EffectState, + )>, } impl Game { @@ -272,334 +285,23 @@ impl Game { }; self.supply = vec![ - (copper(), 60 - self.players.len() * 7), - (silver(), 40), - (gold(), 30), - (estate(), victory_qty), - (duchy(), victory_qty), - (province(), victory_qty), - (curse(), 10), - ( - Card { - name: "Cellar".into(), - cost: 2, - types: vec![CardType::Action(|game| { - action!(game, 1); - game.add_effect(Effect::Resolving( - "Cellar".into(), - ResolveRequest::ChooseHandCardsToDiscard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::Any, - }, - |game, message| { - if let ResolveReply::HandCardsChosen { choice } = message { - let mut discarded = 0; - for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { - game.get_active_player().discard(*c); - //game.players[game.active_player].discard(*c); - discarded += 1; - } - - game.players[game.active_player].draw(discarded); - game.resolving_effect = None; - } - }, - )); - })], - }, - 10, - ), - ( - Card { - name: "Moat".into(), - cost: 2, - types: vec![ - CardType::Action(|game| { - draw!(game, 2); - }), - CardType::Reaction(|_| {}), - ], - }, - 10, - ), - ( - Card { - name: "Village".into(), - cost: 3, - types: vec![CardType::Action(|game| { - draw!(game, 1); - action!(game, 2); - })], - }, - 10, - ), - ( - Card { - name: "Merchant".into(), - cost: 3, - types: vec![CardType::Action(|game| { - draw!(game, 1); - action!(game, 1); - game.add_effect(Effect::OnCardPlayed(|game, card| { - if card.name.as_str() == "Silver" { - coin!(game, 1); - true - } else { - false - } - })); - })], - }, - 10, - ), - ( - Card { - name: "Workshop".into(), - cost: 3, - types: vec![CardType::Action(|game| { - game.add_effect(Effect::Resolving( - "Workshop".into(), - ResolveRequest::GainCard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::MaxCost(4), - }, - |game, message| { - if let ResolveReply::SupplyCardChosen { choice } = message { - if let Some((card, count)) = game.supply.get(*choice) { - if *count < 1 { - return; - } - - if card.cost > 4 { - return; - } - } - game.supply.get_mut(*choice).unwrap().1 = - game.supply.get(*choice).unwrap().1 - 1; - let card = game.supply[*choice].0.clone(); - - game.players[game.active_player] - .discard_pile - .push(card.clone()); - game.resolving_effect = None; - } - }, - )); - })], - }, - 10, - ), - ( - Card { - name: "Smithy".into(), - cost: 4, - types: vec![CardType::Action(|game| draw!(game, 3))], - }, - 10, - ), - ( - Card { - name: "Remodel".into(), - cost: 4, - types: vec![CardType::Action(|game| { - game.add_effect(Effect::Resolving( - "Remodel".into(), - ResolveRequest::ChooseHandCardsToDiscard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::Any, - }, - |game, message| { - if let ResolveReply::HandCardsChosen { choice } = message { - if choice.len() != 1 { - return; - } - - let card = game.players[game.active_player] - .hand - .remove(choice[0]); - let cost = card.cost; - - game.trash.push(card); - - game.add_effect(Effect::Resolving( - "Remodel".into(), - ResolveRequest::GainCard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::MaxCost(cost + 2), - }, - |game, message| { - if let ResolveReply::SupplyCardChosen { - choice, - } = message - { - if let Some((card, count)) = - game.supply.get(*choice) - { - if *count < 1 { - return; - } - - if let Some(( - _, - ResolveRequest::GainCard { - filter: - CardFilter::MaxCost(cost), - .. - }, - _, - )) = game.resolving_effect - { - if card.cost > cost { - return; - } - - game.supply - .get_mut(*choice) - .unwrap() - .1 = game - .supply - .get(*choice) - .unwrap() - .1 - - 1; - let card = - game.supply[*choice].0.clone(); - - game.players[game.active_player] - .discard_pile - .push(card.clone()); - game.resolving_effect = None; - } - } - } - }, - )); - } - }, - )); - })], - }, - 10, - ), - ( - Card { - name: "Militia".into(), - cost: 4, - types: vec![CardType::Action(|game| { - coin!(game, 2); - })], - }, - 10, - ), - ( - Card { - name: "Market".into(), - cost: 5, - types: vec![CardType::Action(|game| { - draw!(game, 1); - action!(game, 1); - buy!(game, 1); - coin!(game, 1); - })], - }, - 10, - ), - ( - Card { - name: "Mine".into(), - cost: 5, - types: vec![CardType::Action(|game| { - game.add_effect(Effect::Resolving( - "Mine".into(), - ResolveRequest::ChooseHandCardsToDiscard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::Type(CardType::Treasure(0)), - }, - |game, message| { - if let ResolveReply::HandCardsChosen { choice } = message { - if choice.len() != 1 { - return; - } - - if let Some(card) = - game.get_active_player().hand.get(choice[0]) - { - if let None = card.treasure() { - return; - } - } - - let card = game.players[game.active_player] - .hand - .remove(choice[0]); - let cost = card.cost; - - game.trash.push(card); - - game.add_effect(Effect::Resolving( - "Mine".into(), - ResolveRequest::GainCard { - player: ResolvingPlayer::ActivePlayer, - filter: CardFilter::MaxCost(cost + 3), - }, - |game, message| { - if let ResolveReply::SupplyCardChosen { - choice, - } = message - { - if let Some((card, count)) = - game.supply.get(*choice) - { - if *count < 1 { - return; - } - - if let Some(( - _, - ResolveRequest::GainCard { - filter: - CardFilter::MaxCost(cost), - .. - }, - _, - )) = game.resolving_effect - { - if card.cost > cost { - return; - } - - if let None = card.treasure() { - return; - } - - game.supply - .get_mut(*choice) - .unwrap() - .1 = game - .supply - .get(*choice) - .unwrap() - .1 - - 1; - let card = - game.supply[*choice].0.clone(); - - game.players[game.active_player] - .discard_pile - .push(card.clone()); - game.resolving_effect = None; - } - } - } - }, - )); - } - }, - )); - })], - }, - 10, - ), + (sets::base::copper(), 60 - self.players.len() * 7), + (sets::base::silver(), 40), + (sets::base::gold(), 30), + (sets::base::estate(), victory_qty), + (sets::base::duchy(), victory_qty), + (sets::base::province(), victory_qty), + (sets::base::curse(), 10), + (sets::base::cellar(), 10), + (sets::base::moat(), 10), + (sets::base::village(), 10), + (sets::base::merchant(), 10), + (sets::base::workshop(), 10), + (sets::base::simthy(), 10), + (sets::base::remodel(), 10), + (sets::base::militia(), 10), + (sets::base::market(), 10), + (sets::base::mine(), 10), ]; } @@ -722,7 +424,7 @@ impl Game { } } - Effect::Resolving(_, _, _) => {} + Effect::Resolving { .. } => {} } true @@ -769,8 +471,14 @@ impl Game { self.effects.push(effect); } - Effect::Resolving(card_name, request, effect) => { - self.resolving_effect = Some((card_name, request, effect)); + Effect::Resolving { + card, + request, + effect, + player, + } => { + let state = EffectState::default(); + self.resolving_effect = Some((card, request, effect, player, state)); } } } @@ -804,8 +512,12 @@ async fn broadcast_state(game: &Game) { name: html_escape::encode_text(&p.name).into(), draw_pile_count: p.draw_pile.len(), hand_count: p.hand.len(), - discard_pile: p.discard_pile.last().map(|c| c.name.clone()), - played_cards: p.played_cards.iter().map(|c| c.name.clone()).collect(), + discard_pile: p.discard_pile.last().map(|c| c.name.clone().into()), + played_cards: p + .played_cards + .iter() + .map(|c| c.name.clone().into()) + .collect(), }) .collect(), active_player: game.active_player, @@ -855,9 +567,9 @@ async fn broadcast_state(game: &Game) { broadcast(&game, &sm).await; } - if let Some((card_name, request, _)) = &game.resolving_effect { + if let Some((card_name, request, _, ref player, _)) = &game.resolving_effect { match request { - ResolveRequest::ChooseHandCardsToDiscard { ref player, .. } => match player { + ResolveRequest::ChooseHandCardsToDiscard { .. } => match player { ResolvingPlayer::ActivePlayer => { let p = game.players.get(game.active_player).unwrap(); let sm = ServerMessage::ResolveRequest { @@ -866,9 +578,22 @@ async fn broadcast_state(game: &Game) { }; send_msg(&game, &p, &sm).await; } + + ResolvingPlayer::AllNonActivePlayers => { + for (id, player) in game.players.iter().enumerate() { + let sm = ServerMessage::ResolveRequest { + card: card_name.clone(), + request: request.clone(), + }; + + if id != game.active_player { + send_msg(&game, &player, &sm).await; + } + } + } }, - ResolveRequest::GainCard { ref player, .. } => match player { + ResolveRequest::GainCard { .. } => match player { ResolvingPlayer::ActivePlayer => { let p = game.players.get(game.active_player).unwrap(); let sm = ServerMessage::ResolveRequest { @@ -877,6 +602,8 @@ async fn broadcast_state(game: &Game) { }; send_msg(&game, &p, &sm).await; } + + _ => unimplemented!("GainCard for non active players?!"), }, } } @@ -1177,16 +904,39 @@ async fn main() -> Result<(), std::io::Error> { let mut games = req.state().games.write().await; let game = games.get_mut(&game_id).unwrap(); - if player_number == game.active_player { - match game.resolving_effect { - Some((_, _, ref effect)) => { - effect(game, &reply); + if let Some((card, request, effect, player, mut state)) = + game.resolving_effect.take() + { + match player { + ResolvingPlayer::ActivePlayer => { + if player_number == game.active_player { + effect( + game, + &reply, + game.active_player, + &request, + &mut state, + ); + } + } + + ResolvingPlayer::AllNonActivePlayers => { + if player_number != game.active_player { + effect(game, &reply, player_number, &request, &mut state); + } } - None => {} } - broadcast_state(&game).await; + match state.resolved { + false => { + game.resolving_effect = + Some((card, request, effect, player, state)); + } + true => {} + } } + + broadcast_state(&game).await; } } } diff --git a/src/sets/base.rs b/src/sets/base.rs new file mode 100644 index 0000000..c2680d4 --- /dev/null +++ b/src/sets/base.rs @@ -0,0 +1,377 @@ +use crate::card::{Card, CardType}; +use crate::{CardFilter, Effect, ResolveReply, ResolveRequest, ResolvingPlayer}; +use itertools::Itertools; + +pub fn copper() -> Card { + Card { + name: "Copper", + cost: 0, + types: vec![CardType::Treasure(1)], + } +} + +pub fn silver() -> Card { + Card { + name: "Silver", + cost: 3, + types: vec![CardType::Treasure(2)], + } +} + +pub fn gold() -> Card { + Card { + name: "Gold", + cost: 6, + types: vec![CardType::Treasure(3)], + } +} + +pub fn estate() -> Card { + Card { + name: "Estate", + cost: 2, + types: vec![CardType::Victory(1)], + } +} + +pub fn duchy() -> Card { + Card { + name: "Duchy", + cost: 5, + types: vec![CardType::Victory(3)], + } +} + +pub fn province() -> Card { + Card { + name: "Province", + cost: 8, + types: vec![CardType::Victory(6)], + } +} + +pub fn curse() -> Card { + Card { + name: "Curse", + cost: 0, + types: vec![CardType::Curse], + } +} + +pub fn cellar() -> Card { + Card { + name: "Cellar", + cost: 2, + types: vec![CardType::Action(|game| { + action!(game, 1); + game.add_effect(Effect::Resolving { + card: "Cellar".into(), + request: ResolveRequest::ChooseHandCardsToDiscard { + filter: CardFilter::Any, + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, _request, state| { + if let ResolveReply::HandCardsChosen { choice } = message { + let mut discarded = 0; + for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { + game.get_active_player().discard(*c); + discarded += 1; + } + + game.players[game.active_player].draw(discarded); + state.resolved = true; + } + }, + }); + })], + } +} + +pub fn moat() -> Card { + Card { + name: "Moat", + cost: 2, + types: vec![ + CardType::Action(|game| { + draw!(game, 2); + }), + CardType::Reaction(|_| {}), + ], + } +} + +pub fn village() -> Card { + Card { + name: "Village", + cost: 3, + types: vec![CardType::Action(|game| { + draw!(game, 1); + action!(game, 2); + })], + } +} + +pub fn merchant() -> Card { + Card { + name: "Merchant", + cost: 3, + types: vec![CardType::Action(|game| { + draw!(game, 1); + action!(game, 1); + game.add_effect(Effect::OnCardPlayed(|game, card| { + if card.name == "Silver" { + coin!(game, 1); + true + } else { + false + } + })); + })], + } +} + +pub fn workshop() -> Card { + Card { + name: "Workshop", + cost: 3, + types: vec![CardType::Action(|game| { + game.add_effect(Effect::Resolving { + card: "Workshop".into(), + request: ResolveRequest::GainCard { + filter: CardFilter::MaxCost(4), + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, _request, state| { + if let ResolveReply::SupplyCardChosen { choice } = message { + if let Some((card, count)) = game.supply.get(*choice) { + if *count < 1 { + return; + } + + if card.cost > 4 { + return; + } + } + game.supply.get_mut(*choice).unwrap().1 = + game.supply.get(*choice).unwrap().1 - 1; + let card = game.supply[*choice].0.clone(); + + game.players[game.active_player] + .discard_pile + .push(card.clone()); + state.resolved = true; + } + }, + }); + })], + } +} + +pub fn simthy() -> Card { + Card { + name: "Smithy", + cost: 4, + types: vec![CardType::Action(|game| draw!(game, 3))], + } +} + +pub fn remodel() -> Card { + Card { + name: "Remodel", + cost: 4, + types: vec![CardType::Action(|game| { + game.add_effect(Effect::Resolving { + card: "Remodel".into(), + request: ResolveRequest::ChooseHandCardsToDiscard { + filter: CardFilter::Any, + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, _request, state| { + if let ResolveReply::HandCardsChosen { choice } = message { + if choice.len() != 1 { + return; + } + + let card = game.players[game.active_player].hand.remove(choice[0]); + let cost = card.cost; + + game.trash.push(card); + + state.resolved = true; + game.add_effect(Effect::Resolving { + card: "Remodel".into(), + request: ResolveRequest::GainCard { + filter: CardFilter::MaxCost(cost + 2), + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, request, state| { + if let ResolveReply::SupplyCardChosen { choice } = message { + if let Some((card, count)) = game.supply.get(*choice) { + if *count < 1 { + return; + } + + if let ResolveRequest::GainCard { + filter: CardFilter::MaxCost(cost), + .. + } = request + { + if card.cost > *cost { + return; + } + + game.supply.get_mut(*choice).unwrap().1 = + game.supply.get(*choice).unwrap().1 - 1; + let card = game.supply[*choice].0.clone(); + + game.players[game.active_player] + .discard_pile + .push(card.clone()); + state.resolved = true; + } + } + } + }, + }); + } + }, + }); + })], + } +} + +pub fn militia() -> Card { + Card { + name: "Militia", + cost: 4, + types: vec![ + CardType::Attack, + CardType::Action(|game| { + coin!(game, 2); + + if game.players.len() == 1 { + return; + } + + game.add_effect(Effect::Resolving { + card: "Militia".into(), + request: ResolveRequest::ChooseHandCardsToDiscard { + filter: CardFilter::Any, + }, + player: ResolvingPlayer::AllNonActivePlayers, + effect: |game, message, player, _request, state| { + if let ResolveReply::HandCardsChosen { choice } = message { + if choice.len() == 0 { + if !game.players[player].hand.iter().any(|c| c.name == "Moat") { + return; + } + } else if game.players[player].hand.len() > 3 + && choice.len() != game.players[player].hand.len() - 3 + { + return; + } + + for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { + game.players[player].discard(*c); + } + + state.players_responded.push(player); + + if state.players_responded.len() == game.players.len() - 1 { + state.resolved = true; + } + } + }, + }); + }), + ], + } +} + +pub fn market() -> Card { + Card { + name: "Market", + cost: 5, + types: vec![CardType::Action(|game| { + draw!(game, 1); + action!(game, 1); + buy!(game, 1); + coin!(game, 1); + })], + } +} + +pub fn mine() -> Card { + Card { + name: "Mine", + cost: 5, + types: vec![CardType::Action(|game| { + game.add_effect(Effect::Resolving { + card: "Mine".into(), + request: ResolveRequest::ChooseHandCardsToDiscard { + filter: CardFilter::Type(CardType::Treasure(0)), + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, _request, state| { + if let ResolveReply::HandCardsChosen { choice } = message { + if choice.len() != 1 { + return; + } + + if let Some(card) = game.get_active_player().hand.get(choice[0]) { + if let None = card.treasure() { + return; + } + } + + let card = game.players[game.active_player].hand.remove(choice[0]); + let cost = card.cost; + + game.trash.push(card); + + state.resolved = true; + game.add_effect(Effect::Resolving { + card: "Mine".into(), + request: ResolveRequest::GainCard { + filter: CardFilter::MaxCost(cost + 3), + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, _player, request, state| { + if let ResolveReply::SupplyCardChosen { choice } = message { + if let Some((card, count)) = game.supply.get(*choice) { + if *count < 1 { + return; + } + + if let ResolveRequest::GainCard { + filter: CardFilter::MaxCost(cost), + .. + } = request + { + if card.cost > *cost { + return; + } + + if let None = card.treasure() { + return; + } + + game.supply.get_mut(*choice).unwrap().1 = + game.supply.get(*choice).unwrap().1 - 1; + let card = game.supply[*choice].0.clone(); + + game.players[game.active_player] + .discard_pile + .push(card.clone()); + state.resolved = true; + } + } + } + }, + }); + } + }, + }); + })], + } +} diff --git a/src/sets/mod.rs b/src/sets/mod.rs new file mode 100644 index 0000000..6cf245d --- /dev/null +++ b/src/sets/mod.rs @@ -0,0 +1 @@ +pub mod base;