@kekkle/shared 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/cjs/fixtures/character-state.fixtures.d.ts +2 -0
  2. package/dist/cjs/fixtures/character-state.fixtures.js +6 -0
  3. package/dist/cjs/fixtures/wordle-row.fixtures.d.ts +5 -0
  4. package/dist/cjs/fixtures/wordle-row.fixtures.js +26 -0
  5. package/dist/cjs/helpers/worlde/all-correct-in-row.d.ts +5 -0
  6. package/dist/cjs/helpers/worlde/all-correct-in-row.js +9 -0
  7. package/dist/cjs/helpers/worlde/calculate-wordle-score.d.ts +12 -0
  8. package/dist/cjs/helpers/worlde/calculate-wordle-score.js +45 -0
  9. package/dist/cjs/types/{worlde-game-state-document.d.ts → wordle-game-state-document.d.ts} +1 -1
  10. package/dist/esm/fixtures/character-state.fixtures.d.ts +2 -0
  11. package/dist/esm/fixtures/character-state.fixtures.js +3 -0
  12. package/dist/esm/fixtures/wordle-row.fixtures.d.ts +5 -0
  13. package/dist/esm/fixtures/wordle-row.fixtures.js +20 -0
  14. package/dist/esm/helpers/worlde/all-correct-in-row.d.ts +5 -0
  15. package/dist/esm/helpers/worlde/all-correct-in-row.js +6 -0
  16. package/dist/esm/helpers/worlde/calculate-wordle-score.d.ts +12 -0
  17. package/dist/esm/helpers/worlde/calculate-wordle-score.js +42 -0
  18. package/dist/esm/types/{worlde-game-state-document.d.ts → wordle-game-state-document.d.ts} +1 -1
  19. package/package.json +14 -3
  20. package/src/fixtures/character-state.fixtures.ts +9 -0
  21. package/src/fixtures/wordle-row.fixtures.ts +36 -0
  22. package/src/helpers/permissions/is-valid-permission.vi.spec.ts +109 -0
  23. package/src/helpers/worlde/all-correct-in-row.ts +8 -0
  24. package/src/helpers/worlde/all-correct-in-row.vi.spec.ts +96 -0
  25. package/src/helpers/worlde/calculate-wordle-score.ts +53 -0
  26. package/src/helpers/worlde/calculate-wordle-score.vi.spec.ts +171 -0
  27. package/src/types/{worlde-game-state-document.ts → wordle-game-state-document.ts} +1 -1
  28. /package/dist/cjs/types/{worlde-game-state-document.js → wordle-game-state-document.js} +0 -0
  29. /package/dist/esm/types/{worlde-game-state-document.js → wordle-game-state-document.js} +0 -0
