Allow client to switch kingdom cards in setup phase

This commit is contained in:
Markus Wagner 2021-02-05 18:59:51 +01:00
parent 246962ca96
commit 73bce1bc6d
4 changed files with 647 additions and 408 deletions

View File

@ -47,12 +47,26 @@ pub enum CardType {
#[derive(Clone)] #[derive(Clone)]
pub struct Card { pub struct Card {
pub name: &'static str,
pub cost: u32, pub cost: u32,
pub name: &'static str,
pub types: Vec<CardType>, pub types: Vec<CardType>,
} }
impl Card { 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<fn(&mut Game)> { pub fn action(&self) -> Option<fn(&mut Game)> {
for t in &self.types { for t in &self.types {
match t { match t {

View File

@ -64,6 +64,7 @@ enum ServerMessage {
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
struct GameSetup { struct GameSetup {
supply: Vec<Card>,
deck: Vec<Card>, deck: Vec<Card>,
} }
@ -148,25 +149,39 @@ enum GameState {
impl Default for GameSetup { impl Default for GameSetup {
fn default() -> GameSetup { fn default() -> GameSetup {
let set = sets::base::base();
GameSetup { 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![ deck: vec![
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::copper(), set.get("Copper").unwrap(),
sets::base::estate(), set.get("Estate").unwrap(),
sets::base::estate(), set.get("Estate").unwrap(),
sets::base::estate(), set.get("Estate").unwrap(),
], ],
} }
} }
} }
/// Which player's input is requested to resolve the current effect /// Which player's input is requested to resolve the current effect
#[derive(Clone, Serialize)] #[derive(Clone, Serialize, PartialEq)]
enum ResolvingPlayer { enum ResolvingPlayer {
ActivePlayer, ActivePlayer,
AllNonActivePlayers, AllNonActivePlayers,
@ -188,6 +203,11 @@ enum ResolveRequest {
GainCard { filter: CardFilter }, GainCard { filter: CardFilter },
} }
enum ResolveResult {
Resolved,
NotResolved,
}
#[derive(Clone)] #[derive(Clone)]
enum Effect { enum Effect {
/// Trigger effect if another card is played while effect is active /// Trigger effect if another card is played while effect is active
@ -213,25 +233,71 @@ struct EffectState {
//#[serde(tag = "type")] //#[serde(tag = "type")]
enum CardFilter { enum CardFilter {
Any, Any,
And(Vec<CardFilter>),
MaxCost(u32), MaxCost(u32),
Type(CardType), 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 = type ResolvingEffectHandler =
fn(&mut Game, &ResolveReply, usize, &ResolveRequest, &mut EffectState); fn(&mut Game, &ResolveReply, usize, &ResolveRequest, &mut EffectState) -> ResolveResult;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
enum Command { enum Command {
BuyCard { index: usize }, BuyCard {
Discard { index: usize }, index: usize,
},
Discard {
index: usize,
},
DrawCard, DrawCard,
EndTurn, EndTurn,
GainCard { index: usize }, GainCard {
PlayCard { index: usize }, index: usize,
ResolveReply { reply: ResolveReply }, },
PlayCard {
index: usize,
},
ResolveReply {
reply: ResolveReply,
},
StartGame, 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)] #[derive(Debug, Serialize)]
@ -243,6 +309,7 @@ enum Event {
CardDiscarded { player: usize }, CardDiscarded { player: usize },
CardTrashed { player: usize, name: &'static str }, CardTrashed { player: usize, name: &'static str },
CardPlayed { player: usize, name: &'static str }, CardPlayed { player: usize, name: &'static str },
CardRevealed { player: usize, name: &'static str },
TurnStarted { player: usize }, TurnStarted { player: usize },
} }
@ -286,6 +353,15 @@ impl Game {
} }
} }
fn supply_name_to_index(&self, name: &String) -> Option<usize> {
for i in 0..self.supply.len() {
if self.supply[i].0.name == name {
return Some(i);
}
}
None
}
fn start(&mut self) { fn start(&mut self) {
match self.state { match self.state {
GameState::Setup => { GameState::Setup => {
@ -306,24 +382,24 @@ impl Game {
}; };
self.supply = vec![ self.supply = vec![
(sets::base::copper(), 60 - self.players.len() * 7), (
(sets::base::silver(), 40), sets::base::base().get("Copper").unwrap(),
(sets::base::gold(), 30), 60 - self.players.len() * 7,
(sets::base::estate(), victory_qty), ),
(sets::base::duchy(), victory_qty), (sets::base::base().get("Silver").unwrap(), 40),
(sets::base::province(), victory_qty), (sets::base::base().get("Gold").unwrap(), 30),
(sets::base::curse(), 10), (sets::base::base().get("Estate").unwrap(), victory_qty),
(sets::base::cellar(), 10), (sets::base::base().get("Duchy").unwrap(), victory_qty),
(sets::base::moat(), 10), (sets::base::base().get("Province").unwrap(), victory_qty),
(sets::base::village(), 10), (sets::base::base().get("Curse").unwrap(), 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),
]; ];
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 _ => {} // Ignore if game is not in setup state
@ -424,7 +500,12 @@ impl Game {
if !self.can_play(player_number, index) { if !self.can_play(player_number, index) {
return false; return false;
} }
let card = self.players[player_number].hand.remove(index); 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() { if let Some(coin) = card.treasure() {
self.turn_state.coin += coin; self.turn_state.coin += coin;
@ -457,6 +538,31 @@ impl Game {
true 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>*/ pub fn buy_card(&mut self, player_number: usize, index: usize) -> bool /*-> Result<(), &'static str>*/
{ {
if let Some(_) = self.resolving_effect { if let Some(_) = self.resolving_effect {
@ -499,6 +605,10 @@ impl Game {
effect, effect,
player, player,
} => { } => {
if player == ResolvingPlayer::AllNonActivePlayers && self.players.len() == 1 {
return;
}
let state = EffectState::default(); let state = EffectState::default();
self.resolving_effect = Some((card, request, effect, player, state)); self.resolving_effect = Some((card, request, effect, player, state));
} }
@ -507,6 +617,14 @@ impl Game {
fn handle_command(&mut self, player: usize, command: Command) { fn handle_command(&mut self, player: usize, command: Command) {
match 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 => { Command::DrawCard => {
if !self.debug_mode { if !self.debug_mode {
return; return;
@ -536,11 +654,7 @@ impl Game {
return; return;
} }
self.supply.get_mut(index).unwrap().1 = self.supply.get(index).unwrap().1 - 1; self.gain_card(player, &index, &CardFilter::Any);
let card = self.supply[index].0.clone();
self.players[player].discard_pile.push(card.clone());
self.emit(Event::CardGained { player, index })
} }
Command::Discard { index } => { Command::Discard { index } => {
@ -564,10 +678,7 @@ impl Game {
} }
Command::PlayCard { index } => { Command::PlayCard { index } => {
let name = self.players[player].hand[index].name.clone(); self.play_card(player, index);
if self.play_card(player, index) {
self.emit(Event::CardPlayed { player, name });
}
} }
Command::ResolveReply { reply } => { Command::ResolveReply { reply } => {
@ -577,13 +688,26 @@ impl Game {
match resolve_player { match resolve_player {
ResolvingPlayer::ActivePlayer => { ResolvingPlayer::ActivePlayer => {
if player == self.active_player { 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 => { ResolvingPlayer::AllNonActivePlayers => {
if player != self.active_player { 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); println!("Emitting event: {:?}", event);
async_std::task::block_on(notify_players(self, 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) { async fn broadcast(game: &Game, sm: &ServerMessage) {
for (_, (_, con)) in game.connections.iter() { 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; 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() { for p in game.players.iter() {
let sm = ServerMessage::PlayerHand { let sm = ServerMessage::PlayerHand {
hand: p.hand.clone(), hand: p.hand.clone(),

View File

@ -1,377 +1,443 @@
use crate::card::{Card, CardType}; use crate::card::{Card, CardType};
use crate::{CardFilter, Effect, ResolveReply, ResolveRequest, ResolvingPlayer}; use crate::{
CardFilter, Effect, Event, ResolveReply, ResolveRequest, ResolveResult::*, ResolvingPlayer,
};
use itertools::Itertools; use itertools::Itertools;
pub fn copper() -> Card { pub struct CardSet {
Card { name: &'static str,
name: "Copper", cards: Vec<Card>,
cost: 0, }
types: vec![CardType::Treasure(1)],
impl CardSet {
pub fn get(&self, name: &str) -> Option<Card> {
for card in self.cards.iter() {
if card.name == name {
return Some(card.clone());
}
}
None
} }
} }
pub fn silver() -> Card { pub fn base() -> CardSet {
Card { CardSet {
name: "Silver", name: "Dominion",
cost: 3, cards: vec![
types: vec![CardType::Treasure(2)], copper(),
} silver(),
} gold(),
estate(),
pub fn gold() -> Card { duchy(),
Card { province(),
name: "Gold", curse(),
cost: 6, artisan(),
types: vec![CardType::Treasure(3)], bandit(),
} bureaucrat(),
} chapel(),
cellar(),
pub fn estate() -> Card { gardens(),
Card { festival(),
name: "Estate", market(),
cost: 2, militia(),
types: vec![CardType::Victory(1)], mine(),
} merchant(),
} moat(),
remodel(),
pub fn duchy() -> Card { sentry(),
Card { smithy(),
name: "Duchy", throne_room(),
cost: 5, village(),
types: vec![CardType::Victory(3)], witch(),
} workshop(),
}
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 { fn copper() -> Card {
Card { Card::new("Copper", 0).with_type(CardType::Treasure(1))
name: "Village",
cost: 3,
types: vec![CardType::Action(|game| {
draw!(game, 1);
action!(game, 2);
})],
}
} }
pub fn merchant() -> Card { fn silver() -> Card {
Card { Card::new("Silver", 3).with_type(CardType::Treasure(2))
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 { fn gold() -> Card {
Card { Card::new("Gold", 6).with_type(CardType::Treasure(3))
name: "Workshop", }
cost: 3,
types: vec![CardType::Action(|game| { 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 { game.add_effect(Effect::Resolving {
card: "Workshop".into(), card: "Bureaucrat".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 { request: ResolveRequest::ChooseHandCardsToDiscard {
filter: CardFilter::Any, filter: CardFilter::Type(CardType::Victory(0)),
}, },
player: ResolvingPlayer::ActivePlayer, player: ResolvingPlayer::AllNonActivePlayers,
effect: |game, message, _player, _request, state| { effect: |game, message, player, _request, _state| {
if let ResolveReply::HandCardsChosen { choice } = message { if let ResolveReply::HandCardsChosen { choice } = message {
if choice.len() != 1 { if choice.len() > 1 {
return; return NotResolved;
} }
let card = game.players[game.active_player].hand.remove(choice[0]); if choice.len() == 0 {
let cost = card.cost; if game.players[player].hand.iter().any(|c| c.name == "Moat") {
game.emit(Event::CardRevealed {
player,
name: "Moat",
});
return Resolved;
}
game.trash.push(card); if !game.players[player]
.hand
state.resolved = true; .iter()
game.add_effect(Effect::Resolving { .any(|c| c.victory().is_some())
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 card in game.players[player].hand.iter() {
} game.emit(Event::CardRevealed {
player,
for c in choice.iter().sorted_by(|a, b| b.cmp(a)) { name: card.name,
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;
}
}
} }
}, 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
},
});
}))
} }

View File

@ -471,6 +471,10 @@ img.card:hover {
</div> </div>
<div id="game"></div> <div id="game"></div>
<script> <script>
var set_data = [
["Cellar", "Market", "Merchant", "Militia", "Mine", "Moat", "Remodel", "Smithy", "Village", "Workshop"],
["Artisan", "Bandit", "Bureaucrat", "Chapel", "Festival", "Gardens", "Sentry", "Throne Room", "Witch", "Workshop"],
];
var turnStartSound = new Audio("/static/audio/chipsStack1.ogg"); var turnStartSound = new Audio("/static/audio/chipsStack1.ogg");
var resolve_request; var resolve_request;
@ -619,7 +623,7 @@ img.card:hover {
}, },
m("img", { m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + vnode.attrs.name.toLowerCase() + ".jpg", src: card_url(vnode.attrs.name),
draggable: false, draggable: false,
onmouseenter: mouseenter, onmouseenter: mouseenter,
onmouseleave: mouseleave, onmouseleave: mouseleave,
@ -690,7 +694,7 @@ img.card:hover {
view: function(vnode) { view: function(vnode) {
var c; var c;
if (vnode.attrs.card) { if (vnode.attrs.card) {
c = m("img", {class: "card", src: "/static/images/cards/" + vnode.attrs.card.toLowerCase() + ".jpg"}); c = m("img", {class: "card", src: card_url(vnode.attrs.card)});
} }
return m(".discard-pile", return m(".discard-pile",
{ {
@ -777,7 +781,7 @@ img.card:hover {
vnode.attrs.hand.map(function(card, i) { vnode.attrs.hand.map(function(card, i) {
return m("img", { return m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg", src: card_url(card),
draggable: true, draggable: true,
ondragstart: dragStart, ondragstart: dragStart,
"data-index": i, "data-index": i,
@ -795,7 +799,7 @@ img.card:hover {
return { return {
view: function(vnode) { view: function(vnode) {
return m(".opponent-hand", return m(".opponent-hand",
m("img", {class: "card", src: "/static/images/cards/Card_back.jpg"}), m("img", {class: "card", src: "/static/images/cards/Card_back.jpg" }),
m("span", {class: "pile-counter" }, vnode.attrs.count) m("span", {class: "pile-counter" }, vnode.attrs.count)
) )
} }
@ -839,7 +843,7 @@ img.card:hover {
cards.map(function(card) { cards.map(function(card) {
return m("img", { return m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg", src: card_url(card),
draggable: true, draggable: true,
//ondragstart: dragStart, //ondragstart: dragStart,
onmouseenter: mouseenter, onmouseenter: mouseenter,
@ -924,6 +928,10 @@ img.card:hover {
} }
} }
function card_url(name) {
return "/static/images/cards/" + name.toLowerCase().replace(" ", "-") + ".jpg";
}
function SetupScreen(initialVnode) { function SetupScreen(initialVnode) {
var start_click = function(e) { var start_click = function(e) {
@ -931,6 +939,14 @@ img.card:hover {
initialVnode.attrs.socket.send(JSON.stringify(msg)); initialVnode.attrs.socket.send(JSON.stringify(msg));
} }
var set_changed = function(e) {
let choice = parseInt(e.srcElement.value);
for (card in set_data[choice]) {
let name = set_data[choice][card];
send_command("ChangeSupply", {index: parseInt(card), name: name });
}
}
return { return {
view: function(vnode) { view: function(vnode) {
var cls = vnode.attrs.active ? "" : "hidden"; var cls = vnode.attrs.active ? "" : "hidden";
@ -941,18 +957,27 @@ img.card:hover {
vnode.attrs.basic_cards.map(function(card) { vnode.attrs.basic_cards.map(function(card) {
return m("img", { return m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg", src: card_url(card),
onmouseenter: mouseenter, onmouseenter: mouseenter,
onmouseleave: mouseleave, onmouseleave: mouseleave,
}) })
}) })
), ),
m("h4", "Kingdom Cards"), m("h4", "Kingdom Cards"),
m("select",
{
name: "set",
onchange: set_changed,
},[
m("option", { value: 0, style: "background-image: url(/static/images/sets/Dominion.png);"}, "First Game"),
m("option", { value: 1}, "Size Distortion"),
]
),
m(".kingdom-cards", m(".kingdom-cards",
vnode.attrs.kingdom_cards.map(function(card) { vnode.attrs.kingdom_cards.map(function(card) {
return m("img", { return m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg", src: card_url(card),
onmouseenter: mouseenter, onmouseenter: mouseenter,
onmouseleave: mouseleave, onmouseleave: mouseleave,
}) })
@ -963,7 +988,7 @@ img.card:hover {
vnode.attrs.starting_deck.map(function(card) { vnode.attrs.starting_deck.map(function(card) {
return m("img", { return m("img", {
class: "card", class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg", src: card_url(card),
onmouseenter: mouseenter, onmouseenter: mouseenter,
onmouseleave: mouseleave, onmouseleave: mouseleave,
}) })
@ -1008,11 +1033,12 @@ img.card:hover {
setup_state = { setup_state = {
starting_deck: [], starting_deck: [],
basic_cards: ["Copper", "Silver", "Gold", "Estate", "Duchy", "Province", "Curse"], basic_cards: ["Copper", "Silver", "Gold", "Estate", "Duchy", "Province", "Curse"],
kingdom_cards: ["Cellar", "Moat", "Village", "Merchant", "Workshop", "Smithy", "Remodel", "Militia", "Market", "Mine"], kingdom_cards: [],
socket: webSocket socket: webSocket
} }
var handle_setup = function(data) { var handle_setup = function(data) {
setup_state.kingdom_cards = data.supply;
setup_state.starting_deck = data.deck; setup_state.starting_deck = data.deck;
} }
@ -1062,7 +1088,7 @@ img.card:hover {
var dlg = document.querySelector("#dialog"); var dlg = document.querySelector("#dialog");
resolve_request = request; resolve_request = request;
document.querySelector("#dialog img").src = "/static/images/cards/" + request.card.toLowerCase() + ".jpg"; document.querySelector("#dialog img").src = card_url(request.card);
if (request.request.type == "GainCard") { if (request.request.type == "GainCard") {
let cost = request.request?.filter?.MaxCost; let cost = request.request?.filter?.MaxCost;
@ -1101,6 +1127,8 @@ img.card:hover {
append_chat(player_name(msg.event.player) + " discards a card."); append_chat(player_name(msg.event.player) + " discards a card.");
} else if (msg.event.type == "CardTrashed") { } else if (msg.event.type == "CardTrashed") {
append_chat(player_name(msg.event.player) + " trashes " + msg.event.name); append_chat(player_name(msg.event.player) + " trashes " + msg.event.name);
} else if (msg.event.type == "CardRevealed") {
append_chat(player_name(msg.event.player) + " reveals " + msg.event.name);
} else if (msg.event.type == "TurnStarted") { } else if (msg.event.type == "TurnStarted") {
if (msg.event.player == my_player_id) { if (msg.event.player == my_player_id) {
turnStartSound.play(); turnStartSound.play();