From 455e5dca805a16bdb4a8b4864cb5da711598de0d Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 19:24:55 +0200 Subject: [PATCH 1/7] chore: rework char counting function a little --- frontend/__tests__/test/events/stats.spec.ts | 8 +- frontend/__tests__/utils/strings.spec.ts | 188 ++++++++++++++++-- frontend/src/ts/input/handlers/insert-text.ts | 5 +- frontend/src/ts/test/events/stats.ts | 10 +- frontend/src/ts/test/events/types.ts | 3 + frontend/src/ts/test/test-stats.ts | 10 +- frontend/src/ts/utils/strings.ts | 34 ++-- 7 files changed, 218 insertions(+), 40 deletions(-) diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index c06d81082843..a30a63e339f9 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -88,6 +88,7 @@ function input( inputType: string; isCompositionEnding: boolean; inputStopped: boolean; + isCommitSpace: true; }> = {}, ): InputEventData { return { @@ -817,7 +818,12 @@ describe("stats.ts", () => { logTestEvent( "input", 1250, - input({ charIndex: 3, wordIndex: 0, data: " " }), + input({ + charIndex: 3, + wordIndex: 0, + data: " ", + isCommitSpace: true, + }), ); // type "w" on second word logTestEvent( diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index dbe026559e5f..ed09dbd7fafb 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -811,7 +811,7 @@ describe("string utils", () => { }, }, { - description: "incorrect, last word, early space", + description: "incorrect, last word, early literal space", input: { inputWord: "he ", targetWord: "hello", @@ -821,9 +821,9 @@ describe("string utils", () => { expected: { allCorrect: 2, correctWord: 0, - incorrect: 0, + incorrect: 1, extra: 0, - missed: 3, + missed: 2, }, }, { @@ -838,17 +838,18 @@ describe("string utils", () => { allCorrect: 4, correctWord: 0, incorrect: 1, - extra: 0, + extra: 1, missed: 0, }, }, { - description: "correct space, incorrect word", + description: "correct space, incorrect word (commit space)", input: { inputWord: "helol ", targetWord: "hello ", lastWord: true, shouldLastPartialWordCount: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 3, @@ -859,12 +860,30 @@ describe("string utils", () => { }, }, { - description: "single incorrect char", + description: "correct space, incorrect word (literal space)", + input: { + inputWord: "helol ", + targetWord: "hello ", + lastWord: true, + shouldLastPartialWordCount: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 3, + correctWord: 0, + incorrect: 2, + extra: 1, + missed: 0, + }, + }, + { + description: "single incorrect char (commit space)", input: { inputWord: "hxllo ", targetWord: "hello ", lastWord: false, shouldLastPartialWordCount: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 4, @@ -874,6 +893,24 @@ describe("string utils", () => { missed: 0, }, }, + { + description: + "single incorrect char (literal space — stopOnError=word)", + input: { + inputWord: "hxllo ", + targetWord: "hello ", + lastWord: false, + shouldLastPartialWordCount: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 4, + correctWord: 0, + incorrect: 1, + extra: 1, + missed: 0, + }, + }, { description: "one extra char", input: { @@ -1020,12 +1057,13 @@ describe("string utils", () => { }, }, { - description: "partial correct, with space", + description: "partial correct, with space (commit)", input: { inputWord: "helxx ", targetWord: "hello ", lastWord: false, shouldLastPartialWordCount: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 3, @@ -1035,6 +1073,23 @@ describe("string utils", () => { missed: 0, }, }, + { + description: "partial correct, with space (literal)", + input: { + inputWord: "helxx ", + targetWord: "hello ", + lastWord: false, + shouldLastPartialWordCount: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 3, + correctWord: 0, + incorrect: 2, + extra: 1, + missed: 0, + }, + }, { description: "newlines", input: { @@ -1085,7 +1140,7 @@ describe("string utils", () => { }, { description: - "incorrect last word, trailing confirm space, timed (stopOnError=word)", + "incorrect last word, trailing literal space, timed (stopOnError=word)", input: { inputWord: "jhow ", targetWord: "how", @@ -1096,6 +1151,112 @@ describe("string utils", () => { allCorrect: 0, correctWord: 0, incorrect: 3, + extra: 2, + missed: 0, + }, + }, + { + description: + "trailing literal space past target — counts as extra (stopOnError=word)", + input: { + inputWord: "xow ", + targetWord: "how", + lastWord: true, + shouldLastPartialWordCount: true, + }, + expected: { + allCorrect: 2, + correctWord: 0, + incorrect: 1, + extra: 1, + missed: 0, + }, + }, + { + description: + "multiple literal trailing spaces on wrong word — all count as extra", + input: { + inputWord: "xonl ", + targetWord: "only ", + lastWord: true, + shouldLastPartialWordCount: true, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 0, + correctWord: 0, + incorrect: 4, + extra: 2, + missed: 0, + }, + }, + { + description: + "early literal space at non-space slot with creditPartial — counts as incorrect", + input: { + inputWord: "x ", + targetWord: "get", + lastWord: true, + shouldLastPartialWordCount: true, + }, + expected: { + allCorrect: 0, + correctWord: 0, + incorrect: 2, + extra: 0, + missed: 0, + }, + }, + { + description: + "trailing commit-space append past target — not counted (correct last word + commit)", + input: { + inputWord: "how ", + targetWord: "how", + lastWord: true, + shouldLastPartialWordCount: true, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 3, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: + "incorrect word with commit space — wrong word advanced (stopOnError=off)", + input: { + inputWord: "xello ", + targetWord: "hello ", + lastWord: false, + shouldLastPartialWordCount: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 4, + correctWord: 0, + incorrect: 2, + extra: 0, + missed: 0, + }, + }, + { + description: + "incorrect word with literal trailing space — uncommitted (stopOnError=word)", + input: { + inputWord: "xello ", + targetWord: "hello ", + lastWord: false, + shouldLastPartialWordCount: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 4, + correctWord: 0, + incorrect: 1, extra: 1, missed: 0, }, @@ -1107,15 +1268,18 @@ describe("string utils", () => { Strings.countChars( input.inputWord, input.targetWord, - input.lastWord, - input.shouldLastPartialWordCount, + input.lastWord && input.shouldLastPartialWordCount, + // non-last words always commit via space in real callers; last + // word is in-flight unless explicitly overridden + input.endsWithCommitSpace ?? !input.lastWord, ), ).toEqual(expected); }); }); - it("space counts as incorrect when word is wrong", () => { - const result = Strings.countChars("hell ", "hello ", false, false); + it("early space (typed before reaching target's space) counts as incorrect", () => { + // non-last word: space commits the (wrong) word, so endsWithCommitSpace=true + const result = Strings.countChars("hell ", "hello ", false, true); expect(result.incorrect).toBe(1); }); }); diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index e4f87c7e4f27..998711ca3c0c 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -221,6 +221,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // (e.g. beforeTestWordChange's updateWordLetters, getWordBurst) see the // completed event in derivation. Otherwise the just-typed trigger char // (space/newline) is missing — visible as missing \n element in zen mode. + const isCommitSpace = charIsSpace && !shouldInsertSpace; logTestEvent("input", now, { inputType: "insertText", data, @@ -232,8 +233,8 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // when shouldInsertSpace is true, the space char was already inserted via // syncWithInputElement above — only append " " for the advance-space case, // else recorded inputValue ends up with a doubled trailing space. - inputValue: - inputValueAfterEvent + (charIsSpace && !shouldInsertSpace ? " " : ""), + inputValue: inputValueAfterEvent + (isCommitSpace ? " " : ""), + isCommitSpace: isCommitSpace ? true : undefined, }); // going to next word diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index c16a98eaedb1..110dbf3f3cc7 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -292,11 +292,17 @@ function countCharsForWords( targetWord = Hangul.disassemble(targetWord).join(""); } + const lastEvent = events[events.length - 1]; + const endsWithCommitSpace = + lastEvent !== undefined && + lastEvent.data.inputType === "insertText" && + lastEvent.data.isCommitSpace === true; + const c = countChars( simulatedInput, targetWord, - lastWord, - shouldCountPartialLastWord, + lastWord && shouldCountPartialLastWord, + endsWithCommitSpace, ); acc.allCorrect += c.allCorrect; acc.correctWord += c.correctWord; diff --git a/frontend/src/ts/test/events/types.ts b/frontend/src/ts/test/events/types.ts index 8f9909549b63..9f113b3d6b12 100644 --- a/frontend/src/ts/test/events/types.ts +++ b/frontend/src/ts/test/events/types.ts @@ -91,6 +91,9 @@ export type InputEventData = correct: boolean; isCompositionEnding: boolean; inputStopped: boolean; + // true when this was a space that advanced to the next word (commit + // attempt) rather than being inserted as a literal character + isCommitSpace?: true; }) | (BaseInputEventData & { inputType: DeleteInputType; diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index c13373c654c0..24895fe2dd41 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -277,16 +277,22 @@ function countChars(final = false): CharCount { Config.mode === "time" || (Config.mode === "custom" && CustomText.getLimit().mode === "time"); + if (final) { + console.log("filan"); + } + for (let i = 0; i < inputWords.length; i++) { const inputWord = inputWords[i] as string; const targetWord = targetWords[i] as string; + const isLastInputWord = i === inputWords.length - 1; const { correctWord, allCorrect, incorrect, missed, extra } = countCharsUtils( inputWord, targetWord, - i === inputWords.length - 1, - (isTimedTest && final) || !final, + isLastInputWord && ((isTimedTest && final) || !final), + // historical words advanced via commit space; last is in-flight + !isLastInputWord, ); correctWordChars += correctWord; diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index df1b253d0551..7ab8e77af587 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -411,8 +411,8 @@ export type CharCounts = { export function countChars( inputWord: string, targetWord: string, - lastWord: boolean, - shouldLastPartialWordCount: boolean, + creditPartial: boolean, + endsWithCommitSpace: boolean, ): CharCounts { let allCorrect = 0; let correctWord = 0; @@ -428,44 +428,36 @@ export function countChars( const targetChar = targetWord[i]; if (inputChar === targetChar) { - // do not count correct space characters if the word is not correct + // matching space on a wrong word: incorrect if it was a commit attempt + // (word advanced via space), extra if it was a literal space (stopOnError + // blocked the commit so the space ended up as a typed character) if (targetChar === " ") { if (wordCorrect) { allCorrect += 1; - } else { + } else if (endsWithCommitSpace) { incorrect += 1; + } else { + extra += 1; } } else { allCorrect += 1; } - if ( - wordCorrect || - (lastWord && shouldLastPartialWordCount && wordPartiallyCorrect) - ) { + if (wordCorrect || (creditPartial && wordPartiallyCorrect)) { correctWord += 1; } } else if (inputChar === undefined) { //missed char - if (!(lastWord && shouldLastPartialWordCount)) { + if (!creditPartial) { missed += 1; } } else if ( - lastWord && + endsWithCommitSpace && inputChar === " " && targetChar === undefined && !targetWord.endsWith(" ") ) { - // trailing confirm space on incorrect last word — not counted - } else if ( - lastWord && - inputChar === " " && - targetChar !== undefined && - targetChar !== " " - ) { - // early submit space on last word — count slot as missed, not incorrect - if (!(lastWord && shouldLastPartialWordCount)) { - missed += 1; - } + // commit-space append past target (e.g. correctly typed last word) — + // not a literal typed char, don't count } else if ( targetChar === undefined || (targetChar === " " && inputChar !== " " && !inputWord.includes(" ")) From 1dbddc45bd46541e51b1c619296e8b4a53511263 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 19:32:48 +0200 Subject: [PATCH 2/7] chore: bump version --- frontend/src/ts/test/test-logic.ts | 8 ++++++-- packages/contracts/src/results.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 030b3b9ee1e9..a48f33f01b84 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1014,6 +1014,10 @@ function compareCompletedEvents( continue; } + if (key === "keyConsistency") { + continue; + } + if (key === "wpm" || key === "rawWpm") { val1 = Numbers.roundTo2(val1 as number); val2 = Numbers.roundTo2(val2 as number); @@ -1103,7 +1107,7 @@ function compareCompletedEvents( ); } } - } else if (key === "wpmConsistency" || key === "keyConsistency") { + } else if (key === "wpmConsistency") { const a = val1 as number; const b = val2 as number; const ref = Math.max( @@ -1313,7 +1317,7 @@ function compareCompletedEvents( difficulty: ce.difficulty, duration: ce.testDuration, funboxes: getActiveFunboxNames().join(","), - version: 13, + version: 14, data: { words: TestWords.words.list.join(" "), events: getAllTestEvents(), diff --git a/packages/contracts/src/results.ts b/packages/contracts/src/results.ts index 981caf7bc2f4..ef63e76b93b0 100644 --- a/packages/contracts/src/results.ts +++ b/packages/contracts/src/results.ts @@ -75,7 +75,7 @@ export const ReportCompletedEventMismatchRequestSchema = z.object({ difficulty: DifficultySchema.optional(), duration: z.number().max(200).optional(), funboxes: z.string().max(100).optional(), - version: z.literal(13), + version: z.literal(14), data: z.object({ words: z.string().max(10000), events: z.array(z.record(z.unknown())), From 7aba9d05854c0c5806bffd59ce24a9f77ba378c3 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 22:04:33 +0200 Subject: [PATCH 3/7] chore: fix early spaces on last words not counted correctly --- frontend/__tests__/utils/strings.spec.ts | 17 +++++++++++++++++ frontend/src/ts/utils/strings.ts | 12 ++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index ed09dbd7fafb..465c2e949a12 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -1261,6 +1261,23 @@ describe("string utils", () => { missed: 0, }, }, + { + description: "early space on last word", + input: { + inputWord: "h ", + targetWord: "hello", + lastWord: true, + shouldLastPartialWordCount: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 1, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 4, + }, + }, ]; it.each(testCases)("$description", ({ input, expected }) => { diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 7ab8e77af587..234df29522ab 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -453,11 +453,15 @@ export function countChars( } else if ( endsWithCommitSpace && inputChar === " " && - targetChar === undefined && - !targetWord.endsWith(" ") + i === inputWord.length - 1 && + !targetWord.endsWith(" ") && + inputWord.length !== targetWord.length ) { - // commit-space append past target (e.g. correctly typed last word) — - // not a literal typed char, don't count + // commit-space on last word — not a literal typed char. If it landed + // before reaching target's end, that slot is effectively missed. + if (targetChar !== undefined && !creditPartial) { + missed += 1; + } } else if ( targetChar === undefined || (targetChar === " " && inputChar !== " " && !inputWord.includes(" ")) From 54d20282b12e8de7019367e524b9f7c51040d231 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 22:14:08 +0200 Subject: [PATCH 4/7] chore: rewrite countChars tests --- frontend/__tests__/utils/strings.spec.ts | 300 ++++++++++++++--------- 1 file changed, 183 insertions(+), 117 deletions(-) diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 465c2e949a12..50b8d3d9941f 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -590,8 +590,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 3, @@ -606,8 +606,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -622,8 +622,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -638,8 +638,8 @@ describe("string utils", () => { input: { inputWord: "hello ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 6, @@ -654,8 +654,8 @@ describe("string utils", () => { input: { inputWord: "hello ", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 6, @@ -670,8 +670,8 @@ describe("string utils", () => { input: { inputWord: "hello", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 5, @@ -686,8 +686,8 @@ describe("string utils", () => { input: { inputWord: "helloxxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 5, @@ -697,30 +697,13 @@ describe("string utils", () => { missed: 0, }, }, - { - description: - "correct, partial, not last, should count (should count last partial)", - input: { - inputWord: "hel", - targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, - }, - expected: { - allCorrect: 3, - correctWord: 0, - incorrect: 0, - extra: 0, - missed: 3, - }, - }, { description: "early space", input: { inputWord: "hel ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 3, @@ -735,8 +718,8 @@ describe("string utils", () => { input: { inputWord: "xxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 0, @@ -751,8 +734,8 @@ describe("string utils", () => { input: { inputWord: "xxxxxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 0, @@ -767,8 +750,8 @@ describe("string utils", () => { input: { inputWord: "xexlxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 2, @@ -783,8 +766,8 @@ describe("string utils", () => { input: { inputWord: "xexl ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 2, @@ -799,8 +782,8 @@ describe("string utils", () => { input: { inputWord: "xello", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: false, }, expected: { allCorrect: 4, @@ -815,8 +798,8 @@ describe("string utils", () => { input: { inputWord: "he ", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: false, }, expected: { allCorrect: 2, @@ -831,8 +814,8 @@ describe("string utils", () => { input: { inputWord: "xello ", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: false, }, expected: { allCorrect: 4, @@ -847,8 +830,7 @@ describe("string utils", () => { input: { inputWord: "helol ", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: true, }, expected: { @@ -864,8 +846,7 @@ describe("string utils", () => { input: { inputWord: "helol ", targetWord: "hello ", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: false, }, expected: { @@ -881,8 +862,7 @@ describe("string utils", () => { input: { inputWord: "hxllo ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: true, }, expected: { @@ -899,8 +879,7 @@ describe("string utils", () => { input: { inputWord: "hxllo ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: false, }, expected: { @@ -916,8 +895,8 @@ describe("string utils", () => { input: { inputWord: "helloo ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 5, @@ -932,8 +911,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 3, @@ -949,8 +928,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -965,8 +944,8 @@ describe("string utils", () => { input: { inputWord: "xxx", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 0, @@ -981,24 +960,8 @@ describe("string utils", () => { input: { inputWord: "hel", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: false, - }, - expected: { - allCorrect: 3, - correctWord: 0, - incorrect: 0, - extra: 0, - missed: 2, - }, - }, - { - description: "non-last word ignores shouldLastPartialWordCount", - input: { - inputWord: "hel", - targetWord: "hello", - lastWord: false, - shouldLastPartialWordCount: true, + creditPartial: false, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -1013,8 +976,8 @@ describe("string utils", () => { input: { inputWord: "", targetWord: "hello", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 0, @@ -1029,8 +992,8 @@ describe("string utils", () => { input: { inputWord: "hello", targetWord: "", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 0, @@ -1045,8 +1008,8 @@ describe("string utils", () => { input: { inputWord: "hello ", targetWord: "hello\n", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 5, @@ -1061,8 +1024,7 @@ describe("string utils", () => { input: { inputWord: "helxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: true, }, expected: { @@ -1078,8 +1040,7 @@ describe("string utils", () => { input: { inputWord: "helxx ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: false, }, expected: { @@ -1095,8 +1056,8 @@ describe("string utils", () => { input: { inputWord: "hello\n", targetWord: "hello\n", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, + endsWithCommitSpace: true, }, expected: { allCorrect: 6, @@ -1111,8 +1072,8 @@ describe("string utils", () => { input: { inputWord: "abcx", targetWord: "abc ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -1127,8 +1088,8 @@ describe("string utils", () => { input: { inputWord: "abcx ", targetWord: "abc ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 3, @@ -1144,8 +1105,8 @@ describe("string utils", () => { input: { inputWord: "jhow ", targetWord: "how", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 0, @@ -1161,8 +1122,8 @@ describe("string utils", () => { input: { inputWord: "xow ", targetWord: "how", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 2, @@ -1178,8 +1139,7 @@ describe("string utils", () => { input: { inputWord: "xonl ", targetWord: "only ", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, endsWithCommitSpace: false, }, expected: { @@ -1196,8 +1156,8 @@ describe("string utils", () => { input: { inputWord: "x ", targetWord: "get", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, + endsWithCommitSpace: false, }, expected: { allCorrect: 0, @@ -1213,8 +1173,7 @@ describe("string utils", () => { input: { inputWord: "how ", targetWord: "how", - lastWord: true, - shouldLastPartialWordCount: true, + creditPartial: true, endsWithCommitSpace: true, }, expected: { @@ -1231,8 +1190,7 @@ describe("string utils", () => { input: { inputWord: "xello ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: true, }, expected: { @@ -1249,8 +1207,7 @@ describe("string utils", () => { input: { inputWord: "xello ", targetWord: "hello ", - lastWord: false, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: false, }, expected: { @@ -1266,8 +1223,7 @@ describe("string utils", () => { input: { inputWord: "h ", targetWord: "hello", - lastWord: true, - shouldLastPartialWordCount: false, + creditPartial: false, endsWithCommitSpace: true, }, expected: { @@ -1278,6 +1234,118 @@ describe("string utils", () => { missed: 4, }, }, + { + description: "early space on last word with creditPartial", + input: { + inputWord: "h ", + targetWord: "hello", + creditPartial: true, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 1, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: "wrong word, trailing commit-space past target", + input: { + inputWord: "xow ", + targetWord: "how", + creditPartial: true, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 2, + correctWord: 0, + incorrect: 1, + extra: 0, + missed: 0, + }, + }, + { + description: "both empty", + input: { + inputWord: "", + targetWord: "", + creditPartial: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 0, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: "empty input with creditPartial", + input: { + inputWord: "", + targetWord: "hello", + creditPartial: true, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 0, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: "correct last word, no trailing space anywhere", + input: { + inputWord: "hello", + targetWord: "hello", + creditPartial: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 5, + correctWord: 5, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: "mid-word literal space followed by more input", + input: { + inputWord: "hel o", + targetWord: "hello ", + creditPartial: false, + endsWithCommitSpace: false, + }, + expected: { + allCorrect: 4, + correctWord: 0, + incorrect: 1, + extra: 0, + missed: 1, + }, + }, + { + description: "newline typed in place of target's trailing space", + input: { + inputWord: "hello\n", + targetWord: "hello ", + creditPartial: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 5, + correctWord: 0, + incorrect: 0, + extra: 1, + missed: 0, + }, + }, ]; it.each(testCases)("$description", ({ input, expected }) => { @@ -1285,10 +1353,8 @@ describe("string utils", () => { Strings.countChars( input.inputWord, input.targetWord, - input.lastWord && input.shouldLastPartialWordCount, - // non-last words always commit via space in real callers; last - // word is in-flight unless explicitly overridden - input.endsWithCommitSpace ?? !input.lastWord, + input.creditPartial, + input.endsWithCommitSpace, ), ).toEqual(expected); }); From de706a9332b7ea4021703afb4f8ba649b5a8f5c3 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 22:14:32 +0200 Subject: [PATCH 5/7] chore: bump version --- frontend/src/ts/test/test-logic.ts | 2 +- packages/contracts/src/results.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index a48f33f01b84..fd32e95332be 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1317,7 +1317,7 @@ function compareCompletedEvents( difficulty: ce.difficulty, duration: ce.testDuration, funboxes: getActiveFunboxNames().join(","), - version: 14, + version: 15, data: { words: TestWords.words.list.join(" "), events: getAllTestEvents(), diff --git a/packages/contracts/src/results.ts b/packages/contracts/src/results.ts index ef63e76b93b0..168dc37f5bf3 100644 --- a/packages/contracts/src/results.ts +++ b/packages/contracts/src/results.ts @@ -75,7 +75,7 @@ export const ReportCompletedEventMismatchRequestSchema = z.object({ difficulty: DifficultySchema.optional(), duration: z.number().max(200).optional(), funboxes: z.string().max(100).optional(), - version: z.literal(14), + version: z.literal(15), data: z.object({ words: z.string().max(10000), events: z.array(z.record(z.unknown())), From e477566a3949d0deab6ed3b725aa4826ac651728 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 22:57:30 +0200 Subject: [PATCH 6/7] chore: more countChars tests, fix one case --- frontend/__tests__/utils/strings.spec.ts | 66 ++++++++++++++++++++++++ frontend/src/ts/utils/strings.ts | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 50b8d3d9941f..697f9d02bae8 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -777,6 +777,72 @@ describe("string utils", () => { missed: 1, }, }, + { + description: + "last word, early commit space, input length == target length", + input: { + inputWord: "no ", + targetWord: "nom", + creditPartial: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 2, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 1, + }, + }, + { + description: "last word correctly typed + commit space (past target)", + input: { + inputWord: "hello ", + targetWord: "hello", + creditPartial: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 5, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, + { + description: "last word with extra char + commit space (past target)", + input: { + inputWord: "hellox ", + targetWord: "hello", + creditPartial: false, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 5, + correctWord: 0, + incorrect: 0, + extra: 1, + missed: 0, + }, + }, + { + description: + "last word, early commit space, equal length, creditPartial (trailing space breaks prefix match)", + input: { + inputWord: "no ", + targetWord: "nom", + creditPartial: true, + endsWithCommitSpace: true, + }, + expected: { + allCorrect: 2, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, { description: "incorrect, last word, quick end", input: { diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 234df29522ab..9599e4de4d70 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -455,7 +455,7 @@ export function countChars( inputChar === " " && i === inputWord.length - 1 && !targetWord.endsWith(" ") && - inputWord.length !== targetWord.length + targetChar !== "\n" ) { // commit-space on last word — not a literal typed char. If it landed // before reaching target's end, that slot is effectively missed. From 1a8d448f704da293729a8205c1fcbfe7f5dbea04 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 9 Jun 2026 23:00:14 +0200 Subject: [PATCH 7/7] chore: bump version --- frontend/src/ts/test/test-logic.ts | 2 +- packages/contracts/src/results.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index fd32e95332be..c337c1eac706 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1317,7 +1317,7 @@ function compareCompletedEvents( difficulty: ce.difficulty, duration: ce.testDuration, funboxes: getActiveFunboxNames().join(","), - version: 15, + version: 16, data: { words: TestWords.words.list.join(" "), events: getAllTestEvents(), diff --git a/packages/contracts/src/results.ts b/packages/contracts/src/results.ts index 168dc37f5bf3..1b1a0bf28214 100644 --- a/packages/contracts/src/results.ts +++ b/packages/contracts/src/results.ts @@ -75,7 +75,7 @@ export const ReportCompletedEventMismatchRequestSchema = z.object({ difficulty: DifficultySchema.optional(), duration: z.number().max(200).optional(), funboxes: z.string().max(100).optional(), - version: z.literal(15), + version: z.literal(16), data: z.object({ words: z.string().max(10000), events: z.array(z.record(z.unknown())),