Add prototype for player resolveable events using Cellar

This commit is contained in:
Markus Wagner 2021-01-14 23:29:50 +01:00
parent b30cc02306
commit d6e2543b9c
3 changed files with 179 additions and 3 deletions

View File

@ -25,6 +25,7 @@ enum ClientMessage {
DrawCard, DrawCard,
Discard { index: usize }, Discard { index: usize },
TrashHand { index: usize }, TrashHand { index: usize },
HandCardsChosen { choice: Vec<usize> },
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -60,6 +61,9 @@ enum ServerMessage {
GameOver { GameOver {
score: Vec<(usize, u32)>, score: Vec<(usize, u32)>,
}, },
ResolveRequest {
request: ResolveRequest,
},
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@ -165,11 +169,29 @@ impl Default for GameSetup {
} }
} }
/// Which player's input is requested to resolve the current effect
#[derive(Clone, Serialize)]
enum ResolvingPlayer {
ActivePlayer,
}
/// Sent by the server to tell the client what information is requested
/// from the player to resolve the current effect
#[derive(Clone, Serialize)]
enum ResolveRequest {
ChooseHandCardsToDiscard { player: ResolvingPlayer },
}
#[derive(Clone)] #[derive(Clone)]
enum Effect { 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), OnCardPlayed(fn(&mut Game, &Card) -> bool),
Resolving(ResolveRequest, ResolvingEffect),
} }
type ResolvingEffect = fn(&mut Game, &ClientMessage);
pub struct Game { pub struct Game {
effects: Vec<Effect>, effects: Vec<Effect>,
players: Vec<Player>, players: Vec<Player>,
@ -180,6 +202,10 @@ pub struct Game {
supply: Vec<(Card, usize)>, supply: Vec<(Card, usize)>,
trash: Vec<Card>, trash: Vec<Card>,
turn_state: TurnState, turn_state: TurnState,
/// Any effect from a card that requires further input from players
/// and blocks the game until fully resolved.
resolving_effect: Option<(ResolveRequest, ResolvingEffect)>,
} }
impl Game { impl Game {
@ -194,6 +220,7 @@ impl Game {
supply: vec![], supply: vec![],
trash: vec![], trash: vec![],
turn_state: TurnState::default(), turn_state: TurnState::default(),
resolving_effect: None,
} }
} }
@ -226,6 +253,23 @@ impl Game {
cost: 2, cost: 2,
types: vec![CardType::Action(|game| { types: vec![CardType::Action(|game| {
action!(game, 1); action!(game, 1);
game.add_effect(Effect::Resolving(
ResolveRequest::ChooseHandCardsToDiscard {
player: ResolvingPlayer::ActivePlayer,
},
|game, message| {
if let ClientMessage::HandCardsChosen { choice } = message {
let mut discarded = 0;
for c in choice.iter().sorted_by(|a, b| b.cmp(a)) {
game.players[game.active_player].discard(*c);
discarded += 1;
}
game.players[game.active_player].draw(discarded);
game.resolving_effect = None;
}
},
));
})], })],
}, },
10, 10,
@ -429,6 +473,7 @@ impl Game {
} }
let mut effects = self.effects.clone(); let mut effects = self.effects.clone();
self.effects.clear();
effects.retain(|effect| { effects.retain(|effect| {
match effect { match effect {
Effect::OnCardPlayed(effect) => { Effect::OnCardPlayed(effect) => {
@ -436,11 +481,13 @@ impl Game {
return false; return false;
} }
} }
Effect::Resolving(_, _) => {}
} }
true true
}); });
self.effects = effects; self.effects.append(&mut effects);
self.players[player_number].played_cards.push(card); self.players[player_number].played_cards.push(card);
true true
@ -473,7 +520,15 @@ impl Game {
} }
fn add_effect(&mut self, effect: Effect) { fn add_effect(&mut self, effect: Effect) {
self.effects.push(effect); match effect {
Effect::OnCardPlayed(_) => {
self.effects.push(effect);
}
Effect::Resolving(request, effect) => {
self.resolving_effect = Some((request, effect));
}
}
} }
} }
@ -555,6 +610,20 @@ async fn broadcast_state(game: &Game) {
}; };
broadcast(&game, &sm).await; broadcast(&game, &sm).await;
} }
if let Some((request, _)) = &game.resolving_effect {
match request {
ResolveRequest::ChooseHandCardsToDiscard { ref player } => match player {
ResolvingPlayer::ActivePlayer => {
let p = game.players.get(game.active_player).unwrap();
let sm = ServerMessage::ResolveRequest {
request: request.clone(),
};
send_msg(&game, &p, &sm).await;
}
},
}
}
} }
async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) { async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) {
@ -847,6 +916,22 @@ async fn main() -> Result<(), std::io::Error> {
.await; .await;
broadcast_state(&game).await; broadcast_state(&game).await;
} }
ClientMessage::HandCardsChosen { .. } => {
let mut games = req.state().games.write().await;
let game = games.get_mut(&game_id).unwrap();
if player_number == game.active_player {
match game.resolving_effect {
Some((_, ref effect)) => {
effect(game, &msg);
}
None => {}
}
broadcast_state(&game).await;
}
}
} }
} }

