mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-23 03:26:16 -05:00
Faster
This commit is contained in:
@@ -98,10 +98,15 @@ sequenceDiagram
|
||||
0.25s — produced/casted VoDs cut screens to ~1s with transition flicker
|
||||
inside, so search cadence must not assume raw-gameplay screen lifetimes;
|
||||
gates are ~ms-cheap, so this costs little); once the gate passes it
|
||||
drops to the dense refine cadence for best-read refinement. Suppression ends a refinement streak
|
||||
once it stagnates by parse count AND elapsed time (~3s — the time floor
|
||||
drops to the dense refine cadence for best-read refinement (detectors
|
||||
with expensive parses override it via `refineIntervalS` — death 0.5s,
|
||||
minimap 0.4s). Suppression ends a refinement streak
|
||||
once it stagnates by parse count (default 6; per-detector
|
||||
`maxStagnantParses` — death 3) AND elapsed time (~3s — the time floor
|
||||
keeps a dense cadence from suppressing during a screen's entry animation
|
||||
before it is readable) or immediately at `sufficientConfidence`; death
|
||||
before it is readable) or immediately at `sufficientConfidence`, which
|
||||
sits just under each detector's measured clean-read confidence floor
|
||||
(fixture suite + confirmed scan events); death
|
||||
adds `rearmCooldownS` (safe because it fits inside the Death timeline
|
||||
merge window). `checkIntervalS` (objective: 1s)
|
||||
still hard-caps both phases and exempts from suppression; a detector can
|
||||
|
||||
@@ -97,6 +97,13 @@ export const DEATH_EVENT_TYPE = "Death";
|
||||
|
||||
/** The constant message line must read back at least this well to emit. */
|
||||
const LINE1_MIN_SCORE = 0.5;
|
||||
/**
|
||||
* A Latin template's constant line reading at least this well settles the
|
||||
* language and the (expensive) JA line reads are skipped. Fixture-measured:
|
||||
* Latin-language frames read their template at 0.889+, while JA frames'
|
||||
* best Latin-template score is 0.222.
|
||||
*/
|
||||
const LATIN_DECISIVE_SCORE = 0.85;
|
||||
/** Snapped weapon reading below this is reported as null (kept in debug). */
|
||||
const WEAPON_MIN_SCORE = 0.55;
|
||||
/** Burst-icon fallback match below this is ignored (kept in debug). */
|
||||
@@ -487,24 +494,32 @@ export function createDeathDetector(
|
||||
};
|
||||
line1 = readLine(SPLAT_LINE1_ROI, weaponGlyphs);
|
||||
line2 = readLine(WEAPON_LINE_ROI, weaponGlyphs);
|
||||
if (jaGlyphs) {
|
||||
jaWeaponLine = readLine(JA_WEAPON_LINE_ROI, jaGlyphs);
|
||||
jaConstLine = readLine(JA_CONST_LINE_ROI, jaGlyphs);
|
||||
}
|
||||
for (const t of DEATH_MESSAGE_TEMPLATES) {
|
||||
let constReading: string;
|
||||
if (isJaTemplate(t)) {
|
||||
if (!jaConstLine) continue;
|
||||
constReading = jaConstLine.text;
|
||||
} else {
|
||||
constReading = t.weaponLine === 1 ? line2.text : line1.text;
|
||||
}
|
||||
if (isJaTemplate(t)) continue;
|
||||
const constReading = t.weaponLine === 1 ? line2.text : line1.text;
|
||||
const score = closestEntry(constReading, [t.constText])?.score ?? 0;
|
||||
if (score > line1Score) {
|
||||
line1Score = score;
|
||||
template = t;
|
||||
}
|
||||
}
|
||||
// the JA line reads cost ~2x the Latin ones (condensed-kana atlas),
|
||||
// so they only run when no Latin template already owns the frame:
|
||||
// measured constant-line scores separate cleanly (Latin frames read
|
||||
// their template at 0.889+, JA frames' best Latin score is <= 0.222)
|
||||
if (jaGlyphs && line1Score < LATIN_DECISIVE_SCORE) {
|
||||
jaWeaponLine = readLine(JA_WEAPON_LINE_ROI, jaGlyphs);
|
||||
jaConstLine = readLine(JA_CONST_LINE_ROI, jaGlyphs);
|
||||
for (const t of DEATH_MESSAGE_TEMPLATES) {
|
||||
if (!isJaTemplate(t)) continue;
|
||||
const score =
|
||||
closestEntry(jaConstLine.text, [t.constText])?.score ?? 0;
|
||||
if (score > line1Score) {
|
||||
line1Score = score;
|
||||
template = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!template || line1Score < LINE1_MIN_SCORE) {
|
||||
gray.delete();
|
||||
rgb.delete();
|
||||
@@ -822,11 +837,17 @@ export function createDeathDetector(
|
||||
|
||||
// the death cam's animated background flickers the gate; after a
|
||||
// sufficient read the 4s rearm hold stays inside the timeline's 8s
|
||||
// Death merge window, so every parse it skips would merge anyway
|
||||
// Death merge window, so every parse it skips would merge anyway.
|
||||
// sufficientConfidence sits just under the measured clean-read floor
|
||||
// (fixtures 0.750-0.825, confirmed scan events 0.751+); the refine and
|
||||
// stagnation overrides cap what a ~1.4s parse can cost when a dirty
|
||||
// read never reaches it
|
||||
return {
|
||||
id: "death",
|
||||
sufficientConfidence: 0.98,
|
||||
refineIntervalS: 0.5,
|
||||
sufficientConfidence: 0.74,
|
||||
rearmCooldownS: 4,
|
||||
maxStagnantParses: 3,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -365,9 +365,11 @@ export function createMapStartDetector(
|
||||
];
|
||||
}
|
||||
|
||||
// just under the measured clean-read floor (fixtures 0.795-0.864,
|
||||
// confirmed scan events 0.854-1.0)
|
||||
return {
|
||||
id: "map-start",
|
||||
sufficientConfidence: 0.98,
|
||||
sufficientConfidence: 0.79,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -639,9 +639,16 @@ export function createMinimapDetector(
|
||||
];
|
||||
}
|
||||
|
||||
// sufficientConfidence sits just under the measured clean-read floor
|
||||
// (fixtures 0.746-0.800; confirmed scan events reach down to 0.699, and
|
||||
// those below the floor fall back to stagnation). The refine override
|
||||
// matters here because a map-open's confidence keeps fluctuating upward,
|
||||
// resetting the stagnation counter — without it a ~0.9s parse runs at
|
||||
// the dense cadence for the whole map-open
|
||||
return {
|
||||
id: "minimap",
|
||||
sufficientConfidence: 0.98,
|
||||
refineIntervalS: 0.4,
|
||||
sufficientConfidence: 0.73,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
* cadence must not assume raw-gameplay screen lifetimes; gates are
|
||||
* ~1.6ms, so searching at the analysis floor costs nothing). Once the
|
||||
* gate passes it drops to the dense `refineIntervalS` so the best-read
|
||||
* refinement loop still sees every frame it wants. `checkIntervalS`
|
||||
* refinement loop still sees every frame it wants; a detector whose
|
||||
* parse is expensive (death ~1.4s) declares its own refineIntervalS so
|
||||
* the dense default cannot multiply that cost. `checkIntervalS`
|
||||
* (objective counter) remains a hard cap on both phases and exempts the
|
||||
* detector from suppression, as before. `nextDueT()` lets the caller
|
||||
* skip a frame's canvas readback + normalize entirely when no detector
|
||||
@@ -47,8 +49,10 @@ export interface SchedulingInfo {
|
||||
id: string;
|
||||
checkIntervalS?: number;
|
||||
searchIntervalS?: number;
|
||||
refineIntervalS?: number;
|
||||
sufficientConfidence?: number;
|
||||
rearmCooldownS?: number;
|
||||
maxStagnantParses?: number;
|
||||
}
|
||||
|
||||
export interface SchedulerOptions {
|
||||
@@ -57,12 +61,15 @@ export interface SchedulerOptions {
|
||||
* parses are never suppressed
|
||||
*/
|
||||
suppressSteadyFrames: boolean;
|
||||
/** check cadence while a detector's gate is passing */
|
||||
/** check cadence while a detector's gate is passing (per-detector
|
||||
* refineIntervalS overrides — for detectors whose parse is expensive
|
||||
* enough that the dense default multiplies real cost) */
|
||||
refineIntervalS: number;
|
||||
/** check cadence while a detector's gate is failing (per-detector
|
||||
* searchIntervalS overrides) */
|
||||
searchIntervalS: number;
|
||||
/** consecutive non-improving parses tolerated before suppression */
|
||||
/** consecutive non-improving parses tolerated before suppression
|
||||
* (per-detector maxStagnantParses overrides) */
|
||||
maxStagnantParses: number;
|
||||
/** seconds without improvement tolerated before suppression */
|
||||
stagnantAfterS: number;
|
||||
@@ -250,8 +257,10 @@ export class DetectorScheduler {
|
||||
return;
|
||||
}
|
||||
streak.stagnant += 1;
|
||||
const maxStagnant =
|
||||
state.info.maxStagnantParses ?? this.#options.maxStagnantParses;
|
||||
if (
|
||||
streak.stagnant >= this.#options.maxStagnantParses &&
|
||||
streak.stagnant >= maxStagnant &&
|
||||
t - streak.lastImprovementT >= this.#options.stagnantAfterS
|
||||
) {
|
||||
streak.suppressed = true;
|
||||
@@ -279,7 +288,8 @@ export class DetectorScheduler {
|
||||
// while suppressed only the gate keeps running (to spot the screen
|
||||
// changing), which the sparser search cadence covers
|
||||
if (state.streak?.suppressed) return search;
|
||||
return state.gatePassing ? this.#options.refineIntervalS : search;
|
||||
if (!state.gatePassing) return search;
|
||||
return info.refineIntervalS ?? this.#options.refineIntervalS;
|
||||
}
|
||||
|
||||
#recordMatchState(
|
||||
|
||||
@@ -256,9 +256,12 @@ export function createScoreboardOwnDetector(
|
||||
];
|
||||
}
|
||||
|
||||
// just under the measured clean-read floor (fixtures 0.562-0.669,
|
||||
// confirmed scan events 0.612-0.635 — this screen's ability-grid scores
|
||||
// keep the mean low even on perfect reads)
|
||||
return {
|
||||
id: "scoreboard-own",
|
||||
sufficientConfidence: 0.98,
|
||||
sufficientConfidence: 0.55,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -427,10 +427,11 @@ export function createScoreboardReplayDetector(
|
||||
}
|
||||
|
||||
// no rearm cooldown — distinct replays browsed in quick succession are
|
||||
// told apart by content
|
||||
// told apart by content. sufficientConfidence just under the measured
|
||||
// clean-read floor (fixtures 0.808-0.890)
|
||||
return {
|
||||
id: "scoreboard-replay",
|
||||
sufficientConfidence: 0.98,
|
||||
sufficientConfidence: 0.8,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -321,10 +321,11 @@ export function createScoreboardDetector(
|
||||
];
|
||||
}
|
||||
|
||||
// a 0.98 mean field score is a clean full read
|
||||
// just under the measured clean-read floor (fixtures 0.865-0.899,
|
||||
// confirmed scan events down to 0.799)
|
||||
return {
|
||||
id: "scoreboard",
|
||||
sufficientConfidence: 0.98,
|
||||
sufficientConfidence: 0.79,
|
||||
gate,
|
||||
parse,
|
||||
};
|
||||
|
||||
@@ -52,12 +52,28 @@ export interface Detector<TData = unknown> {
|
||||
* is effectively free; only override upward with strong evidence.
|
||||
*/
|
||||
searchIntervalS?: number;
|
||||
/**
|
||||
* Check cadence while the gate is passing (the refine phase); unset =
|
||||
* the scheduler's dense default (0.15s). Only for detectors whose parse
|
||||
* is expensive enough that refining at the dense cadence multiplies
|
||||
* real cost — the sparser cadence still has to sample the screen's
|
||||
* lifetime several times.
|
||||
*/
|
||||
refineIntervalS?: number;
|
||||
/**
|
||||
* Consecutive non-improving parses tolerated before stagnation
|
||||
* suppression; unset = the scheduler's default (6). Lower for expensive
|
||||
* parses to cap the worst-case cost of a stagnant streak.
|
||||
*/
|
||||
maxStagnantParses?: number;
|
||||
/**
|
||||
* A parse reaching this confidence ends the streak's refinement
|
||||
* immediately: the timeline keeps the best read per merge window, so a
|
||||
* read this good cannot be improved upon in any way that matters.
|
||||
* Conservative by construction — if real reads never reach it, behavior
|
||||
* falls back to stagnation-based suppression.
|
||||
* Set just under the detector's measured clean-read confidence floor
|
||||
* (fixture-suite reads + confirmed scan events), so a streak's first
|
||||
* full read suppresses the rest; degraded-footage reads below the
|
||||
* floor fall back to stagnation-based suppression.
|
||||
*/
|
||||
sufficientConfidence?: number;
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,22 @@ test("a passing gate drops to the dense refine cadence", () => {
|
||||
assert.equal(feed(s, 0.2, { pass: true, confidence: 0.7 }), "parsed");
|
||||
});
|
||||
|
||||
test("a per-detector refine interval overrides the dense default", () => {
|
||||
const s = make({ refineIntervalS: 0.5 });
|
||||
assert.equal(feed(s, 0, { pass: true, confidence: 0.7 }), "parsed");
|
||||
assert.equal(feed(s, 0.1, { pass: true, confidence: 0.7 }), "skipped");
|
||||
assert.equal(feed(s, 0.4, { pass: true, confidence: 0.7 }), "skipped");
|
||||
assert.equal(feed(s, 0.5, { pass: true, confidence: 0.7 }), "parsed");
|
||||
});
|
||||
|
||||
test("a per-detector stagnation budget suppresses sooner", () => {
|
||||
const s = make({ maxStagnantParses: 2 }, { stagnantAfterS: 0.1 });
|
||||
assert.equal(feed(s, 0.0, { pass: true, confidence: 0.9 }), "parsed");
|
||||
assert.equal(feed(s, 0.1, { pass: true, confidence: 0.9 }), "parsed");
|
||||
assert.equal(feed(s, 0.2, { pass: true, confidence: 0.9 }), "parsed");
|
||||
assert.equal(feed(s, 0.3, { pass: true, confidence: 0.9 }), "gated");
|
||||
});
|
||||
|
||||
test("suppresses after stagnant parses on a static screen", () => {
|
||||
const s = make({});
|
||||
assert.equal(feed(s, 0.0, { pass: true, confidence: 0.9 }), "parsed");
|
||||
|
||||
Reference in New Issue
Block a user