Update matchmaking

This commit is contained in:
2026-05-27 22:41:26 +01:00
parent 9b14a0490c
commit 34faf0c95d
2 changed files with 43 additions and 23 deletions
+1 -1
View File
@@ -51,4 +51,4 @@ jobs:
docker compose up -d
- name: Cleanup
run: rm -rf /deploy/build
run: rm -rf /deploy/build &
+42 -22
View File
@@ -76,8 +76,37 @@ function lensFor(asset: AssetRow, map: Map<number, LensRef>): LensRef | null {
}
/**
* Generate a weighted random pairing. Assets closer in Elo are more
* likely to be paired. Uses inverse-square weighting on Elo difference.
* Inverse-uncertainty proxy for a static-quality asset: each played match shrinks the term, so
* under-played assets are preferentially surfaced.
*/
function uncertaintyWeight(matches: number): number {
return 1 / (1 + matches);
}
/**
* Outcome-entropy proxy: close-Elo pairs have more uncertain outcomes and thus carry more
* information per match. Inverse-square on the diff keeps the tails non-zero so outliers still
* get sampled occasionally.
*/
function proximityWeight(eloA: number, eloB: number): number {
const diff = eloA - eloB;
return 1 / (1 + (diff * diff) / 30000);
}
function weightedPick<T>(items: T[], weights: number[]): T {
const total = weights.reduce((sum, w) => sum + w, 0);
let rand = Math.random() * total;
for (let i = 0; i < items.length; i++) {
rand -= weights[i];
if (rand <= 0) return items[i];
}
return items[items.length - 1];
}
/**
* Generate a weighted random pairing. Sequentially samples two assets to
* approximate joint info-gain: A is drawn by inverse match count (favour
* under-played assets), then B by inverse match count × Elo proximity to A.
*/
export async function generatePairing(
voterIp: string,
@@ -89,27 +118,18 @@ export async function generatePairing(
if (activeAssets.length < 2) return null;
// Pick first asset uniformly at random
const idxA = Math.floor(Math.random() * activeAssets.length);
const assetA = activeAssets[idxA];
const aWeights = activeAssets.map((a) =>
uncertaintyWeight(a.matchesProvisional),
);
const assetA = weightedPick(activeAssets, aWeights);
// Weight remaining assets by Elo proximity (inverse square)
const candidates = activeAssets.filter((_, i) => i !== idxA);
const weights = candidates.map((c) => {
const diff = Math.abs(c.eloProvisional - assetA.eloProvisional);
return 1 / (1 + (diff * diff) / 10000);
});
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
let rand = Math.random() * totalWeight;
let assetB = candidates[candidates.length - 1];
for (let i = 0; i < candidates.length; i++) {
rand -= weights[i];
if (rand <= 0) {
assetB = candidates[i];
break;
}
}
const candidates = activeAssets.filter((c) => c.id !== assetA.id);
const bWeights = candidates.map(
(c) =>
uncertaintyWeight(c.matchesProvisional) *
proximityWeight(c.eloProvisional, assetA.eloProvisional),
);
const assetB = weightedPick(candidates, bWeights);
const uuid = crypto.randomUUID();
const now = new Date();