Allow client to switch kingdom cards in setup phase
This commit is contained in:
parent
246962ca96
commit
73bce1bc6d
16
src/card.rs
16
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<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 {
|
||||
|
227
src/main.rs
227
src/main.rs
@ -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(),
|
||||
|
472
src/sets/base.rs
472
src/sets/base.rs
@ -1,68 +1,167 @@
|
||||
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 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 gold() -> Card {
|
||||
Card {
|
||||
name: "Gold",
|
||||
cost: 6,
|
||||
types: vec![CardType::Treasure(3)],
|
||||
}
|
||||
fn copper() -> Card {
|
||||
Card::new("Copper", 0).with_type(CardType::Treasure(1))
|
||||
}
|
||||
|
||||
pub fn estate() -> Card {
|
||||
Card {
|
||||
name: "Estate",
|
||||
cost: 2,
|
||||
types: vec![CardType::Victory(1)],
|
||||
}
|
||||
fn silver() -> Card {
|
||||
Card::new("Silver", 3).with_type(CardType::Treasure(2))
|
||||
}
|
||||
|
||||
pub fn duchy() -> Card {
|
||||
Card {
|
||||
name: "Duchy",
|
||||
cost: 5,
|
||||
types: vec![CardType::Victory(3)],
|
||||
}
|
||||
fn gold() -> Card {
|
||||
Card::new("Gold", 6).with_type(CardType::Treasure(3))
|
||||
}
|
||||
|
||||
pub fn province() -> Card {
|
||||
Card {
|
||||
name: "Province",
|
||||
cost: 8,
|
||||
types: vec![CardType::Victory(6)],
|
||||
}
|
||||
fn estate() -> Card {
|
||||
Card::new("Estate", 2).with_type(CardType::Victory(1))
|
||||
}
|
||||
|
||||
pub fn curse() -> Card {
|
||||
Card {
|
||||
name: "Curse",
|
||||
cost: 0,
|
||||
types: vec![CardType::Curse],
|
||||
}
|
||||
fn duchy() -> Card {
|
||||
Card::new("Duchy", 5).with_type(CardType::Victory(3))
|
||||
}
|
||||
|
||||
pub fn cellar() -> Card {
|
||||
Card {
|
||||
name: "Cellar",
|
||||
cost: 2,
|
||||
types: vec![CardType::Action(|game| {
|
||||
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: "Bureaucrat".into(),
|
||||
request: ResolveRequest::ChooseHandCardsToDiscard {
|
||||
filter: CardFilter::Type(CardType::Victory(0)),
|
||||
},
|
||||
player: ResolvingPlayer::AllNonActivePlayers,
|
||||
effect: |game, message, player, _request, _state| {
|
||||
if let ResolveReply::HandCardsChosen { choice } = message {
|
||||
if choice.len() > 1 {
|
||||
return NotResolved;
|
||||
}
|
||||
|
||||
if choice.len() == 0 {
|
||||
if game.players[player].hand.iter().any(|c| c.name == "Moat") {
|
||||
game.emit(Event::CardRevealed {
|
||||
player,
|
||||
name: "Moat",
|
||||
});
|
||||
return Resolved;
|
||||
}
|
||||
|
||||
if !game.players[player]
|
||||
.hand
|
||||
.iter()
|
||||
.any(|c| c.victory().is_some())
|
||||
{
|
||||
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(),
|
||||
@ -70,8 +169,8 @@ pub fn cellar() -> Card {
|
||||
filter: CardFilter::Any,
|
||||
},
|
||||
player: ResolvingPlayer::ActivePlayer,
|
||||
effect: |game, message, _player, _request, state| {
|
||||
if let ResolveReply::HandCardsChosen { choice } = message {
|
||||
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);
|
||||
@ -79,43 +178,81 @@ pub fn cellar() -> Card {
|
||||
}
|
||||
|
||||
game.players[game.active_player].draw(discarded);
|
||||
state.resolved = true;
|
||||
Resolved
|
||||
}
|
||||
_ => NotResolved,
|
||||
},
|
||||
});
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn moat() -> Card {
|
||||
Card {
|
||||
name: "Moat",
|
||||
cost: 2,
|
||||
types: vec![
|
||||
CardType::Action(|game| {
|
||||
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);
|
||||
}),
|
||||
CardType::Reaction(|_| {}),
|
||||
],
|
||||
}
|
||||
}))
|
||||
.with_type(CardType::Reaction(|_| {}))
|
||||
}
|
||||
|
||||
pub fn village() -> Card {
|
||||
Card {
|
||||
name: "Village",
|
||||
cost: 3,
|
||||
types: vec![CardType::Action(|game| {
|
||||
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);
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn merchant() -> Card {
|
||||
Card {
|
||||
name: "Merchant",
|
||||
cost: 3,
|
||||
types: vec![CardType::Action(|game| {
|
||||
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| {
|
||||
@ -126,70 +263,55 @@ pub fn merchant() -> Card {
|
||||
false
|
||||
}
|
||||
}));
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn workshop() -> Card {
|
||||
Card {
|
||||
name: "Workshop",
|
||||
cost: 3,
|
||||
types: vec![CardType::Action(|game| {
|
||||
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| {
|
||||
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;
|
||||
if let ResolveRequest::GainCard { filter } = request {
|
||||
if let Ok(()) = game.gain_card(player, choice, filter) {
|
||||
return Resolved;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn simthy() -> Card {
|
||||
Card {
|
||||
name: "Smithy",
|
||||
cost: 4,
|
||||
types: vec![CardType::Action(|game| draw!(game, 3))],
|
||||
}
|
||||
fn smithy() -> Card {
|
||||
Card::new("Smithy", 4).with_type(CardType::Action(|game| draw!(game, 3)))
|
||||
}
|
||||
|
||||
pub fn remodel() -> Card {
|
||||
Card {
|
||||
name: "Remodel",
|
||||
cost: 4,
|
||||
types: vec![CardType::Action(|game| {
|
||||
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| {
|
||||
effect: |game, message, _player, _request, _state| {
|
||||
if let ResolveReply::HandCardsChosen { choice } = message {
|
||||
if choice.len() != 1 {
|
||||
return;
|
||||
return NotResolved;
|
||||
}
|
||||
|
||||
let card = game.players[game.active_player].hand.remove(choice[0]);
|
||||
@ -197,115 +319,78 @@ pub fn remodel() -> Card {
|
||||
|
||||
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| {
|
||||
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;
|
||||
if let ResolveRequest::GainCard { filter } = request {
|
||||
if let Ok(()) = game.gain_card(player, choice, filter) {
|
||||
return Resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
return Resolved;
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn militia() -> Card {
|
||||
Card {
|
||||
name: "Militia",
|
||||
cost: 4,
|
||||
types: vec![
|
||||
CardType::Attack,
|
||||
CardType::Action(|game| {
|
||||
fn militia() -> Card {
|
||||
Card::new("Militia", 4)
|
||||
.with_type(CardType::Attack)
|
||||
.with_type(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| {
|
||||
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;
|
||||
return Resolved;
|
||||
}
|
||||
} else if game.players[player].hand.len() > 3
|
||||
&& choice.len() != game.players[player].hand.len() - 3
|
||||
{
|
||||
return;
|
||||
return NotResolved;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return Resolved;
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn market() -> Card {
|
||||
Card {
|
||||
name: "Market",
|
||||
cost: 5,
|
||||
types: vec![CardType::Action(|game| {
|
||||
fn market() -> Card {
|
||||
Card::new("Market", 5).with_type(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| {
|
||||
fn mine() -> Card {
|
||||
Card::new("Mine", 5).with_type(CardType::Action(|game| {
|
||||
game.add_effect(Effect::Resolving {
|
||||
card: "Mine".into(),
|
||||
request: ResolveRequest::ChooseHandCardsToDiscard {
|
||||
@ -315,12 +400,12 @@ pub fn mine() -> Card {
|
||||
effect: |game, message, _player, _request, state| {
|
||||
if let ResolveReply::HandCardsChosen { choice } = message {
|
||||
if choice.len() != 1 {
|
||||
return;
|
||||
return NotResolved;
|
||||
}
|
||||
|
||||
if let Some(card) = game.get_active_player().hand.get(choice[0]) {
|
||||
if let None = card.treasure() {
|
||||
return;
|
||||
return NotResolved;
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,45 +418,26 @@ pub fn mine() -> Card {
|
||||
game.add_effect(Effect::Resolving {
|
||||
card: "Mine".into(),
|
||||
request: ResolveRequest::GainCard {
|
||||
filter: CardFilter::MaxCost(cost + 3),
|
||||
filter: CardFilter::And(vec![
|
||||
CardFilter::Type(CardType::Treasure(0)),
|
||||
CardFilter::MaxCost(cost + 3),
|
||||
]),
|
||||
},
|
||||
player: ResolvingPlayer::ActivePlayer,
|
||||
effect: |game, message, _player, request, state| {
|
||||
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;
|
||||
if let ResolveRequest::GainCard { filter } = request {
|
||||
if let Ok(()) = game.gain_card(player, choice, filter) {
|
||||
return Resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
}
|
||||
NotResolved
|
||||
},
|
||||
});
|
||||
})],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -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,6 +928,10 @@ img.card:hover {
|
||||
}
|
||||
}
|
||||
|
||||
function card_url(name) {
|
||||
return "/static/images/cards/" + name.toLowerCase().replace(" ", "-") + ".jpg";
|
||||
}
|
||||
|
||||
|
||||
function SetupScreen(initialVnode) {
|
||||
var start_click = function(e) {
|
||||
@ -931,6 +939,14 @@ img.card:hover {
|
||||
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) {
|
||||
var cls = vnode.attrs.active ? "" : "hidden";
|
||||
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user