From 73bce1bc6daf052db16b0bb0890032e5abfd6ed8 Mon Sep 17 00:00:00 2001 From: Markus Wagner Date: Fri, 5 Feb 2021 18:59:51 +0100 Subject: [PATCH] Allow client to switch kingdom cards in setup phase --- src/card.rs | 16 +- src/main.rs | 227 +++++++++++--- src/sets/base.rs | 764 +++++++++++++++++++++++++---------------------- static/game.html | 48 ++- 4 files changed, 647 insertions(+), 408 deletions(-) diff --git a/src/card.rs b/src/card.rs index d98afbe..dc1883f 100644 --- a/src/card.rs +++ b/src/card.rs @@ -47,12 +47,26 @@ pub enum CardType { #[derive(Clone)] pub struct Card { - pub name: &'static str, pub cost: u32, + pub name: &'static str, pub types: Vec, } impl Card { + pub fn new(name: &'static str, cost: u32) -> Card { + Card { + name, + cost, + types: vec![], + } + } + + pub fn with_type(&self, card_type: CardType) -> Card { + let mut card = self.clone(); + card.types.push(card_type); + card + } + pub fn action(&self) -> Option { for t in &self.types { match t { diff --git a/src/main.rs b/src/main.rs index 47feab0..371cc37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,7 @@ enum ServerMessage { #[derive(Clone, Serialize)] struct GameSetup { + supply: Vec, deck: Vec, } @@ -148,25 +149,39 @@ enum GameState { impl Default for GameSetup { fn default() -> GameSetup { + let set = sets::base::base(); + GameSetup { + supply: vec![ + set.get("Chapel").unwrap(), + set.get("Workshop").unwrap(), + set.get("Bureaucrat").unwrap(), + set.get("Gardens").unwrap(), + set.get("Throne Room").unwrap(), + set.get("Bandit").unwrap(), + set.get("Festival").unwrap(), + set.get("Sentry").unwrap(), + set.get("Witch").unwrap(), + set.get("Artisan").unwrap(), + ], deck: vec![ - sets::base::copper(), - 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(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Copper").unwrap(), + set.get("Estate").unwrap(), + set.get("Estate").unwrap(), + set.get("Estate").unwrap(), ], } } } /// Which player's input is requested to resolve the current effect -#[derive(Clone, Serialize)] +#[derive(Clone, Serialize, PartialEq)] enum ResolvingPlayer { ActivePlayer, AllNonActivePlayers, @@ -188,6 +203,11 @@ enum ResolveRequest { GainCard { filter: CardFilter }, } +enum ResolveResult { + Resolved, + NotResolved, +} + #[derive(Clone)] enum Effect { /// Trigger effect if another card is played while effect is active @@ -213,25 +233,71 @@ struct EffectState { //#[serde(tag = "type")] enum CardFilter { Any, + And(Vec), MaxCost(u32), Type(CardType), } +impl CardFilter { + fn match_card(&self, card: &Card) -> bool { + match &self { + CardFilter::Any => true, + CardFilter::And(filter) => filter.iter().all(|f| f.match_card(&card)), + CardFilter::MaxCost(cost) => card.cost <= *cost, + CardFilter::Type(card_type) => match card_type { + CardType::Action(_) => card.action().is_some(), + CardType::Attack => { + unimplemented!() + } + CardType::Curse => { + unimplemented!() + } + CardType::Reaction(_) => { + unimplemented!() + } + CardType::Treasure(_) => card.treasure().is_some(), + CardType::Victory(_) => { + unimplemented!() + } + }, + } + } +} + type ResolvingEffectHandler = - fn(&mut Game, &ResolveReply, usize, &ResolveRequest, &mut EffectState); + fn(&mut Game, &ResolveReply, usize, &ResolveRequest, &mut EffectState) -> ResolveResult; #[derive(Deserialize)] #[serde(tag = "type")] enum Command { - BuyCard { index: usize }, - Discard { index: usize }, + BuyCard { + index: usize, + }, + Discard { + index: usize, + }, DrawCard, EndTurn, - GainCard { index: usize }, - PlayCard { index: usize }, - ResolveReply { reply: ResolveReply }, + GainCard { + index: usize, + }, + PlayCard { + index: usize, + }, + ResolveReply { + reply: ResolveReply, + }, StartGame, - TrashHand { index: usize }, + TrashHand { + index: usize, + }, + + /// Replace a kingdom card in the supply by another one, + /// only viable during Setup state + ChangeSupply { + index: usize, + name: String, + }, } #[derive(Debug, Serialize)] @@ -243,6 +309,7 @@ enum Event { CardDiscarded { player: usize }, CardTrashed { player: usize, name: &'static str }, CardPlayed { player: usize, name: &'static str }, + CardRevealed { player: usize, name: &'static str }, TurnStarted { player: usize }, } @@ -286,6 +353,15 @@ impl Game { } } + fn supply_name_to_index(&self, name: &String) -> Option { + for i in 0..self.supply.len() { + if self.supply[i].0.name == name { + return Some(i); + } + } + None + } + fn start(&mut self) { match self.state { GameState::Setup => { @@ -306,24 +382,24 @@ impl Game { }; self.supply = vec![ - (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), + ( + sets::base::base().get("Copper").unwrap(), + 60 - self.players.len() * 7, + ), + (sets::base::base().get("Silver").unwrap(), 40), + (sets::base::base().get("Gold").unwrap(), 30), + (sets::base::base().get("Estate").unwrap(), victory_qty), + (sets::base::base().get("Duchy").unwrap(), victory_qty), + (sets::base::base().get("Province").unwrap(), victory_qty), + (sets::base::base().get("Curse").unwrap(), 10), ]; + + self.setup + .supply + .sort_by(|a, b| a.cost.cmp(&b.cost).then(a.name.cmp(&b.name))); + for supply_card in self.setup.supply.iter() { + self.supply.push((supply_card.clone(), 10)); + } } _ => {} // Ignore if game is not in setup state @@ -424,7 +500,12 @@ impl Game { if !self.can_play(player_number, index) { return false; } + let card = self.players[player_number].hand.remove(index); + self.emit(Event::CardPlayed { + player: player_number, + name: card.name, + }); if let Some(coin) = card.treasure() { self.turn_state.coin += coin; @@ -457,6 +538,31 @@ impl Game { true } + fn gain_card(&mut self, player: usize, index: &usize, filter: &CardFilter) -> Result<(), ()> { + if let Some((card, count)) = self.supply.get(*index) { + if *count < 1 { + return Err(()); + } + + if !filter.match_card(&card) { + return Err(()); + } + + self.supply.get_mut(*index).unwrap().1 = self.supply.get(*index).unwrap().1 - 1; + let card = self.supply[*index].0.clone(); + + self.players[player].discard_pile.push(card.clone()); + + self.emit(Event::CardGained { + player, + index: *index, + }); + + return Ok(()); + } + Err(()) + } + pub fn buy_card(&mut self, player_number: usize, index: usize) -> bool /*-> Result<(), &'static str>*/ { if let Some(_) = self.resolving_effect { @@ -499,6 +605,10 @@ impl Game { effect, player, } => { + if player == ResolvingPlayer::AllNonActivePlayers && self.players.len() == 1 { + return; + } + let state = EffectState::default(); self.resolving_effect = Some((card, request, effect, player, state)); } @@ -507,6 +617,14 @@ impl Game { fn handle_command(&mut self, player: usize, command: Command) { match command { + Command::ChangeSupply { index, name } => { + if let GameState::Setup = self.state { + if let Some(card) = sets::base::base().get(name.as_ref()) { + self.setup.supply[index] = card; + } + } + } + Command::DrawCard => { if !self.debug_mode { return; @@ -536,11 +654,7 @@ impl Game { return; } - self.supply.get_mut(index).unwrap().1 = self.supply.get(index).unwrap().1 - 1; - let card = self.supply[index].0.clone(); - - self.players[player].discard_pile.push(card.clone()); - self.emit(Event::CardGained { player, index }) + self.gain_card(player, &index, &CardFilter::Any); } Command::Discard { index } => { @@ -564,10 +678,7 @@ impl Game { } Command::PlayCard { index } => { - let name = self.players[player].hand[index].name.clone(); - if self.play_card(player, index) { - self.emit(Event::CardPlayed { player, name }); - } + self.play_card(player, index); } Command::ResolveReply { reply } => { @@ -577,13 +688,26 @@ impl Game { match resolve_player { ResolvingPlayer::ActivePlayer => { if player == self.active_player { - effect(self, &reply, self.active_player, &request, &mut state); + match effect(self, &reply, self.active_player, &request, &mut state) + { + ResolveResult::Resolved => state.resolved = true, + ResolveResult::NotResolved => (), + } } } ResolvingPlayer::AllNonActivePlayers => { if player != self.active_player { - effect(self, &reply, player, &request, &mut state); + match effect(self, &reply, player, &request, &mut state) { + ResolveResult::Resolved => { + state.players_responded.push(player); + } + ResolveResult::NotResolved => (), + } + } + + if state.players_responded.len() == self.players.len() - 1 { + state.resolved = true; } } } @@ -600,7 +724,7 @@ impl Game { } } - fn emit(&mut self, event: Event) { + fn emit(&self, event: Event) { println!("Emitting event: {:?}", event); async_std::task::block_on(notify_players(self, event)); } @@ -618,7 +742,7 @@ async fn notify_players(game: &Game, event: Event) { async fn broadcast(game: &Game, sm: &ServerMessage) { for (_, (_, con)) in game.connections.iter() { - con.send_json(&sm).await.unwrap(); + con.send_json(&sm).await; } } @@ -657,6 +781,13 @@ async fn broadcast_state(game: &Game) { broadcast(&game, &sm).await; + if let GameState::Setup = game.state { + let sm = ServerMessage::GameSetup { + setup: game.setup.clone(), + }; + broadcast(&game, &sm).await; + } + for p in game.players.iter() { let sm = ServerMessage::PlayerHand { hand: p.hand.clone(), diff --git a/src/sets/base.rs b/src/sets/base.rs index c2680d4..56a8c00 100644 --- a/src/sets/base.rs +++ b/src/sets/base.rs @@ -1,377 +1,443 @@ use crate::card::{Card, CardType}; -use crate::{CardFilter, Effect, ResolveReply, ResolveRequest, ResolvingPlayer}; +use crate::{ + CardFilter, Effect, Event, ResolveReply, ResolveRequest, ResolveResult::*, ResolvingPlayer, +}; use itertools::Itertools; -pub fn copper() -> Card { - Card { - name: "Copper", - cost: 0, - types: vec![CardType::Treasure(1)], +pub struct CardSet { + name: &'static str, + cards: Vec, +} + +impl CardSet { + pub fn get(&self, name: &str) -> Option { + for card in self.cards.iter() { + if card.name == name { + return Some(card.clone()); + } + } + None } } -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 base() -> CardSet { + CardSet { + name: "Dominion", + cards: vec![ + copper(), + silver(), + gold(), + estate(), + duchy(), + province(), + curse(), + artisan(), + bandit(), + bureaucrat(), + chapel(), + cellar(), + gardens(), + festival(), + market(), + militia(), + mine(), + merchant(), + moat(), + remodel(), + sentry(), + smithy(), + throne_room(), + village(), + witch(), + workshop(), ], } } -pub fn village() -> Card { - Card { - name: "Village", - cost: 3, - types: vec![CardType::Action(|game| { - draw!(game, 1); - action!(game, 2); - })], - } +fn copper() -> Card { + Card::new("Copper", 0).with_type(CardType::Treasure(1)) } -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 - } - })); - })], - } +fn silver() -> Card { + Card::new("Silver", 3).with_type(CardType::Treasure(2)) } -pub fn workshop() -> Card { - Card { - name: "Workshop", - cost: 3, - types: vec![CardType::Action(|game| { +fn gold() -> Card { + Card::new("Gold", 6).with_type(CardType::Treasure(3)) +} + +fn estate() -> Card { + Card::new("Estate", 2).with_type(CardType::Victory(1)) +} + +fn duchy() -> Card { + Card::new("Duchy", 5).with_type(CardType::Victory(3)) +} + +fn province() -> Card { + Card::new("Province", 8).with_type(CardType::Victory(6)) +} + +fn curse() -> Card { + Card::new("Curse", 8).with_type(CardType::Curse) +} + +fn artisan() -> Card { + Card::new("Artisan", 6).with_type(CardType::Action(|_game| {})) +} + +fn bandit() -> Card { + Card::new("Bandit", 5) + .with_type(CardType::Attack) + .with_type(CardType::Action(|_game| {})) +} + +fn bureaucrat() -> Card { + Card::new("Bureaucrat", 4) + .with_type(CardType::Attack) + .with_type(CardType::Action(|game| { + if let Some(index) = game.supply_name_to_index(&"Silver".into()) { + game.gain_card(game.active_player, &index, &CardFilter::Any); + let card = game.players[game.active_player].discard_pile.pop().unwrap(); + game.players[game.active_player].draw_pile.push(card); + } + 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(), + card: "Bureaucrat".into(), request: ResolveRequest::ChooseHandCardsToDiscard { - filter: CardFilter::Any, + filter: CardFilter::Type(CardType::Victory(0)), }, - player: ResolvingPlayer::ActivePlayer, - effect: |game, message, _player, _request, state| { + player: ResolvingPlayer::AllNonActivePlayers, + effect: |game, message, player, _request, _state| { if let ResolveReply::HandCardsChosen { choice } = message { - if choice.len() != 1 { - return; + if choice.len() > 1 { + return NotResolved; } - let card = game.players[game.active_player].hand.remove(choice[0]); - let cost = card.cost; + if choice.len() == 0 { + if game.players[player].hand.iter().any(|c| c.name == "Moat") { + game.emit(Event::CardRevealed { + player, + name: "Moat", + }); + return Resolved; + } - 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 + if !game.players[player] + .hand + .iter() + .any(|c| c.victory().is_some()) { - 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; - } - } + for card in game.players[player].hand.iter() { + game.emit(Event::CardRevealed { + player, + name: card.name, + }); } - }, + return Resolved; + } + } + + if let Some(card) = game.players[player].hand.get(choice[0]) { + if card.victory().is_none() { + return NotResolved; + } + } + + let card = game.players[player].hand.remove(choice[0]); + game.emit(Event::CardRevealed { + player, + name: card.name, }); + game.players[player].draw_pile.push(card); + + return Resolved; } + + NotResolved }, }); - })], - } + })) +} + +fn cellar() -> Card { + Card::new("Cellar", 2).with_type(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| match message { + ResolveReply::HandCardsChosen { choice } => { + 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); + Resolved + } + _ => NotResolved, + }, + }); + })) +} + +fn chapel() -> Card { + Card::new("Chapel", 2).with_type(CardType::Action(|game| { + game.add_effect(Effect::Resolving { + card: "Chapel".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() > 4 { + return NotResolved; + } + + for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { + let card = game.players[game.active_player].hand.remove(*c); + game.trash.push(card); + } + + return Resolved; + } + NotResolved + }, + }) + })) +} + +fn festival() -> Card { + Card::new("Festival", 5).with_type(CardType::Action(|game| { + action!(game, 2); + buy!(game, 1); + coin!(game, 2); + })) +} + +fn gardens() -> Card { + Card::new("Gardens", 4).with_type(CardType::Victory(0)) +} + +fn moat() -> Card { + Card::new("Moat", 2) + .with_type(CardType::Action(|game| { + draw!(game, 2); + })) + .with_type(CardType::Reaction(|_| {})) +} + +fn sentry() -> Card { + Card::new("Sentry", 5).with_type(CardType::Action(|game| { + draw!(game, 1); + action!(game, 1); + })) +} + +fn throne_room() -> Card { + Card::new("Throne Room", 4).with_type(CardType::Action(|_game| {})) +} + +fn village() -> Card { + Card::new("Village", 3).with_type(CardType::Action(|game| { + draw!(game, 1); + action!(game, 2); + })) +} + +fn merchant() -> Card { + Card::new("Merchant", 3).with_type(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 + } + })); + })) +} + +fn witch() -> Card { + Card::new("Witch", 5) + .with_type(CardType::Attack) + .with_type(CardType::Action(|game| { + draw!(game, 2); + })) +} + +fn workshop() -> Card { + Card::new("Workshop", 3).with_type(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 ResolveRequest::GainCard { filter } = request { + if let Ok(()) = game.gain_card(player, choice, filter) { + return Resolved; + } + } + } + NotResolved + }, + }); + })) +} + +fn smithy() -> Card { + Card::new("Smithy", 4).with_type(CardType::Action(|game| draw!(game, 3))) +} + +fn remodel() -> Card { + Card::new("Remodel", 4).with_type(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 NotResolved; + } + + let card = game.players[game.active_player].hand.remove(choice[0]); + let cost = card.cost; + + game.trash.push(card); + + 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 ResolveRequest::GainCard { filter } = request { + if let Ok(()) = game.gain_card(player, choice, filter) { + return Resolved; + } + } + } + NotResolved + }, + }); + return Resolved; + } + NotResolved + }, + }); + })) +} + +fn militia() -> Card { + Card::new("Militia", 4) + .with_type(CardType::Attack) + .with_type(CardType::Action(|game| { + coin!(game, 2); + + 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 Resolved; + } + } else if game.players[player].hand.len() > 3 + && choice.len() != game.players[player].hand.len() - 3 + { + return NotResolved; + } + + for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { + game.players[player].discard(*c); + } + + return Resolved; + } + NotResolved + }, + }); + })) +} + +fn market() -> Card { + Card::new("Market", 5).with_type(CardType::Action(|game| { + draw!(game, 1); + action!(game, 1); + buy!(game, 1); + coin!(game, 1); + })) +} + +fn mine() -> Card { + Card::new("Mine", 5).with_type(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 NotResolved; + } + + if let Some(card) = game.get_active_player().hand.get(choice[0]) { + if let None = card.treasure() { + return NotResolved; + } + } + + 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::And(vec![ + CardFilter::Type(CardType::Treasure(0)), + CardFilter::MaxCost(cost + 3), + ]), + }, + player: ResolvingPlayer::ActivePlayer, + effect: |game, message, player, request, _state| { + if let ResolveReply::SupplyCardChosen { choice } = message { + if let ResolveRequest::GainCard { filter } = request { + if let Ok(()) = game.gain_card(player, choice, filter) { + return Resolved; + } + } + } + NotResolved + }, + }); + } + NotResolved + }, + }); + })) } diff --git a/static/game.html b/static/game.html index 49c6633..c4ca785 100644 --- a/static/game.html +++ b/static/game.html @@ -471,6 +471,10 @@ img.card:hover {