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 a22cb63507..85e0c6b244 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 8749a5f49f..6c8a453023 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 0000000000..c9d69e2c7c --- /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 0000000000..33747ba9f7 --- /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 5ab476295c..f8e82c5a1c 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 bc6d24b951..c9e285346e 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 d175f21f2f..11a8935be2 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 0000000000..cd990ba775 --- /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 0000000000..8d95204928 --- /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 e29fee3f96..73de01f33a 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 176978d35f..b4f0541712 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 0399536c3e..2b2aaeb91c 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 0000000000..d297d1f01d --- /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 a1c4b0743e..a1a0d8823d 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 0000000000..0d23b9dc79 --- /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 0430e9ca19..1e063c8673 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 fa81380f01..aed352d15e 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 781332d349..df69ce2385 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 f17db41a5d..0f2c8cb754 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 7ff7b5de3a..2d853b94f3 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 0000000000..8edc049500 --- /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 0000000000..5322cd0987 --- /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 0000000000..0b7107bb0d --- /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 0000000000..da5a3409ef --- /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 0000000000..de571053e1 --- /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 0000000000..c47d36be33 --- /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 3bc5380132..998429dd0a 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 0000000000..c67c05fb09 --- /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 0000000000..cb4b1b8d5a --- /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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 literal 0 HcmV?d00001 diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 8c3ce30668..a9a68601fc 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 d9178f3362..22e7ed1ef7 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 33b8a9a86d..16416fd2e4 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 f4aa06950d..ad11ba1940 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 8e1c178ea2..0333770a64 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 a63d61bb8f..9fc3603af0 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 dd3b189c9d..beb2e714e0 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 5d2482ded1..45a135a459 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 0000000000..18fd74427c --- /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 0000000000..301a177de1 --- /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 0000000000..dbbeb20f42 --- /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 0000000000..c483e36c24 --- /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 98fe0043c1..8cdc7b59c6 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 f955cc5cc1..2b95e01533 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 b60f449a71..e4e7d13668 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 dc591a7046..671abd78ce 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 dfe24ce0d8..c14876c0e3 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 5c6bebf2fd..78f14d2250 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 76e2b5309c..36facf6e28 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 0000000000..8d3ca30166 --- /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 0000000000..55d0b84da7 --- /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 0000000000..20ed36f208 --- /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 0000000000..85cf1a0485 --- /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 0000000000..f56b65e868 --- /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 825a7ab860..31394eb081 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 3b2ecec7fd..3a03a58253 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'