diff --git a/server/@types/entities.d.ts b/server/@types/entities.d.ts index a395f481..72d35998 100644 --- a/server/@types/entities.d.ts +++ b/server/@types/entities.d.ts @@ -28,4 +28,9 @@ type Question = { subject: string; }; -export type { Answer, Colour, Player, Question }; +type PlayerScore = { + player: Player; + score: number; +}; + +export type { Answer, Colour, Player, PlayerScore, Question }; diff --git a/server/machines/round.ts b/server/machines/round.ts index 2b295b8b..45e52140 100644 --- a/server/machines/round.ts +++ b/server/machines/round.ts @@ -1,10 +1,12 @@ import { assign, setup } from "xstate"; -import type { Question } from "../@types/entities"; +import type { PlayerScore, Question } from "../@types/entities"; import questions from "../data/questions.json"; const context = { questions: questions as Question[], + playerScores: [] as PlayerScore[], selectedQuestion: {} as Question | undefined, + bonusPoints: 0, }; type Context = typeof context; diff --git a/server/models/round.ts b/server/models/round.ts index c4c34918..e15cc271 100644 --- a/server/models/round.ts +++ b/server/models/round.ts @@ -4,6 +4,7 @@ import { context, roundMachine } from "../machines/round"; import { turnMachine } from "../machines/turn"; import type { SocketServer } from "../socketServer"; import { machineLogger } from "../utils/loggingUtils"; +import { getUpdatedPlayerScoresAndBonusPoints } from "../utils/scoringUtils"; class Round { machine: Actor; @@ -43,18 +44,20 @@ class Round { .selectedQuestion as Question, }, }); - this.turnMachine.subscribe((state) => { - switch (state.value) { - case "finished": { - // TODO: - // - add logic for updating scores then checking if there's a clear winner in the round machine - // - delete the console.info below - console.info( - "turn machine finished with context:", - this.turnMachine?.getSnapshot().context, - ); - } - } + + this.turnMachine.subscribe({ + complete: () => { + const roundMachineSnapshot = this.machine.getSnapshot(); + this.machine.send({ + type: "turnEnd", + scoresAndBonusPoints: getUpdatedPlayerScoresAndBonusPoints( + roundMachineSnapshot.context.bonusPoints, + roundMachineSnapshot.context.playerScores, + this.turnMachine?.getSnapshot()?.output?.correctPlayerSocketIds || + [], + ), + }); + }, }); this.turnMachine.start(); } diff --git a/server/utils/scoringUtils.test.ts b/server/utils/scoringUtils.test.ts index e9f76c6a..bf29f1ff 100644 --- a/server/utils/scoringUtils.test.ts +++ b/server/utils/scoringUtils.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "bun:test"; -import type { Colour } from "../@types/entities"; -import { getCorrectSocketIdsFromAnswers } from "./scoringUtils"; +import type { Colour, Player, PlayerScore } from "../@types/entities"; +import { + getCorrectSocketIdsFromAnswers, + getUpdatedPlayerScoresAndBonusPoints, +} from "./scoringUtils"; describe("scoringUtils", () => { describe("getCorrectSocketIdsFromAnswers", () => { @@ -32,4 +35,100 @@ describe("scoringUtils", () => { ).toBeArrayOfSize(0); }); }); + + describe("getUpdatedPlayerScoresAndBonusPoints", () => { + describe("if all players are correct", () => { + it("increments the bonus points and returns the player scores unchanged", () => { + const currentBonusPoints = 0; + const correctPlayerSocketIds = ["1", "2"]; + const currentPlayerScores: PlayerScore[] = [ + { + player: { name: "olaf", socketId: correctPlayerSocketIds[0] }, + score: 1, + }, + { + player: { name: "alex", socketId: correctPlayerSocketIds[1] }, + score: 0, + }, + ]; + + expect( + getUpdatedPlayerScoresAndBonusPoints( + currentBonusPoints, + currentPlayerScores, + correctPlayerSocketIds, + ), + ).toEqual({ + bonusPoints: 1, + playerScores: currentPlayerScores, + }); + }); + }); + + describe("if all players are incorrect", () => { + it("resets the bonus points and returns the player scores unchanged", () => { + const currentBonusPoints = 3; + const correctPlayerSocketIds: Player["socketId"][] = []; + const currentPlayerScores: PlayerScore[] = [ + { + player: { name: "olaf", socketId: "1" }, + score: 1, + }, + { + player: { name: "alex", socketId: "2" }, + score: 0, + }, + ]; + + expect( + getUpdatedPlayerScoresAndBonusPoints( + currentBonusPoints, + currentPlayerScores, + correctPlayerSocketIds, + ), + ).toEqual({ + bonusPoints: 0, + playerScores: currentPlayerScores, + }); + }); + }); + + describe("if some players are correct and others incorrect", () => { + describe("and there are no bonus points", () => { + it("awards points to the correct players and returns the player scores", () => { + const currentBonusPoints = 2; + const correctPlayerSocketIds = ["1", "3"]; + const currentPlayerScores: PlayerScore[] = [ + { + player: { name: "olaf", socketId: correctPlayerSocketIds[0] }, + score: 0, + }, + { + player: { name: "alex", socketId: "2" }, + score: 0, + }, + { + player: { name: "james", socketId: correctPlayerSocketIds[1] }, + score: 0, + }, + ]; + + expect( + getUpdatedPlayerScoresAndBonusPoints( + currentBonusPoints, + currentPlayerScores, + correctPlayerSocketIds, + ), + ).toEqual({ + bonusPoints: 0, + playerScores: [ + { player: { name: "olaf", socketId: "1" }, score: 3 }, + { player: { name: "alex", socketId: "2" }, score: 0 }, + { player: { name: "james", socketId: "3" }, score: 3 }, + ], + }); + }); + }); + }); + }); }); diff --git a/server/utils/scoringUtils.ts b/server/utils/scoringUtils.ts index f84034a3..f48e2dbb 100644 --- a/server/utils/scoringUtils.ts +++ b/server/utils/scoringUtils.ts @@ -1,4 +1,61 @@ -import type { Answer, Question } from "../@types/entities"; +import type { Answer, Player, PlayerScore, Question } from "../@types/entities"; + +const allCorrect = ( + totalPlayerCount: number, + correctPlayerSocketIds: Player["socketId"][], +): boolean => { + return correctPlayerSocketIds.length === totalPlayerCount; +}; + +const allIncorrect = ( + correctPlayerSocketIds: Player["socketId"][], +): boolean => { + return correctPlayerSocketIds.length === 0; +}; + +const getUpdatedPlayerScores = ( + currentPlayerScores: PlayerScore[], + bonusPoints: number, + correctPlayerSocketIds: Player["socketId"][], +): PlayerScore[] => { + const numberOfIncorrectAnswers = + currentPlayerScores.length - correctPlayerSocketIds.length; + const pointsToAward = numberOfIncorrectAnswers + bonusPoints; + + return currentPlayerScores.map(({ player, score }) => { + if (correctPlayerSocketIds.includes(player.socketId)) { + return { player, score: score + pointsToAward }; + } + + return { player, score }; + }); +}; + +const getUpdatedPlayerScoresAndBonusPoints = ( + currentBonusPoints: number, + currentPlayerScores: PlayerScore[], + correctPlayerSocketIds: Player["socketId"][], +): { bonusPoints: number; playerScores: PlayerScore[] } => { + if (allCorrect(currentPlayerScores.length, correctPlayerSocketIds)) { + return { + bonusPoints: currentBonusPoints + 1, + playerScores: currentPlayerScores, + }; + } + + if (allIncorrect(correctPlayerSocketIds)) { + return { bonusPoints: 0, playerScores: currentPlayerScores }; + } + + return { + bonusPoints: 0, + playerScores: getUpdatedPlayerScores( + currentPlayerScores, + currentBonusPoints, + correctPlayerSocketIds, + ), + }; +}; const getCorrectSocketIdsFromAnswers = ( answers: Answer[], @@ -15,4 +72,4 @@ const getCorrectSocketIdsFromAnswers = ( .map((answer) => answer.socketId); }; -export { getCorrectSocketIdsFromAnswers }; +export { getCorrectSocketIdsFromAnswers, getUpdatedPlayerScoresAndBonusPoints };