diff options
Diffstat (limited to 'packages/backend/src/core/ReversiService.ts')
| -rw-r--r-- | packages/backend/src/core/ReversiService.ts | 325 |
1 files changed, 150 insertions, 175 deletions
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index f97f71eb43..0d5f989c11 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -12,18 +12,14 @@ import { IsNull } from 'typeorm'; import type { MiReversiGame, ReversiGamesRepository, - UsersRepository, } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; import { NotificationService } from '@/core/NotificationService.js'; import { Serialized } from '@/types.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; @@ -58,7 +54,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis private async cacheGame(game: MiReversiGame) { - await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game)); + await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game)); } @bindThis @@ -67,6 +63,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis + private getBakeProps(game: MiReversiGame) { + return { + startedAt: game.startedAt, + endedAt: game.endedAt, + // ゲームの途中からユーザーが変わることは無いので + //user1Id: game.user1Id, + //user2Id: game.user2Id, + user1Ready: game.user1Ready, + user2Ready: game.user2Ready, + black: game.black, + isStarted: game.isStarted, + isEnded: game.isEnded, + winnerId: game.winnerId, + surrenderedUserId: game.surrenderedUserId, + timeoutUserId: game.timeoutUserId, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + timeLimitForEachTurn: game.timeLimitForEachTurn, + logs: game.logs, + map: game.map, + bw: game.bw, + crc32: game.crc32, + } satisfies Partial<MiReversiGame>; + } + + @bindThis public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { if (targetUser.id === me.id) { throw new Error('You cannot match yourself.'); @@ -81,23 +104,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (invitations.includes(targetUser.id)) { await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); - const game = await this.reversiGamesRepository.insert({ - id: this.idService.gen(), - user1Id: targetUser.id, - user2Id: me.id, - user1Ready: false, - user2Ready: false, - isStarted: false, - isEnded: false, - logs: [], - map: Reversi.maps.eighteight.data, - bw: 'random', - isLlotheo: false, - }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - this.cacheGame(game); - - const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id }); - this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed }); + const game = await this.matched(targetUser.id, me.id); return game; } else { @@ -124,23 +131,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const invitorId = invitations[Math.floor(Math.random() * invitations.length)]; await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId); - const game = await this.reversiGamesRepository.insert({ - id: this.idService.gen(), - user1Id: invitorId, - user2Id: me.id, - user1Ready: false, - user2Ready: false, - isStarted: false, - isEnded: false, - logs: [], - map: Reversi.maps.eighteight.data, - bw: 'random', - isLlotheo: false, - }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - this.cacheGame(game); - - const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId }); - this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed }); + const game = await this.matched(invitorId, me.id); return game; } @@ -160,23 +151,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); - const game = await this.reversiGamesRepository.insert({ - id: this.idService.gen(), - user1Id: matchedUserId, - user2Id: me.id, - user1Ready: false, - user2Ready: false, - isStarted: false, - isEnded: false, - logs: [], - map: Reversi.maps.eighteight.data, - bw: 'random', - isLlotheo: false, - }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - this.cacheGame(game); - - const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId }); - this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed }); + const game = await this.matched(matchedUserId, me.id); return game; } else { @@ -204,14 +179,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { let isBothReady = false; if (game.user1Id === user.id) { - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - user1Ready: ready, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); + const updatedGame = { + ...game, + user1Ready: ready, + }; this.cacheGame(updatedGame); this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { @@ -221,14 +192,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (ready && updatedGame.user2Ready) isBothReady = true; } else if (game.user2Id === user.id) { - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - user2Ready: ready, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); + const updatedGame = { + ...game, + user2Ready: ready, + }; this.cacheGame(updatedGame); this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { @@ -254,6 +221,32 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis + private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> { + const game = await this.reversiGamesRepository.insert({ + id: this.idService.gen(), + user1Id: parentId, + user2Id: childId, + user1Ready: false, + user2Ready: false, + isStarted: false, + isEnded: false, + logs: [], + map: Reversi.maps.eighteight.data, + bw: 'random', + isLlotheo: false, + }).then(x => this.reversiGamesRepository.findOneOrFail({ + where: { id: x.identifiers[0].id }, + relations: ['user1', 'user2'], + })); + this.cacheGame(game); + + const packed = await this.reversiGameEntityService.packDetail(game); + this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed }); + + return game; + } + + @bindThis private async startGame(game: MiReversiGame) { let bw: number; if (game.bw === 'random') { @@ -262,63 +255,44 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw = parseInt(game.bw, 10); } - function getRandomMap() { - const mapCount = Object.entries(Reversi.maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.values(Reversi.maps)[rnd].data; - } - - const map = game.map != null ? game.map : getRandomMap(); - const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() .set({ + ...this.getBakeProps(game), startedAt: new Date(), isStarted: true, black: bw, - map: map, + map: game.map, crc32, }) .where('id = :id', { id: game.id }) .returning('*') .execute() .then((response) => response.raw[0]); + // キャッシュ効率化のためにユーザー情報は再利用 + updatedGame.user1 = game.user1; + updatedGame.user2 = game.user2; this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const engine = new Reversi.Game(map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, + const engine = new Reversi.Game(updatedGame.map, { + isLlotheo: updatedGame.isLlotheo, + canPutEverywhere: updatedGame.canPutEverywhere, + loopedBoard: updatedGame.loopedBoard, }); if (engine.isEnded) { - let winner; + let winnerId; if (engine.winner === true) { - winner = bw === 1 ? game.user1Id : game.user2Id; + winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id; } else if (engine.winner === false) { - winner = bw === 1 ? game.user2Id : game.user1Id; + winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id; } else { - winner = null; + winnerId = null; } - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - isEnded: true, - endedAt: new Date(), - winnerId: winner, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'ended', { - winnerId: winner, - game: await this.reversiGameEntityService.packDetail(game.id), - }); + await this.endGame(updatedGame, winnerId, null); return; } @@ -327,7 +301,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, ''); this.globalEventService.publishReversiGameStream(game.id, 'started', { - game: await this.reversiGameEntityService.packDetail(game.id), + game: await this.reversiGameEntityService.packDetail(updatedGame), + }); + } + + @bindThis + private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) { + const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() + .set({ + ...this.getBakeProps(game), + isEnded: true, + endedAt: new Date(), + winnerId: winnerId, + surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null, + timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null, + }) + .where('id = :id', { id: game.id }) + .returning('*') + .execute() + .then((response) => response.raw[0]); + // キャッシュ効率化のためにユーザー情報は再利用 + updatedGame.user1 = game.user1; + updatedGame.user2 = game.user2; + this.cacheGame(updatedGame); + + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winnerId, + game: await this.reversiGameEntityService.packDetail(updatedGame), }); } @@ -354,14 +354,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { // TODO: より厳格なバリデーション - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - [key]: value, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); + const updatedGame = { + ...game, + [key]: value, + }; this.cacheGame(updatedGame); this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { @@ -397,17 +393,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { engine.putStone(pos); - let winner; - if (engine.isEnded) { - if (engine.winner === true) { - winner = game.black === 1 ? game.user1Id : game.user2Id; - } 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 = { @@ -423,17 +408,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - crc32, - isEnded: engine.isEnded, - winnerId: winner, - logs: serializeLogs, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); + const updatedGame = { + ...game, + crc32, + logs: serializeLogs, + }; this.cacheGame(updatedGame); this.globalEventService.publishReversiGameStream(game.id, 'log', { @@ -442,10 +421,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { }); if (engine.isEnded) { - this.globalEventService.publishReversiGameStream(game.id, 'ended', { - winnerId: winner ?? null, - game: await this.reversiGameEntityService.packDetail(game.id), - }); + let winnerId; + if (engine.winner === true) { + winnerId = game.black === 1 ? game.user1Id : game.user2Id; + } else if (engine.winner === false) { + winnerId = game.black === 1 ? game.user2Id : game.user1Id; + } else { + winnerId = null; + } + + await this.endGame(updatedGame, winnerId, null); } else { this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, ''); } @@ -460,23 +445,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - isEnded: true, - endedAt: new Date(), - winnerId: winnerId, - surrenderedUserId: user.id, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'ended', { - winnerId: winnerId, - game: await this.reversiGameEntityService.packDetail(game.id), - }); + await this.endGame(game, winnerId, 'surrender'); } @bindThis @@ -500,23 +469,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (timer === 0) { const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id); - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - isEnded: true, - endedAt: new Date(), - winnerId: winnerId, - timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id), - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'ended', { - winnerId: winnerId, - game: await this.reversiGameEntityService.packDetail(game.id), - }); + await this.endGame(game, winnerId, 'timeout'); } } @@ -539,14 +492,36 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> { const cached = await this.redisClient.get(`reversi:game:cache:${id}`); if (cached != null) { + // TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい const parsed = JSON.parse(cached) as Serialized<MiReversiGame>; return { ...parsed, startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null, endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null, + user1: parsed.user1 != null ? { + ...parsed.user1, + avatar: null, + banner: null, + updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null, + lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null, + lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null, + movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null, + } : null, + user2: parsed.user2 != null ? { + ...parsed.user2, + avatar: null, + banner: null, + updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null, + lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null, + lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null, + movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null, + } : null, }; } else { - const game = await this.reversiGamesRepository.findOneBy({ id }); + const game = await this.reversiGamesRepository.findOne({ + where: { id }, + relations: ['user1', 'user2'], + }); if (game == null) return null; this.cacheGame(game); |