mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 15:17:43 -05:00
Improve match merge logic
This commit is contained in:
@@ -92,6 +92,48 @@ describe("isSameMatch", () => {
|
||||
expect(Matches.isSameMatch(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
test("close play times do not identify back-to-back games against different opponents", () => {
|
||||
const playedAt = 1_700_000_000_000;
|
||||
const nextSet = scannerMatch({ playedAt: playedAt + 5 * 60 * 1000 });
|
||||
nextSet.teams[1] = {
|
||||
players: ["o1", "o2", "o3", "o4"].map((name, i) =>
|
||||
scannerMatchPlayer(name, (100 + 10 * i) as MainWeaponId),
|
||||
),
|
||||
};
|
||||
expect(Matches.isSameMatch(scannerMatch({ playedAt }), nextSet)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("close play times still identify a match with a couple of misread names per team", () => {
|
||||
const playedAt = 1_700_000_000_000;
|
||||
const b = scannerMatch({ playedAt: playedAt + 60 * 1000 });
|
||||
b.teams[0].players[0] = scannerMatchPlayer("misread1", WEAPONS[0]!);
|
||||
b.teams[0].players[1] = scannerMatchPlayer("misread2", WEAPONS[1]!);
|
||||
b.teams[1].players[2] = scannerMatchPlayer("misread3", WEAPONS[6]!);
|
||||
expect(Matches.isSameMatch(scannerMatch({ playedAt }), b)).toBe(true);
|
||||
});
|
||||
|
||||
test("contradicting weapons rule out close play times when names are unread", () => {
|
||||
const playedAt = 1_700_000_000_000;
|
||||
const minimap = scannerMatch({
|
||||
playedAt: playedAt + 5 * 60 * 1000,
|
||||
teams: [
|
||||
{
|
||||
players: WEAPONS.slice(0, 4).map((w) => scannerMatchPlayer(null, w)),
|
||||
},
|
||||
{
|
||||
players: [100, 110, 120, 130].map((w) =>
|
||||
scannerMatchPlayer(null, w as MainWeaponId),
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(Matches.isSameMatch(scannerMatch({ playedAt }), minimap)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("far-apart play times contradict identity even with equal rosters", () => {
|
||||
const a = scannerMatch({ playedAt: 1_700_000_000_000 });
|
||||
const b = scannerMatch({ playedAt: 1_700_000_000_000 + 60 * 60 * 1000 });
|
||||
|
||||
@@ -22,6 +22,11 @@ const MIN_NAME_OVERLAP = 6;
|
||||
const MIN_WEAPON_OVERLAP = 7;
|
||||
const MIN_WEAPON_SLOTS_READ = 7;
|
||||
|
||||
/** A team pair with at least this many names (else weapons) read on both sides can contradict identity... */
|
||||
const MIN_SLOTS_READ_TO_CONTRADICT = 3;
|
||||
/** ...when at most this many of them align: different players, so a different game. */
|
||||
const MAX_CONTRADICTING_OVERLAP = 1;
|
||||
|
||||
const PLAYERS_PER_TEAM = 4;
|
||||
|
||||
/** Rebuilds a match with a fixed key order so `JSON.stringify` is stable — the hashing and change-detection representation. */
|
||||
@@ -66,7 +71,8 @@ export function canonicalMatch(match: ScannerMatch): ScannerMatch {
|
||||
/**
|
||||
* Whether two (possibly partial) matches describe the same game; callers pre-scope to the same
|
||||
* tournament + POV user. Contradicting lobby/mode/stage/replay-code/play-time rules identity out; then a
|
||||
* matching replay code, close play times, or an aligning roster (names, else weapons) rules it in.
|
||||
* matching replay code rules it in, contradicting rosters (a team pair of different players) rule it out,
|
||||
* and close play times or an aligning roster (names, else weapons) rule it in.
|
||||
*/
|
||||
export function isSameMatch(a: ScannerMatch, b: ScannerMatch): boolean {
|
||||
if (a.lobby !== null && b.lobby !== null && a.lobby !== b.lobby) return false;
|
||||
@@ -85,14 +91,16 @@ export function isSameMatch(a: ScannerMatch, b: ScannerMatch): boolean {
|
||||
}
|
||||
|
||||
if (codeDiff !== null) return true;
|
||||
if (playedDiff !== null && playedDiff <= PLAYED_AT_AFFINITY_MS) return true;
|
||||
|
||||
const aligned = bestAlignment(a, b);
|
||||
if (rostersContradict(a, b, aligned.orientation)) return false;
|
||||
if (playedDiff !== null && playedDiff <= PLAYED_AT_AFFINITY_MS) return true;
|
||||
|
||||
if (aligned.nameOverlap >= MIN_NAME_OVERLAP) return true;
|
||||
if (
|
||||
aligned.weaponOverlap >= MIN_WEAPON_OVERLAP &&
|
||||
weaponSlotsRead(a) >= MIN_WEAPON_SLOTS_READ &&
|
||||
weaponSlotsRead(b) >= MIN_WEAPON_SLOTS_READ
|
||||
matchWeaponSlotsRead(a) >= MIN_WEAPON_SLOTS_READ &&
|
||||
matchWeaponSlotsRead(b) >= MIN_WEAPON_SLOTS_READ
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -268,12 +276,47 @@ function weaponOverlap(a: ScannerMatchTeam, b: ScannerMatchTeam): number {
|
||||
return overlap;
|
||||
}
|
||||
|
||||
function weaponSlotsRead(match: ScannerMatch): number {
|
||||
return match.teams.flatMap((team) =>
|
||||
team.players.filter((player) => player.weaponId !== null),
|
||||
function rostersContradict(
|
||||
a: ScannerMatch,
|
||||
b: ScannerMatch,
|
||||
orientation: Alignment["orientation"],
|
||||
): boolean {
|
||||
const [bFirst, bSecond] =
|
||||
orientation === "straight"
|
||||
? [b.teams[0], b.teams[1]]
|
||||
: [b.teams[1], b.teams[0]];
|
||||
return (
|
||||
teamsContradict(a.teams[0], bFirst) || teamsContradict(a.teams[1], bSecond)
|
||||
);
|
||||
}
|
||||
|
||||
function teamsContradict(a: ScannerMatchTeam, b: ScannerMatchTeam): boolean {
|
||||
if (Math.min(namesRead(a), namesRead(b)) >= MIN_SLOTS_READ_TO_CONTRADICT) {
|
||||
return nameOverlap(a, b) <= MAX_CONTRADICTING_OVERLAP;
|
||||
}
|
||||
if (
|
||||
Math.min(weaponSlotsRead(a), weaponSlotsRead(b)) >=
|
||||
MIN_SLOTS_READ_TO_CONTRADICT
|
||||
) {
|
||||
return weaponOverlap(a, b) <= MAX_CONTRADICTING_OVERLAP;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function namesRead(team: ScannerMatchTeam): number {
|
||||
return team.players.filter(
|
||||
(player) => player.name && normalizeInGameName(player.name),
|
||||
).length;
|
||||
}
|
||||
|
||||
function weaponSlotsRead(team: ScannerMatchTeam): number {
|
||||
return team.players.filter((player) => player.weaponId !== null).length;
|
||||
}
|
||||
|
||||
function matchWeaponSlotsRead(match: ScannerMatch): number {
|
||||
return weaponSlotsRead(match.teams[0]) + weaponSlotsRead(match.teams[1]);
|
||||
}
|
||||
|
||||
function swapSides(match: ScannerMatch): ScannerMatch {
|
||||
return {
|
||||
...match,
|
||||
|
||||
Reference in New Issue
Block a user