diff --git a/src/__tests__/ai.test.ts b/src/__tests__/ai.test.ts index d4654ec..0455174 100644 --- a/src/__tests__/ai.test.ts +++ b/src/__tests__/ai.test.ts @@ -6,6 +6,7 @@ import { MultiplayerGame, PASS, SimpleGame, + randomBag, randomColorSelection, } from '../game'; import {effectiveLockout, flexDropletStrategy1} from '../ai'; @@ -115,17 +116,29 @@ test('Ineffective lockout (no bag)', () => { expect(heuristic).toBe(0); }); -// Skipped due to simulating a whole game with non-trivial AI being a bit heavy -test.skip('Server/client pausing game simulation', () => { +test('Server/client pausing game simulation', () => { const maxConsecutiveRerolls = 10; - const gameSeed = randomSeed(); - const screenSeed = randomSeed(); + const gameSeeds = [randomSeed(), randomSeed()]; + const screenSeeds = [randomSeed(), randomSeed()]; const colorSelection = randomColorSelection(); const colorSelections = [colorSelection, colorSelection]; - const main = new MultiplayerGame(gameSeed, screenSeed, colorSelections); + const initialBag = randomBag(colorSelection); + const initialBags = [initialBag, initialBag]; + const main = new MultiplayerGame( + gameSeeds, + screenSeeds, + colorSelections, + initialBags + ); // In practice this would be two mirrors for each client - const mirror = new MultiplayerGame(null, screenSeed, colorSelections); + const knownBags = main.initialBags; + const mirror = new MultiplayerGame( + null, + screenSeeds, + colorSelections, + knownBags + ); for (let i = 0; i < mirror.games.length; ++i) { // Send initial bag and prompt moves with next pieces mirror.games[i].bag = main.games[i].initialBag.concat( @@ -209,25 +222,35 @@ test.skip('Server/client pausing game simulation', () => { } }); -// Skipped due to simulating a whole game with non-trivial AI being a bit heavy -// At least it's not running in wall clock time... -test.skip('Server/client realtime game simulation', () => { +test('Server/client realtime game simulation', () => { const maxConsecutiveRerolls = 10; - const gameSeed = randomSeed(); - const screenSeed = randomSeed(); + const gameSeeds = [randomSeed(), randomSeed()]; + const screenSeeds = [randomSeed(), randomSeed()]; const colorSelection = randomColorSelection(); const colorSelections = [colorSelection, colorSelection]; - const origin = new MultiplayerGame(gameSeed, screenSeed, colorSelections); + const initialBag = randomBag(colorSelection); + const initialBags = [initialBag, initialBag]; + const origin = new MultiplayerGame( + gameSeeds, + screenSeeds, + colorSelections, + initialBags + ); // Server const main = new TimeWarpingGame(origin); - const mirrorOrigin = new MultiplayerGame(null, screenSeed, colorSelections); - const initialBags = origin.initialBags; + const knownBags = origin.initialBags; + const mirrorOrigin = new MultiplayerGame( + null, + screenSeeds, + colorSelections, + knownBags + ); // Two dueling clients const mirrors = [ - new TimeWarpingMirror(mirrorOrigin, initialBags), - new TimeWarpingMirror(mirrorOrigin, initialBags), + new TimeWarpingMirror(mirrorOrigin), + new TimeWarpingMirror(mirrorOrigin), ]; // Client-side @@ -263,7 +286,7 @@ test.skip('Server/client realtime game simulation', () => { } else { const {x1, y1, orientation} = MOVES[strategy.move]; const move = game.play(0, x1, y1, orientation, true); - console.log('Adding', move); + // console.log('Adding', move); const rejectedMoves = main.addMove(move); mirrors[0].addMove(move); mirrors[1].addMove(move); @@ -286,7 +309,7 @@ test.skip('Server/client realtime game simulation', () => { } else { const {x1, y1, orientation} = MOVES[strategy.move]; const move = game.play(1, x1, y1, orientation, Math.random() > 0.2); - console.log('Adding', move); + // console.log('Adding', move); const rejectedMoves = main.addMove(move); mirrors[0].addMove(move); mirrors[1].addMove(move); @@ -311,7 +334,7 @@ test.skip('Server/client realtime game simulation', () => { serverTime++; const pieces = main.revealPieces(serverTime); for (const piece of pieces) { - console.log('Revealing', piece); + // console.log('Revealing', piece); if (piece.player === 0) { botTime = piece.time; } else { @@ -339,12 +362,12 @@ test.skip('Server/client realtime game simulation', () => { } const game = main.warp(serverTime); - game.log(); + // game.log(); const mirrorGames = mirrors.map(m => m.warp(serverTime)[0]); for (const mirrorGame of mirrorGames) { expect(mirrorGame).not.toBeNull(); - mirrorGame!.log(); + // mirrorGame!.log(); } for (let i = 0; i < game.games.length; ++i) { diff --git a/src/__tests__/archive.ts b/src/__tests__/archive.ts index c4d00e4..07c6132 100644 --- a/src/__tests__/archive.ts +++ b/src/__tests__/archive.ts @@ -8,17 +8,19 @@ import {JKISS32} from '../jkiss'; import {Replay} from '../replay'; export function fixedRandomGame() { - const gameSeed = 7; + const gameSeeds = [7, 7]; const colorSelection = [1, 2, 3, 4]; const colorSelections = [colorSelection, colorSelection]; - const screenSeed = 11; + const initialBags = [[], []]; + const screenSeeds = [11, 11]; const targetPoints = [70, 70]; const marginFrames = DEFAULT_MARGIN_FRAMES; const mercyFrames = DEFAULT_MERCY_FRAMES; const game = new MultiplayerGame( - gameSeed, - screenSeed, + gameSeeds, + screenSeeds, colorSelections, + initialBags, targetPoints, marginFrames, mercyFrames @@ -26,9 +28,10 @@ export function fixedRandomGame() { const rng = new JKISS32(8); const replay: Replay = { - gameSeed, - screenSeed, + gameSeeds, + screenSeeds, colorSelections, + initialBags, targetPoints, marginFrames, mercyFrames, @@ -69,12 +72,13 @@ export function fixedRandomGame() { } export const LUMI_VS_FLEX2: Replay = { - gameSeed: 3864657304, - screenSeed: 2580717322, + gameSeeds: [3864657304, 3864657304], + screenSeeds: [2580717322, 2580717322], colorSelections: [ [3, 1, 0, 2], [3, 1, 0, 2], ], + initialBags: [[], []], targetPoints: [70, 70], marginFrames: DEFAULT_MARGIN_FRAMES, mercyFrames: Infinity, diff --git a/src/__tests__/game.test.ts b/src/__tests__/game.test.ts index 5c73236..650051a 100644 --- a/src/__tests__/game.test.ts +++ b/src/__tests__/game.test.ts @@ -3,9 +3,9 @@ import { DEFAULT_TARGET_POINTS, MOVES, MultiplayerGame, - OnePlayerGame, SimpleGame, SinglePlayerGame, + randomBag, randomColorSelection, } from '../game'; import {JKISS32, randomSeed} from '../jkiss'; @@ -72,13 +72,13 @@ test('No pending flash', () => { test('Garbage schedule', () => { // Create a deterministic game. - const game = new MultiplayerGame(0); + const game = new MultiplayerGame([0, 0]); // Create a deterministic player that is somewhat successful. - const jkiss = new JKISS32(7); + const jkiss = new JKISS32(1); // Create a dummy opponent. const dummy = new JKISS32(420); - for (let i = 0; i < 1950; ++i) { + for (let i = 0; i < 1500; ++i) { if (!game.games[0].busy) { const {x1, y1, orientation} = MOVES[jkiss.step() % MOVES.length]; game.play(0, x1, y1, orientation); @@ -102,12 +102,12 @@ test('Garbage schedule', () => { game.tick(); } - expect(puyoCount(game.games[1].screen.grid[GARBAGE])).toBe(3); + expect(puyoCount(game.games[1].screen.grid[GARBAGE])).toBe(11); }); test('Garbage offset in a fixed symmetric game', () => { // Create a random game. - const game = new MultiplayerGame(592624221); + const game = new MultiplayerGame([592624221, 592624221]); // Create players with identical strategies. const players = [new JKISS32(3848740175), new JKISS32(3848740175)]; @@ -126,7 +126,7 @@ test('Garbage offset in a fixed symmetric game', () => { test('Garbage offset in a random symmetric game', () => { // Create a random game. const gameSeed = randomSeed(); - const game = new MultiplayerGame(gameSeed); + const game = new MultiplayerGame([gameSeed, gameSeed]); // Create players with identical strategies. const playerSeed = randomSeed(); const players = [new JKISS32(playerSeed), new JKISS32(playerSeed)]; @@ -186,31 +186,35 @@ test('Simple game pending garbage offsetting', () => { }); test('Roof play', () => { - const game = new OnePlayerGame(); + const game = new SinglePlayerGame(); // Not recommended to play on the garbage insert line but kicks should still apply. game.play(0, 1, 0); expect(puyoCount(game.screen.mask)).toBe(2); }); test('Mirror driving', () => { - const mainSeed = randomSeed(); + const mainSeeds = [randomSeed(), randomSeed()]; const colorSelection = randomColorSelection(); const colorSelections = [colorSelection, colorSelection]; - const screenSeed = randomSeed(); + const screenSeeds = [randomSeed(), randomSeed()]; + const initialBag = randomBag(colorSelection); + const initialBags = [initialBag, initialBag]; const targetPoints = [70, 70]; const marginTime = 5000; const main = new MultiplayerGame( - mainSeed, - screenSeed, + mainSeeds, + screenSeeds, colorSelections, + initialBags, targetPoints, marginTime ); const mirror = new MultiplayerGame( null, - screenSeed, + screenSeeds, colorSelections, + [[], []], targetPoints, marginTime ); @@ -287,7 +291,7 @@ test('Permanent lockout', () => { }); test('To simple game JSON', () => { - const game = new MultiplayerGame(0); + const game = new MultiplayerGame([0, 0]); game.play(0, 1, 2, 3, true); game.play(1, 2, 3, 0, true); while (game.tick()[0].busy); @@ -378,7 +382,7 @@ test('Move count reduction (rerolls)', () => { }); test('Null end', () => { - const game = new MultiplayerGame(0); + const game = new MultiplayerGame([0, 0]); while (true) { if (!game.games[0].busy) { @@ -397,7 +401,7 @@ test('Null end', () => { }); test('AFK end', () => { - const game = new MultiplayerGame(1); + const game = new MultiplayerGame([1, 1]); while (!game.tick()[0].lockedOut); expect(game.age).toBe(12502); @@ -406,7 +410,13 @@ test('AFK end', () => { test('Handicap', () => { const colorSelection = [RED, GREEN, YELLOW, BLUE]; const colorSelections = [colorSelection, colorSelection]; - const game = new MultiplayerGame(11, 17, colorSelections, [1, 70]); + const game = new MultiplayerGame( + [11, 11], + [17, 17], + colorSelections, + [[], []], + [1, 70] + ); game.play(0, 0, 0, 0, true); while (game.tick()[0].busy); game.play(0, 1, 0, 0, true); @@ -459,3 +469,39 @@ test('No mercy flashes', () => { game.tick(); } }); + +test('No mirror cheese', () => { + // Create a deterministic game with anti-cheese seeds. + const game = new MultiplayerGame([0, 1]); + // Create a deterministic player that is somewhat successful. + const jkiss = new JKISS32(7); + + let move: (typeof MOVES)[number] | null = null; + + for (let i = 0; i < 1950; ++i) { + // Play using cheesy mirror strategy. + if (move !== null) { + expect(!game.games[1].busy); + game.play(1, move.x1, move.y1, move.orientation); + move = null; + } + if (!game.games[0].busy) { + // The cheese works but only up to a limit. + if (game.age < 426) { + for (let i = 0; i < game.games[0].screen.grid.length; ++i) { + expect( + puyosEqual( + game.games[0].screen.grid[i], + game.games[1].screen.grid[i] + ) + ).toBeTrue(); + } + } + move = MOVES[jkiss.step() % MOVES.length]; + game.play(0, move.x1, move.y1, move.orientation); + } + game.tick(); + } + + expect(game.games[0].score).not.toBe(game.games[1].score); +}); diff --git a/src/__tests__/realtime.test.ts b/src/__tests__/realtime.test.ts index 3c903c8..adcc9d9 100644 --- a/src/__tests__/realtime.test.ts +++ b/src/__tests__/realtime.test.ts @@ -1,6 +1,11 @@ import {expect, test} from 'bun:test'; import {fixedRandomGame} from './archive'; -import {DEFAULT_MARGIN_FRAMES, MultiplayerGame, PlayedMove} from '../game'; +import { + DEFAULT_MARGIN_FRAMES, + DEFAULT_MERCY_FRAMES, + MultiplayerGame, + PlayedMove, +} from '../game'; import {RevealedPiece, TimeWarpingGame, TimeWarpingMirror} from '../realtime'; import {HEIGHT} from '../bitboard'; @@ -36,11 +41,13 @@ test('Fixed random game (time warp)', () => { const replay = fixedRandomGame(); const origin = new MultiplayerGame( - replay.gameSeed, - replay.screenSeed, + replay.gameSeeds, + replay.screenSeeds, replay.colorSelections, + replay.initialBags, replay.targetPoints, - replay.marginFrames + replay.marginFrames, + replay.mercyFrames ); const main = new TimeWarpingGame(origin); @@ -76,22 +83,26 @@ test('Fixed random game (mirror time warp)', () => { const replay = fixedRandomGame(); const origin = new MultiplayerGame( - replay.gameSeed, - replay.screenSeed, + replay.gameSeeds, + replay.screenSeeds, replay.colorSelections, + replay.initialBags, replay.targetPoints, - replay.marginFrames + replay.marginFrames, + replay.mercyFrames ); const mirrorOrigin = new MultiplayerGame( null, - replay.screenSeed, + replay.screenSeeds, replay.colorSelections, + origin.initialBags, replay.targetPoints, - replay.marginFrames + replay.marginFrames, + replay.mercyFrames ); - const mirror = new TimeWarpingMirror(mirrorOrigin, origin.initialBags); + const mirror = new TimeWarpingMirror(mirrorOrigin); const game = origin.clone(true); @@ -137,7 +148,7 @@ test('Fixed random game (mirror time warp)', () => { test('Sounds of the past', () => { const origin = new MultiplayerGame(); - const mirror = new TimeWarpingMirror(origin, origin.initialBags); + const mirror = new TimeWarpingMirror(origin); mirror.warp(10); @@ -161,13 +172,23 @@ test('Multiplayer subclassability', () => { sound: string; constructor( - seed?: number | null, - screenSeed?: number, + seeds?: number[] | null | null[], + screenSeeds?: number[], colorSelections?: number[][], + initialBags?: number[][], targetPoints?: number[], - marginFrames = DEFAULT_MARGIN_FRAMES + marginFrames = DEFAULT_MARGIN_FRAMES, + mercyFrames = DEFAULT_MERCY_FRAMES ) { - super(seed, screenSeed, colorSelections, targetPoints, marginFrames); + super( + seeds, + screenSeeds, + colorSelections, + initialBags, + targetPoints, + marginFrames, + mercyFrames + ); this.sound = 'tick'; } @@ -231,8 +252,8 @@ test('Has memory limits', () => { }); test('Mirror has memory limits', () => { - const origin = new MultiplayerGame(null, 1, [[], []]); - const mirror = new TimeWarpingMirror(origin, [[], []], 5, 5); + const origin = new MultiplayerGame(null, [1, 1], [[], []]); + const mirror = new TimeWarpingMirror(origin, 5, 5); // Two scheduled checkpoints at 5 and 10 mirror.warp(10); expect(mirror.checkpoints.size).toBe(2); diff --git a/src/__tests__/replay.test.ts b/src/__tests__/replay.test.ts index 7b18657..4e601c5 100644 --- a/src/__tests__/replay.test.ts +++ b/src/__tests__/replay.test.ts @@ -11,6 +11,28 @@ test('Fixed random game', () => { expect(track).toHaveLength(88); expect(track.filter(i => i.type === 'lockout')[0].player).toBe(1); + + const game = new MultiplayerGame( + replay.gameSeeds, + replay.screenSeeds, + replay.colorSelections, + replay.initialBags, + replay.targetPoints, + replay.marginFrames, + replay.mercyFrames + ); + for (const move of replay.moves) { + while (game.age < move.time) { + game.tick(); + } + expect(game.games[move.player].busy).toBeFalse(); + game.play(move.player, move.x1, move.y1, move.orientation); + } + while (game.games.some(g => g.busy)) { + game.tick(); + } + expect(game.games[0].score).toBe(3300); + expect(game.games[1].score).toBe(720); }); test('Lumi vs. Flex2', () => { @@ -25,9 +47,10 @@ test('Re-entrance', () => { const snapShots: MultiplayerGame[] = []; const game = new MultiplayerGame( - LUMI_VS_FLEX2.gameSeed, - LUMI_VS_FLEX2.screenSeed, + LUMI_VS_FLEX2.gameSeeds, + LUMI_VS_FLEX2.screenSeeds, LUMI_VS_FLEX2.colorSelections, + LUMI_VS_FLEX2.initialBags, LUMI_VS_FLEX2.targetPoints, LUMI_VS_FLEX2.marginFrames, LUMI_VS_FLEX2.mercyFrames diff --git a/src/game.ts b/src/game.ts index ef2bf8a..a079616 100644 --- a/src/game.ts +++ b/src/game.ts @@ -78,6 +78,42 @@ export function randomColorSelection(size = COLOR_SELECTION_SIZE): number[] { return [...result]; } +export function randomBag(colorSelection: number[], jkiss?: JKISS32): number[] { + // Bag implementation prevents extreme droughts. + let result = []; + for (let j = 0; j < colorSelection.length; ++j) { + for (let i = 0; i < BAG_QUOTA_PER_COLOR; ++i) { + result.push(colorSelection[j]); + } + } + + // Spice prevents cheesy "all clear" shenanigans. + if (jkiss === undefined) { + const spiceAmount = + BASE_BAG_SPICE + Math.floor(Math.random() * EXTRA_BAG_SPICE); + for (let i = 0; i < spiceAmount; ++i) { + result.push( + colorSelection[Math.floor(Math.random() * colorSelection.length)] + ); + } + } else { + result = result.concat( + jkiss.sample( + colorSelection, + BASE_BAG_SPICE + (jkiss.step() % EXTRA_BAG_SPICE) + ) + ); + } + + // Shake it! + if (jkiss === undefined) { + result.sort(() => Math.random() - 0.5); + } else { + jkiss.shuffle(result); + } + return result; +} + export class OnePlayerGame { age: number; score: number; @@ -93,9 +129,10 @@ export class OnePlayerGame { consecutiveRerolls: number; constructor( - seed?: number | null, - screenSeed?: number, - colorSelection?: number[] + seed: number | null, + screenSeed: number, + colorSelection: number[], + initialBag: number[] ) { this.age = 0; this.score = 0; @@ -104,35 +141,18 @@ export class OnePlayerGame { this.active = false; if (seed === null) { this.jkiss = null; - if (screenSeed === undefined) { - throw new Error( - 'Screen seed must be explicitly provided when running as a mirror.' - ); - } } else { this.jkiss = new JKISS32(seed); - if (screenSeed === undefined) { - screenSeed = this.jkiss.step(); - } } this.screen = new PuyoScreen(screenSeed); + this.colorSelection = [...colorSelection]; - if (colorSelection === undefined) { - if (this.jkiss === null) { - throw new Error( - 'Color selection must be explicitly provided when running as a mirror.' - ); - } - this.colorSelection = this.jkiss.subset( - [...Array(NUM_PUYO_COLORS).keys()], - COLOR_SELECTION_SIZE - ); - } else { - this.colorSelection = [...colorSelection]; + this.bag = [...initialBag]; + if (this.bag.length < 6) { + this.bag.unshift(-1); + this.bag.unshift(-1); + this.advanceColors(); } - - this.bag = []; - this.advanceColors(); this.lockedOut = false; this.hardDropLanded = false; this.consecutiveRerolls = 0; @@ -167,19 +187,7 @@ export class OnePlayerGame { while (this.bag.length < 6) { // Bag implementation prevents extreme droughts. // Spice prevents cheesy "all clear" shenanigans. - let freshBag = []; - for (let j = 0; j < this.colorSelection.length; ++j) { - for (let i = 0; i < BAG_QUOTA_PER_COLOR; ++i) { - freshBag.push(this.colorSelection[j]); - } - } - freshBag = freshBag.concat( - this.jkiss.sample( - this.colorSelection, - BASE_BAG_SPICE + (this.jkiss.step() % EXTRA_BAG_SPICE) - ) - ); - this.jkiss.shuffle(freshBag); + const freshBag = randomBag(this.colorSelection, this.jkiss); this.bag = this.bag.concat(freshBag); } } @@ -383,7 +391,8 @@ export class OnePlayerGame { const result = new (this.constructor as new (...args: any[]) => this)( undefined, undefined, - this.colorSelection + this.colorSelection, + [] ); if (preserveSeed) { if (this.jkiss === null) { @@ -411,6 +420,44 @@ export class OnePlayerGame { } export class SinglePlayerGame extends OnePlayerGame { + constructor( + seed?: number | null, + screenSeed?: number, + colorSelection?: number[], + initialBag?: number[] + ) { + if (seed === undefined) { + seed = randomSeed(); + if (screenSeed === undefined) { + screenSeed = randomSeed(); + } + if (colorSelection === undefined) { + colorSelection = randomColorSelection(); + } + } else if (seed === null) { + if (screenSeed === undefined || colorSelection === undefined) { + throw new Error( + 'A mirror requires an explicit screen seed and color selection' + ); + } + } else { + const jkiss = new JKISS32(seed); + if (screenSeed === undefined) { + screenSeed = jkiss.step(); + } + if (colorSelection === undefined) { + colorSelection = jkiss.subset( + [...Array(NUM_PUYO_COLORS).keys()], + COLOR_SELECTION_SIZE + ); + } + } + if (initialBag === undefined) { + initialBag = []; + } + super(seed, screenSeed, colorSelection, initialBag); + } + tick(): TickResult { const tickResult = super.tick(); this.score += tickResult.allClear ? ALL_CLEAR_BONUS : 0; @@ -447,26 +494,92 @@ export class MultiplayerGame { mercyRemaining: number[]; constructor( - seed?: number | null, - screenSeed?: number, + seeds?: number[] | null | null[], + screenSeeds?: number[], colorSelections?: number[][], + initialBags?: number[][], targetPoints?: number[], marginFrames = DEFAULT_MARGIN_FRAMES, mercyFrames = DEFAULT_MERCY_FRAMES ) { - if (seed === undefined) { - seed = randomSeed(); - } - if (colorSelections === undefined) { - this.games = [ - new OnePlayerGame(seed, screenSeed, undefined), - new OnePlayerGame(seed, screenSeed, undefined), - ]; + if (seeds === undefined) { + // Bags will eventually diverge + seeds = [randomSeed(), randomSeed()]; + if (screenSeeds === undefined) { + // Garbage will fall randomly and independently + screenSeeds = [randomSeed(), randomSeed()]; + } + if (colorSelections === undefined) { + // Colors are the same for both players + const colorSelection = randomColorSelection(); + colorSelections = [colorSelection, colorSelection]; + } + if (initialBags === undefined) { + // The initial pieces are same for both players (if possible) + const initialBag = randomBag(colorSelections[0]); + initialBags = [initialBag, initialBag]; + if (colorSelections[0].length !== colorSelections[1].length) { + initialBags[1] = randomBag(colorSelections[1]); + } else { + for (let i = 0; i < colorSelections[0].length; ++i) { + if (colorSelections[0][i] !== colorSelections[1][i]) { + initialBags[1] = randomBag(colorSelections[1]); + break; + } + } + } + } + } else if (seeds === null || seeds[0] === null) { + if (screenSeeds === undefined || colorSelections === undefined) { + throw new Error( + 'Mirrors require explicit screen seeds and color selections' + ); + } + seeds = [null, null]; } else { - this.games = colorSelections.map( - selection => new OnePlayerGame(seed, screenSeed, selection) - ); + const jkiss = new JKISS32(seeds[0]); + if (screenSeeds === undefined) { + screenSeeds = [jkiss.step(), jkiss.step()]; + } + if (colorSelections === undefined) { + const colorSelection = jkiss.subset( + [...Array(NUM_PUYO_COLORS).keys()], + COLOR_SELECTION_SIZE + ); + colorSelections = [colorSelection, colorSelection]; + } + if (initialBags === undefined) { + const initialBag = randomBag(colorSelections[0], jkiss); + initialBags = [initialBag, initialBag]; + if (colorSelections[0].length !== colorSelections[1].length) { + initialBags[1] = randomBag(colorSelections[1], jkiss); + } else { + for (let i = 0; i < colorSelections[0].length; ++i) { + if (colorSelections[0][i] !== colorSelections[1][i]) { + initialBags[1] = randomBag(colorSelections[1], jkiss); + break; + } + } + } + } } + if (initialBags === undefined) { + initialBags = [[], []]; + } + this.games = [ + new OnePlayerGame( + seeds[0], + screenSeeds[0], + colorSelections[0], + initialBags[0] + ), + new OnePlayerGame( + seeds[1], + screenSeeds[1], + colorSelections[1], + initialBags[1] + ), + ]; if (targetPoints === undefined) { targetPoints = [DEFAULT_TARGET_POINTS, DEFAULT_TARGET_POINTS]; @@ -737,7 +850,12 @@ export class MultiplayerGame { // Random seed. Don't leak original unless specified. clone(preserveSeed = false) { - const result = new (this.constructor as new () => this)(); + const result = new (this.constructor as new (...args: any[]) => this)( + [0, 0], + [0, 0], + this.games.map(g => g.colorSelection), + this.games.map(g => g.bag) + ); result.games = this.games.map(game => game.clone(preserveSeed)); result.targetPoints = [...this.targetPoints]; result.pendingGarbage = [...this.pendingGarbage]; diff --git a/src/realtime.ts b/src/realtime.ts index 5e7dd34..628fa94 100644 --- a/src/realtime.ts +++ b/src/realtime.ts @@ -213,15 +213,10 @@ export class TimeWarpingMirror< bags: number[][]; seenTicks: number[]; - constructor( - origin: T, - initialBags: number[][], - checkpointInterval = 10, - maxCheckpoints = Infinity - ) { + constructor(origin: T, checkpointInterval = 10, maxCheckpoints = Infinity) { super(origin, checkpointInterval, maxCheckpoints); this.moves = [[], []]; - this.bags = initialBags.map(b => [...b]); + this.bags = origin.games.map(g => [...g.bag]); this.seenTicks = Array(origin.games.length).fill(origin.age); } diff --git a/src/replay.ts b/src/replay.ts index 94746db..8e2be3e 100644 --- a/src/replay.ts +++ b/src/replay.ts @@ -66,9 +66,10 @@ export type ReplayResult = { /** Seed data for re-creating a game. */ export type Replay = { - gameSeed: number; - screenSeed: number; + gameSeeds: number[]; + screenSeeds: number[]; colorSelections: number[][]; + initialBags: number[][]; targetPoints: number[]; marginFrames: number; mercyFrames: number; @@ -158,9 +159,10 @@ export function cmpMoves(a: PlayedMove, b: PlayedMove) { export function logReplay(replay: Replay) { const game = new MultiplayerGame( - replay.gameSeed, - replay.screenSeed, + replay.gameSeeds, + replay.screenSeeds, replay.colorSelections, + replay.initialBags, replay.targetPoints, replay.mercyFrames ); @@ -185,9 +187,10 @@ export function* replayToTrack( baseClass: T = MultiplayerGame as T ): ReplayTrack { const game = new baseClass( - replay.gameSeed, - replay.screenSeed, + replay.gameSeeds, + replay.screenSeeds, replay.colorSelections, + replay.initialBags, replay.targetPoints, replay.marginFrames );