From e8ba0b3f54c2f25566f467d27c45c66139cbf102 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 23 Jan 2024 10:51:59 +0900 Subject: enhance(reversi): improve desync handling --- packages/backend/src/core/ReversiService.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 0d5f989c11..66296f1ed4 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; import { IsNull } from 'typeorm'; @@ -255,7 +254,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw = parseInt(game.bw, 10); } - const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); + const engine = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + const crc32 = engine.calcCrc32().toString(); const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() .set({ @@ -276,12 +281,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const engine = new Reversi.Game(updatedGame.map, { - isLlotheo: updatedGame.isLlotheo, - canPutEverywhere: updatedGame.canPutEverywhere, - loopedBoard: updatedGame.loopedBoard, - }); - if (engine.isEnded) { let winnerId; if (engine.winner === true) { @@ -406,7 +405,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const serializeLogs = Reversi.Serializer.serializeLogs(logs); - const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); + const crc32 = engine.calcCrc32().toString(); const updatedGame = { ...game, @@ -536,7 +535,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (game == null) throw new Error('game not found'); if (crc32.toString() !== game.crc32) { - return await this.reversiGameEntityService.packDetail(game); + return game; } else { return null; } -- cgit v1.2.3-freya From 65557d5f27044bd90c538266fde1e6b91b696f80 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 10:16:05 +0900 Subject: enhance(reversi): more robust matching process --- packages/backend/src/core/ReversiService.ts | 62 ++++++++++++++++++---- .../src/queue/processors/CleanProcessorService.ts | 4 ++ .../src/server/api/endpoints/reversi/match.ts | 3 +- packages/frontend/src/pages/reversi/index.vue | 6 ++- packages/misskey-js/src/autogen/apiClientJSDoc.ts | 4 +- packages/misskey-js/src/autogen/endpoint.ts | 4 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/models.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 6 ++- 9 files changed, 73 insertions(+), 24 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 66296f1ed4..1c8635212d 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; -import { IsNull } from 'typeorm'; +import { IsNull, LessThan } from 'typeorm'; import type { MiReversiGame, ReversiGamesRepository, @@ -24,7 +24,7 @@ import { Serialized } from '@/types.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; -const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec +const MATCHING_TIMEOUT_MS = 1000 * 10; // 10sec @Injectable() export class ReversiService implements OnApplicationShutdown, OnModuleInit { @@ -89,11 +89,27 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise { + public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise { if (targetUser.id === me.id) { throw new Error('You cannot match yourself.'); } + if (!multiple) { + // 既にマッチしている対局が無いか探す(3分以内) + const games = await this.reversiGamesRepository.find({ + where: [ + { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false }, + { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false }, + ], + relations: ['user1', 'user2'], + order: { id: 'DESC' }, + }); + if (games.length > 0) { + return games[0]; + } + } + + //#region 相手から既に招待されてないか確認 const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, Date.now() - MATCHING_TIMEOUT_MS, @@ -106,19 +122,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const game = await this.matched(targetUser.id, me.id); return game; - } else { - this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + } + //#endregion - this.globalEventService.publishReversiStream(targetUser.id, 'invited', { - user: await this.userEntityService.pack(me, targetUser), - }); + this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); - return null; - } + this.globalEventService.publishReversiStream(targetUser.id, 'invited', { + user: await this.userEntityService.pack(me, targetUser), + }); + + return null; } @bindThis - public async matchAnyUser(me: MiUser): Promise { + public async matchAnyUser(me: MiUser, multiple = false): Promise { + if (!multiple) { + // 既にマッチしている対局が無いか探す(3分以内) + const games = await this.reversiGamesRepository.find({ + where: [ + { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false }, + { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false }, + ], + relations: ['user1', 'user2'], + order: { id: 'DESC' }, + }); + if (games.length > 0) { + return games[0]; + } + } + //#region まず自分宛ての招待を探す const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, @@ -169,6 +201,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { await this.redisClient.zrem('reversi:matchAny', user.id); } + @bindThis + public async cleanOutdatedGames() { + await this.reversiGamesRepository.delete({ + id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)), + isStarted: false, + }); + } + @bindThis public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) { const game = await this.get(gameId); diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index e252c5d8a1..17b6c8ba0c 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -11,6 +11,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; +import { ReversiService } from '@/core/ReversiService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -32,6 +33,7 @@ export class CleanProcessorService { private roleAssignmentsRepository: RoleAssignmentsRepository, private queueLoggerService: QueueLoggerService, + private reversiService: ReversiService, private idService: IdService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('clean'); @@ -65,6 +67,8 @@ export class CleanProcessorService { }); } + this.reversiService.cleanOutdatedGames(); + this.logger.succ('Cleaned.'); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 1065ce5a89..62682cfb50 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -37,6 +37,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id', nullable: true }, + multiple: { type: 'boolean', default: false }, }, required: [], } as const; @@ -56,7 +57,7 @@ export default class extends Endpoint { // eslint- throw err; }) : null; - const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me); + const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple); if (game == null) return; diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 4c6c99ae51..09f742b83e 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -139,7 +139,9 @@ if ($i) { const connection = useStream().useChannel('reversi'); connection.on('matched', x => { - startGame(x.game); + if (matchingUser.value != null || matchingAny.value) { + startGame(x.game); + } }); connection.on('invited', invitation => { @@ -222,7 +224,7 @@ async function accept(user) { } } -useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true }); +useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true }); onMounted(() => { misskeyApi('reversi/invitations').then(_invitations => { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index fe04658deb..4fa42bb919 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.3 - * generatedAt: 2024-01-23T01:22:13.177Z + * version: 2024.2.0-beta.4 + * generatedAt: 2024-01-24T01:14:40.901Z */ 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 0060003031..6b330c7c11 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.3 - * generatedAt: 2024-01-23T01:22:13.175Z + * version: 2024.2.0-beta.4 + * generatedAt: 2024-01-24T01:14:40.899Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 55afcdeb31..8eb284ae9e 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.3 - * generatedAt: 2024-01-23T01:22:13.173Z + * version: 2024.2.0-beta.4 + * generatedAt: 2024-01-24T01:14:40.897Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index c94dfaa25e..0a6f534aff 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.3 - * generatedAt: 2024-01-23T01:22:13.172Z + * version: 2024.2.0-beta.4 + * generatedAt: 2024-01-24T01:14:40.896Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 761304ef28..44421240d0 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2024.2.0-beta.3 - * generatedAt: 2024-01-23T01:22:13.093Z + * version: 2024.2.0-beta.4 + * generatedAt: 2024-01-24T01:14:40.815Z */ /** @@ -25799,6 +25799,8 @@ export type operations = { 'application/json': { /** Format: misskey:id */ userId?: string | null; + /** @default false */ + multiple?: boolean; }; }; }; -- cgit v1.2.3-freya From 547be1973d6dadfe75bde6d5c1489609cf788683 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 10:35:44 +0900 Subject: fix of 65557d5f27044bd90c538266fde1e6b91b696f80 --- packages/backend/src/core/ReversiService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 1c8635212d..d9bf8b027e 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; -import { IsNull, LessThan } from 'typeorm'; +import { IsNull, LessThan, MoreThan } from 'typeorm'; import type { MiReversiGame, ReversiGamesRepository, @@ -98,8 +98,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { // 既にマッチしている対局が無いか探す(3分以内) const games = await this.reversiGamesRepository.find({ where: [ - { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false }, - { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false }, ], relations: ['user1', 'user2'], order: { id: 'DESC' }, @@ -140,8 +140,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { // 既にマッチしている対局が無いか探す(3分以内) const games = await this.reversiGamesRepository.find({ where: [ - { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false }, - { id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false }, ], relations: ['user1', 'user2'], order: { id: 'DESC' }, -- cgit v1.2.3-freya From b68446b289f9e007aa2612b131169843a321e834 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 13:32:08 +0900 Subject: enhance(reversi): tweak MATCHING_TIMEOUT_MS --- packages/backend/src/core/ReversiService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index d9bf8b027e..923ccc5a64 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -24,7 +24,7 @@ import { Serialized } from '@/types.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; -const MATCHING_TIMEOUT_MS = 1000 * 10; // 10sec +const MATCHING_TIMEOUT_MS = 1000 * 20; // 20sec @Injectable() export class ReversiService implements OnApplicationShutdown, OnModuleInit { -- cgit v1.2.3-freya From 908e0f3b8bb128bf21f23c7e4af5a838ebc612aa Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 13:51:16 +0900 Subject: perf(reversi): set expire matchSpecific and matchAny --- packages/backend/src/core/ReversiService.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 923ccc5a64..699b312949 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -125,7 +125,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } //#endregion - this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX'); + redisPipeline.exec(); this.globalEventService.publishReversiStream(targetUser.id, 'invited', { user: await this.userEntityService.pack(me, targetUser), @@ -186,7 +189,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { return game; } else { - await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id); + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + redisPipeline.expire('reversi:matchAny', 120, 'NX'); + await redisPipeline.exec(); return null; } } -- cgit v1.2.3-freya From a97d4fa4ef9048a33009f7dd600d077a6e2fa240 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 13:53:55 +0900 Subject: fix(reversi): wait redis operation to improve stability --- packages/backend/src/core/ReversiService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 699b312949..1d2c168f59 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -128,7 +128,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const redisPipeline = this.redisClient.pipeline(); redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX'); - redisPipeline.exec(); + await redisPipeline.exec(); this.globalEventService.publishReversiStream(targetUser.id, 'invited', { user: await this.userEntityService.pack(me, targetUser), -- cgit v1.2.3-freya From d27b3525cd9e678d2cf7ae8680af488fc5a30503 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 15:18:50 +0900 Subject: enhance(reversi): improve matching system --- packages/backend/src/core/ReversiService.ts | 19 +++++++++---------- packages/frontend/src/pages/reversi/index.vue | 2 ++ 2 files changed, 11 insertions(+), 10 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 1d2c168f59..f74416a58a 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -24,7 +24,7 @@ import { Serialized } from '@/types.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; -const MATCHING_TIMEOUT_MS = 1000 * 20; // 20sec +const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec @Injectable() export class ReversiService implements OnApplicationShutdown, OnModuleInit { @@ -112,7 +112,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { //#region 相手から既に招待されてないか確認 const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); @@ -157,7 +157,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { //#region まず自分宛ての招待を探す const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); @@ -173,15 +173,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const matchings = await this.redisClient.zrange( 'reversi:matchAny', - Date.now() - MATCHING_TIMEOUT_MS, - '+inf', - 'BYSCORE'); + 0, + 2, // 自分自身のIDが入っている場合もあるので2つ取得 + 'REV'); const userIds = matchings.filter(id => id !== me.id); if (userIds.length > 0) { - // pick random - const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)]; + const matchedUserId = userIds[0]; await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); @@ -191,7 +190,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } else { const redisPipeline = this.redisClient.pipeline(); redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); - redisPipeline.expire('reversi:matchAny', 120, 'NX'); + redisPipeline.expire('reversi:matchAny', 15, 'NX'); await redisPipeline.exec(); return null; } @@ -380,7 +379,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { public async getInvitations(user: MiUser): Promise { const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${user.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); return invitations; diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 1aa68ae871..7c687e813c 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -230,6 +230,8 @@ onMounted(() => { misskeyApi('reversi/invitations').then(_invitations => { invitations.value = _invitations; }); + + window.addEventListener('beforeunload', cancelMatching); }); onDeactivated(() => { -- cgit v1.2.3-freya From 5719a929ad5a048b59cf59b8683dcaaef01f2311 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 16:37:06 +0900 Subject: enhance(reversi): 変則なしマッチングを可能に MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 8 ++ locales/ja-JP.yml | 2 + .../backend/migration/1706081514499-reversi-6.js | 16 +++ packages/backend/src/core/ReversiService.ts | 33 ++++-- .../src/core/entities/ReversiGameEntityService.ts | 2 + packages/backend/src/models/ReversiGame.ts | 5 + .../backend/src/models/json-schema/reversi-game.ts | 8 ++ .../src/server/api/endpoints/reversi/match.ts | 5 +- .../frontend/src/pages/reversi/game.setting.vue | 121 +++++++++++---------- packages/frontend/src/pages/reversi/index.vue | 22 +++- packages/misskey-js/src/autogen/apiClientJSDoc.ts | 4 +- packages/misskey-js/src/autogen/endpoint.ts | 4 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/models.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 8 +- 15 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 packages/backend/migration/1706081514499-reversi-6.js (limited to 'packages/backend/src/core') diff --git a/locales/index.d.ts b/locales/index.d.ts index 63d13f61db..966c2224fb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9595,6 +9595,14 @@ export interface Locale extends ILocale { * 相手が設定を変更しました */ "opponentHasSettingsChanged": string; + /** + * 変則許可 (完全フリー) + */ + "allowIrregularRules": string; + /** + * 変則なし + */ + "disallowIrregularRules": string; }; "_offlineScreen": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9f17de4232..b9e7ea4922 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2556,6 +2556,8 @@ _reversi: shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿" iStartedAGame: "対局を開始しました! #MisskeyReversi" opponentHasSettingsChanged: "相手が設定を変更しました" + allowIrregularRules: "変則許可 (完全フリー)" + disallowIrregularRules: "変則なし" _offlineScreen: title: "オフライン - サーバーに接続できません" diff --git a/packages/backend/migration/1706081514499-reversi-6.js b/packages/backend/migration/1706081514499-reversi-6.js new file mode 100644 index 0000000000..de870be446 --- /dev/null +++ b/packages/backend/migration/1706081514499-reversi-6.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Reversi61706081514499 { + name = 'Reversi61706081514499' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`); + } +} diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index f74416a58a..84721b2217 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -85,6 +85,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: game.map, bw: game.bw, crc32: game.crc32, + noIrregularRules: game.noIrregularRules, } satisfies Partial; } @@ -138,7 +139,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async matchAnyUser(me: MiUser, multiple = false): Promise { + public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise { if (!multiple) { // 既にマッチしている対局が無いか探す(3分以内) const games = await this.reversiGamesRepository.find({ @@ -177,19 +178,29 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { 2, // 自分自身のIDが入っている場合もあるので2つ取得 'REV'); - const userIds = matchings.filter(id => id !== me.id); + const items = matchings.filter(id => !id.startsWith(me.id)); - if (userIds.length > 0) { - const matchedUserId = userIds[0]; + if (items.length > 0) { + const [matchedUserId, option] = items[0].split(':'); - await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); + await this.redisClient.zrem('reversi:matchAny', + me.id, + matchedUserId, + me.id + ':noIrregularRules', + matchedUserId + ':noIrregularRules'); - const game = await this.matched(matchedUserId, me.id); + const game = await this.matched(matchedUserId, me.id, { + noIrregularRules: options.noIrregularRules || option === 'noIrregularRules', + }); return game; } else { const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + if (options.noIrregularRules) { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules'); + } else { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + } redisPipeline.expire('reversi:matchAny', 15, 'NX'); await redisPipeline.exec(); return null; @@ -203,7 +214,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis public async matchAnyUserCancel(user: MiUser) { - await this.redisClient.zrem('reversi:matchAny', user.id); + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zrem('reversi:matchAny', user.id); + redisPipeline.zrem('reversi:matchAny', user.id + ':noIrregularRules'); + await redisPipeline.exec(); } @bindThis @@ -265,7 +279,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise { + private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise { const game = await this.reversiGamesRepository.insert({ id: this.idService.gen(), user1Id: parentId, @@ -278,6 +292,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: Reversi.maps.eighteight.data, bw: 'random', isLlotheo: false, + noIrregularRules: options.noIrregularRules, }).then(x => this.reversiGamesRepository.findOneOrFail({ where: { id: x.identifiers[0].id }, relations: ['user1', 'user2'], diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index 6c89a70599..1a689a7b53 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -61,6 +61,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, logs: game.logs, map: game.map, }); @@ -105,6 +106,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, }); } diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts index 11d236e458..c03335dd63 100644 --- a/packages/backend/src/models/ReversiGame.ts +++ b/packages/backend/src/models/ReversiGame.ts @@ -106,6 +106,11 @@ export class MiReversiGame { }) public bw: string; + @Column('boolean', { + default: false, + }) + public noIrregularRules: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index f8a5e7451c..ff4c78eeb0 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, @@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 62682cfb50..f8dee21c4c 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -37,6 +37,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id', nullable: true }, + noIrregularRules: { type: 'boolean', default: false }, multiple: { type: 'boolean', default: false }, }, required: [], @@ -57,7 +58,9 @@ export default class extends Endpoint { // eslint- throw err; }) : null; - const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple); + const game = target + ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) + : await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple); if (game == null) return; diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index b1e66e4fdd..0fbabfe4de 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -12,69 +12,74 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._reversi.gameSettings }}
-
-
-
{{ mapName }}
- {{ i18n.ts._reversi.chooseBoard }} -
+ +
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 7c687e813c..8deaead698 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -157,6 +157,7 @@ if ($i) { const invitations = ref([]); const matchingUser = ref(null); const matchingAny = ref(false); +const noIrregularRules = ref(false); function startGame(game: Misskey.entities.ReversiGameDetailed) { matchingUser.value = null; @@ -182,6 +183,7 @@ async function matchHeatbeat() { } else if (matchingAny.value) { const res = await misskeyApi('reversi/match', { userId: null, + noIrregularRules: noIrregularRules.value, }); if (res != null) { @@ -199,10 +201,22 @@ async function matchUser() { matchHeatbeat(); } -async function matchAny() { - matchingAny.value = true; - - matchHeatbeat(); +function matchAny(ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts._reversi.allowIrregularRules, + action: () => { + noIrregularRules.value = false; + matchingAny.value = true; + matchHeatbeat(); + }, + }, { + text: i18n.ts._reversi.disallowIrregularRules, + action: () => { + noIrregularRules.value = true; + matchingAny.value = true; + matchHeatbeat(); + }, + }], ev.currentTarget ?? ev.target); } function cancelMatching() { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 4fa42bb919..c97b95e536 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.901Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.455Z */ 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 6b330c7c11..e356de3453 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.899Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.453Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 8eb284ae9e..bfe40dc947 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.897Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.452Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 0a6f534aff..b7dcbfd951 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.896Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.450Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 44421240d0..b5eca12a19 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.815Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.370Z */ /** @@ -4493,6 +4493,7 @@ export type components = { timeoutUserId: string | null; black: number | null; bw: string; + noIrregularRules: boolean; isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; @@ -4528,6 +4529,7 @@ export type components = { timeoutUserId: string | null; black: number | null; bw: string; + noIrregularRules: boolean; isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; @@ -25800,6 +25802,8 @@ export type operations = { /** Format: misskey:id */ userId?: string | null; /** @default false */ + noIrregularRules?: boolean; + /** @default false */ multiple?: boolean; }; }; -- cgit v1.2.3-freya From 60156a40b2984c320eaf9ddb0c7704d1f8e40f93 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 24 Jan 2024 16:44:12 +0900 Subject: fix(reversi/backend): refactor and fixes --- packages/backend/src/core/ReversiService.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'packages/backend/src/core') diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 84721b2217..186ec6d7b1 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -120,7 +120,9 @@ 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.matched(targetUser.id, me.id); + const game = await this.matched(targetUser.id, me.id, { + noIrregularRules: false, + }); return game; } @@ -166,7 +168,9 @@ 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.matched(invitorId, me.id); + const game = await this.matched(invitorId, me.id, { + noIrregularRules: false, + }); return game; } @@ -214,10 +218,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis public async matchAnyUserCancel(user: MiUser) { - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zrem('reversi:matchAny', user.id); - redisPipeline.zrem('reversi:matchAny', user.id + ':noIrregularRules'); - await redisPipeline.exec(); + await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules'); } @bindThis -- cgit v1.2.3-freya