Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update scores and bonus points at turn end #188

Merged
merged 4 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
15 changes: 14 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 Expand Up @@ -40,6 +42,17 @@ const roundMachine = setup({
states: {
turn: {
entry: [{ type: "setQuestion", params: dynamicParamFuncs.setQuestion }],
on: {
turnEnd: {
target: "roundEnd",
// guard: (_, __) => {
// check to see if round end conditions are met
// },
},
},
},
roundEnd: {
richpjames marked this conversation as resolved.
Show resolved Hide resolved
type: "final",
},
},
});
Expand Down
22 changes: 11 additions & 11 deletions server/machines/turn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assign, setup } from "xstate";
import type { Answer, Player, Question } from "../@types/entities";
import { getCorrectSocketIdsFromAnswers } from "../utils/scoringUtils";

const context = {
answers: [] as Answer[],
Expand All @@ -18,6 +19,8 @@ type Events = PlayerSubmitsAnswerEvent;

type Input = { selectedQuestion: Question };

type Output = { correctPlayerSocketIds: Player["socketId"][] };

const dynamicParamFuncs = {
addAnswer: ({
context,
Expand All @@ -36,6 +39,7 @@ const turnMachine = setup({
context: Context;
events: Events;
input: Input;
output: Output;
},
actions: {
addAnswer: assign({
Expand All @@ -49,17 +53,10 @@ const turnMachine = setup({
_,
params: ReturnType<typeof dynamicParamFuncs.recordCorrectPlayers>,
) => {
return params.finalAnswers
.filter((answer) => {
if (params.correctAnswer.length !== answer.colours.length) {
return false;
}

return params.correctAnswer.every((colour) =>
answer.colours.includes(colour),
);
})
.map((answer) => answer.socketId);
return getCorrectSocketIdsFromAnswers(
params.finalAnswers,
params.correctAnswer,
);
},
}),
},
Expand Down Expand Up @@ -94,6 +91,9 @@ const turnMachine = setup({
],
},
},
output: ({ context }) => ({
correctPlayerSocketIds: context.correctPlayerSocketIds,
}),
});

export { turnMachine };
27 changes: 15 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 { getUpdatedPlayerScoresAndBonusPoints } from "../utils/scoringUtils";

class Round {
machine: Actor<typeof roundMachine>;
Expand Down Expand Up @@ -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({
richpjames marked this conversation as resolved.
Show resolved Hide resolved
complete: () => {
richpjames marked this conversation as resolved.
Show resolved Hide resolved
const roundMachineSnapshot = this.machine.getSnapshot();
this.machine.send({
type: "turnEnd",
scoresAndBonusPoints: getUpdatedPlayerScoresAndBonusPoints(
roundMachineSnapshot.context.bonusPoints,
roundMachineSnapshot.context.playerScores,
this.turnMachine?.getSnapshot()?.output?.correctPlayerSocketIds ||
[],
yndajas marked this conversation as resolved.
Show resolved Hide resolved
),
});
},
});
this.turnMachine.start();
}
Expand Down
134 changes: 134 additions & 0 deletions server/utils/scoringUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, expect, it } from "bun:test";
import type { Colour, Player, PlayerScore } from "../@types/entities";
import {
getCorrectSocketIdsFromAnswers,
getUpdatedPlayerScoresAndBonusPoints,
} from "./scoringUtils";

describe("scoringUtils", () => {
describe("getCorrectSocketIdsFromAnswers", () => {
richpjames marked this conversation as resolved.
Show resolved Hide resolved
const correctAnswer: Colour[] = ["red", "blue"];
const incorrectAnswer: Colour[] = ["pink", "blue"];

it("returns the IDs of the players with the correct answers", () => {
expect(
getCorrectSocketIdsFromAnswers(
[
{ colours: correctAnswer, socketId: "1" },
{ colours: incorrectAnswer, socketId: "2" },
{ colours: correctAnswer, socketId: "3" },
],
correctAnswer,
),
).toEqual(["1", "3"]);
});

it("returns an empty array if there are no correct answers", () => {
expect(
getCorrectSocketIdsFromAnswers(
[
{ colours: incorrectAnswer, socketId: "1" },
{ colours: incorrectAnswer, socketId: "2" },
],
correctAnswer,
),
).toBeArrayOfSize(0);
richpjames marked this conversation as resolved.
Show resolved Hide resolved
});
});

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,
richpjames marked this conversation as resolved.
Show resolved Hide resolved
},
];

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 },
],
});
});
});
});
});
});
75 changes: 75 additions & 0 deletions server/utils/scoringUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Answer, Player, PlayerScore, Question } from "../@types/entities";
richpjames marked this conversation as resolved.
Show resolved Hide resolved

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[],
correctAnswer: Question["colours"],
) => {
return answers
.filter((answer) => {
if (correctAnswer.length !== answer.colours.length) {
return false;
}

return correctAnswer.every((colour) => answer.colours.includes(colour));
richpjames marked this conversation as resolved.
Show resolved Hide resolved
})
.map((answer) => answer.socketId);
};

export { getCorrectSocketIdsFromAnswers, getUpdatedPlayerScoresAndBonusPoints };