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)]
pub struct Card {
pub name: &'static str,
pub cost: u32,
pub name: &'static str,
pub types: Vec<CardType>,
}
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)> {
for t in &self.types {
match t {

View File

@ -64,6 +64,7 @@ enum ServerMessage {
#[derive(Clone, Serialize)]
struct GameSetup {
supply: Vec<Card>,
deck: Vec<Card>,
}
@ -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<CardFilter>),
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<usize> {
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(),

View File

@ -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<Card>,
}
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 {
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
},
});
}))
}

View File

@ -471,6 +471,10 @@ img.card:hover {
</div>
<div id="game"></div>
<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 resolve_request;
@ -619,7 +623,7 @@ img.card:hover {
},
m("img", {
class: "card",
src: "/static/images/cards/" + vnode.attrs.name.toLowerCase() + ".jpg",
src: card_url(vnode.attrs.name),
draggable: false,
onmouseenter: mouseenter,
onmouseleave: mouseleave,
@ -690,7 +694,7 @@ img.card:hover {
view: function(vnode) {
var c;
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",
{
@ -777,7 +781,7 @@ img.card:hover {
vnode.attrs.hand.map(function(card, i) {
return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
src: card_url(card),
draggable: true,
ondragstart: dragStart,
"data-index": i,
@ -795,7 +799,7 @@ img.card:hover {
return {
view: function(vnode) {
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)
)
}
@ -839,7 +843,7 @@ img.card:hover {
cards.map(function(card) {
return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
src: card_url(card),
draggable: true,
//ondragstart: dragStart,
onmouseenter: mouseenter,
@ -924,12 +928,24 @@ img.card:hover {
}
}
function card_url(name) {
return "/static/images/cards/" + name.toLowerCase().replace(" ", "-") + ".jpg";
}
function SetupScreen(initialVnode) {
var start_click = function(e) {
let msg = { type: "Command", command: { type: "StartGame" }};
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 {
view: function(vnode) {
@ -941,18 +957,27 @@ img.card:hover {
vnode.attrs.basic_cards.map(function(card) {
return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
src: card_url(card),
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
})
),
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",
vnode.attrs.kingdom_cards.map(function(card) {
return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
src: card_url(card),
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
@ -963,7 +988,7 @@ img.card:hover {
vnode.attrs.starting_deck.map(function(card) {
return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
src: card_url(card),
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
@ -1008,11 +1033,12 @@ img.card:hover {
setup_state = {
starting_deck: [],
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
}
var handle_setup = function(data) {
setup_state.kingdom_cards = data.supply;
setup_state.starting_deck = data.deck;
}
@ -1062,7 +1088,7 @@ img.card:hover {
var dlg = document.querySelector("#dialog");
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") {
let cost = request.request?.filter?.MaxCost;
@ -1101,6 +1127,8 @@ img.card:hover {
append_chat(player_name(msg.event.player) + " discards a card.");
} else if (msg.event.type == "CardTrashed") {
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") {
if (msg.event.player == my_player_id) {
turnStartSound.play();