Skip to content

Commit 444ec08

Browse files
seven-fengclaude
andauthored
fix(arena): backfill recent W/L form on leaderboard load (#58)
The leaderboard's 🔥/loss form strip only accrued from MatchSettled events that fired while the page was open — historical results were read but never fed into recentResultsRef, so the strip started empty on every load and the per-row lengths were inconsistent. - useArenaEngine: query MatchCreated history to learn each match's participants, then backfill recent W/L (newest-first) from the historical MatchSettled walk. Live handling unchanged; historical matches stay deduped via seenMatchSettled so they aren't double-counted. - LeaderboardPanel: render a fixed 5-cell strip (pad short histories with placeholder dots) and distinguish loss (red ✕) from no-game-yet (dim ·). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 488ebc0 commit 444ec08

2 files changed

Lines changed: 40 additions & 10 deletions

File tree

frontend/src/components/arena/LeaderboardPanel.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,20 @@ export function LeaderboardPanel() {
106106
</div>
107107
<div className="flex items-center justify-between mt-0.5">
108108
<div className="flex gap-0.5">
109-
{(g.recentResults.length > 0 ? g.recentResults : ['·','·','·','·','·']).slice(0, 5).map((r, i) => (
109+
{/* Always 5 cells: recent results (newest-first) padded with
110+
empty placeholders so every row reads as a fixed-width form
111+
strip. 🔥 = win, ✕ = loss, · = no game yet. */}
112+
{Array.from({ length: 5 }, (_, i) => g.recentResults[i] ?? null).map((r, i) => (
110113
<span
111114
key={i}
112-
className={
115+
className={[
116+
'leading-none',
113117
r === 'W' ? 'text-emerald-400' :
114118
r === 'L' ? 'text-rose-400' :
115-
'text-zinc-700'
116-
}
119+
'text-zinc-700',
120+
].join(' ')}
117121
>
118-
{r === 'W' ? '🔥' : r === 'L' ? '·' : '·'}
122+
{r === 'W' ? '🔥' : r === 'L' ? '' : '·'}
119123
</span>
120124
))}
121125
</div>

frontend/src/hooks/useArenaEngine.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,26 @@ export function useArenaEngine() {
216216
const latest = await provider.getBlockNumber();
217217
const from = Math.max(0, latest - EVENT_LOOKBACK_BLOCKS);
218218

219-
// MatchCreated events are intentionally NOT upserted here.
220-
// The walk loop calls getMatch() which provides correct bench data.
221-
// Upserting from MatchCreated would write attackerBench:[0,0,0,0,0],
222-
// causing a premature render with empty slots before real data arrives.
219+
// MatchCreated events are intentionally NOT upserted here (the walk loop
220+
// calls getMatch() for correct bench data — upserting empty benches would
221+
// render premature empty slots). We DO read them to learn each match's
222+
// participants, so the settled loop below can identify the loser and
223+
// backfill recent W/L form. Without this, recentResults starts empty on
224+
// load and only accrues from matches that settle while the page is open.
225+
const participants: Record<number, { attackerId: number; defenderId: number }> = {};
226+
try {
227+
const createdFilter = arena.filters.MatchCreated();
228+
const createdEvents = await arena.queryFilter(createdFilter, from, latest);
229+
for (const ev of createdEvents) {
230+
if (!(ev instanceof EventLog)) continue;
231+
const a = ev.args!;
232+
participants[Number(a[0])] = { attackerId: Number(a[1]), defenderId: Number(a[2]) };
233+
}
234+
} catch (e) { console.warn('[arena] MatchCreated history fetch failed', e); }
223235

224-
// MatchSettled — drives highlight detection
236+
// MatchSettled — drives highlight detection + recent-form backfill.
237+
// queryFilter returns logs oldest-first; recentResults is most-recent-first,
238+
// so prepending each result in iteration order leaves the newest at index 0.
225239
try {
226240
const settledFilter = arena.filters.MatchSettled();
227241
const events = await arena.queryFilter(settledFilter, from, latest);
@@ -249,6 +263,18 @@ export function useArenaEngine() {
249263
loserEloAfter: newLoseElo,
250264
});
251265
seenMatchSettled.current.add(matchId);
266+
267+
// Backfill recent W/L. Loser = the participant that isn't the winner.
268+
const pair = participants[matchId];
269+
const loserId = pair
270+
? (pair.attackerId === winnerId ? pair.defenderId : pair.attackerId)
271+
: 0;
272+
if (winnerId) {
273+
recentResultsRef.current[winnerId] = ['W' as const, ...(recentResultsRef.current[winnerId] ?? [])].slice(0, 5);
274+
}
275+
if (loserId) {
276+
recentResultsRef.current[loserId] = ['L' as const, ...(recentResultsRef.current[loserId] ?? [])].slice(0, 5);
277+
}
252278
}
253279
} catch (e) { console.warn('[arena] MatchSettled history fetch failed', e); }
254280
};

0 commit comments

Comments
 (0)