From 4e1fb618b8ddca3dd1d217922acde7ee43e72d86 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 15:46:14 +0900 Subject: [PATCH] wip --- .../backend/src/core/GlobalEventService.ts | 55 +++++- packages/backend/src/core/ReversiService.ts | 180 ++++++++++++++++-- .../api/stream/channels/reversi-game.ts | 167 +--------------- 3 files changed, 231 insertions(+), 171 deletions(-) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d175f21f2f..29a97e94cd 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js'; import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { MiMeta } from '@/models/Meta.js'; -import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js'; +import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -159,6 +159,41 @@ export interface AdminEventTypes { comment: string; }; } + +export interface ReversiEventTypes { + matched: { + game: Packed<'ReversiGame'>; + }; + invited: { + game: Packed<'ReversiGame'>; + }; +} + +export interface ReversiGameEventTypes { + accept: boolean; + cancelAccept: undefined; + updateSettings: { + key: string; + value: any; + }; + initForm: { + userId: MiUser['id']; + form: any; + }; + updateForm: { + id: string; + value: any; + }; + message: { + message: string; + }; + set: { + pos: number; + }; + check: { + crc32: string; + }; +} //#endregion // 辞書(interface or type)から{ type, body }ユニオンを定義 @@ -249,6 +284,14 @@ export type GlobalEvents = { name: 'notesStream'; payload: Serialized>; }; + reversi: { + name: `reversiStream:${MiUser['id']}`; + payload: EventUnionFromDictionary>; + }; + reversiGame: { + name: `reversiGameStream:${MiReversiGame['id']}`; + payload: EventUnionFromDictionary>; + }; }; // API event definitions @@ -338,4 +381,14 @@ export class GlobalEventService { public publishAdminStream(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } + + @bindThis + public publishReversiStream(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { + this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + @bindThis + public publishReversiGameStream(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void { + this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); + } } diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index ad6071d18c..8f9ac25fc3 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { In } from 'typeorm'; +import * as CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; import type { @@ -36,15 +36,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -54,7 +45,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.reversiMatchingsRepository) private reversiMatchingsRepository: ReversiMatchingsRepository, - private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, @@ -96,7 +86,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { isLlotheo: false, }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - publishReversiStream(exist.parentId, 'matched', await this.reversiGameEntityService.pack(game, { id: exist.parentId })); + const packed = await this.reversiGameEntityService.pack(game, { id: exist.parentId }); + this.globalEventService.publishReversiStream(exist.parentId, 'matched', { game: packed }); const other = await this.reversiMatchingsRepository.countBy({ childId: me.id, @@ -121,7 +112,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { }).then(x => this.reversiMatchingsRepository.findOneByOrFail(x.identifiers[0])); const packed = await this.reversiMatchingsEntityService.pack(matching, child); - publishReversiStream(child.id, 'invited', packed); + this.globalEventService.publishReversiStream(child.id, 'invited', { game: packed }); publishMainStream(child.id, 'reversiInvited', packed); return null; @@ -135,6 +126,169 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { }); } + @bindThis + public async matchAccept(game: MiReversiGame, user: MiUser, isAccepted: boolean) { + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user1Accepted: isAccepted, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + user1: isAccepted, + user2: game.user2Accepted, + }); + + if (isAccepted && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user2Accepted: isAccepted, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + user1: game.user1Accepted, + user2: isAccepted, + }); + + if (isAccepted && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.bw === 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = parseInt(freshGame.bw, 10); + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.values(maps)[rnd].data; + } + + const map = freshGame.map != null ? freshGame.map : getRandomMap(); + + await this.reversiGamesRepository.update(game.id, { + startedAt: new Date(), + isStarted: true, + black: bw, + map: map, + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Reversi.Game(map, { + isLlotheo: freshGame.isLlotheo, + canPutEverywhere: freshGame.canPutEverywhere, + loopedBoard: freshGame.loopedBoard, + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await this.reversiGamesRepository.update(game.id, { + isEnded: true, + winnerId: winner, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winner, + game: await this.reversiGameEntityService.pack(game.id, user), + }); + } + //#endregion + + this.globalEventService.publishReversiGameStream(game.id, 'started', + await this.reversiGameEntityService.pack(game.id, user)); + }, 3000); + } + } + + @bindThis + public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) { + if (!game.isStarted) return; + if (game.isEnded) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + + const myColor = + ((game.user1Id === user.id) && game.black === 1) || ((game.user2Id === user.id) && game.black === 2) + ? true + : false; + + const o = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + // 盤面の状態を再生 + for (const log of game.logs) { + o.put(log.color, log.pos); + } + + if (o.turn !== myColor) return; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black === 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black === 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos, + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); + + game.logs.push(log); + + await this.reversiGamesRepository.update(game.id, { + crc32, + isEnded: o.isEnded, + winnerId: winner, + logs: game.logs, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'set', Object.assign(log, { + next: o.turn, + })); + + if (o.isEnded) { + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winner, + game: await this.reversiGameEntityService.pack(game.id, user), + }); + } + } + @bindThis public async get(id: MiReversiGame['id']) { return this.reversiGamesRepository.findOneBy({ id }); 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 4b900d8e89..52cf882e0b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -9,6 +9,7 @@ import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.j import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { ReversiService } from '@/core/ReversiService.js'; import Channel, { type MiChannelService } from '../channel.js'; class ReversiGameChannel extends Channel { @@ -18,6 +19,7 @@ class ReversiGameChannel extends Channel { private gameId: MiReversiGame['id'] | null = null; constructor( + private reversiService: ReversiService, private reversiGamesRepository: ReversiGamesRepository, id: string, @@ -47,7 +49,7 @@ class ReversiGameChannel extends Channel { case 'initForm': this.initForm(body); break; case 'updateForm': this.updateForm(body.id, body.value); break; case 'message': this.message(body); break; - case 'set': this.set(body.pos); break; + case 'putStone': this.putStone(body.pos); break; case 'check': this.check(body.crc32); break; } } @@ -151,170 +153,18 @@ class ReversiGameChannel extends Channel { const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - - let bothAccepted = false; - - if (game.user1Id === this.user.id) { - await this.reversiGamesRepository.update(this.gameId!, { - user1Accepted: accept, - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: accept, - user2: game.user2Accepted, - }); - - if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id === this.user.id) { - await this.reversiGamesRepository.update(this.gameId!, { - user2Accepted: accept, - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: game.user1Accepted, - user2: accept, - }); - - if (accept && game.user1Accepted) bothAccepted = true; - } else { - return; - } - - if (bothAccepted) { - // 3秒後、まだacceptされていたらゲーム開始 - setTimeout(async () => { - const freshGame = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; - - let bw: number; - if (freshGame.bw == 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = parseInt(freshGame.bw, 10); - } - - function getRandomMap() { - const mapCount = Object.entries(maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.values(maps)[rnd].data; - } - - const map = freshGame.map != null ? freshGame.map : getRandomMap(); - - await this.reversiGamesRepository.update(this.gameId!, { - startedAt: new Date(), - isStarted: true, - black: bw, - map: map, - }); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const o = new ReversiGame(map, { - isLlotheo: freshGame.isLlotheo, - canPutEverywhere: freshGame.canPutEverywhere, - loopedBoard: freshGame.loopedBoard, - }); - - if (o.isEnded) { - let winner; - if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; - } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; - } else { - winner = null; - } - - await this.reversiGamesRepository.update(this.gameId!, { - isEnded: true, - winnerId: winner, - }); - - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user), - }); - } - //#endregion - - publishReversiGameStream(this.gameId!, 'started', - await ReversiGames.pack(this.gameId!, this.user)); - }, 3000); - } + this.reversiService.matchAccept(game, this.user, accept); } @bindThis - private async set(pos: number) { + private async putStone(pos: number) { if (this.user == null) return; + // TODO: キャッシュしたい const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - if (!game.isStarted) return; - if (game.isEnded) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const myColor = - ((game.user1Id === this.user.id) && game.black === 1) || ((game.user2Id === this.user.id) && game.black === 2) - ? true - : false; - - const o = new ReversiGame(game.map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - }); - - // 盤面の状態を再生 - for (const log of game.logs) { - o.put(log.color, log.pos); - } - - if (o.turn !== myColor) return; - - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); - - let winner; - if (o.isEnded) { - if (o.winner === true) { - winner = game.black === 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { - winner = game.black === 1 ? game.user2Id : game.user1Id; - } else { - winner = null; - } - } - - const log = { - at: new Date(), - color: myColor, - pos, - }; - - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); - - game.logs.push(log); - - await this.reversiGamesRepository.update(this.gameId!, { - crc32, - isEnded: o.isEnded, - winnerId: winner, - logs: game.logs, - }); - - publishReversiGameStream(this.gameId!, 'set', Object.assign(log, { - next: o.turn, - })); - - if (o.isEnded) { - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user), - }); - } + this.reversiService.putStoneToGame(game, this.user, pos); } @bindThis @@ -345,12 +195,15 @@ export class ReversiGameChannelService implements MiChannelService { constructor( @Inject(DI.reversiGamesRepository) private reversiGamesRepository: ReversiGamesRepository, + + private reversiService: ReversiService, ) { } @bindThis public create(id: string, connection: Channel['connection']): ReversiGameChannel { return new ReversiGameChannel( + this.reversiService, this.reversiGamesRepository, id, connection,