BIN
static/audio/chipsStack1.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -81,6 +81,11 @@ img.card:hover {
transition: transform 0.1s ease-in; 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;
}
.chat-window { .chat-window {
border: 1px solid black; border: 1px solid black;
width: 300px; width: 300px;
@ -411,13 +416,71 @@ img.card:hover {
width: 120px; 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;
}
.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> </style>
<script src="/static/mithril.js"></script> <script src="/static/mithril.js"></script>
</head> </head>
<body> <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> <div id="game"></div>
<script> <script>
var turnStartSound = new Audio("/static/audio/chipsStack1.ogg");
var resolve_request;
var enable_hand_selection = false;
var toggle_hand_selection = function(index) {
document.querySelectorAll(".player-hand img.card")[index].classList.toggle("selected");
}
function dialog_confirm(ev) {
var selected = [];
document.querySelectorAll(".player-hand img.card.selected").forEach((c) => {
selected.push(parseInt(c.dataset.index));
c.classList.remove("selected");
});
var msg = { type: "HandCardsChosen", choice: selected };
webSocket.send(JSON.stringify(msg));
enable_hand_selection = false;
document.querySelector("#dialog").style.visibility = "hidden";
}
var mouseenter = function(ev) { var mouseenter = function(ev) {
var img = ev.target.src; var img = ev.target.src;
document.querySelector("#preview img").src = img; document.querySelector("#preview img").src = img;
@ -651,6 +714,15 @@ img.card:hover {
handle_dnd(data); 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 { return {
view: function(vnode) { view: function(vnode) {
return m(".player-hand", return m(".player-hand",
@ -667,6 +739,7 @@ img.card:hover {
"data-index": i, "data-index": i,
onmouseenter: mouseenter, onmouseenter: mouseenter,
onmouseleave: mouseleave, onmouseleave: mouseleave,
onclick: click,
}) })
}) })
) )
@ -927,6 +1000,7 @@ img.card:hover {
} }
var handle_game_state = function(state) { var handle_game_state = function(state) {
var last_player = game_state.active_player;
game_state = { game_state = {
...game_state, ...game_state,
...state, ...state,
@ -940,6 +1014,19 @@ img.card:hover {
setup_state.active = false; setup_state.active = false;
game_state.active = true; game_state.active = true;
} }
if (last_player != game_state.active_player) {
if (game_state.active_player == my_player_id) {
turnStartSound.play();
}
}
}
var handle_resolve_request = function(request) {
var dlg = document.querySelector("#dialog");
dlg.style.visibility = "visible";
enable_hand_selection = true;
} }
var handle_gameover = function(msg) { var handle_gameover = function(msg) {
@ -951,7 +1038,6 @@ img.card:hover {
webSocket.onopen = function(event) { webSocket.onopen = function(event) {
console.log("ws open"); console.log("ws open");
//webSocket.send("HALLO");
}; };
webSocket.onmessage = function(event) { webSocket.onmessage = function(event) {
@ -990,6 +1076,8 @@ img.card:hover {
game_state.player.hand = msg.hand; game_state.player.hand = msg.hand;
} else if (msg.type == "PlayerId") { } else if (msg.type == "PlayerId") {
my_player_id = msg.id; my_player_id = msg.id;
} else if (msg.type == "ResolveRequest") {
handle_resolve_request(msg.request);
} else { } else {
console.log("event?"); console.log("event?");
console.log(event.data); console.log(event.data);