@@ -0,0 +1,2 @@
1
+ import type { CharacterState } from "../types/wordle-game-state";
2
+ export declare function createCharacterState(character: string, isCorrect: boolean, isLocked?: boolean): CharacterState;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCharacterState = createCharacterState;
4
+ function createCharacterState(character, isCorrect, isLocked = false) {
5
+ return { character, isCorrect, isLocked };
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { WordleRow } from "../types/wordle-game-state";
2
+ export declare function createWordleRow(inputWord: string, correctWord: string): WordleRow;
3
+ export declare function createAllCorrectWordleRow(word: string): WordleRow;
4
+ export declare function createAllIncorrectWordleRow(word: string): WordleRow;
5
+ export declare function createPartialWordleRow(word: string, correctIndices: number[]): WordleRow;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWordleRow = createWordleRow;
4
+ exports.createAllCorrectWordleRow = createAllCorrectWordleRow;
5
+ exports.createAllIncorrectWordleRow = createAllIncorrectWordleRow;
6
+ exports.createPartialWordleRow = createPartialWordleRow;
7
+ const character_state_fixtures_1 = require("./character-state.fixtures");
8
+ function createWordleRow(inputWord, correctWord) {
9
+ if (inputWord.length !== correctWord.length) {
10
+ throw new Error("Input word and correct word must be the same length");
11
+ }
12
+ return inputWord
13
+ .split("")
14
+ .map((char, index) => (0, character_state_fixtures_1.createCharacterState)(char, char === correctWord[index]));
15
+ }
16
+ function createAllCorrectWordleRow(word) {
17
+ return word.split("").map((char) => (0, character_state_fixtures_1.createCharacterState)(char, true, false));
18
+ }
19
+ function createAllIncorrectWordleRow(word) {
20
+ return word.split("").map((char) => (0, character_state_fixtures_1.createCharacterState)(char, false, false));
21
+ }
22
+ function createPartialWordleRow(word, correctIndices) {
23
+ return word
24
+ .split("")
25
+ .map((char, index) => (0, character_state_fixtures_1.createCharacterState)(char, correctIndices.includes(index), false));
26
+ }
@@ -0,0 +1,5 @@
1
+ import type { WordleRow } from "../../types/wordle-game-state";
2
+ /**
3
+ * Method to determine if all characters are correct for a given WordleRow
4
+ */
5
+ export declare function allCorrectInRow(row: WordleRow): boolean;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.allCorrectInRow = allCorrectInRow;
4
+ /**
5
+ * Method to determine if all characters are correct for a given WordleRow
6
+ */
7
+ function allCorrectInRow(row) {
8
+ return row.every((cell) => cell.isCorrect);
9
+ }
@@ -0,0 +1,12 @@
1
+ import type { WordleGameState } from "../../types/wordle-game-state";
2
+ /**
3
+ * Calculates the score for a Wordle game based on the current game state.
4
+ *
5
+ * Scoring rules:
6
+ * - If the game is won (any row has all correct letters):
7
+ * - Base score equals the length of a row (typically 5)
8
+ * - Bonus points are added based on how quickly the word was guessed (6 minus attempts used)
9
+ * - If the game is not won yet:
10
+ * - Score equals the number of correct cells in the latest row
11
+ */
12
+ export declare function calculateWordleScore(gameState: WordleGameState): number;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateWordleScore = calculateWordleScore;
4
+ const all_correct_in_row_1 = require("./all-correct-in-row");
5
+ /**
6
+ * Calculates the score for a Wordle game based on the current game state.
7
+ *
8
+ * Scoring rules:
9
+ * - If the game is won (any row has all correct letters):
10
+ * - Base score equals the length of a row (typically 5)
11
+ * - Bonus points are added based on how quickly the word was guessed (6 minus attempts used)
12
+ * - If the game is not won yet:
13
+ * - Score equals the number of correct cells in the latest row
14
+ */
15
+ function calculateWordleScore(gameState) {
16
+ // Handle empty game state
17
+ if (gameState.length === 0) {
18
+ return 0;
19
+ }
20
+ let score = 0;
21
+ let hasWon = false;
22
+ // Iterate over each row in the game state to check for a win condition
23
+ for (const row of gameState) {
24
+ // If all cells in a row are correct, the game is won
25
+ if ((0, all_correct_in_row_1.allCorrectInRow)(row)) {
26
+ hasWon = true;
27
+ break;
28
+ }
29
+ }
30
+ // Case 1: Game in progress (not yet won)
31
+ if (!hasWon) {
32
+ // Score is based on correct cells in the latest attempt
33
+ score = gameState[gameState.length - 1].filter((cell) => cell.isCorrect).length;
34
+ return score;
35
+ }
36
+ // Case 2: Game is won
37
+ // Base score is the row length (number of letters in the word)
38
+ score = gameState[0].length;
39
+ // Calculate which attempt was successful (1-based index)
40
+ const numberOfTriesNeeded = gameState.findIndex(all_correct_in_row_1.allCorrectInRow) + 1;
41
+ // Award bonus points based on how quickly the word was guessed
42
+ // Maximum bonus (5) for first try, decreasing by 1 for each additional attempt
43
+ const bonusPoints = 6 - numberOfTriesNeeded;
44
+ return score + bonusPoints;
45
+ }
@@ -1,6 +1,6 @@
1
1
  import type { StringifiedWordleGameState } from "./wordle-game-state";
2
2
  import type { Timestamp } from "firebase-admin/firestore";
3
- export type WorldeGameStateDocument = {
3
+ export type WordleGameStateDocument = {
4
4
  calculated_score: number;
5
5
  game_state: StringifiedWordleGameState;
6
6
  has_finished: boolean;
@@ -0,0 +1,2 @@
1
+ import type { CharacterState } from "../types/wordle-game-state";
2
+ export declare function createCharacterState(character: string, isCorrect: boolean, isLocked?: boolean): CharacterState;
@@ -0,0 +1,3 @@
1
+ export function createCharacterState(character, isCorrect, isLocked = false) {
2
+ return { character, isCorrect, isLocked };
3
+ }
@@ -0,0 +1,5 @@
1
+ import type { WordleRow } from "../types/wordle-game-state";
2
+ export declare function createWordleRow(inputWord: string, correctWord: string): WordleRow;
3
+ export declare function createAllCorrectWordleRow(word: string): WordleRow;
4
+ export declare function createAllIncorrectWordleRow(word: string): WordleRow;
5
+ export declare function createPartialWordleRow(word: string, correctIndices: number[]): WordleRow;
@@ -0,0 +1,20 @@
1
+ import { createCharacterState } from "./character-state.fixtures";
2
+ export function createWordleRow(inputWord, correctWord) {
3
+ if (inputWord.length !== correctWord.length) {
4
+ throw new Error("Input word and correct word must be the same length");
5
+ }
6
+ return inputWord
7
+ .split("")
8
+ .map((char, index) => createCharacterState(char, char === correctWord[index]));
9
+ }
10
+ export function createAllCorrectWordleRow(word) {
11
+ return word.split("").map((char) => createCharacterState(char, true, false));
12
+ }
13
+ export function createAllIncorrectWordleRow(word) {
14
+ return word.split("").map((char) => createCharacterState(char, false, false));
15
+ }
16
+ export function createPartialWordleRow(word, correctIndices) {
17
+ return word
18
+ .split("")
19
+ .map((char, index) => createCharacterState(char, correctIndices.includes(index), false));
20
+ }
@@ -0,0 +1,5 @@
1
+ import type { WordleRow } from "../../types/wordle-game-state";
2
+ /**
3
+ * Method to determine if all characters are correct for a given WordleRow
4
+ */
5
+ export declare function allCorrectInRow(row: WordleRow): boolean;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Method to determine if all characters are correct for a given WordleRow
3
+ */
4
+ export function allCorrectInRow(row) {
5
+ return row.every((cell) => cell.isCorrect);
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { WordleGameState } from "../../types/wordle-game-state";
2
+ /**
3
+ * Calculates the score for a Wordle game based on the current game state.
4
+ *
5
+ * Scoring rules:
6
+ * - If the game is won (any row has all correct letters):
7
+ * - Base score equals the length of a row (typically 5)
8
+ * - Bonus points are added based on how quickly the word was guessed (6 minus attempts used)
9
+ * - If the game is not won yet:
10
+ * - Score equals the number of correct cells in the latest row
11
+ */
12
+ export declare function calculateWordleScore(gameState: WordleGameState): number;
@@ -0,0 +1,42 @@
1
+ import { allCorrectInRow } from "./all-correct-in-row";
2
+ /**
3
+ * Calculates the score for a Wordle game based on the current game state.
4
+ *
5
+ * Scoring rules:
6
+ * - If the game is won (any row has all correct letters):
7
+ * - Base score equals the length of a row (typically 5)
8
+ * - Bonus points are added based on how quickly the word was guessed (6 minus attempts used)
9
+ * - If the game is not won yet:
10
+ * - Score equals the number of correct cells in the latest row
11
+ */
12
+ export function calculateWordleScore(gameState) {
13
+ // Handle empty game state
14
+ if (gameState.length === 0) {
15
+ return 0;
16
+ }
17
+ let score = 0;
18
+ let hasWon = false;
19
+ // Iterate over each row in the game state to check for a win condition
20
+ for (const row of gameState) {
21
+ // If all cells in a row are correct, the game is won
22
+ if (allCorrectInRow(row)) {
23
+ hasWon = true;
24
+ break;
25
+ }
26
+ }
27
+ // Case 1: Game in progress (not yet won)
28
+ if (!hasWon) {
29
+ // Score is based on correct cells in the latest attempt
30
+ score = gameState[gameState.length - 1].filter((cell) => cell.isCorrect).length;
31
+ return score;
32
+ }
33
+ // Case 2: Game is won
34
+ // Base score is the row length (number of letters in the word)
35
+ score = gameState[0].length;
36
+ // Calculate which attempt was successful (1-based index)
37
+ const numberOfTriesNeeded = gameState.findIndex(allCorrectInRow) + 1;
38
+ // Award bonus points based on how quickly the word was guessed
39
+ // Maximum bonus (5) for first try, decreasing by 1 for each additional attempt
40
+ const bonusPoints = 6 - numberOfTriesNeeded;
41
+ return score + bonusPoints;
42
+ }
@@ -1,6 +1,6 @@
1
1
  import type { StringifiedWordleGameState } from "./wordle-game-state";
2
2
  import type { Timestamp } from "firebase-admin/firestore";
3
- export type WorldeGameStateDocument = {
3
+ export type WordleGameStateDocument = {
4
4
  calculated_score: number;
5
5
  game_state: StringifiedWordleGameState;
6
6
  has_finished: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kekkle/shared",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Shared logic and types for Kekkle frontend and functions",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,6 +23,11 @@
23
23
  "types": "./src/helpers/*.ts",
24
24
  "import": "./dist/esm/helpers/*.js",
25
25
  "require": "./dist/cjs/helpers/*.js"
26
+ },
27
+ "./fixtures/*": {
28
+ "types": "./src/fixtures/*.ts",
29
+ "import": "./dist/esm/fixtures/*.js",
30
+ "require": "./dist/cjs/fixtures/*.js"
26
31
  }
27
32
  },
28
33
  "files": [
@@ -30,7 +35,10 @@
30
35
  "dist/**/*"
31
36
  ],
32
37
  "scripts": {
33
- "test": "echo \"Error: no test specified\" && exit 1",
38
+ "test": "vitest",
39
+ "test:run": "vitest run",
40
+ "test:ui": "vitest --ui",
41
+ "test:coverage": "vitest run --coverage",
34
42
  "build": "npm run build:esm && npm run build:cjs",
35
43
  "build:esm": "tsc -p tsconfig.esm.json",
36
44
  "build:cjs": "tsc -p tsconfig.cjs.json",
@@ -58,6 +66,8 @@
58
66
  "@semantic-release/github": "^11.0.3",
59
67
  "@semantic-release/release-notes-generator": "^14.0.3",
60
68
  "@types/eslint__js": "^8.42.3",
69
+ "@vitest/coverage-v8": "^3.2.4",
70
+ "@vitest/ui": "^3.2.4",
61
71
  "conventional-changelog-conventionalcommits": "^9.0.0",
62
72
  "eslint": "^9.12.0",
63
73
  "eslint-config-prettier": "^10.1.5",
@@ -65,7 +75,8 @@
65
75
  "prettier": "^3.6.0",
66
76
  "semantic-release": "^24.2.5",
67
77
  "typescript": "^5.6.3",
68
- "typescript-eslint": "^8.8.1"
78
+ "typescript-eslint": "^8.8.1",
79
+ "vitest": "^3.2.4"
69
80
  },
70
81
  "peerDependencies": {
71
82
  "firebase-admin": "11.11.1",
@@ -0,0 +1,9 @@
1
+ import type { CharacterState } from "../types/wordle-game-state";
2
+
3
+ export function createCharacterState(
4
+ character: string,
5
+ isCorrect: boolean,
6
+ isLocked: boolean = false,
7
+ ): CharacterState {
8
+ return { character, isCorrect, isLocked };
9
+ }
@@ -0,0 +1,36 @@
1
+ import type { WordleRow } from "../types/wordle-game-state";
2
+ import { createCharacterState } from "./character-state.fixtures";
3
+
4
+ export function createWordleRow(
5
+ inputWord: string,
6
+ correctWord: string,
7
+ ): WordleRow {
8
+ if (inputWord.length !== correctWord.length) {
9
+ throw new Error("Input word and correct word must be the same length");
10
+ }
11
+
12
+ return inputWord
13
+ .split("")
14
+ .map((char, index) =>
15
+ createCharacterState(char, char === correctWord[index]),
16
+ );
17
+ }
18
+
19
+ export function createAllCorrectWordleRow(word: string): WordleRow {
20
+ return word.split("").map((char) => createCharacterState(char, true, false));
21
+ }
22
+
23
+ export function createAllIncorrectWordleRow(word: string): WordleRow {
24
+ return word.split("").map((char) => createCharacterState(char, false, false));
25
+ }
26
+
27
+ export function createPartialWordleRow(
28
+ word: string,
29
+ correctIndices: number[],
30
+ ): WordleRow {
31
+ return word
32
+ .split("")
33
+ .map((char, index) =>
34
+ createCharacterState(char, correctIndices.includes(index), false),
35
+ );
36
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { isValidPermission } from "./is-valid-permission";
3
+ import { GroupPermission } from "../../enums/group-permission";
4
+
5
+ describe("isValidPermission", () => {
6
+ describe("Valid permissions", () => {
7
+ it("should return true for ADMIN permission", () => {
8
+ expect(isValidPermission(GroupPermission.ADMIN)).toBe(true);
9
+ });
10
+
11
+ it("should return true for ORGANISER permission", () => {
12
+ expect(isValidPermission(GroupPermission.ORGANISER)).toBe(true);
13
+ });
14
+
15
+ it("should return true for MEMBER permission", () => {
16
+ expect(isValidPermission(GroupPermission.MEMBER)).toBe(true);
17
+ });
18
+
19
+ it("should return true for admin string value", () => {
20
+ expect(isValidPermission("admin")).toBe(true);
21
+ });
22
+
23
+ it("should return true for organiser string value", () => {
24
+ expect(isValidPermission("organiser")).toBe(true);
25
+ });
26
+
27
+ it("should return true for member string value", () => {
28
+ expect(isValidPermission("member")).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe("Invalid permissions", () => {
33
+ it("should return false for invalid permission string", () => {
34
+ expect(isValidPermission("invalid")).toBe(false);
35
+ });
36
+
37
+ it("should return false for empty string", () => {
38
+ expect(isValidPermission("")).toBe(false);
39
+ });
40
+
41
+ it("should return false for uppercase permission", () => {
42
+ expect(isValidPermission("ADMIN")).toBe(false);
43
+ });
44
+
45
+ it("should return false for mixed case permission", () => {
46
+ expect(isValidPermission("Admin")).toBe(false);
47
+ });
48
+
49
+ it("should return false for permission with extra spaces", () => {
50
+ expect(isValidPermission(" admin ")).toBe(false);
51
+ });
52
+
53
+ it("should return false for permission with different spelling", () => {
54
+ expect(isValidPermission("administrator")).toBe(false);
55
+ });
56
+
57
+ it("should return false for partial permission match", () => {
58
+ expect(isValidPermission("admi")).toBe(false);
59
+ });
60
+
61
+ it("should return false for null-like string", () => {
62
+ expect(isValidPermission("null")).toBe(false);
63
+ });
64
+
65
+ it("should return false for undefined-like string", () => {
66
+ expect(isValidPermission("undefined")).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe("Edge cases", () => {
71
+ it("should return false for numeric string", () => {
72
+ expect(isValidPermission("123")).toBe(false);
73
+ });
74
+
75
+ it("should return false for special characters", () => {
76
+ expect(isValidPermission("@admin")).toBe(false);
77
+ });
78
+
79
+ it("should return false for permission with hyphen", () => {
80
+ expect(isValidPermission("admin-user")).toBe(false);
81
+ });
82
+
83
+ it("should return false for permission with underscore", () => {
84
+ expect(isValidPermission("admin_user")).toBe(false);
85
+ });
86
+
87
+ it("should handle all enum values dynamically", () => {
88
+ // This test ensures all current enum values are valid
89
+ Object.values(GroupPermission).forEach((permission) => {
90
+ expect(isValidPermission(permission)).toBe(true);
91
+ });
92
+ });
93
+
94
+ it("should return false for non-enum string values", () => {
95
+ const invalidPermissions = [
96
+ "guest",
97
+ "viewer",
98
+ "editor",
99
+ "owner",
100
+ "moderator",
101
+ "user",
102
+ ];
103
+
104
+ invalidPermissions.forEach((permission) => {
105
+ expect(isValidPermission(permission)).toBe(false);
106
+ });
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,8 @@
1
+ import type { CharacterState, WordleRow } from "../../types/wordle-game-state";
2
+
3
+ /**
4
+ * Method to determine if all characters are correct for a given WordleRow
5
+ */
6
+ export function allCorrectInRow(row: WordleRow): boolean {
7
+ return row.every((cell: CharacterState) => cell.isCorrect);
8
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { allCorrectInRow } from "./all-correct-in-row";
3
+ import {
4
+ createWordleRow,
5
+ createAllCorrectWordleRow,
6
+ createAllIncorrectWordleRow,
7
+ createPartialWordleRow,
8
+ } from "../../fixtures/wordle-row.fixtures";
9
+
10
+ describe("allCorrectInRow", () => {
11
+ describe("All correct scenarios", () => {
12
+ it("should return true when all letters in a 5-letter row are correct", () => {
13
+ const row = createAllCorrectWordleRow("HOUSE");
14
+
15
+ expect(allCorrectInRow(row)).toBe(true);
16
+ });
17
+
18
+ it("should return true when all letters in a 4-letter row are correct", () => {
19
+ const row = createAllCorrectWordleRow("WORD");
20
+
21
+ expect(allCorrectInRow(row)).toBe(true);
22
+ });
23
+
24
+ it("should return true when all letters in a 6-letter row are correct", () => {
25
+ const row = createAllCorrectWordleRow("PUZZLE");
26
+
27
+ expect(allCorrectInRow(row)).toBe(true);
28
+ });
29
+
30
+ it("should return true for a single correct letter", () => {
31
+ const row = createAllCorrectWordleRow("A");
32
+
33
+ expect(allCorrectInRow(row)).toBe(true);
34
+ });
35
+ });
36
+
37
+ describe("All incorrect scenarios", () => {
38
+ it("should return false when all letters are incorrect", () => {
39
+ const row = createAllIncorrectWordleRow("WRONG");
40
+
41
+ expect(allCorrectInRow(row)).toBe(false);
42
+ });
43
+
44
+ it("should return false for a single incorrect letter", () => {
45
+ const row = createAllIncorrectWordleRow("X");
46
+
47
+ expect(allCorrectInRow(row)).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe("Partial correct scenarios", () => {
52
+ it("should return false when only first letter is correct", () => {
53
+ const row = createWordleRow("HOUSE", "HXXXX");
54
+
55
+ expect(allCorrectInRow(row)).toBe(false);
56
+ });
57
+
58
+ it("should return false when only last letter is correct", () => {
59
+ const row = createWordleRow("HOUSE", "XXXXE");
60
+
61
+ expect(allCorrectInRow(row)).toBe(false);
62
+ });
63
+
64
+ it("should return false when middle letters are correct", () => {
65
+ const row = createWordleRow("HOUSE", "XOUSX");
66
+
67
+ expect(allCorrectInRow(row)).toBe(false);
68
+ });
69
+
70
+ it("should return false when most letters are correct but one is wrong", () => {
71
+ const row = createWordleRow("HOUSE", "HOUSX");
72
+
73
+ expect(allCorrectInRow(row)).toBe(false);
74
+ });
75
+
76
+ it("should return false using partial row fixture with specific indices", () => {
77
+ const row = createPartialWordleRow("FLAME", [0, 2, 4]); // F, A, E correct
78
+
79
+ expect(allCorrectInRow(row)).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe("Edge cases", () => {
84
+ it("should return true for empty row", () => {
85
+ const row = createWordleRow("", "");
86
+
87
+ expect(allCorrectInRow(row)).toBe(true);
88
+ });
89
+
90
+ it("should handle mixed correct and locked states", () => {
91
+ const row = createWordleRow("TEST", "TEXX");
92
+
93
+ expect(allCorrectInRow(row)).toBe(false);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,53 @@
1
+ import type { WordleGameState } from "../../types/wordle-game-state";
2
+ import { allCorrectInRow } from "./all-correct-in-row";
3
+
4
+ /**
5
+ * Calculates the score for a Wordle game based on the current game state.
6
+ *
7
+ * Scoring rules:
8
+ * - If the game is won (any row has all correct letters):
9
+ * - Base score equals the length of a row (typically 5)
10
+ * - Bonus points are added based on how quickly the word was guessed (6 minus attempts used)
11
+ * - If the game is not won yet:
12
+ * - Score equals the number of correct cells in the latest row
13
+ */
14
+ export function calculateWordleScore(gameState: WordleGameState): number {
15
+ // Handle empty game state
16
+ if (gameState.length === 0) {
17
+ return 0;
18
+ }
19
+
20
+ let score = 0;
21
+ let hasWon = false;
22
+
23
+ // Iterate over each row in the game state to check for a win condition
24
+ for (const row of gameState) {
25
+ // If all cells in a row are correct, the game is won
26
+ if (allCorrectInRow(row)) {
27
+ hasWon = true;
28
+ break;
29
+ }
30
+ }
31
+
32
+ // Case 1: Game in progress (not yet won)
33
+ if (!hasWon) {
34
+ // Score is based on correct cells in the latest attempt
35
+ score = gameState[gameState.length - 1].filter(
36
+ (cell) => cell.isCorrect,
37
+ ).length;
38
+ return score;
39
+ }
40
+
41
+ // Case 2: Game is won
42
+ // Base score is the row length (number of letters in the word)
43
+ score = gameState[0].length;
44
+
45
+ // Calculate which attempt was successful (1-based index)
46
+ const numberOfTriesNeeded = gameState.findIndex(allCorrectInRow) + 1;
47
+
48
+ // Award bonus points based on how quickly the word was guessed
49
+ // Maximum bonus (5) for first try, decreasing by 1 for each additional attempt
50
+ const bonusPoints = 6 - numberOfTriesNeeded;
51
+
52
+ return score + bonusPoints;
53
+ }
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { calculateWordleScore } from "./calculate-wordle-score";
3
+ import type { WordleGameState } from "../../types/wordle-game-state";
4
+ import {
5
+ createWordleRow,
6
+ createAllCorrectWordleRow,
7
+ createAllIncorrectWordleRow,
8
+ } from "../../fixtures/wordle-row.fixtures";
9
+
10
+ describe("calculateWordleScore", () => {
11
+ describe("Game in progress (not won)", () => {
12
+ it("should return 0 when no letters are correct in the latest row", () => {
13
+ const gameState: WordleGameState = [createAllIncorrectWordleRow("WRONG")];
14
+
15
+ expect(calculateWordleScore(gameState)).toBe(0);
16
+ });
17
+
18
+ it("should return 1 when one letter is correct in the latest row", () => {
19
+ const gameState: WordleGameState = [
20
+ createAllIncorrectWordleRow("WRONG"),
21
+ createWordleRow("HOUSE", "HXXXX"),
22
+ ];
23
+
24
+ expect(calculateWordleScore(gameState)).toBe(1);
25
+ });
26
+
27
+ it("should return 3 when three letters are correct in the latest row", () => {
28
+ const gameState: WordleGameState = [
29
+ createAllIncorrectWordleRow("WRONG"),
30
+ createWordleRow("HOUSE", "HXUXE"),
31
+ ];
32
+
33
+ expect(calculateWordleScore(gameState)).toBe(3);
34
+ });
35
+
36
+ it("should only count correct letters from the latest row when game not won", () => {
37
+ const gameState: WordleGameState = [
38
+ createAllIncorrectWordleRow("WRONG"), // No correct in first row
39
+ createWordleRow("HOUSE", "HXXXX"), // Only one correct in latest row
40
+ ];
41
+
42
+ expect(calculateWordleScore(gameState)).toBe(1);
43
+ });
44
+ });
45
+
46
+ describe("Game won", () => {
47
+ it("should return 10 when won on first try (5 + 5 bonus)", () => {
48
+ const gameState: WordleGameState = [createAllCorrectWordleRow("HOUSE")];
49
+
50
+ expect(calculateWordleScore(gameState)).toBe(10);
51
+ });
52
+
53
+ it("should return 9 when won on second try (5 + 4 bonus)", () => {
54
+ const gameState: WordleGameState = [
55
+ createAllIncorrectWordleRow("WRONG"),
56
+ createAllCorrectWordleRow("HOUSE"),
57
+ ];
58
+
59
+ expect(calculateWordleScore(gameState)).toBe(9);
60
+ });
61
+
62
+ it("should return 8 when won on third try (5 + 3 bonus)", () => {
63
+ const gameState: WordleGameState = [
64
+ createAllIncorrectWordleRow("WRONG"),
65
+ createAllIncorrectWordleRow("TESTS"),
66
+ createAllCorrectWordleRow("HOUSE"),
67
+ ];
68
+
69
+ expect(calculateWordleScore(gameState)).toBe(8);
70
+ });
71
+
72
+ it("should return 7 when won on fourth try (5 + 2 bonus)", () => {
73
+ const gameState: WordleGameState = [
74
+ createAllIncorrectWordleRow("WRONG"),
75
+ createAllIncorrectWordleRow("TESTS"),
76
+ createAllIncorrectWordleRow("FLAME"),
77
+ createAllCorrectWordleRow("HOUSE"),
78
+ ];
79
+
80
+ expect(calculateWordleScore(gameState)).toBe(7);
81
+ });
82
+
83
+ it("should return 6 when won on fifth try (5 + 1 bonus)", () => {
84
+ const gameState: WordleGameState = [
85
+ createAllIncorrectWordleRow("WRONG"),
86
+ createAllIncorrectWordleRow("TESTS"),
87
+ createAllIncorrectWordleRow("FLAME"),
88
+ createAllIncorrectWordleRow("STEAM"),
89
+ createAllCorrectWordleRow("HOUSE"),
90
+ ];
91
+
92
+ expect(calculateWordleScore(gameState)).toBe(6);
93
+ });
94
+
95
+ it("should return 5 when won on sixth try (5 + 0 bonus)", () => {
96
+ const gameState: WordleGameState = [
97
+ createAllIncorrectWordleRow("WRONG"),
98
+ createAllIncorrectWordleRow("TESTS"),
99
+ createAllIncorrectWordleRow("FLAME"),
100
+ createAllIncorrectWordleRow("STEAM"),
101
+ createAllIncorrectWordleRow("GRAND"),
102
+ createAllCorrectWordleRow("HOUSE"),
103
+ ];
104
+
105
+ expect(calculateWordleScore(gameState)).toBe(5);
106
+ });
107
+
108
+ it("should find winning row even when not the last row", () => {
109
+ const gameState: WordleGameState = [
110
+ createAllIncorrectWordleRow("WRONG"),
111
+ createAllCorrectWordleRow("HOUSE"), // Winning row
112
+ createAllIncorrectWordleRow("TESTS"), // Additional row after win
113
+ ];
114
+
115
+ expect(calculateWordleScore(gameState)).toBe(9); // Won on second try
116
+ });
117
+ });
118
+
119
+ describe("Edge cases", () => {
120
+ it("should handle single row game state", () => {
121
+ const gameState: WordleGameState = [createWordleRow("HOUSE", "HXUXX")];
122
+
123
+ expect(calculateWordleScore(gameState)).toBe(2);
124
+ });
125
+
126
+ it("should handle empty game state", () => {
127
+ const gameState: WordleGameState = [];
128
+
129
+ expect(calculateWordleScore(gameState)).toBe(0);
130
+ });
131
+
132
+ it("should handle different word lengths", () => {
133
+ const gameState: WordleGameState = [createAllCorrectWordleRow("HELLO")];
134
+
135
+ expect(calculateWordleScore(gameState)).toBe(10); // 5 + 5 bonus
136
+ });
137
+
138
+ it("should handle 4-letter words", () => {
139
+ const gameState: WordleGameState = [createAllCorrectWordleRow("WORD")];
140
+
141
+ expect(calculateWordleScore(gameState)).toBe(9); // 4 + 5 bonus
142
+ });
143
+
144
+ it("should handle 6-letter words", () => {
145
+ const gameState: WordleGameState = [createAllCorrectWordleRow("PUZZLE")];
146
+
147
+ expect(calculateWordleScore(gameState)).toBe(11); // 6 + 5 bonus
148
+ });
149
+ });
150
+
151
+ describe("Mixed scenarios", () => {
152
+ it("should handle partial correct letters in non-winning rows", () => {
153
+ const gameState: WordleGameState = [
154
+ createWordleRow("HOUSE", "HXUXX"), // 2 correct
155
+ createWordleRow("TESTS", "XEXEX"), // 2 correct
156
+ createAllCorrectWordleRow("FLAME"), // All correct - winning row
157
+ ];
158
+
159
+ expect(calculateWordleScore(gameState)).toBe(8); // Won on third try: 5 + 3 bonus
160
+ });
161
+
162
+ it("should prioritize win condition over partial correct letters", () => {
163
+ const gameState: WordleGameState = [
164
+ createAllCorrectWordleRow("HOUSE"), // All correct - should win here
165
+ createWordleRow("TESTS", "TXXXX"), // Partial correct in later row
166
+ ];
167
+
168
+ expect(calculateWordleScore(gameState)).toBe(10); // Won on first try: 5 + 5 bonus
169
+ });
170
+ });
171
+ });
@@ -1,7 +1,7 @@
1
1
  import type { StringifiedWordleGameState } from "./wordle-game-state";
2
2
  import type { Timestamp } from "firebase-admin/firestore";
3
3
 
4
- export type WorldeGameStateDocument = {
4
+ export type WordleGameStateDocument = {
5
5
  calculated_score: number;
6
6
  game_state: StringifiedWordleGameState;
7
7
  has_finished: boolean;