Show personal map records on join and replay

This commit is contained in:
2026-10-19 14:11:54 -04:00
parent 9ef5932d36
commit e93182cd02
11 changed files with 620 additions and 200 deletions
+65 -35
View File
@@ -132,7 +132,7 @@ function engine(game, count = 8, withHud = false) {
c.hud_connect(); c.afk_hud_create();
},
addPlayer() { const player = { sessionstate: 'spectator' }; players.add(player); return player; },
help: () => c.show_help(), record: snapshot => c.show_record(snapshot),
help: () => c.show_help(), record: (snapshot, personal) => c.show_record(snapshot, personal),
reminder: (snapshot, message) => c.show_record_reminder(snapshot, message),
event: name => emit(self, name), end: name => emit(level, name),
get state() { return self.tws_service; },
@@ -295,18 +295,18 @@ for (const game of ['t5', 't6']) {
assert.equal(e.elements.length, count);
assert.equal(e.live.length, 0);
});
check(`${game} help pauses record time and resumes the same page`, () => {
check(`${game} help pauses the complete record and resumes its remaining time`, () => {
const e = engine(game);
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
e.advance(15000);
assert.equal(e.state.record.page, 1);
e.advance(3000);
assert.equal(e.state.record.page, 0);
const remaining = e.state.record.remaining;
e.help();
e.advance(12000);
assert.equal(e.state.help, undefined);
assert.equal(e.state.record.page, 1);
assert.equal(e.state.record.page, 0);
assert.equal(e.state.record.remaining, remaining);
assert.equal(e.state.rows[0].text, 'Four');
assert.deepEqual(Array.from(e.state.rows, row => row.text), ['One', 'Two', 'Three', 'Four']);
assert.equal(e.live.length, 5);
const resumedTexts = e.textCalls;
e.c.service_render(e.state);
@@ -344,7 +344,7 @@ for (const game of ['t5', 't6']) {
assert.equal(request.kind, 'reminder');
assert.equal(request.pages, 1);
assert.equal(request.remaining, 6000);
assert.equal(e.state.record_title.text, 'Map record | Round 82');
assert.equal(e.state.record_title.text, 'Map Record | Round 82');
assert.equal(e.state.rows.length, 1);
assert.equal(e.state.rows[0].text, '5 rounds to tie');
assert.equal(e.live.length, 2);
@@ -416,16 +416,14 @@ for (const game of ['t5', 't6']) {
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
assert.equal(e.state.record.kind, 'record');
assert.equal(e.state.record.remaining, 12000);
assert.equal(e.state.record.pages, 2);
assert.equal(e.state.record.pages, 1);
assert.equal(e.live.length, 5);
assert(replaced.every(elem => elem.destroyCalls === 1));
e.advance(100);
assert.equal(e.state.record.page, 0, 'old reminder expiry cannot dismiss manual replay');
assert.equal(e.state.record.remaining, 11900);
e.advance(11900);
assert.equal(e.state.record.page, 1);
assert.equal(e.state.rows[0].text, 'Four');
e.advance(12000);
assert.equal(e.state.record, undefined);
assert.equal(e.live.length, 0);
});
check(`${game} invalid reminders do not initialize a service or allocate elements`, () => {
@@ -451,35 +449,67 @@ for (const game of ['t5', 't6']) {
assert.equal(e.elements.length, 2);
assert(e.elements.every(elem => elem.destroyCalls === 1));
});
check(`${game} unavailable, empty and paged records rebuild only when their allocation changes`, () => {
check(`${game} all holders precede a separate personal heading without record pages`, () => {
const e = engine(game);
e.record(snapshot(['Player 1', 'Player 2', 'Player 3', 'Player 4']), 'Your Best | Round 15');
assert.equal(e.state.record.pages, 1);
assert.equal(e.live.length, 6);
assertCounter(e, 1, 1);
assert.deepEqual(e.visible.map(x => x.text), [
'Map Record | Round 82', 'Player 1', 'Player 2', 'Player 3', 'Player 4', 'Your Best | Round 15',
]);
const personal = e.state.personal_title, header = e.state.record_title;
assert.equal(personal.y, 150, 'leave a blank row after all four holders');
assert.deepEqual(personal.color, header.color);
assert.equal(personal.fontScale, header.fontScale);
assert.notDeepEqual(personal.color, e.state.rows[0].color, 'personal best uses the heading style');
e.advance(11000);
assert.equal(e.state.personal_title, personal);
assert.equal(e.visible.length, 6, 'all holders stay visible until dismissal');
e.advance(1000);
assert.equal(e.live.length, 0);
e.record({ round: 0, lines: [] }, 'Your Best | None yet');
assert.equal(e.state.personal_title.text, 'Your Best | None yet');
assert.equal(e.state.personal_title.alpha, 1);
e.record(undefined, 'Your Best | Unavailable');
assert.equal(e.state.personal_title.text, 'Your Best | Unavailable');
assert.equal(e.state.personal_title.alpha, 1);
e.record(snapshot(['A', 'B', 'C']), 'Your Best | Round 10');
e.advance(2000); e.help(); e.advance(12000);
assert.equal(e.state.record.remaining, 10000);
assert.equal(e.state.personal_title.text, 'Your Best | Round 10');
e.record(snapshot(['A', 'B', 'C']));
assert.equal(e.state.personal_title, undefined, 'legacy callers clear the personal heading');
e.event('disconnect');
assert.equal(e.live.length, 0);
});
check(`${game} record allocation follows the full holder count and reuses unchanged layouts`, () => {
const e = engine(game);
e.record(undefined);
assert.equal(e.state.record_title.alpha, 1);
assert.equal(e.state.record_title.text, 'Map record unavailable');
assert.equal(e.live.length, 4);
assert.equal(e.state.record_title.text, 'Map Record unavailable');
assert.equal(e.live.length, 1);
assertCounter(e, 1, 1);
const first = [...e.live];
e.record({ round: 0, lines: [] });
assert.equal(e.state.record_title.text, 'No recorded high round yet');
assert.deepEqual(e.live, first);
e.record(snapshot(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']));
assert(first.every(elem => elem.destroyCalls === 1), 'holder count changes release the old view first');
assert.equal(e.live.length, 9);
assertCounter(e, 1, 1);
e.record(snapshot(['A', 'B', 'C', 'D']));
assert(first.every(elem => elem.destroyCalls === 1), 'pagedness changes release the old view first');
assert.equal(e.state.record_title.text, 'Map record | Round 82');
assert.equal(e.live.length, 5);
const paged = [...e.live];
assertCounter(e, 1, 2);
e.advance(12000);
assert.deepEqual(Array.from(e.state.rows, row => row.alpha), [1, 0, 0]);
assert.equal(e.state.rows[0].text, 'D');
assertCounter(e, 2, 2);
assert.deepEqual(e.live, paged, 'page changes reuse the same allocation');
e.record(snapshot(['Single page']));
assert(paged.every(elem => elem.destroyCalls === 1));
const full = [...e.live];
e.advance(1000);
assert(e.state.rows.every(row => row.alpha === 1));
e.record(snapshot(['I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']));
assert.deepEqual(e.live, full);
assert.deepEqual(Array.from(e.state.rows, row => row.text), ['I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']);
e.record(snapshot(['Single holder']));
assert(full.every(elem => elem.destroyCalls === 1));
assertCounter(e, 1, 1);
assert.equal(e.state.rows[0].text, 'Single page');
assert.equal(e.live.length, 4);
assert.equal(e.peakLive, 5, 'new allocations never overlap the released view');
assert.equal(e.state.rows[0].text, 'Single holder');
assert.equal(e.live.length, 2);
assert.equal(e.peakLive, 9, 'new allocations never overlap the released view');
assertTextOnly(e);
});
for (const event of ['end_game', 'game_ended', 'intermission', 'disconnect']) check(`${game} ${event} cleans active and pending pages once`, () => {
@@ -512,11 +542,11 @@ for (const game of ['t5', 't6']) {
e.advance(11000);
assert.equal(e.textCalls, calls);
assert.equal(e.elements.length, count);
assert.equal(e.state.record_title.text, 'Map record | Round 82');
assert.equal(e.state.record_title.text, 'Map Record | Round 82');
assert.equal(e.state.commands[3].command, '.join');
assert.match(e.c.help_description(e.state.commands[3]), /newcomer/);
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
assert.equal(e.elements.length, 4, 'record allocation includes all hidden holder rows');
assert.equal(e.elements.length, 2, 'record allocation includes only the header and actual holders');
assertTextOnly(e);
});
check(`${game} service coordinates clear the watermark and stock lower-right HUD`, () => {
@@ -606,7 +636,7 @@ for (const game of ['t5', 't6']) {
assert.equal(e.textCalls, calls);
e.record(snapshot()); e.advance(1000);
assert(rows.every(row => row.destroyCalls === 1));
assert.equal(e.live.length, 4);
assert.equal(e.live.length, 2);
e.help();
assert.equal(e.live.length, 19);
assert(e.state.command_rows.every(row => !rows.includes(row)));
@@ -677,7 +707,7 @@ check('T5 service transitions preserve actual core element identities, styles, d
assert.equal(e.live.length, 12);
assert.deepEqual(state(), initial);
for (let i = 0; i < 4; i++) {
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(15000); e.help();
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(3000); e.help();
assert.equal(e.live.length, 31, '10 core plus two AFK plus 19 help elements; this is not an engine pool-limit assertion');
assert.equal(e.live.filter(elem => elem.archived === false).length, 19);
e.c.toggle_core_hud();
@@ -688,7 +718,7 @@ check('T5 service transitions preserve actual core element identities, styles, d
e.advance(12000);
assert.equal(e.state.rows[0].text, 'Latest');
assert.equal(e.state.record.remaining, 12000);
assert.equal(e.live.length, 16);
assert.equal(e.live.length, 14);
assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0));
e.c.toggle_core_hud();
assert.deepEqual(state(), initial);