feat: fuzzy-match disposition names to the 5 canonical ITC dispositions
Hand-typed disposition text (submitted-list free text, BCP's own field) often has small typos like "Priority Assests". Match it against the fixed Purge the Foe / Take and Hold / Priority Assets / Disruption / Reconnaissance list via Levenshtein distance so typo'd variants merge into the correct bar instead of forking off as duplicates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+51
-3
@@ -114,6 +114,48 @@ const TOKEN_KEY = "bcp_auth_token";
|
||||
const getToken = () => { try { return localStorage.getItem(TOKEN_KEY) || ""; } catch { return ""; } };
|
||||
const setToken = t => { try { t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); } catch {} };
|
||||
|
||||
// This event tracks the five ITC-standard mission dispositions. Hand-typed
|
||||
// sources (a "Dispositions Used:" line in a submitted list, BCP's own free
|
||||
// text) routinely have small typos (e.g. "Priority Assests"), so every
|
||||
// disposition value is fuzzy-matched against this fixed list before being
|
||||
// counted or displayed, keeping typo'd variants from forking off as their
|
||||
// own duplicate bar.
|
||||
const CANONICAL_DISPOSITIONS = ["Purge the Foe", "Take and Hold", "Priority Assets", "Disruption", "Reconnaissance"];
|
||||
|
||||
function levenshtein(a, b) {
|
||||
const m = a.length, n = b.length;
|
||||
if (!m) return n;
|
||||
if (!n) return m;
|
||||
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
||||
for (let i = 1; i <= m; i++) {
|
||||
const cur = [i];
|
||||
for (let j = 1; j <= n; j++) {
|
||||
cur[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j - 1], prev[j], cur[j - 1]);
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
return prev[n];
|
||||
}
|
||||
|
||||
// Nearest canonical disposition, or null if nothing is close enough to be
|
||||
// confident it's a typo rather than unrelated text. Threshold scales with
|
||||
// name length (~25%) so e.g. "Priority Assests" (1 char off "Priority
|
||||
// Assets") matches, but a short unrelated line doesn't accidentally match
|
||||
// "Disruption".
|
||||
function closestDisposition(name) {
|
||||
const norm = String(name || "").trim().toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " ");
|
||||
if (!norm) return null;
|
||||
let best = null, bestDist = Infinity;
|
||||
for (const canonical of CANONICAL_DISPOSITIONS) {
|
||||
const dist = levenshtein(norm, canonical.toLowerCase());
|
||||
if (dist < bestDist) { bestDist = dist; best = canonical; }
|
||||
}
|
||||
const threshold = Math.max(1, Math.round(best.length * 0.25));
|
||||
return bestDist <= threshold ? best : null;
|
||||
}
|
||||
|
||||
const correctDisposition = name => closestDisposition(name) || name;
|
||||
|
||||
// The vocabulary of disposition names is whatever this event's own roster
|
||||
// already shows (e.g. "Take and Hold", "Priority Assets", …) — deriving it
|
||||
// from the event rather than hardcoding keeps this working for other game
|
||||
@@ -172,11 +214,13 @@ function extractFactionAndDisposition(armyList, knownDisp) {
|
||||
const dm = text.match(/^Dispositions?\s+Used:\s*(.+)$/im);
|
||||
if (dm) {
|
||||
const captured = dm[1].trim();
|
||||
disposition = knownDisp.get(captured.toLowerCase()) || captured;
|
||||
disposition = knownDisp.get(captured.toLowerCase()) || closestDisposition(captured) || captured;
|
||||
}
|
||||
if (!disposition) {
|
||||
for (const rawLine of text.split("\n")) {
|
||||
const canonical = knownDisp.get(rawLine.trim().toLowerCase());
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
const canonical = knownDisp.get(line.toLowerCase()) || closestDisposition(line);
|
||||
if (canonical) { disposition = canonical; break; }
|
||||
}
|
||||
}
|
||||
@@ -546,7 +590,11 @@ function clearBanner() {
|
||||
function render() {
|
||||
if (!state) return;
|
||||
const { event, teamplayers, isTeamEvent, itcRanks, itcLoading, backfill, backfillStatus } = state;
|
||||
const roster = applyBackfill(state.roster, backfill);
|
||||
const roster = applyBackfill(state.roster, backfill).map(r =>
|
||||
r.subFaction?.name
|
||||
? { ...r, subFaction: { ...r.subFaction, name: correctDisposition(r.subFaction.name) } }
|
||||
: r
|
||||
);
|
||||
const teams = isTeamEvent ? computeTeamRollups(roster, teamplayers) : [];
|
||||
|
||||
const tokenStatusEl = $("bcp-token-status");
|
||||
|
||||
Reference in New Issue
Block a user