Better set detection

This commit is contained in:
Kalle
2026-08-06 09:38:44 +03:00
parent 1d4c9a3b89
commit 015f557d42
2 changed files with 41 additions and 5 deletions

View File

@@ -8,7 +8,7 @@
* readable names is inconclusive and never opens a new set.
*/
import type { ScannerMatch } from "./scanner-match";
import { closestBy } from "./text";
import { matchKey, rankBy } from "./text";
/**
* closestBy score two reads of the same name must reach — 0.7 forgives one
@@ -48,7 +48,7 @@ function rosterNames(match: ScannerMatch): string[] {
/**
* Whether two rosters read as the same eight players: every name of the
* smaller roster must find a fuzzy partner in the other, short of
* smaller roster must find a partner in the other, short of
* MAX_SUBS_PER_GAME misses. Partial reads compare only what both saw, so a
* minimap-sourced half-roster still chains a set together.
*/
@@ -56,11 +56,33 @@ function sameRoster(a: readonly string[], b: readonly string[]): boolean {
const remaining = [...b];
let matched = 0;
for (const name of a) {
const best = closestBy(name, remaining, (entry) => entry);
if (best && best.score >= SAME_NAME_SCORE) {
const partner = rankBy(name, remaining, (entry) => entry).find(
({ entry, score }) =>
score >= SAME_NAME_SCORE || sharedPrefixRead(name, entry),
);
if (partner) {
matched++;
remaining.splice(remaining.indexOf(best.entry), 1);
remaining.splice(remaining.indexOf(partner.entry), 1);
}
}
return Math.min(a.length, b.length) - matched <= MAX_SUBS_PER_GAME;
}
/**
* Whether two reads of a name agree on a leading prefix long enough that the
* difference reads as tail damage, not another player. CJK names run 2-4
* glyphs, so a single truncated (れた → れ) or garbled (ほった → ほっ′`)
* tail glyph sinks the edit-distance score below any usable threshold while
* the OCR damage sits, as it nearly always does, at the end of the row.
*/
function sharedPrefixRead(a: string, b: string): boolean {
const keyA = matchKey(a);
const keyB = matchKey(b);
const shorter = Math.min(keyA.length, keyB.length);
if (shorter === 0) return false;
let prefix = 0;
while (prefix < shorter && keyA[prefix] === keyB[prefix]) prefix++;
// a whole-read prefix is a truncation; a 2+ glyph prefix covering two
// thirds of the shorter read is a solid read with a garbled tail
return prefix === shorter || (prefix >= 2 && prefix * 3 >= shorter * 2);
}

View File

@@ -72,6 +72,20 @@ test("assignMatchSets", async (t) => {
assert.deepEqual(assignMatchSets(games), [1, 1]);
});
await t.test("tail damage on short CJK names is not a roster change", () => {
const games = [
match(
["まどろみ", "ほった", "きのちゃんねる !", "くてんちゃんねる"],
["かなえる", "たん", "ロット", "れた"],
),
match(
["まどろみ", "ほっ′`", "きのちゃんねる !", "くてんちゃんねる"],
["かなえる", "たん", "ロット", "れ"],
),
];
assert.deepEqual(assignMatchSets(games), [1, 1]);
});
await t.test("one sub is tolerated, two are a new set", () => {
const oneSub = [
match(TEAM_A, TEAM_B),