Add game end condition check and simple scoring end screen

This commit is contained in:
Markus Wagner 2021-01-05 20:40:42 +01:00
parent a67aa2c0d9
commit 7b172b5f03
4 changed files with 186 additions and 11 deletions

16
Cargo.lock generated
View File

@ -580,6 +580,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-std", "async-std",
"html-escape", "html-escape",
"itertools",
"rand 0.8.0", "rand 0.8.0",
"serde", "serde",
"serde_json", "serde_json",
@ -588,6 +589,12 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.1" version = "2.5.1"
@ -902,6 +909,15 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "itertools"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.6" version = "0.4.6"

View File

@ -9,6 +9,7 @@ edition = "2018"
[dependencies] [dependencies]
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
html-escape = "0.2.6" html-escape = "0.2.6"
itertools = "0.10.0"
rand = "0.8.0" rand = "0.8.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.60" serde_json = "1.0.60"

View File

@ -1,4 +1,5 @@
use async_std::{prelude::*, sync::RwLock}; use async_std::{prelude::*, sync::RwLock};
use itertools::Itertools;
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
@ -53,6 +54,9 @@ enum ServerMessage {
Notification { Notification {
text: String, text: String,
}, },
GameOver {
score: Vec<(usize, usize)>,
},
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@ -130,6 +134,7 @@ impl Player {
enum GameState { enum GameState {
Setup, Setup,
InProgress, InProgress,
Over,
} }
impl Default for GameSetup { impl Default for GameSetup {
@ -219,10 +224,29 @@ impl Game {
} }
} }
self.end_game();
self.active_player += 1; self.active_player += 1;
self.active_player %= self.players.len(); self.active_player %= self.players.len();
} }
// Check if the end game condition is reached and finish the game if so,
// sending a message to all clients.
fn end_game(&mut self) {
let provinces = self.supply.iter().any(|(c, n)| *c == "Province" && *n == 0);
let supply = self.supply.iter().filter(|(_, n)| *n == 0).count() > 2;
if supply || provinces {
self.state = GameState::Over;
}
for p in self.players.iter_mut() {
p.draw_pile.append(&mut p.played_cards);
p.draw_pile.append(&mut p.hand);
p.draw_pile.append(&mut p.discard_pile);
}
}
fn is_active_player(&self, player_id: &str) -> bool { fn is_active_player(&self, player_id: &str) -> bool {
match self.players.get(self.active_player) { match self.players.get(self.active_player) {
None => false, None => false,
@ -294,6 +318,27 @@ async fn broadcast_state(game: &Game) {
send_msg(&game, &p, &sm).await; send_msg(&game, &p, &sm).await;
} }
if game.state == GameState::Over {
let sm = ServerMessage::GameOver {
score: game
.players
.iter()
.enumerate()
.map(|(i, p)| {
let score = p.draw_pile.iter().fold(0, |acc, card| match card.as_str() {
"Province" => acc + 6,
"Duchery" => acc + 3,
"Estate" => acc + 1,
_ => acc,
});
(i, score)
})
.sorted_by(|a, b| Ord::cmp(&b.1, &a.1))
.collect::<Vec<_>>(),
};
broadcast(&game, &sm).await;
}
} }
async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) { async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) {

View File

@ -8,8 +8,9 @@
<style type="text/css"> <style type="text/css">
#game { #game {
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto 300px;
user-select: none; user-select: none;
position: relative;
} }
.pile-counter { .pile-counter {
@ -53,7 +54,7 @@
.player-hand { .player-hand {
border: 1px solid red; border: 1px solid red;
height: 160px; height: 165px;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
overflow-x: scroll; overflow-x: scroll;
@ -68,6 +69,8 @@
border: 1px solid black; border: 1px solid black;
width: 300px; width: 300px;
grid-column-start: 2; grid-column-start: 2;
grid-row-start: 1;
box-sizing: border-box;
} }
#chat li[data-repeat]::after { #chat li[data-repeat]::after {
@ -109,7 +112,7 @@
.supply-pile::after { .supply-pile::after {
position: absolute; position: absolute;
bottom: 5px; bottom: 20px;
right: 5px; right: 5px;
color: wheat; color: wheat;
background-color: darkred; background-color: darkred;
@ -211,6 +214,10 @@
padding-top: 0; padding-top: 0;
} }
.inplay-area img.card {
margin-left: -55px;
}
.turn-status { .turn-status {
line-height: 24px; line-height: 24px;
height: 24px; height: 24px;
@ -309,6 +316,57 @@
z-index: -1; z-index: -1;
} }
#preview {
grid-column-start: 2;
grid-row-start: 1;
height: 290px;
margin-top: 500px;
}
#preview img {
height: 290px;
width: 180px;
}
#modal {
display: none;
grid-row-start: 1;
grid-column-end: 2;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(1.0, 1.0, 1.0, 0.6);
}
.modal-content {
width: 600px;
height: 300px;
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: lightgray;
box-shadow: 0 0 10px;
border: 1px solid black;
}
.modal-content h1 {
text-align: center;
margin-top: 1px;
}
.rank {
display: flex;
border-bottom: 1px solid dimgray;
padding: 2px;
}
.rank span:first-child {
width: 120px;
}
</style> </style>
<script src="/static/mithril.js"></script> <script src="/static/mithril.js"></script>
</head> </head>
@ -316,6 +374,16 @@
<body> <body>
<div id="game"></div> <div id="game"></div>
<script> <script>
var mouseenter = function(ev) {
var img = ev.target.src;
document.querySelector("#preview img").src = img;
document.querySelector("#preview img").style.visibility = "visible";
}
var mouseleave = function(ev) {
document.querySelector("#preview img").style.visibility = "hidden";
}
function handle_dnd(data) { function handle_dnd(data) {
if (data.source == "Hand" && data.dest == "InPlay") { if (data.source == "Hand" && data.dest == "InPlay") {
var msg = { type: "PlayCard", name: "", index: data.index}; var msg = { type: "PlayCard", name: "", index: data.index};
@ -348,7 +416,7 @@
return { return {
view: function(vnode) { view: function(vnode) {
return m(".chat-window", return [m(".chat-window",
m("h4", "Players"), m("h4", "Players"),
m("ul", vnode.attrs.players.map(function(p) { m("ul", vnode.attrs.players.map(function(p) {
return m("li", p.name); return m("li", p.name);
@ -359,8 +427,10 @@
id: "chat_input", id: "chat_input",
placeholder: "Type to chat...", placeholder: "Type to chat...",
onkeyup: keyup onkeyup: keyup
}) }),
) ),
m("#preview", m("img")),
]
} }
} }
} }
@ -390,6 +460,8 @@
class: "card", class: "card",
src: "/static/images/cards/" + vnode.attrs.name.toLowerCase() + ".jpg", src: "/static/images/cards/" + vnode.attrs.name.toLowerCase() + ".jpg",
draggable: false, draggable: false,
onmouseenter: mouseenter,
onmouseleave: mouseleave,
}) })
) )
} }
@ -693,19 +765,34 @@
m("h4", "Basic cards"), m("h4", "Basic cards"),
m(".basic-cards", m(".basic-cards",
vnode.attrs.basic_cards.map(function(card) { vnode.attrs.basic_cards.map(function(card) {
return m("img", {class: "card", src: "/static/images/cards/" + card.toLowerCase() + ".jpg"}) return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
}) })
), ),
m("h4", "Kingdom Cards"), m("h4", "Kingdom Cards"),
m(".kingdom-cards", m(".kingdom-cards",
vnode.attrs.kingdom_cards.map(function(card) { vnode.attrs.kingdom_cards.map(function(card) {
return m("img", {class: "card", src: "/static/images/cards/" + card.toLowerCase() + ".jpg"}) return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
}) })
), ),
m("h4", "Starting Deck"), m("h4", "Starting Deck"),
m(".start-deck", m(".start-deck",
vnode.attrs.starting_deck.map(function(card) { vnode.attrs.starting_deck.map(function(card) {
return m("img", {class: "card", src: "/static/images/cards/" + card.toLowerCase() + ".jpg"}) return m("img", {
class: "card",
src: "/static/images/cards/" + card.toLowerCase() + ".jpg",
onmouseenter: mouseenter,
onmouseleave: mouseleave,
})
}) })
), ),
m("button", {onclick: start_click}, "Start Game") m("button", {onclick: start_click}, "Start Game")
@ -714,10 +801,27 @@
} }
} }
function EndScreen(initialVnode) {
return {
view: function(vnode) {
return m(".modal-content",
m("h1", "Score"),
gameover_state.map(function(rank) {
return m(".rank",
m("span", game_state.players[rank[0]].name),
m("span", rank[1])
)
})
)
}
}
}
var game_state; var game_state;
var setup_state; var setup_state;
var my_player_id = 0; var my_player_id = 0;
var webSocket; var webSocket;
var gameover_state;
function App(initialVnode) { function App(initialVnode) {
if (document.location.protocol == "https:") { if (document.location.protocol == "https:") {
@ -779,6 +883,12 @@
} }
} }
var handle_gameover = function(msg) {
gameover_state = msg.score;
var modal = document.querySelector("#modal");
modal.style.display = "block";
m.mount(modal, EndScreen);
}
webSocket.onopen = function(event) { webSocket.onopen = function(event) {
console.log("ws open"); console.log("ws open");
@ -815,6 +925,8 @@
handle_game_state(msg); handle_game_state(msg);
} else if (msg.type == "GameSetup") { } else if (msg.type == "GameSetup") {
handle_setup(msg.setup); handle_setup(msg.setup);
} else if (msg.type == "GameOver") {
handle_gameover(msg);
} else if (msg.type == "PlayerHand") { } else if (msg.type == "PlayerHand") {
game_state.player.hand = msg.hand; game_state.player.hand = msg.hand;
} else if (msg.type == "PlayerId") { } else if (msg.type == "PlayerId") {
@ -832,6 +944,7 @@
return [ return [
m(SetupScreen, setup_state), m(SetupScreen, setup_state),
m(GameScreen, game_state), m(GameScreen, game_state),
m("#modal"),
m(Chat, chat_state) m(Chat, chat_state)
] ]
} }