From a637b4e28259e89285fc1c67589c731a053f5562 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 Jan 2024 20:51:49 +0900 Subject: [PATCH] feat: reversi Resolve #12962 --- locales/index.d.ts | 35 + locales/ja-JP.yml | 35 + .../migration/1705475608437-reversi.js | 22 + .../migration/1705654039457-reversi-2.js | 18 + packages/backend/package.json | 2 + packages/backend/src/core/CoreModule.ts | 27 + .../backend/src/core/GlobalEventService.ts | 57 +- packages/backend/src/core/ReversiService.ts | 411 ++++++++++ .../core/entities/ReversiGameEntityService.ts | 115 +++ packages/backend/src/di-symbols.ts | 1 + packages/backend/src/misc/json-schema.ts | 3 + .../backend/src/models/RepositoryModule.ts | 12 +- packages/backend/src/models/ReversiGame.ts | 127 ++++ packages/backend/src/models/_.ts | 4 + .../src/models/json-schema/reversi-game.ts | 234 ++++++ packages/backend/src/postgres.ts | 2 + packages/backend/src/server/ServerModule.ts | 11 +- .../backend/src/server/api/EndpointsModule.ts | 24 + packages/backend/src/server/api/endpoints.ts | 12 + .../api/endpoints/renote-mute/create.ts | 2 +- .../api/endpoints/reversi/cancel-match.ts | 44 ++ .../src/server/api/endpoints/reversi/games.ts | 61 ++ .../api/endpoints/reversi/invitations.ts | 39 + .../src/server/api/endpoints/reversi/match.ts | 66 ++ .../server/api/endpoints/reversi/show-game.ts | 54 ++ .../server/api/endpoints/reversi/surrender.ts | 68 ++ .../src/server/api/stream/ChannelsService.ts | 6 + .../api/stream/channels/reversi-game.ts | 130 ++++ .../src/server/api/stream/channels/reversi.ts | 52 ++ packages/frontend/assets/reversi/logo.png | Bin 0 -> 96293 bytes packages/frontend/package.json | 2 + packages/frontend/src/components/MkRadios.vue | 3 + packages/frontend/src/components/MkSelect.vue | 4 +- .../src/components/MkUserSelectDialog.vue | 12 +- .../frontend/src/global/router/definition.ts | 17 +- packages/frontend/src/os.ts | 2 +- .../frontend/src/pages/drop-and-fusion.vue | 2 +- packages/frontend/src/pages/games.vue | 15 +- .../frontend/src/pages/reversi/game.board.vue | 428 +++++++++++ .../src/pages/reversi/game.setting.vue | 236 ++++++ packages/frontend/src/pages/reversi/game.vue | 68 ++ packages/frontend/src/pages/reversi/index.vue | 271 +++++++ packages/frontend/vite.config.ts | 4 +- packages/misskey-js/etc/misskey-js.api.md | 50 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 68 +- packages/misskey-js/src/autogen/endpoint.ts | 18 +- packages/misskey-js/src/autogen/entities.ts | 12 +- packages/misskey-js/src/autogen/models.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 442 ++++++++++- packages/misskey-reversi/package.json | 26 + packages/misskey-reversi/src/game.ts | 216 ++++++ packages/misskey-reversi/src/index.ts | 7 + packages/misskey-reversi/src/maps.ts | 715 ++++++++++++++++++ packages/misskey-reversi/tsconfig.json | 33 + pnpm-lock.yaml | 479 ++++++++++-- pnpm-workspace.yaml | 1 + 56 files changed, 4701 insertions(+), 108 deletions(-) create mode 100644 packages/backend/migration/1705475608437-reversi.js create mode 100644 packages/backend/migration/1705654039457-reversi-2.js create mode 100644 packages/backend/src/core/ReversiService.ts create mode 100644 packages/backend/src/core/entities/ReversiGameEntityService.ts create mode 100644 packages/backend/src/models/ReversiGame.ts create mode 100644 packages/backend/src/models/json-schema/reversi-game.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/cancel-match.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/games.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/invitations.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/match.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/show-game.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/surrender.ts create mode 100644 packages/backend/src/server/api/stream/channels/reversi-game.ts create mode 100644 packages/backend/src/server/api/stream/channels/reversi.ts create mode 100644 packages/frontend/assets/reversi/logo.png create mode 100644 packages/frontend/src/pages/reversi/game.board.vue create mode 100644 packages/frontend/src/pages/reversi/game.setting.vue create mode 100644 packages/frontend/src/pages/reversi/game.vue create mode 100644 packages/frontend/src/pages/reversi/index.vue create mode 100644 packages/misskey-reversi/package.json create mode 100644 packages/misskey-reversi/src/game.ts create mode 100644 packages/misskey-reversi/src/index.ts create mode 100644 packages/misskey-reversi/src/maps.ts create mode 100644 packages/misskey-reversi/tsconfig.json diff --git a/locales/index.d.ts b/locales/index.d.ts index a22cb6350..85e0c6b24 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2633,6 +2633,41 @@ export interface Locale extends ILocale { "description": string; }; }; + "_reversi": { + "reversi": string; + "gameSettings": string; + "chooseBoard": string; + "blackOrWhite": string; + "blackIs": ParameterizedString<"name">; + "rules": string; + "thisGameIsStartedSoon": string; + "waitingForOther": string; + "waitingForMe": string; + "waitingBoth": string; + "ready": string; + "cancelReady": string; + "opponentTurn": string; + "myTurn": string; + "turnOf": ParameterizedString<"name">; + "pastTurnOf": ParameterizedString<"name">; + "surrender": string; + "surrendered": string; + "drawn": string; + "won": ParameterizedString<"name">; + "black": string; + "white": string; + "total": string; + "turnCount": ParameterizedString<"count">; + "myGames": string; + "allGames": string; + "ended": string; + "playing": string; + "isLlotheo": string; + "loopedMap": string; + "canPutEverywhere": string; + "freeMatch": string; + "lookingForPlayer": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8749a5f49..6c8a45302 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2506,3 +2506,38 @@ _dataSaver: _code: title: "コードハイライト" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" + +_reversi: + reversi: "リバーシ" + gameSettings: "対局の設定" + chooseBoard: "ボードを選択" + blackOrWhite: "先行/後攻" + blackIs: "{name}が黒(先行)" + rules: "ルール" + thisGameIsStartedSoon: "対局はまもなく開始されます" + waitingForOther: "相手の準備が完了するのを待っています" + waitingForMe: "あなたの準備が完了するのを待っています" + waitingBoth: "準備してください" + ready: "準備完了" + cancelReady: "準備を再開" + opponentTurn: "相手のターンです" + myTurn: "あなたのターンです" + turnOf: "{name}のターンです" + pastTurnOf: "{name}のターン" + surrender: "投了" + surrendered: "投了により" + drawn: "引き分け" + won: "{name}の勝ち" + black: "黒" + white: "白" + total: "合計" + turnCount: "{count}ターン目" + myGames: "自分の対局" + allGames: "みんなの対局" + ended: "終了" + playing: "対局中" + isLlotheo: "石の少ない方が勝ち(ロセオ)" + loopedMap: "ループマップ" + canPutEverywhere: "どこでも置けるモード" + freeMatch: "フリーマッチ" + lookingForPlayer: "対戦相手を探しています" diff --git a/packages/backend/migration/1705475608437-reversi.js b/packages/backend/migration/1705475608437-reversi.js new file mode 100644 index 000000000..c9d69e2c7 --- /dev/null +++ b/packages/backend/migration/1705475608437-reversi.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Reversi1705475608437 { + name = 'Reversi1705475608437' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_b46ec40746efceac604142be1c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b604d92d6c7aec38627f6eaf16"`); + await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "reversi_matching" DROP COLUMN "createdAt"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_matching" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`ALTER TABLE "reversi_game" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `); + } +} diff --git a/packages/backend/migration/1705654039457-reversi-2.js b/packages/backend/migration/1705654039457-reversi-2.js new file mode 100644 index 000000000..33747ba9f --- /dev/null +++ b/packages/backend/migration/1705654039457-reversi-2.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Reversi21705654039457 { + name = 'Reversi21705654039457' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`); + await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`); + await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5ab476295..f8e82c5a1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -107,6 +107,7 @@ "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", + "crc-32": "^1.2.2", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", "fastify": "4.24.3", @@ -133,6 +134,7 @@ "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", + "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", "nanoid": "5.0.4", "nested-property": "4.0.0", diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index bc6d24b95..c9e285346 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -66,6 +66,8 @@ import { FeaturedService } from './FeaturedService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; +import { ReversiService } from './ReversiService.js'; + import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -80,6 +82,7 @@ import PerUserFollowingChart from './chart/charts/per-user-following.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js'; import ApRequestChart from './chart/charts/ap-request.js'; import { ChartManagementService } from './chart/ChartManagementService.js'; + import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js'; import { AntennaEntityService } from './entities/AntennaEntityService.js'; import { AppEntityService } from './entities/AppEntityService.js'; @@ -112,6 +115,8 @@ import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js'; +import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; + import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -199,6 +204,7 @@ const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', use const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; +const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -247,6 +253,7 @@ const $UserListEntityService: Provider = { provide: 'UserListEntityService', use const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; +const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -336,6 +343,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + ChartLoggerService, FederationChart, NotesChart, @@ -350,6 +359,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -382,6 +392,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -466,6 +478,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $ChartLoggerService, $FederationChart, $NotesChart, @@ -480,6 +494,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -512,6 +527,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -597,6 +614,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + FederationChart, NotesChart, UsersChart, @@ -610,6 +629,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -642,6 +662,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -726,6 +748,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $FederationChart, $NotesChart, $UsersChart, @@ -739,6 +763,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -771,6 +796,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d175f21f2..11a8935be 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,43 @@ export interface AdminEventTypes { comment: string; }; } + +export interface ReversiEventTypes { + matched: { + game: Packed<'ReversiGameDetailed'>; + }; + invited: { + user: Packed<'User'>; + }; +} + +export interface ReversiGameEventTypes { + changeReadyStates: { + user1: boolean; + user2: boolean; + }; + updateSettings: { + userId: MiUser['id']; + key: string; + value: any; + }; + putStone: { + at: number; + color: boolean; + pos: number; + next: boolean; + }; + syncState: { + crc32: string; + }; + started: { + game: Packed<'ReversiGameDetailed'>; + }; + ended: { + winnerId: MiUser['id'] | null; + game: Packed<'ReversiGameDetailed'>; + }; +} //#endregion // 辞書(interface or type)から{ type, body }ユニオンを定義 @@ -249,6 +286,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 +383,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 new file mode 100644 index 000000000..cd990ba77 --- /dev/null +++ b/packages/backend/src/core/ReversiService.ts @@ -0,0 +1,411 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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'; +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 { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; +import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; + +const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec + +@Injectable() +export class ReversiService implements OnApplicationShutdown, OnModuleInit { + private notificationService: NotificationService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private cacheService: CacheService, + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private reversiGameEntityService: ReversiGameEntityService, + private idService: IdService, + ) { + } + + async onModuleInit() { + this.notificationService = this.moduleRef.get(NotificationService.name); + } + + @bindThis + public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise { + if (targetUser.id === me.id) { + throw new Error('You cannot match yourself.'); + } + + const invitations = await this.redisClient.zrange( + `reversi:matchSpecific:${me.id}`, + Date.now() - MATCHING_TIMEOUT_MS, + '+inf', + 'BYSCORE'); + + 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])); + + const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id }); + this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed }); + + return game; + } else { + this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + + this.globalEventService.publishReversiStream(targetUser.id, 'invited', { + user: await this.userEntityService.pack(me, targetUser), + }); + + return null; + } + } + + @bindThis + public async matchAnyUser(me: MiUser): Promise { + //#region まず自分宛ての招待を探す + const invitations = await this.redisClient.zrange( + `reversi:matchSpecific:${me.id}`, + Date.now() - MATCHING_TIMEOUT_MS, + '+inf', + 'BYSCORE'); + + if (invitations.length > 0) { + 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])); + + const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId }); + this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed }); + + return game; + } + //#endregion + + const matchings = await this.redisClient.zrange( + 'reversi:matchAny', + Date.now() - MATCHING_TIMEOUT_MS, + '+inf', + 'BYSCORE'); + + const userIds = matchings.filter(id => id !== me.id); + + if (userIds.length > 0) { + // pick random + const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)]; + + 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])); + + const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId }); + this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed }); + + return game; + } else { + await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id); + return null; + } + } + + @bindThis + public async matchSpecificUserCancel(user: MiUser, targetUserId: MiUser['id']) { + await this.redisClient.zrem(`reversi:matchSpecific:${targetUserId}`, user.id); + } + + @bindThis + public async matchAnyUserCancel(user: MiUser) { + await this.redisClient.zrem('reversi:matchAny', user.id); + } + + @bindThis + public async gameReady(game: MiReversiGame, user: MiUser, ready: boolean) { + if (game.isStarted) return; + + let isBothReady = false; + + if (game.user1Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user1Ready: ready, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { + user1: ready, + user2: game.user2Ready, + }); + + if (ready && game.user2Ready) isBothReady = true; + } else if (game.user2Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user2Ready: ready, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { + user1: game.user1Ready, + user2: ready, + }); + + if (ready && game.user1Ready) isBothReady = true; + } else { + return; + } + + if (isBothReady) { + // 3秒後、両者readyならゲーム開始 + setTimeout(async () => { + const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Ready || !freshGame.user2Ready) 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(Reversi.maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.values(Reversi.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.packDetail(game.id, user), + }); + } + //#endregion + + this.globalEventService.publishReversiGameStream(game.id, 'started', { + game: await this.reversiGameEntityService.packDetail(game.id, user), + }); + }, 3000); + } + } + + @bindThis + public async getInvitations(user: MiUser): Promise { + const invitations = await this.redisClient.zrange( + `reversi:matchSpecific:${user.id}`, + Date.now() - MATCHING_TIMEOUT_MS, + '+inf', + 'BYSCORE'); + return invitations; + } + + @bindThis + public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) { + if (game.isStarted) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + if ((game.user1Id === user.id) && game.user1Ready) return; + if ((game.user2Id === user.id) && game.user2Ready) return; + + if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; + + await this.reversiGamesRepository.update(game.id, { + [key]: value, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { + userId: user.id, + key: key, + value: value, + }); + } + + @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: Date.now(), + 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, 'putStone', { + ...log, + next: o.turn, + }); + + if (o.isEnded) { + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winner, + game: await this.reversiGameEntityService.packDetail(game.id, user), + }); + } + } + + @bindThis + public async surrender(game: MiReversiGame, user: MiUser) { + if (game.isEnded) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + + const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; + + await this.reversiGamesRepository.update(game.id, { + surrendered: user.id, + isEnded: true, + winnerId: winnerId, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winnerId, + game: await this.reversiGameEntityService.packDetail(game.id, user), + }); + } + + @bindThis + public async get(id: MiReversiGame['id']) { + return this.reversiGamesRepository.findOneBy({ id }); + } + + @bindThis + public dispose(): void { + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts new file mode 100644 index 000000000..8d9520492 --- /dev/null +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { ReversiGamesRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiReversiGame } from '@/models/ReversiGame.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ReversiGameEntityService { + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async packDetail( + src: MiReversiGame['id'] | MiReversiGame, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: game.id, + createdAt: this.idService.parse(game.id).date.toISOString(), + startedAt: game.startedAt && game.startedAt.toISOString(), + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Ready: game.user1Ready, + user2Ready: game.user2Ready, + user1Id: game.user1Id, + user2Id: game.user2Id, + user1: this.userEntityService.pack(game.user1Id, me), + user2: this.userEntityService.pack(game.user2Id, me), + winnerId: game.winnerId, + winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + logs: game.logs.map(log => ({ + at: log.at, + color: log.color, + pos: log.pos, + })), + map: game.map, + }); + } + + @bindThis + public packDetailMany( + xs: MiReversiGame[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.packDetail(x, me))); + } + + @bindThis + public async packLite( + src: MiReversiGame['id'] | MiReversiGame, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: game.id, + createdAt: this.idService.parse(game.id).date.toISOString(), + startedAt: game.startedAt && game.startedAt.toISOString(), + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Ready: game.user1Ready, + user2Ready: game.user2Ready, + user1Id: game.user1Id, + user2Id: game.user2Id, + user1: this.userEntityService.pack(game.user1Id, me), + user2: this.userEntityService.pack(game.user2Id, me), + winnerId: game.winnerId, + winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + } + + @bindThis + public packLiteMany( + xs: MiReversiGame[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.packLite(x, me))); + } +} + diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index e29fee3f9..73de01f33 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -79,5 +79,6 @@ export const DI = { flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), + reversiGamesRepository: Symbol('reversiGamesRepository'), //#endregion }; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 176978d35..b4f054171 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -39,6 +39,7 @@ import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; +import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -78,6 +79,8 @@ export const refs = { Signin: packedSigninSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, + ReversiGameLite: packedReversiGameLiteSchema, + ReversiGameDetailed: packedReversiGameDetailedSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 0399536c3..2b2aaeb91 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -399,12 +399,18 @@ const $userMemosRepository: Provider = { inject: [DI.db], }; -export const $bubbleGameRecordsRepository: Provider = { +const $bubbleGameRecordsRepository: Provider = { provide: DI.bubbleGameRecordsRepository, useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord), inject: [DI.db], }; +const $reversiGamesRepository: Provider = { + provide: DI.reversiGamesRepository, + useFactory: (db: DataSource) => db.getRepository(MiReversiGame), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -475,6 +481,7 @@ export const $bubbleGameRecordsRepository: Provider = { $flashLikesRepository, $userMemosRepository, $bubbleGameRecordsRepository, + $reversiGamesRepository, ], exports: [ $usersRepository, @@ -543,6 +550,7 @@ export const $bubbleGameRecordsRepository: Provider = { $flashLikesRepository, $userMemosRepository, $bubbleGameRecordsRepository, + $reversiGamesRepository, ], }) export class RepositoryModule {} diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts new file mode 100644 index 000000000..d297d1f01 --- /dev/null +++ b/packages/backend/src/models/ReversiGame.ts @@ -0,0 +1,127 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('reversi_game') +export class MiReversiGame { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + comment: 'The started date of the ReversiGame.', + }) + public startedAt: Date | null; + + @Column(id()) + public user1Id: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user1: MiUser | null; + + @Column(id()) + public user2Id: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user2: MiUser | null; + + @Column('boolean', { + default: false, + }) + public user1Ready: boolean; + + @Column('boolean', { + default: false, + }) + public user2Ready: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + @Column('integer', { + nullable: true, + }) + public black: number | null; + + @Column('boolean', { + default: false, + }) + public isStarted: boolean; + + @Column('boolean', { + default: false, + }) + public isEnded: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public winnerId: MiUser['id'] | null; + + @Column({ + ...id(), + nullable: true, + }) + public surrendered: MiUser['id'] | null; + + @Column('jsonb', { + default: [], + }) + public logs: { + at: number; + color: boolean; + pos: number; + }[]; + + @Column('varchar', { + array: true, length: 64, + }) + public map: string[]; + + @Column('varchar', { + length: 32, + }) + public bw: string; + + @Column('boolean', { + default: false, + }) + public isLlotheo: boolean; + + @Column('boolean', { + default: false, + }) + public canPutEverywhere: boolean; + + @Column('boolean', { + default: false, + }) + public loopedBoard: boolean; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form1: any | null; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form2: any | null; + + /** + * ログのposを文字列としてすべて連結したもののCRC32値 + */ + @Column('varchar', { + length: 32, nullable: true, + }) + public crc32: string | null; +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index a1c4b0743..a1a0d8823 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -69,6 +69,8 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; +import { MiReversiGame } from '@/models/ReversiGame.js'; + import type { Repository } from 'typeorm'; export { @@ -138,6 +140,7 @@ export { MiFlashLike, MiUserMemo, MiBubbleGameRecord, + MiReversiGame, }; export type AbuseUserReportsRepository = Repository; @@ -206,3 +209,4 @@ export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; export type UserMemoRepository = Repository; export type BubbleGameRecordsRepository = Repository; +export type ReversiGamesRepository = Repository; diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts new file mode 100644 index 000000000..0d23b9dc7 --- /dev/null +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedReversiGameLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + startedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + isStarted: { + type: 'boolean', + optional: false, nullable: false, + }, + isEnded: { + type: 'boolean', + optional: false, nullable: false, + }, + form1: { + type: 'any', + optional: false, nullable: true, + }, + form2: { + type: 'any', + optional: false, nullable: true, + }, + user1Ready: { + type: 'boolean', + optional: false, nullable: false, + }, + user2Ready: { + type: 'boolean', + optional: false, nullable: false, + }, + user1Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user2Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user1: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + user2: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + winnerId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + winner: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + surrendered: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + black: { + type: 'number', + optional: false, nullable: true, + }, + bw: { + type: 'string', + optional: false, nullable: false, + }, + isLlotheo: { + type: 'boolean', + optional: false, nullable: false, + }, + canPutEverywhere: { + type: 'boolean', + optional: false, nullable: false, + }, + loopedBoard: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedReversiGameDetailedSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + startedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + isStarted: { + type: 'boolean', + optional: false, nullable: false, + }, + isEnded: { + type: 'boolean', + optional: false, nullable: false, + }, + form1: { + type: 'any', + optional: false, nullable: true, + }, + form2: { + type: 'any', + optional: false, nullable: true, + }, + user1Ready: { + type: 'boolean', + optional: false, nullable: false, + }, + user2Ready: { + type: 'boolean', + optional: false, nullable: false, + }, + user1Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user2Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user1: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + user2: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + winnerId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + winner: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + surrendered: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + black: { + type: 'number', + optional: false, nullable: true, + }, + bw: { + type: 'string', + optional: false, nullable: false, + }, + isLlotheo: { + type: 'boolean', + optional: false, nullable: false, + }, + canPutEverywhere: { + type: 'boolean', + optional: false, nullable: false, + }, + loopedBoard: { + type: 'boolean', + optional: false, nullable: false, + }, + logs: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + 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: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 0430e9ca1..1e063c867 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -77,6 +77,7 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; +import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -192,6 +193,7 @@ export const entities = [ MiFlashLike, MiUserMemo, MiBubbleGameRecord, + MiReversiGame, ...charts, ]; diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index fa81380f0..aed352d15 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -22,9 +22,13 @@ import { SigninApiService } from './api/SigninApiService.js'; import { SigninService } from './api/SigninService.js'; import { SignupApiService } from './api/SignupApiService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; +import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { FeedService } from './web/FeedService.js'; import { UrlPreviewService } from './web/UrlPreviewService.js'; +import { ClientLoggerService } from './web/ClientLoggerService.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; + import { MainChannelService } from './api/stream/channels/main.js'; import { AdminChannelService } from './api/stream/channels/admin.js'; import { AntennaChannelService } from './api/stream/channels/antenna.js'; @@ -38,10 +42,9 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; -import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; -import { ClientLoggerService } from './web/ClientLoggerService.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; -import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; +import { ReversiChannelService } from './api/stream/channels/reversi.js'; +import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; @Module({ imports: [ @@ -77,6 +80,8 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; GlobalTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, + ReversiChannelService, + ReversiGameChannelService, HomeTimelineChannelService, HybridTimelineChannelService, LocalTimelineChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 781332d34..df69ce238 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -366,6 +366,12 @@ import * as ep___fetchExternalResources from './endpoints/fetch-external-resourc import * as ep___retention from './endpoints/retention.js'; import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; +import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; +import * as ep___reversi_games from './endpoints/reversi/games.js'; +import * as ep___reversi_match from './endpoints/reversi/match.js'; +import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; +import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; +import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -730,6 +736,12 @@ const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resource const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default }; const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default }; +const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default }; +const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default }; +const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default }; +const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default }; +const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; +const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; @Module({ imports: [ @@ -1098,6 +1110,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $retention, $bubbleGame_register, $bubbleGame_ranking, + $reversi_cancelMatch, + $reversi_games, + $reversi_match, + $reversi_invitations, + $reversi_showGame, + $reversi_surrender, ], exports: [ $admin_meta, @@ -1457,6 +1475,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $retention, $bubbleGame_register, $bubbleGame_ranking, + $reversi_cancelMatch, + $reversi_games, + $reversi_match, + $reversi_invitations, + $reversi_showGame, + $reversi_surrender, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f17db41a5..0f2c8cb75 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -367,6 +367,12 @@ import * as ep___fetchExternalResources from './endpoints/fetch-external-resourc import * as ep___retention from './endpoints/retention.js'; import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; +import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; +import * as ep___reversi_games from './endpoints/reversi/games.js'; +import * as ep___reversi_match from './endpoints/reversi/match.js'; +import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; +import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; +import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -729,6 +735,12 @@ const eps = [ ['retention', ep___retention], ['bubble-game/register', ep___bubbleGame_register], ['bubble-game/ranking', ep___bubbleGame_ranking], + ['reversi/cancel-match', ep___reversi_cancelMatch], + ['reversi/games', ep___reversi_games], + ['reversi/match', ep___reversi_match], + ['reversi/invitations', ep___reversi_invitations], + ['reversi/show-game', ep___reversi_showGame], + ['reversi/surrender', ep___reversi_surrender], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 7ff7b5de3..2d853b94f 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -73,7 +73,7 @@ export default class extends Endpoint { // eslint- } // Get mutee - const mutee = await getterService.getUser(ps.userId).catch(err => { + const mutee = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts new file mode 100644 index 000000000..8edc04950 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.userId) { + await this.reversiService.matchSpecificUserCancel(me, ps.userId); + return; + } else { + await this.reversiService.matchAnyUserCancel(me); + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts new file mode 100644 index 000000000..5322cd098 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { DI } from '@/di-symbols.js'; +import type { ReversiGamesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; + +export const meta = { + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { ref: 'ReversiGameLite' }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + my: { type: 'boolean', default: false }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private reversiGameEntityService: ReversiGameEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) + .andWhere('game.isStarted = TRUE'); + + if (ps.my && me) { + query.andWhere(new Brackets(qb => { + qb + .where('game.user1Id = :userId', { userId: me.id }) + .orWhere('game.user2Id = :userId', { userId: me.id }); + })); + } + + const games = await query.take(ps.limit).getMany(); + + return await this.reversiGameEntityService.packLiteMany(games, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts new file mode 100644 index 000000000..0b7107bb0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/invitations.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ReversiService } from '@/core/ReversiService.js'; + +export const meta = { + requireCredential: true, + + kind: 'read:account', + + res: { + type: 'array', + optional: false, nullable: false, + items: { ref: 'UserLite' }, + }, +} as const; + +export const paramDef = { +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private userEntityService: UserEntityService, + private reversiService: ReversiService, + ) { + super(meta, paramDef, async (ps, me) => { + const invitations = await this.reversiService.getInvitations(me); + + return await this.userEntityService.packMany(invitations, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts new file mode 100644 index 000000000..da5a3409e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '../../GetterService.js'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '0b4f0559-b484-4e31-9581-3f73cee89b28', + }, + + isYourself: { + message: 'Target user is yourself.', + code: 'TARGET_IS_YOURSELF', + id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e', + }, + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private getterService: GetterService, + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself); + + const target = ps.userId ? await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }) : null; + + const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me); + + if (game == null) return; + + return await this.reversiGameEntityService.packDetail(game, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts new file mode 100644 index 000000000..de571053e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + requireCredential: false, + + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: 'f13a03db-fae1-46c9-87f3-43c8165419e1', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ReversiGameDetailed', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + }, + required: ['gameId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.get(ps.gameId); + + if (game == null) { + throw new ApiError(meta.errors.noSuchGame); + } + + return await this.reversiGameEntityService.packDetail(game, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts new file mode 100644 index 000000000..c47d36be3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/surrender.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df', + }, + + alreadyEnded: { + message: 'That game has already ended.', + code: 'ALREADY_ENDED', + id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d', + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '6e04164b-a992-4c93-8489-2123069973e1', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + }, + required: ['gameId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.get(ps.gameId); + + if (game == null) { + throw new ApiError(meta.errors.noSuchGame); + } + + if (game.isEnded) { + throw new ApiError(meta.errors.alreadyEnded); + } + + if ((game.user1Id !== me.id) && (game.user2Id !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + await this.reversiService.surrender(game, me); + }); + } +} diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 3bc538013..998429dd0 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -19,6 +19,8 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; +import { ReversiChannelService } from './channels/reversi.js'; +import { ReversiGameChannelService } from './channels/reversi-game.js'; import { type MiChannelService } from './channel.js'; @Injectable() @@ -38,6 +40,8 @@ export class ChannelsService { private serverStatsChannelService: ServerStatsChannelService, private queueStatsChannelService: QueueStatsChannelService, private adminChannelService: AdminChannelService, + private reversiChannelService: ReversiChannelService, + private reversiGameChannelService: ReversiGameChannelService, ) { } @@ -58,6 +62,8 @@ export class ChannelsService { case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; case 'admin': return this.adminChannelService; + case 'reversi': return this.reversiChannelService; + case 'reversiGame': return this.reversiGameChannelService; default: throw new Error(`no such channel: ${name}`); diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts new file mode 100644 index 000000000..c67c05fb0 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ReversiGameChannel extends Channel { + public readonly chName = 'reversiGame'; + public static shouldShare = false; + public static requireCredential = false as const; + private gameId: MiReversiGame['id'] | null = null; + + constructor( + private reversiService: ReversiService, + private reversiGamesRepository: ReversiGamesRepository, + private reversiGameEntityService: ReversiGameEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: any) { + this.gameId = params.gameId as string; + + const game = await this.reversiGamesRepository.findOneBy({ + id: this.gameId, + }); + if (game == null) return; + + this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); + } + + @bindThis + public onMessage(type: string, body: any) { + 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 'syncState': this.syncState(body.crc32); break; + } + } + + @bindThis + private async updateSettings(key: string, value: any) { + 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.updateSettings(game, this.user, key, value); + } + + @bindThis + private async ready(ready: boolean) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + this.reversiService.gameReady(game, this.user, ready); + } + + @bindThis + 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'); + + this.reversiService.putStoneToGame(game, this.user, pos); + } + + @bindThis + private async syncState(crc32: string | number) { + // TODO: キャッシュしたい + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (!game.isStarted) return; + + if (crc32.toString() !== game.crc32) { + this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user)); + } + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off(`reversiGameStream:${this.gameId}`, this.send); + } +} + +@Injectable() +export class ReversiGameChannelService implements MiChannelService { + public readonly shouldShare = ReversiGameChannel.shouldShare; + public readonly requireCredential = ReversiGameChannel.requireCredential; + public readonly kind = ReversiGameChannel.kind; + + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ReversiGameChannel { + return new ReversiGameChannel( + this.reversiService, + this.reversiGamesRepository, + this.reversiGameEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts new file mode 100644 index 000000000..cb4b1b8d5 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/reversi.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ReversiChannel extends Channel { + public readonly chName = 'reversi'; + public static shouldShare = true; + public static requireCredential = true as const; + public static kind = 'read:account'; + + constructor( + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: any) { + this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off(`reversiStream:${this.user!.id}`, this.send); + } +} + +@Injectable() +export class ReversiChannelService implements MiChannelService { + public readonly shouldShare = ReversiChannel.shouldShare; + public readonly requireCredential = ReversiChannel.requireCredential; + public readonly kind = ReversiChannel.kind; + + constructor( + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ReversiChannel { + return new ReversiChannel( + id, + connection, + ); + } +} diff --git a/packages/frontend/assets/reversi/logo.png b/packages/frontend/assets/reversi/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d807ef1dc57af5ca60cad70277d9112e307d0fc GIT binary patch literal 96293 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 R^%ZKXoRqR;iTG>({{v=rb<_X= literal 0 HcmV?d00001 diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 8c3ce3066..a9a68601f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -41,6 +41,7 @@ "chartjs-plugin-zoom": "2.0.1", "chromatic": "10.1.0", "compare-versions": "6.1.0", + "crc-32": "^1.2.2", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", "escape-regexp": "0.0.1", @@ -53,6 +54,7 @@ "matter-js": "0.19.0", "mfm-js": "0.24.0", "misskey-js": "workspace:*", + "misskey-reversi": "workspace:*", "photoswipe": "5.4.3", "punycode": "2.3.1", "rollup": "4.9.1", diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index d9178f336..22e7ed1ef 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -18,6 +18,9 @@ export default defineComponent({ watch(value, () => { context.emit('update:modelValue', value.value); }); + watch(() => props.modelValue, v => { + value.value = v; + }); if (!context.slots.default) return null; let options = context.slots.default(); const label = context.slots.label && context.slots.label(); diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 33b8a9a86..16416fd2e 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -52,7 +52,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: 'change', _ev: KeyboardEvent): void; + (ev: 'changeByUser'): void; (ev: 'update:modelValue', value: string | null): void; }>(); @@ -77,7 +77,6 @@ const height = const focus = () => inputEl.value.focus(); const onInput = (ev) => { changed.value = true; - emit('change', ev); }; const updated = () => { @@ -136,6 +135,7 @@ function show(ev: MouseEvent) { active: computed(() => v.value === option.props.value), action: () => { v.value = option.props.value; + emit('changeByUser', v.value); }, }); }; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index f4aa06950..ad11ba194 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -85,7 +85,7 @@ const recentUsers = ref([]); const selected = ref(null); const dialogEl = ref(); -const search = () => { +function search() { if (username.value === '' && host.value === '') { users.value = []; return; @@ -98,9 +98,9 @@ const search = () => { }).then(_users => { users.value = _users; }); -}; +} -const ok = () => { +function ok() { if (selected.value == null) return; emit('ok', selected.value); dialogEl.value.close(); @@ -110,12 +110,12 @@ const ok = () => { recents = recents.filter(x => x !== selected.value.id); recents.unshift(selected.value.id); defaultStore.set('recentlyUsedUsers', recents.splice(0, 16)); -}; +} -const cancel = () => { +function cancel() { emit('cancel'); dialogEl.value.close(); -}; +} onMounted(() => { misskeyApi('users/show', { diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts index 8e1c178ea..0333770a6 100644 --- a/packages/frontend/src/global/router/definition.ts +++ b/packages/frontend/src/global/router/definition.ts @@ -15,6 +15,7 @@ const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ loadingComponent: MkLoading, errorComponent: MkError, }); + const routes = [{ path: '/@:initUser/pages/:initPageName/view-source', component: page(() => import('@/pages/page-editor/page-editor.vue')), @@ -528,18 +529,26 @@ const routes = [{ path: '/timeline/antenna/:antennaId', component: page(() => import('@/pages/antenna-timeline.vue')), loginRequired: true, -}, { - path: '/games', - component: page(() => import('@/pages/games.vue')), - loginRequired: true, }, { path: '/clicker', component: page(() => import('@/pages/clicker.vue')), loginRequired: true, +}, { + path: '/games', + component: page(() => import('@/pages/games.vue')), + loginRequired: false, }, { path: '/bubble-game', component: page(() => import('@/pages/drop-and-fusion.vue')), loginRequired: true, +}, { + path: '/reversi', + component: page(() => import('@/pages/reversi/index.vue')), + loginRequired: false, +}, { + path: '/reversi/g/:gameId', + component: page(() => import('@/pages/reversi/game.vue')), + loginRequired: false, }, { path: '/timeline', component: page(() => import('@/pages/timeline.vue')), diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index a63d61bb8..9fc3603af 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -419,7 +419,7 @@ export function form(title, form) { }); } -export async function selectUser(opts: { includeSelf?: boolean } = {}) { +export async function selectUser(opts: { includeSelf?: boolean } = {}): Promise { return new Promise((resolve, reject) => { popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { includeSelf: opts.includeSelf, diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index dd3b189c9..beb2e714e 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -123,7 +123,7 @@ function onGameEnd() { definePageMetadata({ title: i18n.ts.bubbleGame, - icon: 'ti ti-apple', + icon: 'ti ti-device-gamepad', }); diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue index 5d2482ded..45a135a45 100644 --- a/packages/frontend/src/pages/games.vue +++ b/packages/frontend/src/pages/games.vue @@ -7,10 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only -
- - - +
+
+ + + +
+
+ + + +
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue new file mode 100644 index 000000000..18fd74427 --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -0,0 +1,428 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue new file mode 100644 index 000000000..301a177de --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -0,0 +1,236 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue new file mode 100644 index 000000000..dbbeb20f4 --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue new file mode 100644 index 000000000..c483e36c2 --- /dev/null +++ b/packages/frontend/src/pages/reversi/index.vue @@ -0,0 +1,271 @@ + + + + + + + diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 98fe0043c..8cdc7b59c 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -103,7 +103,7 @@ export function getConfig(): UserConfig { // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies optimizeDeps: { - include: ['misskey-js'], + include: ['misskey-js', 'misskey-reversi'], }, build: { @@ -135,7 +135,7 @@ export function getConfig(): UserConfig { // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies commonjsOptions: { - include: [/misskey-js/, /node_modules/], + include: [/misskey-js/, /misskey-reversi/, /node_modules/], }, }, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f955cc5cc..2b95e0153 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1623,6 +1623,16 @@ declare namespace entities { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -1659,7 +1669,9 @@ declare namespace entities { Flash, Signin, RoleLite, - Role + Role, + ReversiGameLite, + ReversiGameDetailed } } export { entities } @@ -2596,6 +2608,42 @@ type ResetPasswordRequest = operations['reset-password']['requestBody']['content // @public (undocumented) type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; + +// @public (undocumented) +type ReversiGameLite = components['schemas']['ReversiGameLite']; + +// @public (undocumented) +type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; + // @public (undocumented) type Role = components['schemas']['Role']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index b60f449a7..e4e7d1366 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-13T04:31:38.782Z + * generatedAt: 2024-01-19T11:00:07.160Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -4007,5 +4007,71 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; } } diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index dc591a704..671abd78c 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-13T04:31:38.778Z + * generatedAt: 2024-01-19T11:00:07.158Z */ import type { @@ -544,6 +544,16 @@ import type { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, } from './entities.js'; export type Endpoints = { @@ -907,4 +917,10 @@ export type Endpoints = { 'retention': { req: EmptyRequest; res: RetentionResponse }; 'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; + 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: ReversiCancelMatchResponse }; + 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse }; + 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse }; + 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; + 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; + 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; } diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index dfe24ce0d..c14876c0e 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-13T04:31:38.775Z + * generatedAt: 2024-01-19T11:00:07.156Z */ import { operations } from './types.js'; @@ -546,3 +546,13 @@ export type BubbleGameRegisterRequest = operations['bubble-game/register']['requ export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; +export type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; +export type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; +export type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; +export type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; +export type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; +export type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; +export type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; +export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; +export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; +export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 5c6bebf2f..78f14d225 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-13T04:31:38.773Z + * generatedAt: 2024-01-19T11:00:07.155Z */ import { components } from './types.js'; @@ -41,3 +41,5 @@ export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; +export type ReversiGameLite = components['schemas']['ReversiGameLite']; +export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 76e2b5309..36facf6e2 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-13T04:31:38.633Z + * generatedAt: 2024-01-19T11:00:07.077Z */ /** @@ -3472,6 +3472,60 @@ export type paths = { */ post: operations['bubble-game/ranking']; }; + '/reversi/cancel-match': { + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/cancel-match']; + }; + '/reversi/games': { + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/games']; + }; + '/reversi/match': { + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/match']; + }; + '/reversi/invitations': { + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['reversi/invitations']; + }; + '/reversi/show-game': { + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/show-game']; + }; + '/reversi/surrender': { + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/surrender']; + }; }; export type webhooks = Record; @@ -4404,6 +4458,72 @@ export type components = { }; usersCount: number; }); + ReversiGameLite: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Ready: boolean; + user2Ready: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + }; + ReversiGameDetailed: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Ready: boolean; + user2Ready: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + logs: { + at: number; + color: boolean; + pos: number; + }[]; + map: string[]; + }; }; responses: never; parameters: never; @@ -25542,5 +25662,325 @@ export type operations = { }; }; }; + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/cancel-match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/games': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default false */ + my?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGameLite'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'reversi/invitations': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/show-game': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGameDetailed']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/surrender': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; }; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json new file mode 100644 index 000000000..8d3ca3016 --- /dev/null +++ b/packages/misskey-reversi/package.json @@ -0,0 +1,26 @@ +{ + "name": "misskey-reversi", + "version": "0.0.1", + "main": "./built/index.js", + "types": "./built/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", + "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@misskey-dev/eslint-plugin": "1.0.0", + "@types/node": "20.11.5", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", + "eslint": "8.56.0", + "typescript": "5.3.3" + }, + "files": [ + "built" + ], + "dependencies": { + } +} diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts new file mode 100644 index 000000000..55d0b84da --- /dev/null +++ b/packages/misskey-reversi/src/game.ts @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * true ... 黒 + * false ... 白 + */ +export type Color = boolean; +const BLACK = true; +const WHITE = false; + +export type MapCell = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; +}; + +export type Undo = { + color: Color; + pos: number; + + /** + * 反転した石の位置の配列 + */ + effects: number[]; + + turn: Color | null; +}; + +export class Game { + public map: MapCell[]; + public mapWidth: number; + public mapHeight: number; + public board: (Color | null | undefined)[]; + public turn: Color | null = BLACK; + public opts: Options; + + public prevPos = -1; + public prevColor: Color | null = null; + + private logs: Undo[] = []; + + constructor(map: string[], opts: Options) { + //#region binds + this.put = this.put.bind(this); + //#endregion + + //#region Options + this.opts = opts; + if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; + if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; + if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + //#endregion + + //#region Parse map data + this.mapWidth = map[0].length; + this.mapHeight = map.length; + const mapData = map.join(''); + + this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined); + + this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null'); + //#endregion + + // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある + if (!this.canPutSomewhere(BLACK)) + this.turn = this.canPutSomewhere(WHITE) ? WHITE : null; + } + + public get blackCount() { + return this.board.filter(x => x === BLACK).length; + } + + public get whiteCount() { + return this.board.filter(x => x === WHITE).length; + } + + public posToXy(pos: number): number[] { + const x = pos % this.mapWidth; + const y = Math.floor(pos / this.mapWidth); + return [x, y]; + } + + public xyToPos(x: number, y: number): number { + return x + (y * this.mapWidth); + } + + public put(color: Color, pos: number) { + this.prevPos = pos; + this.prevColor = color; + + this.board[pos] = color; + + // 反転させられる石を取得 + const effects = this.effects(color, pos); + + // 反転させる + for (const pos of effects) { + this.board[pos] = color; + } + + const turn = this.turn; + + this.logs.push({ + color, + pos, + effects, + turn + }); + + this.calcTurn(); + } + + private calcTurn() { + // ターン計算 + this.turn = + this.canPutSomewhere(!this.prevColor) ? !this.prevColor : + this.canPutSomewhere(this.prevColor!) ? this.prevColor : + null; + } + + public undo() { + const undo = this.logs.pop()!; + this.prevColor = undo.color; + this.prevPos = undo.pos; + this.board[undo.pos] = null; + for (const pos of undo.effects) { + const color = this.board[pos]; + this.board[pos] = !color; + } + this.turn = undo.turn; + } + + public mapDataGet(pos: number): MapCell { + const [x, y] = this.posToXy(pos); + return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos]; + } + + public getPuttablePlaces(color: Color): number[] { + return Array.from(this.board.keys()).filter(i => this.canPut(color, i)); + } + + public canPutSomewhere(color: Color): boolean { + return this.getPuttablePlaces(color).length > 0; + } + + public canPut(color: Color, pos: number): boolean { + return ( + this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない + this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード + this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param initPos 位置 + */ + public effects(color: Color, initPos: number): number[] { + const enemyColor = !color; + + const diffVectors: [number, number][] = [ + [ 0, -1], // 上 + [+1, -1], // 右上 + [+1, 0], // 右 + [+1, +1], // 右下 + [ 0, +1], // 下 + [-1, +1], // 左下 + [-1, 0], // 左 + [-1, -1] // 左上 + ]; + + const effectsInLine = ([dx, dy]: [number, number]): number[] => { + const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy]; + + const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列 + let [x, y] = this.posToXy(initPos); + while (true) { + [x, y] = nextPos(x, y); + + // 座標が指し示す位置がボード外に出たとき + if (this.opts.loopedBoard && this.xyToPos( + (x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), + (y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) + // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) + return found; + else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) + return []; // 挟めないことが確定 (盤面外に到達) + + const pos = this.xyToPos(x, y); + if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) + const stone = this.board[pos]; + if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達) + if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見) + if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見) + } + }; + + return ([] as number[]).concat(...diffVectors.map(effectsInLine)); + } + + public get isEnded(): boolean { + return this.turn === null; + } + + public get winner(): Color | null { + return this.isEnded ? + this.blackCount == this.whiteCount ? null : + this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : + undefined as never; + } +} diff --git a/packages/misskey-reversi/src/index.ts b/packages/misskey-reversi/src/index.ts new file mode 100644 index 000000000..20ed36f20 --- /dev/null +++ b/packages/misskey-reversi/src/index.ts @@ -0,0 +1,7 @@ +import { Game } from './game.js'; + +export { + Game, +}; + +export * as maps from './maps.js'; diff --git a/packages/misskey-reversi/src/maps.ts b/packages/misskey-reversi/src/maps.ts new file mode 100644 index 000000000..85cf1a048 --- /dev/null +++ b/packages/misskey-reversi/src/maps.ts @@ -0,0 +1,715 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + category?: string; + author?: string; + data: string[]; +}; + +export const fourfour: Map = { + name: '4x4', + category: '4x4', + data: [ + '----', + '-wb-', + '-bw-', + '----' + ] +}; + +export const sixsix: Map = { + name: '6x6', + category: '6x6', + data: [ + '------', + '------', + '--wb--', + '--bw--', + '------', + '------' + ] +}; + +export const roundedSixsix: Map = { + name: '6x6 rounded', + category: '6x6', + author: 'syuilo', + data: [ + ' ---- ', + '------', + '--wb--', + '--bw--', + '------', + ' ---- ' + ] +}; + +export const roundedSixsix2: Map = { + name: '6x6 rounded 2', + category: '6x6', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + '--wb--', + '--bw--', + ' ---- ', + ' -- ' + ] +}; + +export const eighteight: Map = { + name: '8x8', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH28: Map = { + name: '8x8 handicap 28', + category: '8x8', + data: [ + 'bbbbbbbb', + 'b------b', + 'b------b', + 'b--wb--b', + 'b--bw--b', + 'b------b', + 'b------b', + 'bbbbbbbb' + ] +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + ' ------ ' + ] +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + category: '8x8', + author: 'syuilo', + data: [ + ' ---- ', + ' ------ ', + '--------', + '---wb---', + '---bw---', + '--------', + ' ------ ', + ' ---- ' + ] +}; + +export const roundedEighteight3: Map = { + name: '8x8 rounded 3', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ---- ', + ' -- ' + ] +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + category: '8x8', + author: 'syuilo', + data: [ + '--- ---', + '--------', + '--------', + ' --wb-- ', + ' --bw-- ', + '--------', + '--------', + '--- ---' + ] +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + category: '8x8', + author: 'syuilo', + data: [ + '--- ----', + '----- --', + '-- -----', + '---wb---', + '---bw- -', + ' -------', + '--- ----', + '--------' + ] +}; + +export const circle: Map = { + name: 'Circle', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ------ ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ------ ', + ' -- ' + ] +}; + +export const smile: Map = { + name: 'Smile', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '-- -- --', + '---wb---', + '-- bw --', + '--- ---', + '--------', + ' ------ ' + ] +}; + +export const window: Map = { + name: 'Window', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '- -- -', + '- -- -', + '---wb---', + '---bw---', + '- -- -', + '- -- -', + '--------' + ] +}; + +export const reserved: Map = { + name: 'Reserved', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------w' + ] +}; + +export const x: Map = { + name: 'X', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '-w----b-', + '--w--b--', + '---wb---', + '---bw---', + '--b--w--', + '-b----w-', + 'b------w' + ] +}; + +export const parallel: Map = { + name: 'Parallel', + category: '8x8', + author: 'Aya', + data: [ + '--------', + '--------', + '--------', + '---bb---', + '---ww---', + '--------', + '--------', + '--------' + ] +}; + +export const lackOfBlack: Map = { + name: 'Lack of Black', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---w----', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const squareParty: Map = { + name: 'Square Party', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '-wwwbbb-', + '-w-wb-b-', + '-wwwbbb-', + '-bbbwww-', + '-b-bw-w-', + '-bbbwww-', + '--------' + ] +}; + +export const minesweeper: Map = { + name: 'Minesweeper', + category: '8x8', + author: 'syuilo', + data: [ + 'b-b--w-w', + '-w-wb-b-', + 'w-b--w-b', + '-b-wb-w-', + '-w-bw-b-', + 'b-w--b-w', + '-b-bw-w-', + 'w-w--b-b' + ] +}; + +export const tenthtenth: Map = { + name: '10x10', + category: '10x10', + data: [ + '----------', + '----------', + '----------', + '----------', + '----wb----', + '----bw----', + '----------', + '----------', + '----------', + '----------' + ] +}; + +export const hole: Map = { + name: 'The Hole', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '----------', + '--wb--wb--', + '--bw--bw--', + '---- ----', + '---- ----', + '--wb--wb--', + '--bw--bw--', + '----------', + '----------' + ] +}; + +export const grid: Map = { + name: 'Grid', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '- - -- - -', + '----------', + '- - -- - -', + '----wb----', + '----bw----', + '- - -- - -', + '----------', + '- - -- - -', + '----------' + ] +}; + +export const cross: Map = { + name: 'Cross', + category: '10x10', + author: 'Aya', + data: [ + ' ---- ', + ' ---- ', + ' ---- ', + '----------', + '----wb----', + '----bw----', + '----------', + ' ---- ', + ' ---- ', + ' ---- ' + ] +}; + +export const charX: Map = { + name: 'Char X', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + '----------', + '---- ----', + '--- ---' + ] +}; + +export const charY: Map = { + name: 'Char Y', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' ------ ', + ' ------ ', + ' ------ ', + ' ------ ' + ] +}; + +export const walls: Map = { + name: 'Walls', + category: '10x10', + author: 'Aya', + data: [ + ' bbbbbbbb ', + 'w--------w', + 'w--------w', + 'w--------w', + 'w---wb---w', + 'w---bw---w', + 'w--------w', + 'w--------w', + 'w--------w', + ' bbbbbbbb ' + ] +}; + +export const cpu: Map = { + name: 'CPU', + category: '10x10', + author: 'syuilo', + data: [ + ' b b b b ', + 'w--------w', + ' -------- ', + 'w--------w', + ' ---wb--- ', + ' ---bw--- ', + 'w--------w', + ' -------- ', + 'w--------w', + ' b b b b ' + ] +}; + +export const checker: Map = { + name: 'Checker', + category: '10x10', + author: 'Aya', + data: [ + '----------', + '----------', + '----------', + '---wbwb---', + '---bwbw---', + '---wbwb---', + '---bwbw---', + '----------', + '----------', + '----------' + ] +}; + +export const japaneseCurry: Map = { + name: 'Japanese curry', + category: '10x10', + author: 'syuilo', + data: [ + 'w-b-b-b-b-', + '-w-b-b-b-b', + 'w-w-b-b-b-', + '-w-w-b-b-b', + 'w-w-wwb-b-', + '-w-wbb-b-b', + 'w-w-w-b-b-', + '-w-w-w-b-b', + 'w-w-w-w-b-', + '-w-w-w-w-b' + ] +}; + +export const mosaic: Map = { + name: 'Mosaic', + category: '10x10', + author: 'syuilo', + data: [ + '- - - - - ', + ' - - - - -', + '- - - - - ', + ' - w w - -', + '- - b b - ', + ' - w w - -', + '- - b b - ', + ' - - - - -', + '- - - - - ', + ' - - - - -', + ] +}; + +export const arena: Map = { + name: 'Arena', + category: '10x10', + author: 'syuilo', + data: [ + '- - -- - -', + ' - - - - ', + '- ------ -', + ' -------- ', + '- --wb-- -', + '- --bw-- -', + ' -------- ', + '- ------ -', + ' - - - - ', + '- - -- - -' + ] +}; + +export const reactor: Map = { + name: 'Reactor', + category: '10x10', + author: 'syuilo', + data: [ + '-w------b-', + 'b- - - -w', + '- --wb-- -', + '---b w---', + '- b wb w -', + '- w bw b -', + '---w b---', + '- --bw-- -', + 'w- - - -b', + '-b------w-' + ] +}; + +export const sixeight: Map = { + name: '6x8', + category: 'Special', + data: [ + '------', + '------', + '------', + '--wb--', + '--bw--', + '------', + '------', + '------' + ] +}; + +export const spark: Map = { + name: 'Spark', + category: 'Special', + author: 'syuilo', + data: [ + ' - - ', + '----------', + ' -------- ', + ' -------- ', + ' ---wb--- ', + ' ---bw--- ', + ' -------- ', + ' -------- ', + '----------', + ' - - ' + ] +}; + +export const islands: Map = { + name: 'Islands', + category: 'Special', + author: 'syuilo', + data: [ + '-------- ', + '---wb--- ', + '---bw--- ', + '-------- ', + ' - - ', + ' - - ', + ' --------', + ' --------', + ' --------', + ' --------' + ] +}; + +export const galaxy: Map = { + name: 'Galaxy', + category: 'Special', + author: 'syuilo', + data: [ + ' ------ ', + ' --www--- ', + ' ------w--- ', + '---bbb--w---', + '--b---b-w-b-', + '-b--wwb-w-b-', + '-b-w-bww--b-', + '-b-w-b---b--', + '---w--bbb---', + ' ---w------ ', + ' ---www-- ', + ' ------ ' + ] +}; + +export const triangle: Map = { + name: 'Triangle', + category: 'Special', + author: 'syuilo', + data: [ + ' -- ', + ' -- ', + ' ---- ', + ' ---- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + ' -------- ', + '----------', + '----------' + ] +}; + +export const iphonex: Map = { + name: 'iPhone X', + category: 'Special', + author: 'syuilo', + data: [ + ' -- -- ', + '--------', + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------', + '--------', + ' ------ ' + ] +}; + +export const dealWithIt: Map = { + name: 'Deal with it!', + category: 'Special', + author: 'syuilo', + data: [ + '------------', + '--w-b-------', + ' --b-w------', + ' --w-b---- ', + ' ------- ' + ] +}; + +export const bigBoard: Map = { + name: 'Big board', + category: 'Special', + data: [ + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '-------wb-------', + '-------bw-------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------' + ] +}; + +export const twoBoard: Map = { + name: 'Two board', + category: 'Special', + author: 'Aya', + data: [ + '-------- --------', + '-------- --------', + '-------- --------', + '---wb--- ---wb---', + '---bw--- ---bw---', + '-------- --------', + '-------- --------', + '-------- --------' + ] +}; diff --git a/packages/misskey-reversi/tsconfig.json b/packages/misskey-reversi/tsconfig.json new file mode 100644 index 000000000..f56b65e86 --- /dev/null +++ b/packages/misskey-reversi/tsconfig.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./built/", + "removeComments": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 825a7ab86..31394eb08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: content-disposition: specifier: 0.5.4 version: 0.5.4 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -263,6 +266,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi ms: specifier: 3.0.0-canary.1 version: 3.0.0-canary.1 @@ -736,6 +742,9 @@ importers: compare-versions: specifier: 6.1.0 version: 6.1.0 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 cropperjs: specifier: 2.0.0-beta.4 version: 2.0.0-beta.4 @@ -772,6 +781,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi photoswipe: specifier: 5.4.3 version: 5.4.3 @@ -1114,6 +1126,27 @@ importers: specifier: 5.3.3 version: 5.3.3 + packages/misskey-reversi: + devDependencies: + '@misskey-dev/eslint-plugin': + specifier: 1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + '@types/node': + specifier: 20.11.5 + version: 20.11.5 + '@typescript-eslint/eslint-plugin': + specifier: 6.19.0 + version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: 6.19.0 + version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: + specifier: 8.56.0 + version: 8.56.0 + typescript: + specifier: 5.3.3 + version: 5.3.3 + packages/sw: dependencies: esbuild: @@ -1128,7 +1161,7 @@ importers: devDependencies: '@misskey-dev/eslint-plugin': specifier: ^1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) '@typescript-eslint/parser': specifier: 6.14.0 version: 6.14.0(eslint@8.56.0)(typescript@5.3.3) @@ -1812,7 +1845,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.17 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1835,7 +1868,7 @@ packages: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1937,7 +1970,7 @@ packages: '@babel/core': 7.23.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -3336,7 +3369,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.22.17 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3354,7 +3387,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.23.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4233,7 +4266,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4250,7 +4283,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4515,7 +4548,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4571,7 +4604,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4592,14 +4625,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.5) + jest-config: 29.7.0(@types/node@20.11.5) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4634,7 +4667,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 dev: true @@ -4661,7 +4694,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4694,7 +4727,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4788,7 +4821,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4800,7 +4833,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true @@ -4992,6 +5025,34 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) dev: true + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) + dev: true + + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0) + dev: true + /@misskey-dev/sharp-read-bmp@1.1.1: resolution: {integrity: sha512-X52BQYL/I9mafypQ+wBhst+BUlYiPWnHhKGcF6ybcYSLl+zhcV0q5mezIXHozhM0Sv0A7xCdrWmR7TCNxHLrtQ==} dependencies: @@ -5089,7 +5150,7 @@ packages: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) headers-polyfill: 3.2.5 outvariant: 1.4.0 strict-event-emitter: 0.2.8 @@ -7992,7 +8053,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/responselike': 1.0.0 dev: false @@ -8025,7 +8086,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/content-disposition@0.5.8: @@ -8039,7 +8100,7 @@ packages: /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/debug@4.1.7: @@ -8097,7 +8158,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -8125,13 +8186,13 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/http-cache-semantics@4.0.1: @@ -8212,7 +8273,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/lodash@4.14.191: @@ -8261,7 +8322,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -8279,6 +8340,11 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 + /@types/node@20.9.1: resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} dependencies: @@ -8381,7 +8447,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/rename@1.0.7: @@ -8395,7 +8461,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/sanitize-html@2.9.5: @@ -8421,7 +8487,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/serviceworker@0.0.67: @@ -8431,7 +8497,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/sharp@0.32.0: @@ -8534,7 +8600,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true optional: true @@ -8555,7 +8621,7 @@ packages: '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8584,7 +8650,65 @@ packages: '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8610,7 +8734,7 @@ packages: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8631,7 +8755,28 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8654,6 +8799,14 @@ packages: '@typescript-eslint/visitor-keys': 6.14.0 dev: true + /@typescript-eslint/scope-manager@6.19.0: + resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8666,7 +8819,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8686,7 +8839,27 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8704,6 +8877,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@6.19.0: + resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3): resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8715,7 +8893,7 @@ packages: dependencies: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8736,7 +8914,7 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8746,6 +8924,28 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3): + resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8784,6 +8984,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@6.11.0: resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8800,6 +9019,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.19.0: + resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -9193,7 +9420,7 @@ packages: engines: {node: '>= 6.0.0'} requiresBuild: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -9201,7 +9428,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -9587,7 +9814,7 @@ packages: resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} dependencies: archy: 1.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fastq: 1.15.0 transitivePeerDependencies: - supports-color @@ -11036,7 +11263,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: true /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -11049,6 +11275,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -11265,7 +11492,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -11589,7 +11816,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -11806,6 +12033,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0)(eslint@8.53.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} @@ -11876,6 +12132,41 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-vue@9.19.2(eslint@8.56.0): resolution: {integrity: sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==} engines: {node: ^14.17.0 || >=16.0.0} @@ -11927,7 +12218,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11974,7 +12265,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -12604,7 +12895,7 @@ packages: debug: optional: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -13160,7 +13451,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -13298,7 +13588,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13360,7 +13650,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -13370,7 +13660,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -13379,7 +13669,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13389,7 +13679,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13549,7 +13839,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -13990,7 +14280,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -14044,7 +14334,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -14133,6 +14423,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@20.11.5): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.11 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.5 + babel-jest: 29.7.0(@babel/core@7.22.11) + chalk: 4.1.2 + ci-info: 3.7.1 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@28.1.3: resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -14188,7 +14518,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -14218,7 +14548,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.10.5 + '@types/node': 20.11.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14279,7 +14609,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /jest-mock@29.7.0: @@ -14342,7 +14672,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -14373,7 +14703,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -14425,7 +14755,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -14450,7 +14780,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -14469,7 +14799,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -14667,7 +14997,7 @@ packages: resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) rfdc: 1.3.0 uri-js: 4.4.1 transitivePeerDependencies: @@ -17278,7 +17608,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -18275,7 +18605,7 @@ packages: dependencies: '@hapi/hoek': 10.0.1 '@hapi/wreck': 18.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) joi: 17.7.0 transitivePeerDependencies: - supports-color @@ -18475,7 +18805,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -18628,7 +18958,7 @@ packages: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -18892,7 +19222,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -19515,7 +19844,7 @@ packages: chalk: 4.1.2 cli-highlight: 2.1.11 date-fns: 2.30.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) dotenv: 16.0.3 glob: 8.1.0 ioredis: 5.3.2 @@ -19880,7 +20209,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -19992,7 +20321,7 @@ packages: acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.10 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) happy-dom: 10.0.3 local-pkg: 0.4.3 magic-string: 0.30.3 @@ -20074,7 +20403,7 @@ packages: peerDependencies: eslint: '>=6.0.0' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3b2ecec7f..3a03a5825 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'packages/sw' - 'packages/misskey-js' - 'packages/misskey-js/generator' + - 'packages/misskey-reversi'