fixed voting race condition

added session ping
cleaned up vote text
This commit is contained in:
Steffen 2020-09-16 11:39:36 +02:00
parent dda8192e2e
commit 1409bea1f3
4 changed files with 64 additions and 15 deletions

View File

@ -7,7 +7,7 @@
v-if="session.sessionId" v-if="session.sessionId"
@click="leaveSession" @click="leaveSession"
:title=" :title="
`You're currently in a live game with ${session.playerCount} other players!` `${session.playerCount} other players in this session (${session.ping}ms latency)`
" "
> >
<font-awesome-icon icon="broadcast-tower" /> <font-awesome-icon icon="broadcast-tower" />
@ -91,6 +91,10 @@
<li @click="joinSession" v-if="!session.sessionId"> <li @click="joinSession" v-if="!session.sessionId">
Join (Player)<em>[J]</em> Join (Player)<em>[J]</em>
</li> </li>
<li v-if="session.sessionId && session.ping">
Latency to {{ session.isSpectator ? "storyteller" : "players" }}
<em>{{ session.ping }}ms</em>
</li>
<li v-if="session.sessionId" @click="copySessionUrl"> <li v-if="session.sessionId" @click="copySessionUrl">
Copy player link Copy player link
<em><font-awesome-icon icon="copy"/></em> <em><font-awesome-icon icon="copy"/></em>

View File

@ -10,8 +10,9 @@
>! >!
<br /> <br />
<template v-if="nominee.role.team !== 'traveler'"> <template v-if="nominee.role.team !== 'traveler'">
<em class="blue">{{ Math.ceil(alive / 2) }} votes</em> required for a <em class="blue">{{ voters.length }} vote{{ voters.length != 1 ? 's':'' }}</em>
<em>majority</em>. in favor
<em>(majority is {{ Math.ceil(alive / 2) }})</em>
</template> </template>
<template v-else> <template v-else>
<em>{{ Math.ceil(players.length / 2) }} votes</em> required for a <em>{{ Math.ceil(players.length / 2) }} votes</em> required for a

View File

