Skip to content

Commit

Permalink
Update scores and bonus points at turn end
Browse files Browse the repository at this point in the history
The logic here is relatively complex so worthwhile testing.
I have opted to only test the exported functions as the smaller, local
ones are very simple.

Co-authored-by: Rich James <[email protected]>
  • Loading branch information
yndajas and Rich James committed Aug 20, 2024
1 parent 4fd04ea commit 1de426a
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 15 deletions.
7 changes: 6 additions & 1 deletion server/@types/entities.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
4 changes: 3 additions & 1 deletion server/machines/round.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
26 changes: 14 additions & 12 deletions server/models/round.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getUpdatedScoresAndBonusPoints } from "../utils/scoringUtils";

class Round {
machine: Actor<typeof roundMachine>;
Expand Down Expand Up @@ -43,18 +44,19 @@ 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: () => {
this.machine.send({
type: "turnEnd",
scoresAndBonusPoints: getUpdatedScoresAndBonusPoints(
this.machine.getSnapshot().context.bonusPoints,
this.machine.getSnapshot().context.playerScores,
this.turnMachine?.getSnapshot()?.output?.correctPlayerSocketIds ||
[],
),
});
},
});
this.turnMachine.start();
}
Expand Down
135 changes: 135 additions & 0 deletions server/utils/scoringUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, expect, it } from "bun:test";
import type { Colour, Player, PlayerScore } from "../@types/entities";
import {
getCorrectSocketIdsFromAnswers,
getUpdatedScoresAndBonusPoints,
} from "./scoringUtils";

describe("scoringUtils", () => {
describe("getCorrectSocketIdsFromAnswers", () => {
it("returns the IDs of the players with the correct answers", () => {
const correctAnswer: Colour[] = ["red", "blue"];
const incorrectAnswer: Colour[] = ["pink", "blue"];

expect(
getCorrectSocketIdsFromAnswers(
[
{ colours: correctAnswer, socketId: "1" },
{ colours: incorrectAnswer, socketId: "2" },
{ colours: correctAnswer, socketId: "3" },
],
correctAnswer,
),
).toContainValues(["1", "3"]);
});

it("returns an empty array if there are no correct answers", () => {
const correctAnswer: Colour[] = ["red", "blue"];
const incorrectAnswer: Colour[] = ["pink", "blue"];

expect(
getCorrectSocketIdsFromAnswers(
[
{ colours: incorrectAnswer, socketId: "1" },
{ colours: incorrectAnswer, socketId: "2" },
],
correctAnswer,
),
).toContainValues([]);
});
});

describe("getUpdatedScoresAndBonusPoints", () => {
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: 0,
},
{
player: { name: "alex", socketId: correctPlayerSocketIds[1] },
score: 0,
},
];

expect(
getUpdatedScoresAndBonusPoints(
currentBonusPoints,
currentPlayerScores,
correctPlayerSocketIds,
),
).toEqual({
bonusPoints: 1,
scores: 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: 0,
},
{
player: { name: "alex", socketId: "2" },
score: 0,
},
];

expect(
getUpdatedScoresAndBonusPoints(
currentBonusPoints,
currentPlayerScores,
correctPlayerSocketIds,
),
).toEqual({
bonusPoints: 0,
scores: currentPlayerScores,
});
});
});

describe("if some players are correct and others incorrect", () => {
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: "1" },
score: 0,
},
{
player: { name: "alex", socketId: "2" },
score: 0,
},
{
player: { name: "james", socketId: "3" },
score: 0,
},
];

expect(
getUpdatedScoresAndBonusPoints(
currentBonusPoints,
currentPlayerScores,
correctPlayerSocketIds,
),
).toEqual({
bonusPoints: 0,
scores: [
{ player: { name: "olaf", socketId: "1" }, score: 3 },
{ player: { name: "alex", socketId: "2" }, score: 0 },
{ player: { name: "james", socketId: "3" }, score: 3 },
],
});
});
});
});
});
58 changes: 57 additions & 1 deletion server/utils/scoringUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
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 awardPointsForCorrectAnswers = (
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 getUpdatedScoresAndBonusPoints = (
currentBonusPoints: number,
currentPlayerScores: PlayerScore[],
correctPlayerSocketIds: Player["socketId"][],
): { bonusPoints: number; scores: PlayerScore[] } => {
if (allCorrect(currentPlayerScores.length, correctPlayerSocketIds)) {
return { bonusPoints: currentBonusPoints + 1, scores: currentPlayerScores };
}

if (allIncorrect(correctPlayerSocketIds)) {
return { bonusPoints: 0, scores: currentPlayerScores };
}

return {
bonusPoints: 0,
scores: awardPointsForCorrectAnswers(
currentPlayerScores,
currentBonusPoints,
correctPlayerSocketIds,
),
};
};

const getCorrectSocketIdsFromAnswers = (
answers: Answer[],
correctAnswer: Question["colours"],
Expand All @@ -13,4 +69,4 @@ const getCorrectSocketIdsFromAnswers = (
.map((answer) => answer.socketId);
};

export { getCorrectSocketIdsFromAnswers };
export { getCorrectSocketIdsFromAnswers, getUpdatedScoresAndBonusPoints };

0 comments on commit 1de426a

Please sign in to comment.