Compare commits

...

4 Commits

Author SHA1 Message Date
675543b18e
Add connection indicator and player count
Some checks failed
CI Workflow / test (push) Has been cancelled
CI Workflow / gh-page-publish (push) Has been cancelled
closes #1
2024-08-21 22:53:00 +02:00
df0ff292e0
Fix: Increase timeout for inactive instances 2024-08-21 20:56:35 +02:00
4deb1e1502
Display state of other participants
closes #4
2024-08-21 20:55:36 +02:00
d1fb9d4ced
Drop inactive instances after timeout
in order to no longer show them in the answers list after they left the
game

closes #5
2024-08-21 20:04:49 +02:00
2 changed files with 88 additions and 9 deletions

View File

@ -1,8 +1,25 @@
<template>
<div class="container my-3">
<div class="row mb-4">
<div class="col text-center">
<div class="col text-center position-relative">
<h1>Stadt-Land-Fluss</h1>
<span class="position-absolute top-0 end-0">
<span
v-if="!gameSocketConnected"
class="badge text-danger"
title="Spiel ist vom Server getrennt"
>
<i class="fas fa-cloud fa-fw" />
</span>
<span
class="badge ms-1"
title="Anzahl aktiver Spieler"
>
<i class="fas fa-user fa-fw me-1" />
{{ numberActivePlayers }}
</span>
</span>
</div>
</div>
@ -19,7 +36,7 @@
@keypress.enter="broadcastName"
>
<button
class="btn btn-success"
class="btn btn-secondary"
@click="broadcastName"
>
<i class="fas fa-pencil fa-fw me-1" />
@ -93,6 +110,20 @@
<i class="fas fa-play fa-fw" />
</button>
</div>
<div class="text-center text-muted small mt-1 text-nowrap">
<span class="badge bg-success me-2">
<i class="fas fa-check fa-fw me-1" />
{{ foreignInstanceAnswerStatus[generateKey(cat, letter)].answered }}
</span>
<span class="badge bg-warning text-dark me-2">
<i class="fas fa-forward fa-fw me-1" />
{{ foreignInstanceAnswerStatus[generateKey(cat, letter)].skipped }}
</span>
<span class="badge bg-secondary">
<i class="fas fa-question fa-fw me-1" />
{{ foreignInstanceAnswerStatus[generateKey(cat, letter)].thinking }}
</span>
</div>
</template>
<template v-else>
<div
@ -133,7 +164,7 @@
</template>
<script lang="ts">
import { categories, gameSocketTemplate } from './config'
import { categories, gameSocketTemplate, instanceTimeout } from './config'
import { defineComponent } from 'vue'
const baseBackoff = 100 // ms
@ -141,7 +172,12 @@ const maxBackoff = 10000 // ms
export default defineComponent({
computed: {
answersGiven(): Object {
activeInstances(): any {
return Object.fromEntries(Object.entries(this.knownInstances)
.filter((e: any[]) => this.now.getTime() - e[1].lastActive < instanceTimeout))
},
answerKeys(): string[] {
const keys: string[] = []
for (const cat of this.gameState.categories) {
for (const letter of this.gameState.letters) {
@ -149,13 +185,42 @@ export default defineComponent({
}
}
return Object.fromEntries(keys.map(key => [
key, Object.keys(this.knownInstances).map(instId => ({
answer: this.knownInstances[instId].answers[key],
name: this.knownInstances[instId].name,
return keys
},
answersGiven(): Object {
return Object.fromEntries(this.answerKeys.map(key => [
key, Object.keys(this.activeInstances).map(instId => ({
answer: this.activeInstances[instId].answers[key],
name: this.activeInstances[instId].name,
})),
]))
},
foreignInstanceAnswerStatus(): any {
const foreignInstances = Object.entries(this.activeInstances)
.filter(e => e[0] !== this.instance)
.map(e => e[1])
return Object.fromEntries(this.answerKeys
.map((key: string) => [
key, {
answered: foreignInstances
.filter((instance: any) => instance.answers[key])
.length,
skipped: foreignInstances
.filter((instance: any) => instance.answers[key] === '')
.length,
thinking: foreignInstances
.filter((instance: any) => instance.answers[key] === undefined)
.length,
},
]))
},
numberActivePlayers(): number {
return Object.keys(this.activeInstances).length
},
},
created(): void {
@ -169,6 +234,7 @@ export default defineComponent({
this.knownInstances[this.instance] = {
answers: {},
lastActive: new Date().getTime(),
name: this.name,
}
},
@ -178,6 +244,7 @@ export default defineComponent({
backoffCurrent: baseBackoff,
gameId: '',
gameSocket: null as WebSocket | null,
gameSocketConnected: false,
gameState: {
categories: [],
gameId: '',
@ -189,6 +256,7 @@ export default defineComponent({
knownInstances: {} as any,
localAnswers: {} as any,
name: '',
now: new Date(),
}
},
@ -225,11 +293,13 @@ export default defineComponent({
this.gameSocket = new WebSocket(gameSocketTemplate.replace('{gameId}', this.gameId))
this.gameSocket.addEventListener('close', () => {
this.gameSocketConnected = false
this.backoffCurrent = Math.min(maxBackoff, this.backoffCurrent * 1.5)
window.setTimeout(() => this.connectToGame(), this.backoffCurrent)
})
this.gameSocket.addEventListener('open', () => {
this.gameSocketConnected = true
this.sendMessage({ type: 'ohai' })
})
@ -312,7 +382,10 @@ export default defineComponent({
},
sendPing(): void {
this.sendMessage({ instanceState: this.knownInstances[this.instance], type: 'ping' })
this.sendMessage({ instanceState: {
...this.knownInstances[this.instance],
lastActive: new Date().getTime(),
}, type: 'ping' })
},
shuffle(list: Array<any>): Array<any> {
@ -364,6 +437,9 @@ export default defineComponent({
this.connectToGame()
window.setInterval(() => this.sendPing(), 10000)
window.setInterval(() => {
this.now = new Date()
}, 1000)
},
name: 'StadtLandFlussApp',

View File

@ -34,7 +34,10 @@ const categories: string[] = [
const gameSocketTemplate: string = 'wss://tools.hub.luzifer.io/ws/slf-{gameId}'
const instanceTimeout: number = 120000 // ms
export {
categories,
gameSocketTemplate,
instanceTimeout,
}