diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 11a8935be2b7c2c4b5d013a253b1e36ecdc7cbb7..896149f2381e00a0c83f6bf6689df4378ae81777 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import * as Reversi from 'misskey-reversi'; import type { MiChannel } from '@/models/Channel.js'; import type { MiUser } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -179,12 +180,7 @@ export interface ReversiGameEventTypes { key: string; value: any; }; - putStone: { - at: number; - color: boolean; - pos: number; - next: boolean; - }; + log: Reversi.Serializer.Log & { id: string | null }; syncState: { crc32: string; }; diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 6e80261330d98b482e2b39781f7be6fcaa59c398..9fe7255e48119a619a54cbc88d2bd7c1d459390f 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -235,11 +235,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const map = freshGame.map != null ? freshGame.map : getRandomMap(); + const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString(); + await this.reversiGamesRepository.update(game.id, { startedAt: new Date(), isStarted: true, black: bw, map: map, + crc32, }); //#region 盤é¢ã«æœ€åˆã‹ã‚‰çŸ³ãŒãªã„ãªã©ã—ã¦å§‹ã¾ã£ãŸçž¬é–“ã«å‹æ•—ãŒæ±ºå®šã™ã‚‹å ´åˆãŒã‚ã‚‹ã®ã§ãã®å‡¦ç† @@ -309,7 +312,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) { + public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number, id?: string | null) { if (!game.isStarted) return; if (game.isEnded) return; if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; @@ -319,56 +322,58 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { ? true : false; - const o = new Reversi.Game(game.map, { + const engine = Reversi.Serializer.restoreGame({ + map: game.map, isLlotheo: game.isLlotheo, canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, + logs: game.logs, }); - // 盤é¢ã®çŠ¶æ…‹ã‚’å†ç”Ÿ - for (const log of game.logs) { - o.put(log.color, log.pos); - } - - if (o.turn !== myColor) return; + if (engine.turn !== myColor) return; + if (!engine.canPut(myColor, pos)) return; - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); + engine.putStone(pos); let winner; - if (o.isEnded) { - if (o.winner === true) { + if (engine.isEnded) { + if (engine.winner === true) { winner = game.black === 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { + } else if (engine.winner === false) { winner = game.black === 1 ? game.user2Id : game.user1Id; } else { winner = null; } } + const logs = Reversi.Serializer.deserializeLogs(game.logs); + const log = { - at: Date.now(), - color: myColor, + time: Date.now(), + player: myColor, + operation: 'put', pos, - }; + } as const; + + logs.push(log); - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); + const serializeLogs = Reversi.Serializer.serializeLogs(logs); - game.logs.push(log); + const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); await this.reversiGamesRepository.update(game.id, { crc32, - isEnded: o.isEnded, + isEnded: engine.isEnded, winnerId: winner, - logs: game.logs, + logs: serializeLogs, }); - this.globalEventService.publishReversiGameStream(game.id, 'putStone', { + this.globalEventService.publishReversiGameStream(game.id, 'log', { ...log, - next: o.turn, + id: id ?? null, }); - if (o.isEnded) { + if (engine.isEnded) { this.globalEventService.publishReversiGameStream(game.id, 'ended', { winnerId: winner ?? null, game: await this.reversiGameEntityService.packDetail(game.id, user), diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index 8d95204928a754f415ee0c8d7c42d6c579318a7f..a7adc681f67f07182942075b183a6079c5177aa8 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -55,11 +55,7 @@ export class ReversiGameEntityService { isLlotheo: game.isLlotheo, canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, - logs: game.logs.map(log => ({ - at: log.at, - color: log.color, - pos: log.pos, - })), + logs: game.logs, map: game.map, }); } diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts index d297d1f01df4d7901a66b7709ab600dc9f55f239..dcaa5c9fa9f84167558bb3f494269c1457de1c3b 100644 --- a/packages/backend/src/models/ReversiGame.ts +++ b/packages/backend/src/models/ReversiGame.ts @@ -76,11 +76,7 @@ export class MiReversiGame { @Column('jsonb', { default: [], }) - public logs: { - at: number; - color: boolean; - pos: number; - }[]; + public logs: number[][]; @Column('varchar', { array: true, length: 64, @@ -117,9 +113,6 @@ export class MiReversiGame { }) public form2: any | null; - /** - * ãƒã‚°ã®posã‚’æ–‡å—列ã¨ã—ã¦ã™ã¹ã¦é€£çµã—ãŸã‚‚ã®ã®CRC32値 - */ @Column('varchar', { length: 32, nullable: true, }) diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index 0d23b9dc79358a4a9cbcf93ad1160bc216d1d390..b94046438b5caf403714929cf7c26e41f20226f3 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -204,22 +204,8 @@ export const packedReversiGameDetailedSchema = { type: 'array', optional: false, nullable: false, items: { - type: 'object', + type: 'array', optional: false, nullable: false, - properties: { - at: { - type: 'number', - optional: false, nullable: false, - }, - color: { - type: 'boolean', - optional: false, nullable: false, - }, - pos: { - type: 'number', - optional: false, nullable: false, - }, - }, }, }, map: { diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index c67c05fb0912b68d26b26f47e012519cf03d96fd..2d8c396db9e5ae0649cf49d073d148f8808bef98 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -45,7 +45,7 @@ class ReversiGameChannel extends Channel { switch (type) { case 'ready': this.ready(body); break; case 'updateSettings': this.updateSettings(body.key, body.value); break; - case 'putStone': this.putStone(body.pos); break; + case 'putStone': this.putStone(body.pos, body.id); break; case 'syncState': this.syncState(body.crc32); break; } } @@ -72,14 +72,14 @@ class ReversiGameChannel extends Channel { } @bindThis - private async putStone(pos: number) { + private async putStone(pos: number, id: string) { if (this.user == null) return; // TODO: ã‚ャッシュã—ãŸã„ const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - this.reversiService.putStoneToGame(game, this.user, pos); + this.reversiService.putStoneToGame(game, this.user, pos, id); } @bindThis diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 582967ad2ba55ff54b5b936ab8091261d84f0496..bf45fc41193aa7f163f61226e0602a4f06a457fd 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -13,12 +13,12 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> <MkEllipsis/> </div> - <div v-if="(logPos !== logs.length) && turnUser" class="turn"> + <div v-if="(logPos !== game.logs.length) && turnUser" class="turn"> <Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> </div> <div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div> <div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div> - <div v-if="game.isEnded && logPos == logs.length" class="result"> + <div v-if="game.isEnded && logPos == game.logs.length" class="result"> <template v-if="game.winner"> <Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/> <span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span> @@ -69,12 +69,12 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;"> - <div>{{ logPos }} / {{ logs.length }}</div> + <div>{{ logPos }} / {{ game.logs.length }}</div> <div v-if="!autoplaying" class="_buttonsCenter"> <MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton> <MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton> - <MkButton :disabled="logPos === logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton> - <MkButton :disabled="logPos === logs.length" @click="logPos = logs.length"><i class="ti ti-chevrons-right"></i></MkButton> + <MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton> + <MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton> </div> <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton> </div> @@ -115,18 +115,15 @@ const props = defineProps<{ const showBoardLabels = true; const autoplaying = ref<boolean>(false); const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game)); -const logs = ref<Misskey.entities.ReversiLog[]>(game.value.logs); -const logPos = ref<number>(logs.value.length); -const engine = shallowRef<Reversi.Game>(new Reversi.Game(game.value.map, { +const logPos = ref<number>(game.value.logs.length); +const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({ + map: game.value.map, isLlotheo: game.value.isLlotheo, canPutEverywhere: game.value.canPutEverywhere, loopedBoard: game.value.loopedBoard, + logs: game.value.logs, })); -for (const log of game.value.logs) { - engine.value.put(log.color, log.pos); -} - const iAmPlayer = computed(() => { return game.value.user1Id === $i.id || game.value.user2Id === $i.id; }); @@ -177,60 +174,76 @@ const cellsStyle = computed(() => { watch(logPos, (v) => { if (!game.value.isEnded) return; - const _o = new Reversi.Game(game.value.map, { + engine.value = Reversi.Serializer.restoreGame({ + map: game.value.map, isLlotheo: game.value.isLlotheo, canPutEverywhere: game.value.canPutEverywhere, loopedBoard: game.value.loopedBoard, + logs: game.value.logs.slice(0, v), }); - for (const log of logs.value.slice(0, v)) { - _o.put(log.color, log.pos); - } - engine.value = _o; }); if (game.value.isStarted && !game.value.isEnded) { useInterval(() => { if (game.value.isEnded) return; - const crc32 = CRC32.str(logs.value.map(x => x.pos.toString()).join('')); + const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); + if (_DEV_) console.log('crc32', crc32); props.connection.send('syncState', { crc32: crc32, }); - }, 5000, { immediate: false, afterMounted: true }); + }, 10000, { immediate: false, afterMounted: true }); } +const appliedOps: string[] = []; + function putStone(pos) { if (game.value.isEnded) return; if (!iAmPlayer.value) return; if (!isMyTurn.value) return; if (!engine.value.canPut(myColor.value!, pos)) return; - engine.value.put(myColor.value!, pos); + engine.value.putStone(pos); + triggerRef(engine); // サウンドをå†ç”Ÿã™ã‚‹ //sound.play(myColor.value ? 'reversiPutBlack' : 'reversiPutWhite'); + const id = Math.random().toString(36).slice(2); props.connection.send('putStone', { pos: pos, + id, }); + appliedOps.push(id); checkEnd(); } -function onPutStone(x) { - logs.value.push(x); +function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { + game.value.logs = Reversi.Serializer.serializeLogs([ + ...Reversi.Serializer.deserializeLogs(game.value.logs), + log, + ]); + logPos.value++; - engine.value.put(x.color, x.pos); - triggerRef(engine); - checkEnd(); - // サウンドをå†ç”Ÿã™ã‚‹ - if (x.color !== myColor.value) { - //sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); + if (log.id == null || !appliedOps.includes(log.id)) { + switch (log.operation) { + case 'put': { + engine.value.putStone(log.pos); + triggerRef(engine); + checkEnd(); + //sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); + break; + } + + default: + break; + } } } -function onEnded(x) { +function onStreamEnded(x) { game.value = deepClone(x.game); } @@ -250,23 +263,20 @@ function checkEnd() { } } -function onRescue(_game) { +function onStreamRescue(_game) { + console.log('rescue'); + game.value = deepClone(_game); - engine.value = new Reversi.Game(game.value.map, { + engine.value = Reversi.Serializer.restoreGame({ + map: game.value.map, isLlotheo: game.value.isLlotheo, canPutEverywhere: game.value.canPutEverywhere, loopedBoard: game.value.loopedBoard, + logs: game.value.logs, }); - for (const log of game.value.logs) { - engine.value.put(log.color, log.pos); - } - - triggerRef(engine); - - logs.value = game.value.logs; - logPos.value = logs.value.length; + logPos.value = game.value.logs.length; checkEnd(); } @@ -280,21 +290,22 @@ function surrender() { function autoplay() { autoplaying.value = true; logPos.value = 0; + const logs = Reversi.Serializer.deserializeLogs(game.value.logs); window.setTimeout(() => { logPos.value = 1; let i = 1; - let previousLog = game.value.logs[0]; + let previousLog = logs[0]; const tick = () => { - const log = game.value.logs[i]; - const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime(); + const log = logs[i]; + const time = log.time - previousLog.time; setTimeout(() => { i++; logPos.value++; previousLog = log; - if (i < game.value.logs.length) { + if (i < logs.length) { tick(); } else { autoplaying.value = false; @@ -307,15 +318,15 @@ function autoplay() { } onMounted(() => { - props.connection.on('putStone', onPutStone); - props.connection.on('rescue', onRescue); - props.connection.on('ended', onEnded); + props.connection.on('log', onStreamLog); + props.connection.on('rescue', onStreamRescue); + props.connection.on('ended', onStreamEnded); }); onUnmounted(() => { - props.connection.off('putStone', onPutStone); - props.connection.off('rescue', onRescue); - props.connection.off('ended', onEnded); + props.connection.off('log', onStreamLog); + props.connection.off('rescue', onStreamRescue); + props.connection.off('ended', onStreamEnded); }); </script> @@ -389,6 +400,7 @@ $gap: 4px; background: transparent; border-radius: 6px; overflow: clip; + aspect-ratio: 1; &.boardCell_empty { border: solid 2px var(--divider); diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index e4e7d13668fc9327aa1ee9d84e1515a606378d89..e1d9d7517cdf18531c7c362d1d9239c44eb240a0 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-19T11:00:07.160Z + * generatedAt: 2024-01-20T01:28:01.779Z */ import type { SwitchCaseResponseType } from '../api.js'; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 671abd78ce62f3c841c08c32c049d842c53b4b33..61fc519c17291f2c00a788d850bc07d2985e54d9 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-19T11:00:07.158Z + * generatedAt: 2024-01-20T01:28:01.777Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index c14876c0e3771fa5e2f12b59ae79387e49b1f994..79b5fb0ae40a974bbab6bccccf3e610a85db9397 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-19T11:00:07.156Z + * generatedAt: 2024-01-20T01:28:01.775Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 78f14d2250c11375a15800f741db1dc820b4bc1e..fbd32f62d016b28c8608b6b8541aeea324115e98 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-19T11:00:07.155Z + * generatedAt: 2024-01-20T01:28:01.774Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 36facf6e28ae5111adbe0ade35fad6e2804894a0..d59f51073221e723e53c185165aa33e198cc2c5d 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-19T11:00:07.077Z + * generatedAt: 2024-01-20T01:28:01.695Z */ /** @@ -4517,11 +4517,7 @@ export type components = { isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; - logs: { - at: number; - color: boolean; - pos: number; - }[]; + logs: unknown[][]; map: string[]; }; }; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index 8d3ca30166258b487a3b40a6d7f59c34da2bfa7a..34b29f5b7c3a4c1fb4f4c68aded9dfb6506853f9 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -1,10 +1,22 @@ { + "type": "module", "name": "misskey-reversi", "version": "0.0.1", - "main": "./built/index.js", - "types": "./built/index.d.ts", + "exports": { + ".": { + "import": "./built/esm/index.js", + "types": "./built/dts/index.d.ts" + }, + "./*": { + "import": "./built/esm/*", + "types": "./built/dts/*" + } + }, "scripts": { - "build": "tsc", + "build": "npm run ts", + "ts": "npm run ts-esm && npm run ts-dts", + "ts-esm": "tsc --outDir built/esm", + "ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true", "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "typecheck": "tsc --noEmit", @@ -16,6 +28,7 @@ "@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/parser": "6.19.0", "eslint": "8.56.0", + "nodemon": "3.0.2", "typescript": "5.3.3" }, "files": [ diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts index 72aace7c0b34c6f53483ed0a9d84aeb077091355..f29b0014479d2b00f7dd5d6374216f1f35896e9c 100644 --- a/packages/misskey-reversi/src/game.ts +++ b/packages/misskey-reversi/src/game.ts @@ -46,7 +46,7 @@ export class Game { constructor(map: string[], opts: Options) { //#region binds - this.put = this.put.bind(this); + this.putStone = this.putStone.bind(this); //#endregion //#region Options @@ -88,7 +88,10 @@ export class Game { return x + (y * this.mapWidth); } - public put(color: Color, pos: number) { + public putStone(pos: number) { + const color = this.turn; + if (color == null) return; + this.prevPos = pos; this.prevColor = color; diff --git a/packages/misskey-reversi/src/index.ts b/packages/misskey-reversi/src/index.ts index 28964413b70340f185b7478c12cfb8b2f8597ccc..883b16e3d7f221d241ce4c6c27d849502b827515 100644 --- a/packages/misskey-reversi/src/index.ts +++ b/packages/misskey-reversi/src/index.ts @@ -3,10 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Game } from './game.js'; - -export { - Game, -}; - +export { Game } from './game.js'; +export * as Serializer from './serializer.js'; export * as maps from './maps.js'; diff --git a/packages/misskey-reversi/src/serializer.ts b/packages/misskey-reversi/src/serializer.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e6e0475d6d7f39e79e53ddc1bf202efd2518322 --- /dev/null +++ b/packages/misskey-reversi/src/serializer.ts @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Game } from './game.js'; + +export type Log = { + time: number; + player: boolean; + operation: 'put'; + pos: number; +}; + +export type SerializedLog = number[]; + +export function serializeLogs(logs: Log[]) { + const _logs: number[][] = []; + + for (let i = 0; i < logs.length; i++) { + const log = logs[i]; + const timeDelta = i === 0 ? log.time : log.time - logs[i - 1].time; + + switch (log.operation) { + case 'put': + _logs.push([timeDelta, log.player ? 1 : 0, 0, log.pos]); + break; + //case 'surrender': + // _logs.push([timeDelta, log.player, 1]); + // break; + } + } + + return _logs; +} + +export function deserializeLogs(logs: SerializedLog[]) { + const _logs: Log[] = []; + + let time = 0; + + for (const log of logs) { + const timeDelta = log[0]; + time += timeDelta; + + const player = log[1]; + const operation = log[2]; + + switch (operation) { + case 0: + _logs.push({ + time, + player: player === 1, + operation: 'put', + pos: log[3], + }); + break; + //case 1: + // _logs.push({ + // time, + // player: player === 1, + // operation: 'surrender', + // }); + // break; + } + } + + return _logs; +} + +export function restoreGame(env: { + map: string[]; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + logs: SerializedLog[]; +}) { + const logs = deserializeLogs(env.logs); + + const game = new Game(env.map, { + isLlotheo: env.isLlotheo, + canPutEverywhere: env.canPutEverywhere, + loopedBoard: env.loopedBoard, + }); + + for (const log of logs) { + switch (log.operation) { + case 'put': + game.putStone(log.pos); + break; + //case 'surrender': + // game.surrender(log.player); + // break; + } + } + + return game; +}