diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 11a8935be..896149f23 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 6e8026133..9fe7255e4 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 (engine.turn !== myColor) return; + if (!engine.canPut(myColor, pos)) return; - if (o.turn !== myColor) 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; - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); + logs.push(log); - game.logs.push(log); + const serializeLogs = Reversi.Serializer.serializeLogs(logs); + + 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 8d9520492..a7adc681f 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 d297d1f01..dcaa5c9fa 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 0d23b9dc7..b94046438 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 c67c05fb0..2d8c396db 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 582967ad2..bf45fc411 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 -
+
{{ i18n.ts._reversi.opponentTurn }}
{{ i18n.ts._reversi.myTurn }}
-
+