@kekkle/shared 1.2.0 → 1.3.0
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.
- package/dist/cjs/fixtures/character-state.fixtures.d.ts +2 -0
- package/dist/cjs/fixtures/character-state.fixtures.js +6 -0
- package/dist/cjs/fixtures/wordle-row.fixtures.d.ts +5 -0
- package/dist/cjs/fixtures/wordle-row.fixtures.js +26 -0
- package/dist/cjs/helpers/worlde/all-correct-in-row.d.ts +5 -0
- package/dist/cjs/helpers/worlde/all-correct-in-row.js +9 -0
- package/dist/cjs/helpers/worlde/calculate-wordle-score.d.ts +12 -0
- package/dist/cjs/helpers/worlde/calculate-wordle-score.js +45 -0
- package/dist/esm/fixtures/character-state.fixtures.d.ts +2 -0
- package/dist/esm/fixtures/character-state.fixtures.js +3 -0
- package/dist/esm/fixtures/wordle-row.fixtures.d.ts +5 -0
- package/dist/esm/fixtures/wordle-row.fixtures.js +20 -0
- package/dist/esm/helpers/worlde/all-correct-in-row.d.ts +5 -0
- package/dist/esm/helpers/worlde/all-correct-in-row.js +6 -0
- package/dist/esm/helpers/worlde/calculate-wordle-score.d.ts +12 -0
- package/dist/esm/helpers/worlde/calculate-wordle-score.js +42 -0
- package/package.json +14 -3
- package/src/fixtures/character-state.fixtures.ts +9 -0
- package/src/fixtures/wordle-row.fixtures.ts +36 -0
- package/src/helpers/permissions/is-valid-permission.vi.spec.ts +109 -0
- package/src/helpers/worlde/all-correct-in-row.ts +8 -0
- package/src/helpers/worlde/all-correct-in-row.vi.spec.ts +96 -0
- package/src/helpers/worlde/calculate-wordle-score.ts +53 -0
- package/src/helpers/worlde/calculate-wordle-score.vi.spec.ts +171 -0
|
@@ -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,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
|
+
}
|
|
@@ -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,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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kekkle/shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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": "
|
|
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,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
|
+
});
|