@ -3,6 +3,14 @@ const set = key => (state, val) => {
state[key] = val; state[key] = val;
}; };
/**
* Handle a vote request.
* If the vote is from a seat that is already locked, ignore it.
* @param state session state
* @param index seat of the player in the circle
* @param vote true or false
* @param indexAdjusted seat of the player counted from the nominated player
*/
const handleVote = (state, [index, vote]) => { const handleVote = (state, [index, vote]) => {
if (!state.nomination) return; if (!state.nomination) return;
state.votes = [...state.votes]; state.votes = [...state.votes];
@ -13,6 +21,7 @@ const state = () => ({
sessionId: "", sessionId: "",
isSpectator: false, isSpectator: false,
playerCount: 0, playerCount: 0,
ping: 0,
playerId: "", playerId: "",
claimedSeat: -1, claimedSeat: -1,
nomination: false, nomination: false,
@ -30,6 +39,7 @@ const mutations = {
setPlayerId: set("playerId"), setPlayerId: set("playerId"),
setSpectator: set("isSpectator"), setSpectator: set("isSpectator"),
setPlayerCount: set("playerCount"), setPlayerCount: set("playerCount"),
setPing: set("ping"),
setVotingSpeed: set("votingSpeed"), setVotingSpeed: set("votingSpeed"),
claimSeat: set("claimedSeat"), claimSeat: set("claimedSeat"),
nomination(state, { nomination, votes, votingSpeed, lockedVote } = {}) { nomination(state, { nomination, votes, votingSpeed, lockedVote } = {}) {

View File

@ -11,6 +11,7 @@ class LiveSession {
this._pingInterval = 30 * 1000; // 30 seconds between pings this._pingInterval = 30 * 1000; // 30 seconds between pings
this._pingTimer = null; this._pingTimer = null;
this._players = {}; // map of players connected to a session this._players = {}; // map of players connected to a session
this._pings = {}; // map of player IDs to ping
// reconnect to previous session // reconnect to previous session
if (this._store.state.session.sessionId) { if (this._store.state.session.sessionId) {
this.connect(this._store.state.session.sessionId); this.connect(this._store.state.session.sessionId);
@ -65,7 +66,11 @@ class LiveSession {
* @private * @private
*/ */
_ping() { _ping() {
this._send("ping", [this._isSpectator, this._store.state.session.playerId]); this._send("ping", [
this._isSpectator,
this._store.state.session.playerId,
new Date().getTime()
]);
this._handlePing(); this._handlePing();
clearTimeout(this._pingTimer); clearTimeout(this._pingTimer);
this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval); this._pingTimer = setTimeout(this._ping.bind(this), this._pingInterval);
@ -149,7 +154,9 @@ class LiveSession {
.substr(2) .substr(2)
); );
} }
this._pings = {};
this._store.commit("session/setPlayerCount", 0); this._store.commit("session/setPlayerCount", 0);
this._store.commit("session/setPing", 0);
this._isSpectator = this._store.state.session.isSpectator; this._isSpectator = this._store.state.session.isSpectator;
this._open(channel); this._open(channel);
} }
@ -158,7 +165,9 @@ class LiveSession {
* Close the current session, if any. * Close the current session, if any.
*/ */
disconnect() { disconnect() {
this._pings = {};
this._store.commit("session/setPlayerCount", 0); this._store.commit("session/setPlayerCount", 0);
this._store.commit("session/setPing", 0);
if (this._socket) { if (this._socket) {
this._send("bye", this._store.state.session.playerId); this._send("bye", this._store.state.session.playerId);
this._socket.close(); this._socket.close();
@ -366,20 +375,16 @@ class LiveSession {
* Handle a ping message by another player / storyteller * Handle a ping message by another player / storyteller
* @param isSpectator * @param isSpectator
* @param playerId * @param playerId
* @param timestamp
* @private * @private
*/ */
_handlePing([isSpectator, playerId] = []) { _handlePing([isSpectator, playerId, timestamp] = []) {
const now = new Date().getTime(); const now = new Date().getTime();
if (playerId) {
this._players[playerId] = now;
if (!this._isSpectator && !isSpectator) {
alert("Another storyteller joined the session!");
}
}
// remove players that haven't sent a ping in twice the timespan // remove players that haven't sent a ping in twice the timespan
for (let player in this._players) { for (let player in this._players) {
if (now - this._players[player] > this._pingInterval * 2) { if (now - this._players[player] > this._pingInterval * 2) {
delete this._players[player]; delete this._players[player];
delete this._pings[player];
} }
} }
// remove claimed seats from players that are no longer connected // remove claimed seats from players that are no longer connected
@ -392,6 +397,24 @@ class LiveSession {
}); });
} }
}); });
// store new player data
if (playerId) {
this._players[playerId] = now;
if (!this._isSpectator && !isSpectator) {
alert("Another storyteller joined the session!");
} else if (this._isSpectator && !isSpectator) {
// ping to ST
this._store.commit("session/setPing", now - timestamp);
} else {
// ping to Players
this._pings[playerId] = now - timestamp;
const pings = Object.values(this._pings);
this._store.commit(
"session/setPing",
Math.round(pings.reduce((a, b) => a + b, 0) / pings.length)
);
}
}
this._store.commit( this._store.commit(
"session/setPlayerCount", "session/setPlayerCount",
Object.keys(this._players).length Object.keys(this._players).length
@ -491,18 +514,29 @@ class LiveSession {
!this._isSpectator !this._isSpectator
) { ) {
// send vote only if it is your own vote or you are the storyteller // send vote only if it is your own vote or you are the storyteller
this._send("vote", [index, this._store.state.session.votes[index]]); this._send("vote", [
index,
this._store.state.session.votes[index],
!this._isSpectator
]);
} }
} }
/** /**
* Handle an incoming vote, but not if it is for your own seat. * Handle an incoming vote, but only if it is from ST or unlocked.
* @param index * @param index
* @param vote * @param vote
* @param fromST
*/ */
_handleVote([index, vote]) { _handleVote([index, vote, fromST]) {
const { session, players } = this._store.state;
const playerCount = players.players.length;
const indexAdjusted =
(index - 1 + playerCount - session.nomination[1]) % playerCount;
if (fromST || indexAdjusted >= session.lockedVote - 1) {
this._store.commit("session/vote", [index, vote]); this._store.commit("session/vote", [index, vote]);
} }
}
/** /**
* Lock a vote. ST only * Lock a vote. ST only