This commit is contained in:
2025-09-06 12:25:18 +02:00
parent 985f6d9bf9
commit 79c5f5625c
8 changed files with 212 additions and 25 deletions

View File

@@ -4,5 +4,7 @@ export enum MessageType {
GOTO = "GOTO", GOTO = "GOTO",
SHOW_ANSWER = "SHOW_ANSWER", SHOW_ANSWER = "SHOW_ANSWER",
HIDE_ANSWER = "HIDE_ANSWER", HIDE_ANSWER = "HIDE_ANSWER",
SHOW_QUESTION = "SHOW_QUESTION",
HIDE_QUESTION = "HIDE_QUESTION",
VISITED_QUESTIONS = "VISITED_QUESTIONS" VISITED_QUESTIONS = "VISITED_QUESTIONS"
} }

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import type { MultipleChoiceQuestion } from "./games/games";
interface Props {
question: MultipleChoiceQuestion;
showAnswer: boolean;
showQuestion: boolean;
[key: string]: unknown;
}
function shuffle<T>(array: T[]) {
let currentIndex = array.length;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
}
let { question, showAnswer, showQuestion }: Props = $props();
const answer = question.data.choices[0];
let _choices = [...question.data.choices];
shuffle(_choices);
</script>
<div class="mb-4 flex grow flex-col items-center text-6xl">
{#if showQuestion}
<div class="flex grow-1 items-center">
<div>{question.data.question}</div>
</div>
<div class="flex w-full grow-1 flex-wrap items-center justify-around gap-2">
{#each _choices as choice}
<div class="choiceCard {showAnswer && choice === answer ? 'answer' : ''}">
{choice}
</div>
{/each}
</div>
{/if}
</div>
<style>
.choiceCard {
border: 1px solid black;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
height: fit-content;
padding: 32px;
/* font-size: larger;
font-weight: bold;
cursor: pointer; */
}
.answer {
background-color: rgb(87, 255, 87);
box-shadow: 0 0 20px 5px rgb(87, 255, 87);
border: 1px solid rgb(50, 141, 50);
}
</style>

View File

@@ -3,18 +3,24 @@
label: string; label: string;
plus: (label: string) => void; plus: (label: string) => void;
minus: (label: string) => void; minus: (label: string) => void;
showPlus?: boolean;
showMinus?: boolean;
[key: string]: unknown; [key: string]: unknown;
} }
let { label, plus, minus }: Props = $props(); let { label, plus, minus, showPlus = true, showMinus = true }: Props = $props();
</script> </script>
<div class="specialBtn flex w-min gap-2 text-2xl"> <div class="specialBtn flex w-min gap-2 text-2xl">
<button class="innerBtn" onclick={() => minus(label)}>-</button> {#if showMinus}
<button class="innerBtn" onclick={() => minus(label)}>-</button>
{/if}
<div class="whitespace-nowrap"> <div class="whitespace-nowrap">
{label} {label}
</div> </div>
<button class="innerBtn" onclick={() => plus(label)}>+</button> {#if showPlus}
<button class="innerBtn" onclick={() => plus(label)}>+</button>
{/if}
</div> </div>
<style> <style>

View File

@@ -4,10 +4,12 @@
interface Props { interface Props {
players: Player[]; players: Player[];
currentPlayer: string; currentPlayer: string;
editable?: boolean;
onReload?: () => void;
[key: string]: unknown; [key: string]: unknown;
} }
let { players, currentPlayer }: Props = $props(); let { players, currentPlayer, editable = false, onReload = () => {} }: Props = $props();
let _players = $derived(() => { let _players = $derived(() => {
let p = [...players]; let p = [...players];
@@ -16,18 +18,33 @@
</script> </script>
<div <div
class="h-full w-fit max-w-[400px] overflow-hidden border-r-1 border-solid border-gray-300 pr-4 pl-4" class="h-full w-fit {editable === true
? 'max-w-[600px]'
: 'max-w-[400px]'} overflow-hidden border-r-1 border-solid border-gray-300 pr-4 pl-4"
> >
<div class="flex justify-center"> <div class="flex items-center justify-around">
<h3 class="text-5xl">Scoreboard</h3> <h3 class="text-5xl">Scoreboard</h3>
{#if editable === true}
<button class="btn w-16" onclick={() => onReload()}>&#x21bb;</button>
{/if}
</div> </div>
<div class="mt-4 text-3xl"> <div class="mt-4 text-3xl">
<table class="w-1/1"> <table class="w-1/1">
<tbody> <tbody>
{#each _players() as player} {#each _players() as player (player.name)}
<tr class="{currentPlayer === player.name ? 'current' : ''} h-12"> <tr class="{currentPlayer === player.name ? 'current' : ''} h-12">
<td class="pr-4 pl-2">{player.points}</td> <td class="pr-4 pl-2">
<td>{player.name}</td> {#if editable === true}
<input
class="inputField max-w-[200px]"
type="number"
bind:value={player.points}
/>
{:else}
{player.points}
{/if}
</td>
<td class="whitespace-nowrap">{player.name}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>

View File

@@ -4,16 +4,19 @@
interface Props { interface Props {
question: SimpleQuestion; question: SimpleQuestion;
showAnswer: boolean; showAnswer: boolean;
showQuestion: boolean;
[key: string]: unknown; [key: string]: unknown;
} }
let { question, showAnswer }: Props = $props(); let { question, showAnswer, showQuestion }: Props = $props();
</script> </script>
<div class="mb-4 flex grow flex-col items-center text-6xl"> <div class="mb-4 flex grow flex-col items-center text-6xl">
<div class="flex grow-1 items-center"> {#if showQuestion || showAnswer}
<div>{question.data.question}</div> <div class="flex grow-1 items-center">
</div> <div>{question.data.question}</div>
</div>
{/if}
{#if showAnswer} {#if showAnswer}
<div class="flex grow-1 items-center"> <div class="flex grow-1 items-center">
{question.data.answer} {question.data.answer}

View File

@@ -10,10 +10,20 @@ const games: Games = [
questions: [ questions: [
{ {
points: 100, points: 100,
type: "SIMPLE", type: "MULTIPLE_CHOICE",
data: { data: {
question: "Question 1?", question: "Wie heißt unser Planet?",
answer: "Answer 1" choices: [
"Erde",
"Sonne",
"Mars",
"Venus",
"Saturn",
"Jupiter",
"Merkur",
"Uranus",
"Neptun"
]
} }
}, },
{ {
@@ -946,11 +956,13 @@ export type SimpleQuestion = Question & {
}; };
}; };
/**
* First choice is correct answer. Choices need to be shuffled in component
*/
export type MultipleChoiceQuestion = Question & { export type MultipleChoiceQuestion = Question & {
type: "MULTIPLE_CHOICE"; type: "MULTIPLE_CHOICE";
data: { data: {
question: string; question: string;
answer: number;
choices: string[]; choices: string[];
}; };
}; };

View File

@@ -3,10 +3,11 @@
import { page } from "$app/state"; import { page } from "$app/state";
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte"; import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
import { isSimpleQuestion } from "$lib/games/games"; import { isMultipleChoiceQuestion, isSimpleQuestion } from "$lib/games/games";
import ws from "$lib/websocket.svelte"; import ws from "$lib/websocket.svelte";
import { MessageType } from "$lib/MessageType"; import { MessageType } from "$lib/MessageType";
import { untrack } from "svelte"; import { untrack } from "svelte";
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
console.log("wall:", page.params.wall); console.log("wall:", page.params.wall);
@@ -58,6 +59,7 @@
} }
let showAnswer = $state(false); let showAnswer = $state(false);
let showQuestion = $state(false);
$effect(() => { $effect(() => {
if (ws.messageNum <= 0) return; if (ws.messageNum <= 0) return;
@@ -67,6 +69,7 @@
if (json.type == MessageType.SHOW_ANSWER) { if (json.type == MessageType.SHOW_ANSWER) {
ws.nextMessage(); ws.nextMessage();
untrack(() => { untrack(() => {
showQuestion = true;
showAnswer = true; showAnswer = true;
}); });
} }
@@ -76,6 +79,19 @@
showAnswer = false; showAnswer = false;
}); });
} }
if (json.type == MessageType.SHOW_QUESTION) {
ws.nextMessage();
untrack(() => {
showQuestion = true;
});
}
if (json.type == MessageType.HIDE_QUESTION) {
ws.nextMessage();
untrack(() => {
showQuestion = false;
showAnswer = false;
});
}
} catch (e) {} } catch (e) {}
}); });
</script> </script>
@@ -91,9 +107,11 @@
{#if question === undefined} {#if question === undefined}
<p>Question is undefined</p> <p>Question is undefined</p>
{:else if isSimpleQuestion(question)} {:else if isSimpleQuestion(question)}
<SimpleQuestionComponent {question} {showAnswer} /> <SimpleQuestionComponent {question} {showAnswer} {showQuestion} />
{:else if isMultipleChoiceQuestion(question)}
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
{:else} {:else}
<p>Type of question unknown: {question.type}</p> <p>Type of question unknown</p>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -2,7 +2,12 @@
import Wall from "$lib/Wall.svelte"; import Wall from "$lib/Wall.svelte";
import type { Player } from "$lib/Player"; import type { Player } from "$lib/Player";
import { GameState } from "$lib/GameState"; import { GameState } from "$lib/GameState";
import { isSimpleQuestion, type Game, type Question } from "$lib/games/games"; import {
isMultipleChoiceQuestion,
isSimpleQuestion,
type Game,
type Question
} from "$lib/games/games";
import ws from "$lib/websocket.svelte"; import ws from "$lib/websocket.svelte";
import { page } from "$app/state"; import { page } from "$app/state";
import { MessageType } from "$lib/MessageType"; import { MessageType } from "$lib/MessageType";
@@ -11,6 +16,7 @@
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte"; import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
import PlusMinusButton from "$lib/PlusMinusButton.svelte"; import PlusMinusButton from "$lib/PlusMinusButton.svelte";
import type { VisitedQuestions } from "$lib/Types.js"; import type { VisitedQuestions } from "$lib/Types.js";
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
let startDisabled = $state(true); let startDisabled = $state(true);
@@ -60,6 +66,9 @@
public currentQuestion = $state(0); public currentQuestion = $state(0);
public answerIsShowing = $state(false); public answerIsShowing = $state(false);
public questionIsShowing = $state(false);
public pointsGivenForCurrentQuestion = $state(false);
constructor(game: Game) { constructor(game: Game) {
this.game = game; this.game = game;
@@ -146,6 +155,7 @@
showAnswer() { showAnswer() {
this.answerIsShowing = true; this.answerIsShowing = true;
this.questionIsShowing = true;
ws.sendMessage({ ws.sendMessage({
type: MessageType.SHOW_ANSWER type: MessageType.SHOW_ANSWER
}); });
@@ -158,12 +168,29 @@
}); });
} }
showQuestion() {
this.questionIsShowing = true;
ws.sendMessage({
type: MessageType.SHOW_QUESTION
});
}
hideQuestion() {
this.questionIsShowing = false;
this.answerIsShowing = false;
ws.sendMessage({
type: MessageType.HIDE_QUESTION
});
}
plus(player: Player) { plus(player: Player) {
if (!this.answerIsShowing) return;
if (player.name === this.currentPlayerToName()) { if (player.name === this.currentPlayerToName()) {
player.points += this.question.points * 2; player.points += this.question.points * 2;
} else { } else {
player.points += this.question.points; player.points += this.question.points;
} }
this.pointsGivenForCurrentQuestion = true;
this.sendPlayers(); this.sendPlayers();
} }
@@ -189,6 +216,9 @@
) { ) {
this.visitedQuestions[this.currentCategory].push(this.currentQuestion); this.visitedQuestions[this.currentCategory].push(this.currentQuestion);
} }
this.pointsGivenForCurrentQuestion = false;
this.answerIsShowing = false;
this.questionIsShowing = false;
this.nextPlayer(); this.nextPlayer();
if (this.wallIsDone()) { if (this.wallIsDone()) {
this.goToNextWall(); this.goToNextWall();
@@ -242,8 +272,12 @@
return this.game.walls[this.currentWall]; return this.game.walls[this.currentWall];
} }
get category() {
return this.wall.categories[this.currentCategory];
}
get question() { get question() {
return this.wall.categories[this.currentCategory].questions[this.currentQuestion]; return this.category.questions[this.currentQuestion];
} }
} }
@@ -281,9 +315,17 @@
<Scoreboard <Scoreboard
players={gameManager.players} players={gameManager.players}
currentPlayer={gameManager.currentPlayerToName()} currentPlayer={gameManager.currentPlayerToName()}
editable={true}
onReload={() => gameManager.sendPlayers()}
/> />
{#if gameManager.state === GameState.SHOW_QUESTION} {#if gameManager.state === GameState.SHOW_QUESTION}
<div class="flex grow flex-col"> <div class="flex grow flex-col">
<div class="m-4 flex justify-between text-4xl">
<div>{gameManager.category.name}</div>
<div>
{gameManager.question.points} Punkte
</div>
</div>
<div class="flex grow ps-4 pe-4"> <div class="flex grow ps-4 pe-4">
{#if gameManager.question === undefined} {#if gameManager.question === undefined}
<p>Question is undefined</p> <p>Question is undefined</p>
@@ -291,6 +333,13 @@
<SimpleQuestionComponent <SimpleQuestionComponent
question={gameManager.question} question={gameManager.question}
showAnswer={true} showAnswer={true}
showQuestion={true}
/>
{:else if isMultipleChoiceQuestion(gameManager.question)}
<MultipleChoiceQuestionComponent
question={gameManager.question}
showAnswer={true}
showQuestion={true}
/> />
{:else} {:else}
<p>Type of question unknown: {gameManager.question.type}</p> <p>Type of question unknown: {gameManager.question.type}</p>
@@ -298,6 +347,15 @@
</div> </div>
<div class="m-4 flex items-center gap-4"> <div class="m-4 flex items-center gap-4">
<button class="btn" onclick={() => gameManager.goBack()}>Zurück</button> <button class="btn" onclick={() => gameManager.goBack()}>Zurück</button>
{#if gameManager.questionIsShowing}
<button class="btn" onclick={() => gameManager.hideQuestion()}
>Frage verstecken</button
>
{:else}
<button class="btn" onclick={() => gameManager.showQuestion()}
>Frage aufdecken</button
>
{/if}
{#if gameManager.answerIsShowing} {#if gameManager.answerIsShowing}
<button class="btn" onclick={() => gameManager.hideAnswer()} <button class="btn" onclick={() => gameManager.hideAnswer()}
>Antwort verstecken</button >Antwort verstecken</button
@@ -312,11 +370,15 @@
label={player.name} label={player.name}
plus={() => gameManager.plus(player)} plus={() => gameManager.plus(player)}
minus={() => gameManager.minus(player)} minus={() => gameManager.minus(player)}
showPlus={gameManager.answerIsShowing}
/> />
{/each} {/each}
<button class="btn" onclick={() => gameManager.finishQuestion()} <div class="grow"></div>
>Abschließen</button {#if gameManager.answerIsShowing && gameManager.pointsGivenForCurrentQuestion}
> <button class="btn" onclick={() => gameManager.finishQuestion()}
>Abschließen</button
>
{/if}
</div> </div>
</div> </div>
{:else if gameManager.state === GameState.END} {:else if gameManager.state === GameState.END}