diff --git a/src/browser/SyncTableEngine.ts b/src/browser/SyncTableEngine.ts index d106818e..1d21bde9 100644 --- a/src/browser/SyncTableEngine.ts +++ b/src/browser/SyncTableEngine.ts @@ -1,6 +1,6 @@ import InputWatcher from '../browser/InputWatcher' import ResizeWatcher from '../browser/ResizeWatcher' -import { GameEngine } from '../engine' +import { CellSaveState, GameEngine } from '../engine' import { LEVEL_TYPE } from '../parser/astTypes' import Parser from '../parser/parser' import TableUI from '../ui/table' @@ -43,12 +43,12 @@ class SubTableEngine { this.tableUI = new TableUI(table, handler) } - public setGame(code: string, levelNum: number) { + public setGame(code: string, levelNum: number, checkpoint: Optional) { const { data } = Parser.parse(code) this.engine = new GameEngine(data, this.tableUI) this.tableUI.onGameChange(data) - this.engine.setLevel(levelNum) + this.engine.setLevel(levelNum, checkpoint) if (data.metadata.keyRepeatInterval) { this.inputWatcher.setKeyRepeatInterval(data.metadata.keyRepeatInterval) @@ -108,8 +108,8 @@ export default class SyncTableEngine implements Engineish { this.table.addEventListener('blur', this.boundPause) this.table.addEventListener('focus', this.boundResume) } - public setGame(source: string, level: number = 0) { - this.subEngine.setGame(source, level) + public setGame(source: string, level: number = 0, checkpoint: Optional) { + this.subEngine.setGame(source, level, checkpoint) const engine = this.subEngine.getEngine() if (engine.getCurrentLevel().type === LEVEL_TYPE.MAP) { diff --git a/src/browser/WebworkerTableEngine.ts b/src/browser/WebworkerTableEngine.ts index 75317a97..8bc190cb 100644 --- a/src/browser/WebworkerTableEngine.ts +++ b/src/browser/WebworkerTableEngine.ts @@ -1,3 +1,4 @@ +import { CellSaveState } from '../engine' import { GameData } from '../models/game' import { Dimension } from '../models/metadata' import { A11Y_MESSAGE, A11Y_MESSAGE_TYPE } from '../models/rule' @@ -15,7 +16,7 @@ import { Cellish, pollingPromise, PuzzlescriptWorker, RULE_DIRECTION, - WorkerResponse} from '../util' + WorkerResponse } from '../util' import InputWatcher from './InputWatcher' import ResizeWatcher from './ResizeWatcher' @@ -91,8 +92,8 @@ export default class WebworkerTableEngine implements Engineish { this.inputInterval = window.setInterval(this.pollInputWatcher, 10) } - public setGame(code: string, level: number) { - this.worker.postMessage({ type: MESSAGE_TYPE.ON_GAME_CHANGE, code, level }) + public setGame(code: string, level: number, checkpoint: Optional) { + this.worker.postMessage({ type: MESSAGE_TYPE.ON_GAME_CHANGE, code, level, checkpoint }) } public dispose() { @@ -160,7 +161,7 @@ export default class WebworkerTableEngine implements Engineish { this.ui.onPress(data.direction) break case MESSAGE_TYPE.ON_TICK: - this.ui.onTick(new Set(data.changedCells.map((x) => this.convertToCellish(x))), data.hasAgain, this.convertToA11yMessages(data.a11yMessages)) + this.ui.onTick(new Set(data.changedCells.map((x) => this.convertToCellish(x))), data.checkpoint, data.hasAgain, this.convertToA11yMessages(data.a11yMessages)) break case MESSAGE_TYPE.ON_SOUND: await this.ui.onSound({ soundCode: data.soundCode }) diff --git a/src/cli/playGame.ts b/src/cli/playGame.ts index 403b7be6..fa171946 100644 --- a/src/cli/playGame.ts +++ b/src/cli/playGame.ts @@ -345,7 +345,8 @@ async function playGame(data: GameData, currentLevelNum: number, recordings: ISa } }) TerminalUI.clearScreen() - engine.setLevel(data.levels.indexOf(level)) + // TODO: Support saving and loading checkpoints in the CLI + engine.setLevel(data.levels.indexOf(level), null/*no checkpoint*/) function restartLevel() { engine.press(INPUT_BUTTON.RESTART) diff --git a/src/cli/runGames.ts b/src/cli/runGames.ts index f82d4319..642f4729 100644 --- a/src/cli/runGames.ts +++ b/src/cli/runGames.ts @@ -90,7 +90,7 @@ async function run() { startTime = Date.now() const engine = new GameEngine(data, TerminalUI) const levelNum = data.levels.indexOf(currentLevel) - engine.setLevel(levelNum) + engine.setLevel(levelNum, null/*no checkpoint*/) logger.debug(() => `\n\nStart playing "${data.title}". Level ${levelNum}`) logger.info(() => `Loading Cells into the level took ${Date.now() - startTime}ms`) diff --git a/src/engine.ts b/src/engine.ts index 719aa11b..b98e8852 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -17,7 +17,7 @@ interface ITickResult { changedCells: Set, didWinGame: boolean, didLevelChange: boolean, - wasAgainTick: boolean + wasAgainTick: boolean, } type Snapshot = Array>> @@ -464,6 +464,7 @@ export class LevelEngine extends EventEmitter2 { changedCells: new Set(this.getCells()), soundToPlay: null, messageToShow: null, + hasCheckpoint: false, hasRestart: false, isWinning: false, mutations: [], @@ -476,6 +477,7 @@ export class LevelEngine extends EventEmitter2 { changedCells: new Set(this.getCells()), soundToPlay: null, messageToShow: null, + hasCheckpoint: false, hasRestart: true, isWinning: false, mutations: [], @@ -488,6 +490,7 @@ export class LevelEngine extends EventEmitter2 { // TODO: Handle the commands like RESTART, CANCEL, WIN at this point let soundToPlay: Optional> = null let messageToShow: Optional = null + let hasCheckpoint = false let hasWinCommand = false let hasRestart = false for (const command of ret.commands) { @@ -505,9 +508,11 @@ export class LevelEngine extends EventEmitter2 { case COMMAND_TYPE.WIN: hasWinCommand = true break + case COMMAND_TYPE.CHECKPOINT: + hasCheckpoint = true + break case COMMAND_TYPE.AGAIN: case COMMAND_TYPE.CANCEL: - case COMMAND_TYPE.CHECKPOINT: break default: throw new Error(`BUG: Unsupported command "${command}"`) @@ -520,6 +525,7 @@ export class LevelEngine extends EventEmitter2 { return { changedCells: new Set(ret.changedCells.keys()), + hasCheckpoint, soundToPlay, messageToShow, hasRestart, @@ -603,6 +609,11 @@ export class LevelEngine extends EventEmitter2 { return { movedCells, a11yMessages } } + // Used for UNDO and RESTART + public createSnapshot() { + return this.getCurrentLevel().getCells().map((row) => row.map((cell) => cell.toSnapshot())) + } + private pressDir(direction: INPUT_BUTTON) { // Should disable keypresses if `AGAIN` is running. // It is commented because the didSpritesChange logic is not correct. @@ -776,16 +787,19 @@ export class LevelEngine extends EventEmitter2 { } return { changedCells: new Set(), + checkpoint: null, commands: new Set>>(), evaluatedRules, mutations: new Set(), a11yMessages: [] } } + let checkpoint: Optional = null const didCheckpoint = !!allCommands.find((c) => c.type === COMMAND_TYPE.CHECKPOINT) if (didCheckpoint) { this.undoStack = [] - this.takeSnapshot(this.createSnapshot()) + checkpoint = this.createSnapshot() + this.takeSnapshot(checkpoint) } // set this only if we did not CANCEL and if some cell changed const changedCells = setAddAll(setAddAll(changedCellMutations, changedCellsLate), movedCells) @@ -811,11 +825,6 @@ export class LevelEngine extends EventEmitter2 { }) return conditionsSatisfied } - - // Used for UNDO and RESTART - private createSnapshot() { - return this.getCurrentLevel().getCells().map((row) => row.map((cell) => cell.toSnapshot())) - } private takeSnapshot(snapshot: Snapshot) { this.undoStack.push(snapshot) } @@ -900,12 +909,15 @@ export class GameEngine { public hasAgain() { return this.levelEngine.hasAgain() } - public setLevel(levelNum: number) { + public setLevel(levelNum: number, checkpoint: Optional) { this.levelEngine.hasAgainThatNeedsToRun = false this.currentLevelNum = levelNum const level = this.getGameData().levels[levelNum] if (level.type === LEVEL_TYPE.MAP) { this.levelEngine.setLevel(levelNum) + if (checkpoint) { + this.loadSnapshotFromJSON(checkpoint) + } this.handler.onLevelChange(this.currentLevelNum, this.levelEngine.getCurrentLevel().getCells(), null) } else { this.handler.onLevelChange(this.currentLevelNum, null, level.message) @@ -921,7 +933,7 @@ export class GameEngine { this.handler.onWin() didWinGameInMessage = true } else { - this.setLevel(this.currentLevelNum + 1) + this.setLevel(this.currentLevelNum + 1, null/*no checkpoint*/) } // clear any keys that were pressed this.levelEngine.pendingPlayerWantsToMove = null @@ -944,14 +956,16 @@ export class GameEngine { } const previousPending = this.levelEngine.pendingPlayerWantsToMove - const { changedCells, soundToPlay, messageToShow, isWinning, hasRestart, a11yMessages } = this.levelEngine.tick() + const { changedCells, hasCheckpoint, soundToPlay, messageToShow, isWinning, hasRestart, a11yMessages } = this.levelEngine.tick() if (previousPending && !this.levelEngine.pendingPlayerWantsToMove) { this.handler.onPress(previousPending) } + const checkpoint = hasCheckpoint ? this.saveSnapshotToJSON() : null + if (hasRestart) { - this.handler.onTick(changedCells, hasAgain, a11yMessages) + this.handler.onTick(changedCells, checkpoint, hasAgain, a11yMessages) return { changedCells, didWinGame: false, @@ -961,14 +975,14 @@ export class GameEngine { } hasAgain = this.levelEngine.hasAgain() - this.handler.onTick(changedCells, hasAgain, a11yMessages) + this.handler.onTick(changedCells, checkpoint, hasAgain, a11yMessages) let didWinGame = false if (isWinning) { if (this.currentLevelNum === this.levelEngine.gameData.levels.length - 1) { didWinGame = true this.handler.onWin() } else { - this.setLevel(this.currentLevelNum + 1) + this.setLevel(this.currentLevelNum + 1, null/*no checkpoint*/) } } diff --git a/src/index-webworker.ts b/src/index-webworker.ts index ee8c9a03..7084068f 100644 --- a/src/index-webworker.ts +++ b/src/index-webworker.ts @@ -1,5 +1,5 @@ import 'babel-polyfill' // tslint:disable-line:no-implicit-dependencies -import { Cell, GameEngine } from './engine' +import { Cell, CellSaveState, GameEngine } from './engine' import { GameData } from './models/game' import { A11Y_MESSAGE, A11Y_MESSAGE_TYPE } from './models/rule' import { GameSprite } from './models/tile' @@ -19,7 +19,7 @@ let lastTick = 0 onmessage = (event: TypedMessageEvent) => { const msg = event.data switch (msg.type) { - case MESSAGE_TYPE.ON_GAME_CHANGE: loadGame(msg.code, msg.level); break + case MESSAGE_TYPE.ON_GAME_CHANGE: loadGame(msg.code, msg.level, msg.checkpoint); break case MESSAGE_TYPE.PAUSE: postMessage({ type: msg.type, payload: pauseGame() }); break case MESSAGE_TYPE.RESUME: postMessage({ type: msg.type, payload: resumeGame() }); break case MESSAGE_TYPE.PRESS: postMessage({ type: msg.type, payload: press(msg.button) }); break @@ -99,8 +99,8 @@ class Handler implements GameEngineHandler { public async onSound(sound: Soundish) { postMessage({ type: MESSAGE_TYPE.ON_SOUND, soundCode: sound.soundCode }) } - public onTick(changedCells: Set, hasAgain: boolean, a11yMessages: Array>) { - postMessage({ type: MESSAGE_TYPE.ON_TICK, changedCells: toCellsJson(changedCells), hasAgain, a11yMessages: a11yMessages.map(toA11yMessageJson) }) + public onTick(changedCells: Set, checkpoint: Optional, hasAgain: boolean, a11yMessages: Array>) { + postMessage({ type: MESSAGE_TYPE.ON_TICK, changedCells: toCellsJson(changedCells), checkpoint, hasAgain, a11yMessages: a11yMessages.map(toA11yMessageJson) }) } public onPause() { postMessage({ type: MESSAGE_TYPE.ON_PAUSE }) @@ -110,13 +110,13 @@ class Handler implements GameEngineHandler { } } -const loadGame = (code: string, level: number) => { +const loadGame = (code: string, level: number, checkpoint: Optional) => { pauseGame() previousMessage = '' // clear this dev-invariant-tester field since it is a new game const { data } = Parser.parse(code) postMessage({ type: MESSAGE_TYPE.ON_GAME_CHANGE, payload: (new Serializer(data)).toJson() }) currentEngine = new GameEngine(data, new Handler()) - currentEngine.setLevel(level) + currentEngine.setLevel(level, checkpoint) runPlayLoop() // tslint:disable-line:no-floating-promises } diff --git a/src/pwa-app.ts b/src/pwa-app.ts index 86f0c409..b93a5b7a 100644 --- a/src/pwa-app.ts +++ b/src/pwa-app.ts @@ -4,6 +4,7 @@ import TimeAgo from 'javascript-time-ago' // tslint:disable-line:no-implicit-dep import TimeAgoEn from 'javascript-time-ago/locale/en' // tslint:disable-line import { BUTTON_TYPE } from './browser/controller/controller' import WebworkerTableEngine from './browser/WebworkerTableEngine' +import { CellSaveState } from './engine' import { IGameTile } from './models/tile' import { Level } from './parser/astTypes' import { GameEngineHandlerOptional, Optional, pollingPromise } from './util' @@ -20,6 +21,11 @@ type PromptEvent = Event & { // notification: Notification // } +interface StorageCheckpoint { + levelNum: number, + data: CellSaveState +} + interface StorageGameInfo { currentLevelNum: number completedLevelAt: number @@ -55,6 +61,7 @@ window.addEventListener('load', () => { const WEBWORKER_URL = './puzzlescript-webworker.js' const GAME_STORAGE_ID = 'puzzlescriptGameProgress' + const GAME_STORAGE_CHECKPOINT_PREFIX = 'puzzlescriptGameCheckpoint' const table: HTMLTableElement = getElement('#theGame') const gameSelection: HTMLSelectElement = getElement('#gameSelection') const loadingIndicator = getElement('#loadingIndicator') @@ -68,8 +75,6 @@ window.addEventListener('load', () => { TimeAgo.addLocale(TimeAgoEn) const timeAgo = new TimeAgo('en-US') - let currentGameId = '' // used for loading and saving game progress - if (!gameSelection) { throw new Error(`BUG: Could not find game selection dropdown`) } if (!loadingIndicator) { throw new Error(`BUG: Could not find loading indicator`) } @@ -77,6 +82,83 @@ window.addEventListener('load', () => { messageDialog.close() }) + // Functions for loading/saving game progress + const currentInfo = new class { + private gameId: string + private levelNum: number + + constructor() { + this.gameId = 'INVALID_GAME' + this.levelNum = -1 + } + + public setGameId(gameId: string) { + this.gameId = gameId + this.levelNum = -1 + } + public getGameId() { + if (this.gameId === 'INVALID_GAME') { + throw new Error(`BUG: Did not set game id`) + } + return this.gameId + } + public getLevelNum() { + if (this.levelNum < 0) { + throw new Error(`BUG: Did not set level num`) + } + return this.levelNum + } + public loadStorage() { + const storage = this.loadJson(GAME_STORAGE_ID, { _version: 1 }) + return storage as Storage + } + public loadCurrentLevelNum() { + const gameId = this.getGameId() + const storage = this.loadStorage() + const gameData = storage[gameId] + return (gameData || null) && gameData.currentLevelNum + } + public loadCheckpoint(): Optional { + const gameId = this.getGameId() + return this.loadJson(`${GAME_STORAGE_CHECKPOINT_PREFIX}.${gameId}`, null) + } + public saveCurrentLevelNum(levelNum: number) { + const gameId = this.getGameId() + const storage = this.loadStorage() + storage[gameId] = storage[gameId] || {} + storage[gameId].currentLevelNum = levelNum + storage[gameId].completedLevelAt = Date.now() + storage[gameId].lastPlayedAt = Date.now() + // storage[gameId].checkpoint = null + this.saveJson(GAME_STORAGE_ID, storage) + ga && ga('send', 'event', 'game', 'level', gameId, levelNum) + + currentInfo.levelNum = levelNum + } + public saveGameInfo(levels: Array>, title: string) { + const gameId = this.getGameId() + const storage = this.loadStorage() + storage[gameId] = storage[gameId] || {} + storage[gameId].levelMaps = levels.map((l) => l.type === 'LEVEL_MAP') + storage[gameId].title = title + storage[gameId].lastPlayedAt = Date.now() + this.saveJson(GAME_STORAGE_ID, storage) + } + public saveCheckpoint(checkpoint: CellSaveState) { + const gameId = this.getGameId() + const storage = { _version: 1, levelNum: this.getLevelNum(), data: checkpoint } + this.saveJson(`${GAME_STORAGE_CHECKPOINT_PREFIX}.${gameId}`, storage) + } + + private loadJson(key: string, defaultValue: T) { + const str = window.localStorage.getItem(key) + return str ? JSON.parse(str) : defaultValue + } + private saveJson(key: string, value: T) { + window.localStorage.setItem(key, JSON.stringify(value)) + } + }() + closeInstructions.addEventListener('click', () => { instructionsContainer.classList.add('hidden') // resize the game @@ -149,7 +231,7 @@ window.addEventListener('load', () => { loadingIndicator.classList.add('hidden') gameSelection.removeAttribute('disabled') - saveCurrentLevelNum(currentGameId, newLevelNum) + currentInfo.saveCurrentLevelNum(newLevelNum) updateGameSelectionInfo(false) }, onGameChange(gameData) { @@ -157,7 +239,14 @@ window.addEventListener('load', () => { const { backgroundColor } = gameData.metadata window.document.body.style.backgroundColor = backgroundColor ? backgroundColor.toHex() : 'black' - saveGameInfo(currentGameId, gameData.levels, gameData.title) + currentInfo.saveGameInfo(gameData.levels, gameData.title) + }, + onTick(_changedCells, checkpoint) { + if (checkpoint) { + // Ideally, include the level number so we can verify the checkpoint applies to the level + // This might require creating an onCheckpoint(levelNum, checkpoint) event + currentInfo.saveCheckpoint(checkpoint) + } } } @@ -182,16 +271,27 @@ window.addEventListener('load', () => { loadingIndicator.classList.remove('hidden') // Show the "Loading..." text gameSelection.setAttribute('disabled', 'disabled') - currentGameId = gameSelection.value - if (!currentGameId) { + currentInfo.setGameId(gameSelection.value) + if (!currentInfo.getGameId()) { return } - fetch(`./games/${currentGameId}/script.txt`, { redirect: 'follow' }) + fetch(`./games/${currentInfo.getGameId()}/script.txt`, { redirect: 'follow' }) .then((resp) => { if (resp.ok) { return resp.text().then((source) => { // Load the game - tableEngine.setGame(source, loadCurrentLevelNum(currentGameId) || 0) + const levelNum = currentInfo.loadCurrentLevelNum() + const checkpoint = currentInfo.loadCheckpoint() + if (checkpoint) { + // verify that the currentLevelNum is the same as the checkpoint level num + const { levelNum: checkpointLevelNum, data: checkpointData } = checkpoint + if (levelNum !== checkpointLevelNum) { + throw new Error(`BUG: Checkpoint level number (${checkpointLevelNum}) does not match current level number (${levelNum})`) + } + tableEngine.setGame(source, currentInfo.loadCurrentLevelNum() || 0, checkpointData) + } else { + tableEngine.setGame(source, currentInfo.loadCurrentLevelNum() || 0, null) + } }) } else { alert(`Problem finding game file. Please choose another one`) @@ -249,42 +349,13 @@ window.addEventListener('load', () => { } - // Functions for loading/saving game progress - function loadStorage() { - const storageStr = window.localStorage.getItem(GAME_STORAGE_ID) - const storage = storageStr ? JSON.parse(storageStr) : { _version: 1 } - return storage as Storage - } - function loadCurrentLevelNum(gameId: string) { - const storage = loadStorage() - const gameData = storage[gameId] - return (gameData || null) && gameData.currentLevelNum - } - function saveCurrentLevelNum(gameId: string, levelNum: number) { - const storage = loadStorage() - storage[gameId] = storage[gameId] || {} - storage[gameId].currentLevelNum = levelNum - storage[gameId].completedLevelAt = Date.now() - storage[gameId].lastPlayedAt = Date.now() - window.localStorage.setItem(GAME_STORAGE_ID, JSON.stringify(storage)) - ga && ga('send', 'event', 'game', 'level', gameId, levelNum) - } - function saveGameInfo(gameId: string, levels: Array>, title: string) { - const storage = loadStorage() - storage[gameId] = storage[gameId] || {} - storage[gameId].levelMaps = levels.map((l) => l.type === 'LEVEL_MAP') - storage[gameId].title = title - storage[gameId].lastPlayedAt = Date.now() - window.localStorage.setItem(GAME_STORAGE_ID, JSON.stringify(storage)) - } - // store the original sort order so that we fall back to it. gameSelection.querySelectorAll('option').forEach((option, index) => { option.setAttribute('data-original-index', `${index}`) }) function updateGameSelectionInfo(selectFirstGame: boolean) { - const storage = loadStorage() + const storage = currentInfo.loadStorage() // Update the last-updated time for all of the games and then sort them const gameOptions = getAllElements('option', gameSelection) diff --git a/src/replaySolutions/helper.ts b/src/replaySolutions/helper.ts index 6a531fc9..5d73260f 100644 --- a/src/replaySolutions/helper.ts +++ b/src/replaySolutions/helper.ts @@ -90,7 +90,7 @@ export function createTests(moduloNumber: number, moduloTotal: number) { numPlayed++ - engine.setLevel(index) + engine.setLevel(index, null/*no checkpoint*/) // UI.setGame(engine) diff --git a/src/ui.spec.ts b/src/ui.spec.ts index db661ca9..8e5629b6 100644 --- a/src/ui.spec.ts +++ b/src/ui.spec.ts @@ -11,7 +11,7 @@ const C_BLACK = { r: 0, g: 0, b: 0 } function parseAndReturnFirstSpritePixels(code: string) { const { data } = Parser.parse(code) const engine = new GameEngine(data, new EmptyGameEngineHandler()) - engine.setLevel(0) + engine.setLevel(0, null/*no checkpoint*/) const cell = engine.getCurrentLevelCells()[0][0] // console.log(cell.getSprites()) UI.onGameChange(engine.getGameData()) diff --git a/src/ui/table.ts b/src/ui/table.ts index c30d9892..94deb203 100644 --- a/src/ui/table.ts +++ b/src/ui/table.ts @@ -1,3 +1,4 @@ +import { CellSaveState } from '../engine' import { IColor } from '../models/colors' import { GameData } from '../models/game' import { A11Y_MESSAGE, A11Y_MESSAGE_TYPE } from '../models/rule' @@ -182,13 +183,13 @@ class TableUI extends BaseUI implements GameEngineHandler { playSound(sound.soundCode) // tslint:disable-line:no-floating-promises await this.handler.onSound(sound) } - public onTick(changedCells: Set, hasAgain: boolean, a11yMessages: Array>) { + public onTick(changedCells: Set, checkpoint: Optional, hasAgain: boolean, a11yMessages: Array>) { this.collectingTickCount++ this.printMessageLog(a11yMessages, hasAgain) this.drawCells(changedCells, false) this.markAcceptingInput(!hasAgain) this.didPressCauseTick = false - this.handler.onTick(changedCells, hasAgain, a11yMessages) + this.handler.onTick(changedCells, checkpoint, hasAgain, a11yMessages) } public willAllLevelsFitOnScreen(gameData: GameData) { diff --git a/src/util.ts b/src/util.ts index 72cda409..07cf0634 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ import { GameData } from '.' -import { Cell } from './engine' +import { Cell, CellSaveState } from './engine' import { GameMetadata } from './models/metadata' import { A11Y_MESSAGE } from './models/rule' import { GameSprite } from './models/tile' @@ -304,6 +304,7 @@ export type WorkerMessage = { type: MESSAGE_TYPE.ON_GAME_CHANGE code: string level: number + checkpoint: Optional } | { type: MESSAGE_TYPE.PRESS button: INPUT_BUTTON @@ -358,6 +359,7 @@ export type WorkerResponse = { } | { type: MESSAGE_TYPE.ON_TICK changedCells: CellishJson[] + checkpoint: Optional hasAgain: boolean a11yMessages: Array> } @@ -392,7 +394,7 @@ export interface GameEngineHandler { onLevelChange(level: number, cells: Optional, message: Optional): void onWin(): void onSound(sound: Soundish): Promise - onTick(changedCells: Set, hasAgain: boolean, a11yMessages: Array>): void + onTick(changedCells: Set, checkpoint: Optional, hasAgain: boolean, a11yMessages: Array>): void onPause(): void onResume(): void // onGameChange(data: GameData): void @@ -405,7 +407,7 @@ export interface GameEngineHandlerOptional { onLevelChange?(level: number, cells: Optional, message: Optional): void onWin?(): void onSound?(sound: Soundish): Promise - onTick?(changedCells: Set, hasAgain: boolean, a11yMessages: Array>): void + onTick?(changedCells: Set, checkpoint: Optional, hasAgain: boolean, a11yMessages: Array>): void onPause?(): void onResume?(): void // onGameChange?(data: GameData): void @@ -422,8 +424,8 @@ export class EmptyGameEngineHandler implements GameEngineHandler { public onLevelChange(level: number, cells: Optional, message: Optional) { for (const h of this.subHandlers) { h.onLevelChange && h.onLevelChange(level, cells, message) } } public onWin() { for (const h of this.subHandlers) { h.onWin && h.onWin() } } public async onSound(sound: Soundish) { for (const h of this.subHandlers) { h.onSound && h.onSound(sound) } } - public onTick(changedCells: Set, hasAgain: boolean, a11yMessages: Array>) { - for (const h of this.subHandlers) { h.onTick && h.onTick(changedCells, hasAgain, a11yMessages) } + public onTick(changedCells: Set, checkpoint: Optional, hasAgain: boolean, a11yMessages: Array>) { + for (const h of this.subHandlers) { h.onTick && h.onTick(changedCells, checkpoint, hasAgain, a11yMessages) } } public onPause() { for (const h of this.subHandlers) { h.onPause && h.onPause() } } public onResume() { for (const h of this.subHandlers) { h.onResume && h.onResume() } } @@ -431,7 +433,7 @@ export class EmptyGameEngineHandler implements GameEngineHandler { } export interface Engineish { - setGame(code: string, level: number): void + setGame(code: string, level: number, checkpoint: Optional): void dispose(): void pause?(): void resume?(): void