Compare commits
14 Commits
9ab7d7fcfb
...
master
Author | SHA1 | Date | |
---|---|---|---|
60c213baa6 | |||
73bce1bc6d | |||
246962ca96 | |||
c94876c54c | |||
d18942feda | |||
998d27764d | |||
f2dab8a71c | |||
d6e2543b9c | |||
b30cc02306 | |||
83d5062a84 | |||
ea94aa5230 | |||
7434dee715 | |||
ab4ab2926b | |||
0f2f085efe |
135
src/card.rs
Normal file
135
src/card.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
pub use super::Game;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! draw {
|
||||
($g:ident, $e:expr) => {
|
||||
$g.players[$g.active_player].draw($e)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! action {
|
||||
($g:ident, $e:expr) => {
|
||||
$g.turn_state.actions += $e
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! buy {
|
||||
($g:ident, $e:expr) => {
|
||||
$g.turn_state.buys += $e
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! coin {
|
||||
($g:ident, $e:expr) => {
|
||||
$g.turn_state.coin += $e
|
||||
};
|
||||
}
|
||||
|
||||
fn serialize_card_type<S>(_: &fn(&mut super::Game), serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str("ActionSer")
|
||||
}
|
||||
|
||||
fn serialize_card_type_0<S>(
|
||||
_: &fn(&super::Game, usize) -> u32,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str("ActionSer")
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum CardType {
|
||||
#[serde(serialize_with = "serialize_card_type")]
|
||||
Action(fn(&mut Game)),
|
||||
Attack,
|
||||
Curse,
|
||||
#[serde(serialize_with = "serialize_card_type")]
|
||||
Reaction(fn(&mut Game)),
|
||||
Treasure(u32),
|
||||
#[serde(serialize_with = "serialize_card_type_0")]
|
||||
Victory(fn(&Game, usize) -> u32),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Card {
|
||||
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 {
|
||||
CardType::Action(effects) => return Some(*effects),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn curse(&self) -> Option<()> {
|
||||
for t in &self.types {
|
||||
match t {
|
||||
CardType::Curse => return Some(()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn treasure(&self) -> Option<u32> {
|
||||
for t in &self.types {
|
||||
match t {
|
||||
CardType::Treasure(coin) => return Some(*coin),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn victory(&self) -> Option<fn(&Game, usize) -> u32> {
|
||||
for t in &self.types {
|
||||
match t {
|
||||
CardType::Victory(points) => return Some(*points),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Card {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Card {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
770
src/main.rs
770
src/main.rs
@@ -1,9 +1,11 @@
|
||||
mod cards;
|
||||
#[macro_use]
|
||||
mod card;
|
||||
mod sets;
|
||||
|
||||
use async_std::{prelude::*, sync::RwLock};
|
||||
use cards::*;
|
||||
use card::*;
|
||||
use itertools::Itertools;
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tide::{Body, Redirect, Request, Response};
|
||||
@@ -16,13 +18,7 @@ enum ClientMessage {
|
||||
Chat { message: String },
|
||||
CreateGame { name: String },
|
||||
JoinGame { name: String },
|
||||
StartGame,
|
||||
EndTurn,
|
||||
PlayCard { name: String, index: usize },
|
||||
GainCard { name: String, index: usize },
|
||||
DrawCard,
|
||||
Discard { index: usize },
|
||||
TrashHand { index: usize },
|
||||
Command { command: Command },
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -53,15 +49,22 @@ enum ServerMessage {
|
||||
id: usize,
|
||||
},
|
||||
Notification {
|
||||
text: String,
|
||||
event: Event,
|
||||
},
|
||||
GameOver {
|
||||
score: Vec<(usize, u32)>,
|
||||
},
|
||||
ResolveRequest {
|
||||
/// Request type and context
|
||||
request: ResolveRequest,
|
||||
/// The card that is the source of the request to display to the player
|
||||
card: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct GameSetup {
|
||||
supply: Vec<Card>,
|
||||
deck: Vec<Card>,
|
||||
}
|
||||
|
||||
@@ -146,24 +149,172 @@ 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![
|
||||
copper(),
|
||||
copper(),
|
||||
copper(),
|
||||
copper(),
|
||||
copper(),
|
||||
copper(),
|
||||
copper(),
|
||||
estate(),
|
||||
estate(),
|
||||
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(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Game {
|
||||
/// Which player's input is requested to resolve the current effect
|
||||
#[derive(Clone, Serialize, PartialEq)]
|
||||
enum ResolvingPlayer {
|
||||
ActivePlayer,
|
||||
AllNonActivePlayers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ResolveReply {
|
||||
HandCardsChosen { choice: Vec<usize> },
|
||||
SupplyCardChosen { choice: usize },
|
||||
}
|
||||
|
||||
/// Sent by the server to tell the client what information is requested
|
||||
/// from the player to resolve the current effect
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ResolveRequest {
|
||||
ChooseHandCardsToDiscard { filter: CardFilter },
|
||||
GainCard { filter: CardFilter },
|
||||
}
|
||||
|
||||
enum ResolveResult {
|
||||
Resolved,
|
||||
NotResolved,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Effect {
|
||||
/// Trigger effect if another card is played while effect is active
|
||||
/// return true to indicate the event is resolved and should be removed
|
||||
OnCardPlayed(fn(&mut Game, &Card) -> bool),
|
||||
/// Effect that blocks further processing of the game state until
|
||||
/// some user input is received that allows to fully resolve the effect
|
||||
Resolving {
|
||||
card: String,
|
||||
player: ResolvingPlayer,
|
||||
request: ResolveRequest,
|
||||
effect: ResolvingEffectHandler,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EffectState {
|
||||
resolved: bool,
|
||||
players_responded: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
//#[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) -> ResolveResult;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum Command {
|
||||
BuyCard {
|
||||
index: usize,
|
||||
},
|
||||
Discard {
|
||||
index: usize,
|
||||
},
|
||||
DrawCard,
|
||||
EndTurn,
|
||||
GainCard {
|
||||
index: usize,
|
||||
},
|
||||
PlayCard {
|
||||
index: usize,
|
||||
},
|
||||
ResolveReply {
|
||||
reply: ResolveReply,
|
||||
},
|
||||
StartGame,
|
||||
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)]
|
||||
#[serde(tag = "type")]
|
||||
enum Event {
|
||||
CardBought { player: usize, name: &'static str },
|
||||
CardDrawn { player: usize },
|
||||
CardGained { player: usize, index: usize },
|
||||
CardDiscarded { player: usize },
|
||||
CardTrashed { player: usize, name: &'static str },
|
||||
CardPlayed { player: usize, name: &'static str },
|
||||
CardRevealed { player: usize, name: &'static str },
|
||||
TurnStarted { player: usize },
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
effects: Vec<Effect>,
|
||||
players: Vec<Player>,
|
||||
state: GameState,
|
||||
setup: GameSetup,
|
||||
@@ -172,11 +323,23 @@ struct Game {
|
||||
supply: Vec<(Card, usize)>,
|
||||
trash: Vec<Card>,
|
||||
turn_state: TurnState,
|
||||
debug_mode: bool,
|
||||
|
||||
/// Any effect from a card that requires further input from players
|
||||
/// and blocks the game until fully resolved.
|
||||
resolving_effect: Option<(
|
||||
String,
|
||||
ResolveRequest,
|
||||
ResolvingEffectHandler,
|
||||
ResolvingPlayer,
|
||||
EffectState,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn new(player: Player) -> Self {
|
||||
Self {
|
||||
effects: vec![],
|
||||
players: vec![player],
|
||||
state: GameState::Setup,
|
||||
setup: GameSetup::default(),
|
||||
@@ -185,14 +348,29 @@ impl Game {
|
||||
supply: vec![],
|
||||
trash: vec![],
|
||||
turn_state: TurnState::default(),
|
||||
resolving_effect: None,
|
||||
debug_mode: true,
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
self.state = GameState::InProgress;
|
||||
|
||||
if self.players.len() > 1 {
|
||||
self.active_player = thread_rng().gen_range(0..self.players.len());
|
||||
}
|
||||
|
||||
for p in self.players.iter_mut() {
|
||||
(*p).discard_pile = self.setup.deck.clone();
|
||||
p.draw(5);
|
||||
@@ -204,101 +382,24 @@ impl Game {
|
||||
};
|
||||
|
||||
self.supply = vec![
|
||||
(copper(), 60 - self.players.len() * 7),
|
||||
(silver(), 40),
|
||||
(gold(), 30),
|
||||
(estate(), victory_qty),
|
||||
(duchy(), victory_qty),
|
||||
(province(), victory_qty),
|
||||
(
|
||||
Card {
|
||||
name: "Curse".into(),
|
||||
cost: 0,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Cellar".into(),
|
||||
cost: 2,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Moat".into(),
|
||||
cost: 2,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Village".into(),
|
||||
cost: 3,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Merchant".into(),
|
||||
cost: 3,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Workshop".into(),
|
||||
cost: 3,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Smithy".into(),
|
||||
cost: 4,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Remodel".into(),
|
||||
cost: 4,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Militia".into(),
|
||||
cost: 4,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Market".into(),
|
||||
cost: 5,
|
||||
types: vec![],
|
||||
},
|
||||
10,
|
||||
),
|
||||
(
|
||||
Card {
|
||||
name: "Mine".into(),
|
||||
cost: 5,
|
||||
types: vec![],
|
||||
},
|
||||
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
|
||||
@@ -306,6 +407,10 @@ impl Game {
|
||||
}
|
||||
|
||||
fn end_turn(&mut self) {
|
||||
if let Some(_) = self.resolving_effect {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.players.get_mut(self.active_player) {
|
||||
None => {}
|
||||
Some(p) => {
|
||||
@@ -320,6 +425,12 @@ impl Game {
|
||||
self.active_player += 1;
|
||||
self.active_player %= self.players.len();
|
||||
self.turn_state = TurnState::default();
|
||||
|
||||
self.effects.clear();
|
||||
|
||||
self.emit(Event::TurnStarted {
|
||||
player: self.active_player,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the end game condition is reached and finish the game if so,
|
||||
@@ -342,27 +453,280 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_active_player(&self, player_id: &str) -> bool {
|
||||
match self.players.get(self.active_player) {
|
||||
None => false,
|
||||
Some(p) => p.id == *player_id,
|
||||
}
|
||||
fn is_active_player(&self, player: usize) -> bool {
|
||||
self.active_player == player
|
||||
}
|
||||
|
||||
fn get_active_player(&mut self) -> &mut Player {
|
||||
return &mut self.players[self.active_player];
|
||||
}
|
||||
|
||||
pub fn trash_hand(&mut self, player_number: usize, index: usize) {
|
||||
if let Some(_) = self.resolving_effect {
|
||||
return;
|
||||
}
|
||||
|
||||
self.trash
|
||||
.push(self.players[player_number].hand.remove(index));
|
||||
}
|
||||
|
||||
pub fn play_card(&mut self, player_number: usize, index: usize) {
|
||||
let player = self.players.get_mut(player_number).unwrap();
|
||||
let card = player.hand.remove(index);
|
||||
pub fn can_play(&self, player_number: usize, index: usize) -> bool {
|
||||
if let Some(_) = self.resolving_effect {
|
||||
return false;
|
||||
}
|
||||
|
||||
let card = self.players[player_number].hand.get(index).unwrap();
|
||||
|
||||
match card.action() {
|
||||
Some(_) => {
|
||||
if self.turn_state.actions > 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
match card.treasure() {
|
||||
Some(_) => {
|
||||
return true;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn play_card(&mut self, player_number: usize, index: usize) -> bool {
|
||||
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;
|
||||
}
|
||||
|
||||
player.played_cards.push(card);
|
||||
if let Some(effect) = card.action() {
|
||||
self.turn_state.actions -= 1;
|
||||
|
||||
effect(self);
|
||||
}
|
||||
|
||||
let mut effects = self.effects.clone();
|
||||
self.effects.clear();
|
||||
effects.retain(|effect| {
|
||||
match effect {
|
||||
Effect::OnCardPlayed(effect) => {
|
||||
if effect(self, &card) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Effect::Resolving { .. } => {}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
self.effects.append(&mut effects);
|
||||
|
||||
self.players[player_number].played_cards.push(card);
|
||||
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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if player_number != self.active_player {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.turn_state.buys < 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(card) = self.supply.get(index).as_deref() {
|
||||
if card.0.cost <= self.turn_state.coin {
|
||||
let card = card.0.clone();
|
||||
|
||||
self.supply.get_mut(index).unwrap().1 = self.supply.get(index).unwrap().1 - 1;
|
||||
self.turn_state.coin -= card.cost;
|
||||
self.turn_state.buys -= 1;
|
||||
self.players[player_number].discard_pile.push(card);
|
||||
return true;
|
||||
//return Ok(());
|
||||
}
|
||||
}
|
||||
//Err("Not enough coin");
|
||||
false
|
||||
}
|
||||
|
||||
fn add_effect(&mut self, effect: Effect) {
|
||||
match effect {
|
||||
Effect::OnCardPlayed(_) => {
|
||||
self.effects.push(effect);
|
||||
}
|
||||
|
||||
Effect::Resolving {
|
||||
card,
|
||||
request,
|
||||
effect,
|
||||
player,
|
||||
} => {
|
||||
if player == ResolvingPlayer::AllNonActivePlayers && self.players.len() == 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = EffectState::default();
|
||||
self.resolving_effect = Some((card, request, effect, player, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
self.players[player].draw(1);
|
||||
self.emit(Event::CardDrawn { player });
|
||||
}
|
||||
|
||||
Command::BuyCard { index } => {
|
||||
if self.buy_card(player, index) {
|
||||
self.emit(Event::CardBought {
|
||||
player,
|
||||
name: self.supply[index].0.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Command::EndTurn => {
|
||||
if self.is_active_player(player) {
|
||||
self.end_turn();
|
||||
}
|
||||
}
|
||||
|
||||
Command::GainCard { index } => {
|
||||
if !self.debug_mode {
|
||||
return;
|
||||
}
|
||||
|
||||
self.gain_card(player, &index, &CardFilter::Any);
|
||||
}
|
||||
|
||||
Command::Discard { index } => {
|
||||
if !self.debug_mode {
|
||||
return;
|
||||
}
|
||||
|
||||
//let card_name = self.players[player].hand[index].clone();
|
||||
self.players[player].discard(index);
|
||||
self.emit(Event::CardDiscarded { player });
|
||||
}
|
||||
|
||||
Command::StartGame => {
|
||||
self.start();
|
||||
}
|
||||
|
||||
Command::TrashHand { index } => {
|
||||
let name = self.players[player].hand[index].name.clone();
|
||||
self.trash_hand(player, index);
|
||||
self.emit(Event::CardTrashed { player, name });
|
||||
}
|
||||
|
||||
Command::PlayCard { index } => {
|
||||
self.play_card(player, index);
|
||||
}
|
||||
|
||||
Command::ResolveReply { reply } => {
|
||||
if let Some((card, request, effect, resolve_player, mut state)) =
|
||||
self.resolving_effect.take()
|
||||
{
|
||||
match resolve_player {
|
||||
ResolvingPlayer::ActivePlayer => {
|
||||
if player == self.active_player {
|
||||
match effect(self, &reply, self.active_player, &request, &mut state)
|
||||
{
|
||||
ResolveResult::Resolved => state.resolved = true,
|
||||
ResolveResult::NotResolved => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResolvingPlayer::AllNonActivePlayers => {
|
||||
if player != self.active_player {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match state.resolved {
|
||||
false => {
|
||||
self.resolving_effect =
|
||||
Some((card, request, effect, resolve_player, state));
|
||||
}
|
||||
true => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&self, event: Event) {
|
||||
println!("Emitting event: {:?}", event);
|
||||
async_std::task::block_on(notify_players(self, event));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,14 +735,14 @@ struct State {
|
||||
games: Arc<RwLock<HashMap<Uuid, Game>>>,
|
||||
}
|
||||
|
||||
async fn notify_players(game: &Game, text: String) {
|
||||
let sm = ServerMessage::Notification { text };
|
||||
async fn notify_players(game: &Game, event: Event) {
|
||||
let sm = ServerMessage::Notification { event };
|
||||
broadcast(game, &sm).await;
|
||||
}
|
||||
|
||||
async fn broadcast(game: &Game, sm: &ServerMessage) {
|
||||
for (_, (_, con)) in game.connections.iter() {
|
||||
con.send_json(&sm).await.unwrap();
|
||||
con.send_json(&sm).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +758,12 @@ async fn broadcast_state(game: &Game) {
|
||||
name: html_escape::encode_text(&p.name).into(),
|
||||
draw_pile_count: p.draw_pile.len(),
|
||||
hand_count: p.hand.len(),
|
||||
discard_pile: p.discard_pile.last().map(|c| c.name.clone()),
|
||||
played_cards: p.played_cards.iter().map(|c| c.name.clone()).collect(),
|
||||
discard_pile: p.discard_pile.last().map(|c| c.name.clone().into()),
|
||||
played_cards: p
|
||||
.played_cards
|
||||
.iter()
|
||||
.map(|c| c.name.clone().into())
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
active_player: game.active_player,
|
||||
@@ -413,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(),
|
||||
@@ -430,7 +805,9 @@ async fn broadcast_state(game: &Game) {
|
||||
.map(|(i, p)| {
|
||||
let score = p.draw_pile.iter().fold(0, |acc, card| {
|
||||
if let Some(points) = card.victory() {
|
||||
acc + points
|
||||
acc + points(&game, i)
|
||||
} else if let Some(_) = card.curse() {
|
||||
acc - 1
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
@@ -442,6 +819,47 @@ async fn broadcast_state(game: &Game) {
|
||||
};
|
||||
broadcast(&game, &sm).await;
|
||||
}
|
||||
|
||||
if let Some((card_name, request, _, ref player, _)) = &game.resolving_effect {
|
||||
match request {
|
||||
ResolveRequest::ChooseHandCardsToDiscard { .. } => match player {
|
||||
ResolvingPlayer::ActivePlayer => {
|
||||
let p = game.players.get(game.active_player).unwrap();
|
||||
let sm = ServerMessage::ResolveRequest {
|
||||
card: card_name.clone(),
|
||||
request: request.clone(),
|
||||
};
|
||||
send_msg(&game, &p, &sm).await;
|
||||
}
|
||||
|
||||
ResolvingPlayer::AllNonActivePlayers => {
|
||||
for (id, player) in game.players.iter().enumerate() {
|
||||
let sm = ServerMessage::ResolveRequest {
|
||||
card: card_name.clone(),
|
||||
request: request.clone(),
|
||||
};
|
||||
|
||||
if id != game.active_player {
|
||||
send_msg(&game, &player, &sm).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ResolveRequest::GainCard { .. } => match player {
|
||||
ResolvingPlayer::ActivePlayer => {
|
||||
let p = game.players.get(game.active_player).unwrap();
|
||||
let sm = ServerMessage::ResolveRequest {
|
||||
card: card_name.clone(),
|
||||
request: request.clone(),
|
||||
};
|
||||
send_msg(&game, &p, &sm).await;
|
||||
}
|
||||
|
||||
_ => unimplemented!("GainCard for non active players?!"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) {
|
||||
@@ -608,106 +1026,16 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
broadcast(&game, &sm).await;
|
||||
}
|
||||
|
||||
ClientMessage::StartGame => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
game.start();
|
||||
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
|
||||
ClientMessage::EndTurn => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
if game.is_active_player(&player_id) {
|
||||
game.end_turn();
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
}
|
||||
|
||||
ClientMessage::CreateGame { .. } => {}
|
||||
|
||||
ClientMessage::JoinGame { .. } => {}
|
||||
|
||||
ClientMessage::PlayCard { name: _, index } => {
|
||||
ClientMessage::Command { command } => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
let card_name = game.players[player_number].hand[index].name.clone();
|
||||
game.play_card(player_number, index);
|
||||
game.handle_command(player_number, command);
|
||||
|
||||
notify_players(
|
||||
&game,
|
||||
format!("{} spielt {}", game.players[player_number].name, card_name),
|
||||
)
|
||||
.await;
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
|
||||
ClientMessage::GainCard { name: _, index } => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
game.supply.get_mut(index).unwrap().1 =
|
||||
game.supply.get(index).unwrap().1 - 1;
|
||||
let card = game.supply[index].0.clone();
|
||||
|
||||
game.players[player_number].discard_pile.push(card.clone());
|
||||
|
||||
notify_players(
|
||||
&game,
|
||||
format!("{} nimmt {}", game.players[player_number].name, card.name),
|
||||
)
|
||||
.await;
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
|
||||
ClientMessage::DrawCard => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
game.players[player_number].draw(1);
|
||||
|
||||
notify_players(
|
||||
&game,
|
||||
format!("{} zieht eine Karte", game.players[player_number].name),
|
||||
)
|
||||
.await;
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
|
||||
ClientMessage::Discard { index } => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
let card_name = game.players[player_number].hand[index].clone();
|
||||
game.players[player_number].discard(index);
|
||||
|
||||
notify_players(
|
||||
&game,
|
||||
format!("{} legt {} ab", game.players[player_number].name, card_name),
|
||||
)
|
||||
.await;
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
|
||||
ClientMessage::TrashHand { index } => {
|
||||
let mut games = req.state().games.write().await;
|
||||
let game = games.get_mut(&game_id).unwrap();
|
||||
|
||||
let card_name = game.players[player_number].hand[index].clone();
|
||||
game.trash_hand(player_number, index);
|
||||
|
||||
notify_players(
|
||||
&game,
|
||||
format!(
|
||||
"{} entsorgt {}",
|
||||
game.players[player_number].name, card_name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
broadcast_state(&game).await;
|
||||
}
|
||||
}
|
||||
|
445
src/sets/base.rs
Normal file
445
src/sets/base.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
use crate::card::{Card, CardType};
|
||||
use crate::{
|
||||
CardFilter, Effect, Event, ResolveReply, ResolveRequest, ResolveResult::*, ResolvingPlayer,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
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 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(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn copper() -> Card {
|
||||
Card::new("Copper", 0).with_type(CardType::Treasure(1))
|
||||
}
|
||||
|
||||
fn silver() -> Card {
|
||||
Card::new("Silver", 3).with_type(CardType::Treasure(2))
|
||||
}
|
||||
|
||||
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: "Bureaucrat".into(),
|
||||
request: ResolveRequest::ChooseHandCardsToDiscard {
|
||||
filter: CardFilter::Type(CardType::Victory(|_, _| 0)), //FIXME!
|
||||
},
|
||||
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(),
|
||||
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(|game, player| {
|
||||
game.players[player].draw_pile.len() as u32 / 10
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
});
|
||||
}))
|
||||
}
|
1
src/sets/mod.rs
Normal file
1
src/sets/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod base;
|
BIN
static/audio/chipsStack1.ogg
(Stored with Git LFS)
Normal file
BIN
static/audio/chipsStack1.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
279
static/game.html
279
static/game.html
@@ -81,6 +81,15 @@ img.card:hover {
|
||||
transition: transform 0.1s ease-in;
|
||||
}
|
||||
|
||||
.player-hand img.card.selected {
|
||||
transform: translate(0px, -40px) scale(1.1) !important;
|
||||
box-shadow: 0 0 10px lime;
|
||||
}
|
||||
|
||||
.supply-pile.selected {
|
||||
box-shadow: 0 0 10px lime;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
border: 1px solid black;
|
||||
width: 300px;
|
||||
@@ -411,13 +420,118 @@ img.card:hover {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
border: 1px solid dimgray;
|
||||
top: 50%;
|
||||
z-index: 200;
|
||||
background-color: rgba(1.0, 1.0, 1.0, 0.9);
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 20px black;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
color: white;
|
||||
visibility: hidden;
|
||||
grid-template-rows: 5fr auto;
|
||||
transition: top 0.1s ease-in;
|
||||
}
|
||||
|
||||
.dialog.supply {
|
||||
top: 80%;
|
||||
}
|
||||
|
||||
.dialog.hand {
|
||||
top: 20%;
|
||||
}
|
||||
|
||||
.dialog img {
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
margin: 5px;
|
||||
width: 180px;
|
||||
height: 288px;
|
||||
}
|
||||
|
||||
.dialog button {
|
||||
grid-row-start: 2;
|
||||
grid-column-start: 2;
|
||||
margin: 10px auto;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/mithril.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="dialog" class="dialog">
|
||||
<img class="card" style="margin: 5px; width:180px; height:288px;" src="/static/images/cards/cellar.jpg">
|
||||
<p style="margin: 20px;">Choose any number of cards to discard.</p>
|
||||
<button onclick="dialog_confirm()">Confirm</button>
|
||||
</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;
|
||||
var enable_hand_selection = false;
|
||||
var enable_supply_selection = false;
|
||||
|
||||
var toggle_hand_selection = function(index) {
|
||||
document.querySelectorAll(".player-hand img.card")[index].classList.toggle("selected");
|
||||
}
|
||||
|
||||
var toggle_supply_selection = function(index) {
|
||||
document.querySelectorAll(".supply-area .supply-pile").forEach((c) => {
|
||||
c.classList.remove("selected");
|
||||
});
|
||||
document.querySelectorAll(".supply-area .supply-pile")[index].classList.toggle("selected");
|
||||
}
|
||||
|
||||
function send_command(command, data) {
|
||||
var payload = data || {};
|
||||
payload.type = command;
|
||||
var msg = { type: "Command", "command": payload };
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
function dialog_confirm(ev) {
|
||||
if (resolve_request.request.type == "GainCard") {
|
||||
var selected = document.querySelector(".supply-pile.selected");
|
||||
if (selected) {
|
||||
selected = parseInt(selected.dataset.index);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".supply-pile.selected").forEach((c) => {
|
||||
c.classList.remove("selected");
|
||||
});
|
||||
|
||||
var reply = { type: "SupplyCardChosen", choice: selected };
|
||||
var msg = { type: "ResolveReply", reply: reply };
|
||||
send_command("ResolveReply", msg);
|
||||
enable_supply_selection = false;
|
||||
} else {
|
||||
var selected = [];
|
||||
|
||||
document.querySelectorAll(".player-hand img.card.selected").forEach((c) => {
|
||||
selected.push(parseInt(c.dataset.index));
|
||||
c.classList.remove("selected");
|
||||
});
|
||||
|
||||
var reply = { type: "HandCardsChosen", choice: selected };
|
||||
var msg = { type: "ResolveReply", reply: reply };
|
||||
send_command("ResolveReply", msg);
|
||||
enable_hand_selection = false;
|
||||
}
|
||||
document.querySelector("#dialog").style.visibility = "hidden";
|
||||
dialog.classList.remove("hand");
|
||||
dialog.classList.remove("supply");
|
||||
}
|
||||
|
||||
var mouseenter = function(ev) {
|
||||
var img = ev.target.src;
|
||||
document.querySelector("#preview img").src = img;
|
||||
@@ -430,20 +544,15 @@ img.card:hover {
|
||||
|
||||
function handle_dnd(data) {
|
||||
if (data.source == "Hand" && data.dest == "InPlay") {
|
||||
var msg = { type: "PlayCard", name: "", index: data.index};
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("PlayCard", { index: data.index });
|
||||
} else if (data.source == "Hand" && data.dest == "Discard") {
|
||||
var msg = { type: "Discard", index: data.index};
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("Discard", { index: data.index });
|
||||
} else if (data.source == "Supply" && data.dest == "Discard") {
|
||||
var msg = { type: "GainCard", name: data.name, index: parseInt(data.index) };
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("GainCard", { index: parseInt(data.index)});
|
||||
} else if (data.source == "DrawPile" && data.dest == "Hand") {
|
||||
var msg = { type: "DrawCard" };
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("DrawCard", null);
|
||||
} else if (data.source == "Hand" && data.dest == "Trash") {
|
||||
var msg = { type: "TrashHand", index: data.index };
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("TrashHand", { index: data.index });
|
||||
} else {
|
||||
console.log("handle_dnd: unhandled data", data);
|
||||
}
|
||||
@@ -481,7 +590,6 @@ img.card:hover {
|
||||
|
||||
function SupplyPile(initialVnode) {
|
||||
var dragStart = function(ev) {
|
||||
console.log(ev);
|
||||
let data = {
|
||||
source: "Supply",
|
||||
name: ev.target.dataset.name,
|
||||
@@ -490,7 +598,18 @@ img.card:hover {
|
||||
ev.dataTransfer.setData("text", JSON.stringify(data));
|
||||
}
|
||||
|
||||
return {
|
||||
var click = function(e) {
|
||||
e.preventDefault();
|
||||
if (enable_supply_selection) {
|
||||
toggle_supply_selection(parseInt(e.srcElement.parentElement.dataset.index));
|
||||
}
|
||||
}
|
||||
|
||||
var doubleclick = function(ev) {
|
||||
send_command("BuyCard", { index: parseInt(ev.srcElement.parentElement.dataset.index) });
|
||||
}
|
||||
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m(".supply-pile",
|
||||
{
|
||||
@@ -499,10 +618,12 @@ img.card:hover {
|
||||
"data-index": vnode.attrs.index,
|
||||
ondragstart: dragStart,
|
||||
draggable: true,
|
||||
onclick: click,
|
||||
ondblclick: doubleclick,
|
||||
},
|
||||
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,
|
||||
@@ -573,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",
|
||||
{
|
||||
@@ -642,6 +763,14 @@ img.card:hover {
|
||||
handle_dnd(data);
|
||||
}
|
||||
|
||||
var click = function(e) {
|
||||
e.preventDefault();
|
||||
console.log("hand card click", e.target.dataset.index);
|
||||
if (enable_hand_selection) {
|
||||
toggle_hand_selection(parseInt(e.target.dataset.index));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m(".player-hand",
|
||||
@@ -652,12 +781,13 @@ 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,
|
||||
onmouseenter: mouseenter,
|
||||
onmouseleave: mouseleave,
|
||||
onclick: click,
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -669,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)
|
||||
)
|
||||
}
|
||||
@@ -713,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,
|
||||
@@ -727,8 +857,7 @@ img.card:hover {
|
||||
|
||||
function PlayerArea(initialVnode) {
|
||||
var end_turn_click = function(e) {
|
||||
var msg = { type: "EndTurn" };
|
||||
webSocket.send(JSON.stringify(msg));
|
||||
send_command("EndTurn", null);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -799,13 +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: "StartGame" };
|
||||
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) {
|
||||
@@ -817,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,
|
||||
})
|
||||
@@ -839,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,
|
||||
})
|
||||
@@ -884,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;
|
||||
}
|
||||
|
||||
@@ -918,6 +1068,7 @@ img.card:hover {
|
||||
}
|
||||
|
||||
var handle_game_state = function(state) {
|
||||
var last_player = game_state.active_player;
|
||||
game_state = {
|
||||
...game_state,
|
||||
...state,
|
||||
@@ -933,16 +1084,79 @@ img.card:hover {
|
||||
}
|
||||
}
|
||||
|
||||
var handle_resolve_request = function(request) {
|
||||
var dlg = document.querySelector("#dialog");
|
||||
resolve_request = request;
|
||||
|
||||
document.querySelector("#dialog img").src = card_url(request.card);
|
||||
|
||||
if (request.request.type == "GainCard") {
|
||||
let cost = request.request?.filter?.MaxCost;
|
||||
if (cost) {
|
||||
document.querySelector("#dialog p").innerHTML = "Choose a card to gain with max cost of " + cost + ".";
|
||||
} else {
|
||||
document.querySelector("#dialog p").innerHTML = "Choose a card to gain.";
|
||||
}
|
||||
enable_supply_selection = true;
|
||||
dialog.classList.add("supply");
|
||||
} else if (request.request.type == "ChooseHandCardsToDiscard") {
|
||||
document.querySelector("#dialog p").innerHTML = "Choose any number of cards to discard.";
|
||||
enable_hand_selection = true;
|
||||
dialog.classList.add("hand");
|
||||
}
|
||||
|
||||
dlg.style.visibility = "visible";
|
||||
}
|
||||
|
||||
var handle_gameover = function(msg) {
|
||||
gameover_state = msg.score;
|
||||
var modal = document.querySelector("#modal");
|
||||
modal.style.display = "block";
|
||||
m.mount(modal, EndScreen);
|
||||
}
|
||||
|
||||
var handle_notification = function(msg) {
|
||||
if (msg.event.type == "CardPlayed") {
|
||||
append_chat(player_name(msg.event.player) + " plays " + msg.event.name);
|
||||
} else if (msg.event.type == "CardBought") {
|
||||
append_chat(player_name(msg.event.player) + " buys " + msg.event.name);
|
||||
} else if (msg.event.type == "CardGained") {
|
||||
let card_name = game_state.supply[msg.event.index].name;
|
||||
append_chat(player_name(msg.event.player) + " gains " + card_name);
|
||||
} else if (msg.event.type == "CardDiscarded") {
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var player_name = function(index) {
|
||||
return game_state.players[index].name;
|
||||
}
|
||||
|
||||
var append_chat = function(text) {
|
||||
let chatDiv = document.getElementById("chat");
|
||||
let last_element = document.querySelector("#chat li:last-child");
|
||||
if (last_element.innerText == text) {
|
||||
last_element.dataset.repeat = (parseInt(last_element.dataset.repeat || 1)) + 1;
|
||||
} else {
|
||||
let newmsg = document.createElement("li");
|
||||
newmsg.innerHTML = text;
|
||||
chatDiv.append(newmsg);
|
||||
newmsg.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
webSocket.onopen = function(event) {
|
||||
console.log("ws open");
|
||||
//webSocket.send("HALLO");
|
||||
};
|
||||
|
||||
webSocket.onmessage = function(event) {
|
||||
@@ -955,17 +1169,8 @@ img.card:hover {
|
||||
chatDiv.append(newmsg);
|
||||
newmsg.scrollIntoView();
|
||||
} else if (msg.type == "Notification") {
|
||||
let chatDiv = document.getElementById("chat");
|
||||
let last_element = document.querySelector("#chat li:last-child");
|
||||
if (last_element.innerText == msg.text) {
|
||||
last_element.dataset.repeat = (parseInt(last_element.dataset.repeat || 1)) + 1;
|
||||
} else {
|
||||
let newmsg = document.createElement("li");
|
||||
newmsg.innerHTML = msg.text;
|
||||
chatDiv.append(newmsg);
|
||||
newmsg.scrollIntoView();
|
||||
}
|
||||
} else if (msg.type == "PlayerJoined") {
|
||||
handle_notification(msg);
|
||||
} else if (msg.type == "PlayerJoined") {
|
||||
let chatDiv = document.getElementById("chat");
|
||||
let newmsg = document.createElement("li");
|
||||
newmsg.innerHTML = msg.player + " joined the game.";
|
||||
@@ -981,6 +1186,8 @@ img.card:hover {
|
||||
game_state.player.hand = msg.hand;
|
||||
} else if (msg.type == "PlayerId") {
|
||||
my_player_id = msg.id;
|
||||
} else if (msg.type == "ResolveRequest") {
|
||||
handle_resolve_request(msg);
|
||||
} else {
|
||||
console.log("event?");
|
||||
console.log(event.data);
|
||||
|
BIN
static/images/cards/duchy.jpg
(Stored with Git LFS)
Normal file
BIN
static/images/cards/duchy.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
21
static/join.html
Normal file
21
static/join.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DnD</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
<div>
|
||||
<form id="login_form" method="post">
|
||||
Your name: <input type="text" name="name"> <br />
|
||||
<input type="hidden" name="type" value="JoinGame">
|
||||
<input type="submit" value="Join game">
|
||||
</form>
|
||||
</div>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
Reference in New Issue
Block a user