Skip to content

Commit

Permalink
Merge pull request #188 from dxw/update-answers-and-bonus-points-at-t…
Browse files Browse the repository at this point in the history
…urn-end

Update scores and bonus points at turn end
  • Loading branch information
richpjames authored Aug 21, 2024
2 parents 183ec74 + 1f086f6 commit b3172c4
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 25 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 };
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: {
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({
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();
}
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", () => {
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);
});
});

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 },
],
});
});
});
});
});
});
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";

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));
})
.map((answer) => answer.socketId);
};

export { getCorrectSocketIdsFromAnswers, getUpdatedPlayerScoresAndBonusPoints };

0 comments on commit b3172c4

Please sign in to comment.