Split The code is public; the app answers only to the crew.

A resumed match comes back on whatever map the menu last chose: `Saved` does not record one #158

Closed opened this
Closed without merging

scout/cs:scout/155-a-resumed-match-comes-back-on-whatever-m → main 6 commits fork

  • Closed without merging. Nothing was applied to main.

Review

Sign in to review this change.
4 files changed +35 −8
src/client/saved.test.ts +7
@@ -60,6 +60,7 @@ const validSaved: Saved = {
6060 headshots: Math.min(i, 2),
6161 damage: i * 37,
6262 })),
63 map: "de_cinder",
6364 };
6465
6566 test("a whole, valid snapshot round-trips through JSON exactly", () => {
@@ -77,6 +78,7 @@ test("snapshot reads only what world.ts already exports, and parses back", () =>
7778 expect(saved.matchId).toBe(matchId);
7879 expect(saved.seed).toBe(world.seed);
7980 expect(saved.name).toBe("scout");
81 expect(saved.map).toBe("de_forge"); // createWorld's own default, unset here
8082 expect(parseSaved(JSON.stringify(saved))).toEqual(saved);
8183 });
8284
@@ -127,6 +129,11 @@ test("an unknown weapon id never reaches the game", () => {
127129 expect(parseSaved(JSON.stringify(bad))).toBeNull();
128130 });
129131
132 test("an unknown map name never reaches the game", () => {
133 expect(parseSaved(JSON.stringify({ ...validSaved, map: "de_forgery" }))).toBeNull();
134 expect(parseSaved(JSON.stringify({ ...validSaved, map: 7 }))).toBeNull();
135 });
136
130137 test("money out of range is not a match", () => {
131138 const tooMuch: Saved = { ...validSaved, players: [{ ...validSaved.players[0], money: 999999 }, ...validSaved.players.slice(1)] };
132139 expect(parseSaved(JSON.stringify(tooMuch))).toBeNull();
src/client/saved.ts +10 −1
@@ -14,6 +14,7 @@
1414 * match — wiring this into `main.ts` and `world.ts` — is the next issue.
1515 */
1616
17 import { MAP_NAMES, type MapName } from "./map.ts";
1718 import type { Rules, Team } from "./match.ts";
1819 import { LOSS_BONUS } from "./match.ts";
1920 import { WEAPONS, type WeaponId } from "./weapons.ts";
@@ -53,6 +54,8 @@ export type Saved = {
5354 seed: number;
5455 name: string;
5556 players: SavedPlayer[];
57 /** The layout the match was played on: #153 landed a second one, so this can no longer default. */
58 map: MapName;
5659 };
5760
5861 /** `createWorld` is only ever called with a 5-a-side match. */
@@ -97,6 +100,9 @@ export function snapshot(world: World, matchId: string): Saved {
97100 seed: world.seed,
98101 name: world.me.name,
99102 players: world.players.map(savePlayer),
103 // world.map.name is always one of MAP_NAMES: it only ever comes from a
104 // MapDef built by map.ts's own catalogue.
105 map: world.map.name as MapName,
100106 };
101107 }
102108
@@ -207,7 +213,10 @@ export function parseSaved(raw: string | null): Saved | null {
207213 players.push(player);
208214 }
209215
210 return { rules, matchId, round, score, losses, playerTeam, seed, name, players };
216 const map = parsed.map;
217 if (typeof map !== "string" || !(MAP_NAMES as readonly string[]).includes(map)) return null;
218
219 return { rules, matchId, round, score, losses, playerTeam, seed, name, players, map: map as MapName };
211220 }
212221
213222 const KEY = "cs.saved";
src/client/world.test.ts +10
@@ -682,6 +682,16 @@ test("restoreWorld(snapshot(world, id)) round-trips score, round, money and gear
682682 expect(restored.difficulty).toBe(world.difficulty);
683683 });
684684
685 test("a match played on de_cinder resumes on de_cinder, not restoreWorld's own default", () => {
686 const world = createWorld(SHORT, "me", "ct", 5, 901, undefined, MAPS.de_cinder);
687 const saved = snapshot(world, crypto.randomUUID());
688 expect(saved.map).toBe("de_cinder");
689 const restored = restoreWorld(saved);
690 expect(restored).not.toBeNull();
691 if (restored === null) return;
692 expect(restored.map.name).toBe("de_cinder");
693 });
694
685695 test("a restored round leaves bot money alone", () => {
686696 const world = createWorld(SHORT, "me", "ct", 5, 401);
687697 world.match.round = 2;
src/client/world.ts +8 −7
@@ -14,7 +14,7 @@ import {
1414 blastDamage, GRENADE, step as stepGrenade, thrown, type Grenade,
1515 } from "./grenades.ts";
1616 import { pose, remember, rememberPos, type Pose } from "./interpolate.ts";
17 import { DE_FORGE, siteAt, type MapDef } from "./map.ts";
17 import { DE_FORGE, MAPS, siteAt, type MapDef } from "./map.ts";
1818 export { DEFAULT_MAP, MAP_NAMES, mapFor } from "./map.ts";
1919 import { angleVectors, angleDelta, clamp, dist, dist2d, normalize, rng, sub, type Vec3 } from "./math.ts";
2020 import * as rules from "./match.ts";
@@ -329,17 +329,18 @@ const RESTORE_TEAM_SIZE = 5;
329329 * the flip, rather than trusting `save.playerTeam` at face value, which past
330330 * half time would hand a player the slot, and the gear, of whoever was on
331331 * their side before the swap.
332 *
333 * The map comes from `save.map`, not a parameter: a resumed match was played
334 * on one specific layout, and only `snapshot` gets to say which.
332335 */
333 export function restoreWorld(
334 save: Saved,
335 difficulty: Difficulty = DEFAULT_DIFFICULTY,
336 map: MapDef = DE_FORGE,
337 ): World | null {
336 export function restoreWorld(save: Saved, difficulty: Difficulty = DEFAULT_DIFFICULTY): World | null {
338337 if (save.players.length !== RESTORE_TEAM_SIZE * 2) return null;
339338 if (save.score.t >= save.rules.winScore || save.score.ct >= save.rules.winScore) return null;
340339 const pastHalf = save.round > rules.halfTime(save.rules);
341340 const startTeam = pastHalf ? rules.other(save.playerTeam) : save.playerTeam;
342 const world = buildWorld(save.rules, save.name, startTeam, RESTORE_TEAM_SIZE, save.seed, difficulty, map);
341 const world = buildWorld(
342 save.rules, save.name, startTeam, RESTORE_TEAM_SIZE, save.seed, difficulty, MAPS[save.map],
343 );
343344 world.match.round = save.round;
344345 world.match.score = { ...save.score };
345346 world.match.losses = { ...save.losses };

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs