From a637b4e28259e89285fc1c67589c731a053f5562 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> 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<Packed<'Note'>>; }; + reversi: { + name: `reversiStream:${MiUser['id']}`; + payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>; + }; + reversiGame: { + name: `reversiGameStream:${MiReversiGame['id']}`; + payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>; + }; }; // API event definitions @@ -338,4 +383,14 @@ export class GlobalEventService { public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } + + @bindThis + public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { + this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + @bindThis + public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(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<MiReversiGame | null> { + 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<MiReversiGame | null> { + //#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<MiUser['id'][]> { + 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<Packed<'ReversiGameDetailed'>> { + 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<Packed<'ReversiGameLite'>> { + 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<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; 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<MiAbuseUserReport>; @@ -206,3 +209,4 @@ export type FlashsRepository = Repository<MiFlash>; export type FlashLikesRepository = Repository<MiFlashLike>; export type UserMemoRepository = Repository<MiUserMemo>; export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>; +export type ReversiGamesRepository = Repository<MiReversiGame>; 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<false> { + 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<true> { + public readonly shouldShare = ReversiChannel.shouldShare; + public readonly requireCredential = ReversiChannel.requireCredential; + public readonly kind = ReversiChannel.kind; + + constructor( + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ReversiChannel { + return new ReversiChannel( + id, + connection, + ); + } +} diff --git a/packages/frontend/assets/reversi/logo.png b/packages/frontend/assets/reversi/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d807ef1dc57af5ca60cad70277d9112e307d0fc GIT binary patch literal 96293 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 R^%ZKXoRqR;iTG>({{v=rb<_X= literal 0 HcmV?d00001 diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 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<Misskey.entities.UserDetailed[]>([]); const selected = ref<Misskey.entities.UserDetailed | null>(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<any>) => 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<Misskey.entities.UserLite> { 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', }); </script> 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 <MkStickyContainer> <template #header><MkPageHeader/></template> <MkSpacer :contentMax="800"> - <div class="_panel"> - <MkA to="/bubble-game"> - <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> - </MkA> + <div class="_gaps"> + <div class="_panel"> + <MkA to="/bubble-game"> + <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> + </MkA> + </div> + <div class="_panel"> + <MkA to="/reversi"> + <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> + </MkA> + </div> </div> </MkSpacer> </MkStickyContainer> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkSpacer :contentMax="600"> + <div :class="$style.root" class="_gaps"> + <header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ i18n.ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ i18n.ts._reversi.white }})</header> + + <div style="overflow: clip; line-height: 28px;"> + <div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn"> + <Mfm :key="'turn:' + turnUser.id" :text="i18n.t('_reversi.turnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> + <MkEllipsis/> + </div> + <div v-if="(logPos !== logs.length) && turnUser" class="turn"> + <Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.t('_reversi.pastTurnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> + </div> + <div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div> + <div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div> + <div v-if="game.isEnded && logPos == logs.length" class="result"> + <template v-if="game.winner"> + <Mfm :key="'won'" :text="i18n.t('_reversi.won', { name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/> + <span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span> + </template> + <template v-else>{{ i18n.ts._reversi.drawn }}</template> + </div> + </div> + + <div :class="$style.board"> + <div v-if="showBoardLabels" :class="$style.labelsX"> + <span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span> + </div> + <div style="display: flex;"> + <div v-if="showBoardLabels" :class="$style.labelsY"> + <div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div> + </div> + <div :class="$style.boardCells" :style="cellsStyle"> + <div + v-for="(stone, i) in engine.board" + v-tooltip="`${String.fromCharCode(65 + engine.posToXy(i)[0])}${engine.posToXy(i)[1] + 1}`" + :class="[$style.boardCell, { + [$style.boardCell_empty]: stone == null, + [$style.boardCell_none]: engine.map[i] === 'null', + [$style.boardCell_isEnded]: game.isEnded, + [$style.boardCell_myTurn]: !game.isEnded && isMyTurn, + [$style.boardCell_can]: turnUser ? engine.canPut(turnUser.id === blackUser.id, i) : null, + [$style.boardCell_prev]: engine.prevPos === i + }]" + @click="putStone(i)" + > + <img v-if="stone === true" style="pointer-events: none; user-select: none; display: block; width: 100%; height: 100%;" :src="blackUser.avatarUrl"> + <img v-if="stone === false" style="pointer-events: none; user-select: none; display: block; width: 100%; height: 100%;" :src="whiteUser.avatarUrl"> + </div> + </div> + <div v-if="showBoardLabels" :class="$style.labelsY"> + <div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div> + </div> + </div> + <div v-if="showBoardLabels" :class="$style.labelsX"> + <span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span> + </div> + </div> + + <div class="status"><b>{{ i18n.t('_reversi.turnCount', { count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div> + + <div v-if="!game.isEnded && iAmPlayer" class="_buttonsCenter"> + <MkButton danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton> + </div> + + <div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;"> + <div>{{ logPos }} / {{ logs.length }}</div> + <div v-if="!autoplaying" class="_buttonsCenter"> + <MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton> + <MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton> + <MkButton :disabled="logPos === logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton> + <MkButton :disabled="logPos === logs.length" @click="logPos = logs.length"><i class="ti ti-chevrons-right"></i></MkButton> + </div> + <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton> + </div> + + <div> + <p v-if="game.isLlotheo">{{ i18n.ts._reversi.isLlotheo }}</p> + <p v-if="game.loopedBoard">{{ i18n.ts._reversi.loopedMap }}</p> + <p v-if="game.canPutEverywhere">{{ i18n.ts._reversi.canPutEverywhere }}</p> + </div> + + <MkA v-if="game.isEnded" :to="`/reversi`"> + <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; width: 200px; margin: auto;"/> + </MkA> + </div> +</MkSpacer> +</template> + +<script lang="ts" setup> +import { computed, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; +import * as CRC32 from 'crc-32'; +import * as Misskey from 'misskey-js'; +import * as Reversi from 'misskey-reversi'; +import MkButton from '@/components/MkButton.vue'; +import { deepClone } from '@/scripts/clone.js'; +import { useInterval } from '@/scripts/use-interval.js'; +import { signinRequired } from '@/account.js'; +import { i18n } from '@/i18n.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { userPage } from '@/filters/user.js'; + +const $i = signinRequired(); + +const props = defineProps<{ + game: Misskey.entities.ReversiGameDetailed; + connection: Misskey.ChannelConnection; +}>(); + +const showBoardLabels = true; +const autoplaying = ref<boolean>(false); +const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game)); +const logs = ref<Misskey.entities.ReversiLog[]>(game.value.logs); +const logPos = ref<number>(logs.value.length); +const engine = shallowRef<Reversi.Game>(new Reversi.Game(game.value.map, { + isLlotheo: game.value.isLlotheo, + canPutEverywhere: game.value.canPutEverywhere, + loopedBoard: game.value.loopedBoard, +})); + +for (const log of game.value.logs) { + engine.value.put(log.color, log.pos); +} + +const iAmPlayer = computed(() => { + return game.value.user1Id === $i.id || game.value.user2Id === $i.id; +}); + +const myColor = computed(() => { + if (!iAmPlayer.value) return null; + if (game.value.user1Id === $i.id && game.value.black === 1) return true; + if (game.value.user2Id === $i.id && game.value.black === 2) return true; + return false; +}); + +const opColor = computed(() => { + if (!iAmPlayer.value) return null; + return !myColor.value; +}); + +const blackUser = computed(() => { + return game.value.black === 1 ? game.value.user1 : game.value.user2; +}); + +const whiteUser = computed(() => { + return game.value.black === 1 ? game.value.user2 : game.value.user1; +}); + +const turnUser = computed(() => { + if (engine.value.turn === true) { + return game.value.black === 1 ? game.value.user1 : game.value.user2; + } else if (engine.value.turn === false) { + return game.value.black === 1 ? game.value.user2 : game.value.user1; + } else { + return null; + } +}); + +const isMyTurn = computed(() => { + if (!iAmPlayer.value) return false; + const u = turnUser.value; + if (u == null) return false; + return u.id === $i.id; +}); + +const cellsStyle = computed(() => { + return { + 'grid-template-rows': `repeat(${game.value.map.length}, 1fr)`, + 'grid-template-columns': `repeat(${game.value.map[0].length}, 1fr)`, + }; +}); + +watch(logPos, (v) => { + if (!game.value.isEnded) return; + const _o = new Reversi.Game(game.value.map, { + isLlotheo: game.value.isLlotheo, + canPutEverywhere: game.value.canPutEverywhere, + loopedBoard: game.value.loopedBoard, + }); + for (const log of logs.value.slice(0, v)) { + _o.put(log.color, log.pos); + } + engine.value = _o; +}); + +if (game.value.isStarted && !game.value.isEnded) { + useInterval(() => { + if (game.value.isEnded) return; + const crc32 = CRC32.str(logs.value.map(x => x.pos.toString()).join('')); + props.connection.send('syncState', { + crc32: crc32, + }); + }, 5000, { immediate: false, afterMounted: true }); +} + +function putStone(pos) { + if (game.value.isEnded) return; + if (!iAmPlayer.value) return; + if (!isMyTurn.value) return; + if (!engine.value.canPut(myColor.value!, pos)) return; + + engine.value.put(myColor.value!, pos); + triggerRef(engine); + + // サウンドをå†ç”Ÿã™ã‚‹ + //sound.play(myColor.value ? 'reversiPutBlack' : 'reversiPutWhite'); + + props.connection.send('putStone', { + pos: pos, + }); + + checkEnd(); +} + +function onPutStone(x) { + logs.value.push(x); + logPos.value++; + engine.value.put(x.color, x.pos); + triggerRef(engine); + checkEnd(); + + // サウンドをå†ç”Ÿã™ã‚‹ + if (x.color !== myColor.value) { + //sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); + } +} + +function onEnded(x) { + game.value = deepClone(x.game); +} + +function checkEnd() { + game.value.isEnded = engine.value.isEnded; + if (game.value.isEnded) { + if (engine.value.winner === true) { + game.value.winnerId = game.value.black === 1 ? game.value.user1Id : game.value.user2Id; + game.value.winner = game.value.black === 1 ? game.value.user1 : game.value.user2; + } else if (engine.value.winner === false) { + game.value.winnerId = game.value.black === 1 ? game.value.user2Id : game.value.user1Id; + game.value.winner = game.value.black === 1 ? game.value.user2 : game.value.user1; + } else { + game.value.winnerId = null; + game.value.winner = null; + } + } +} + +function onRescue(_game) { + game.value = deepClone(_game); + + engine.value = new Reversi.Game(game.value.map, { + isLlotheo: game.value.isLlotheo, + canPutEverywhere: game.value.canPutEverywhere, + loopedBoard: game.value.loopedBoard, + }); + + for (const log of game.value.logs) { + engine.value.put(log.color, log.pos); + } + + triggerRef(engine); + + logs.value = game.value.logs; + logPos.value = logs.value.length; + + checkEnd(); +} + +function surrender() { + misskeyApi('reversi/surrender', { + gameId: game.value.id, + }); +} + +function autoplay() { + autoplaying.value = true; + logPos.value = 0; + + window.setTimeout(() => { + logPos.value = 1; + + let i = 1; + let previousLog = game.value.logs[0]; + const tick = () => { + const log = game.value.logs[i]; + const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime(); + setTimeout(() => { + i++; + logPos.value++; + previousLog = log; + + if (i < game.value.logs.length) { + tick(); + } else { + autoplaying.value = false; + } + }, time); + }; + + tick(); + }, 1000); +} + +onMounted(() => { + props.connection.on('putStone', onPutStone); + props.connection.on('rescue', onRescue); + props.connection.on('ended', onEnded); +}); + +onUnmounted(() => { + props.connection.off('putStone', onPutStone); + props.connection.off('rescue', onRescue); + props.connection.off('ended', onEnded); +}); +</script> + +<style lang="scss" module> +@use "sass:math"; + +$label-size: 16px; +$gap: 4px; + +.root { + text-align: center; +} + +.board { + width: calc(100% - 16px); + max-width: 500px; + margin: 0 auto; +} + +.labelsX { + height: $label-size; + padding: 0 $label-size; + display: flex; +} + +.labelsXLabel { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8em; + + &:first-child { + margin-left: -(math.div($gap, 2)); + } + + &:last-child { + margin-right: -(math.div($gap, 2)); + } +} + +.labelsY { + width: $label-size; + display: flex; + flex-direction: column; +} + +.labelsYLabel { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + + &:first-child { + margin-top: -(math.div($gap, 2)); + } + + &:last-child { + margin-bottom: -(math.div($gap, 2)); + } +} + +.boardCells { + flex: 1; + display: grid; + grid-gap: $gap; +} + +.boardCell { + background: transparent; + border-radius: 6px; + overflow: clip; + + &.boardCell_empty { + border: solid 2px var(--divider); + } + + &.boardCell_empty.boardCell_can { + border-color: var(--accent); + opacity: 0.5; + } + + &.boardCell_empty.boardCell_myTurn { + border-color: var(--divider); + opacity: 1; + + &.boardCell_can { + border-color: var(--accent); + cursor: pointer; + + &:hover { + background: var(--accent); + } + } + } + + &.boardCell_prev { + box-shadow: 0 0 0 4px var(--accent); + } + + &.boardCell_isEnded { + border-color: var(--divider); + } + + &.boardCell_none { + border-color: transparent !important; + } +} +</style> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <MkSpacer :contentMax="600"> + <div style="text-align: center;"><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></div> + + <div class="_gaps"> + <div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div> + + <div class="_panel"> + <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> + <div>{{ mapName }}</div> + <MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton> + </div> + + <div style="padding: 16px;"> + <div v-if="game.map == null"><i class="ti ti-dice"></i></div> + <div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> + <div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)"> + <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i> + </div> + </div> + </div> + </div> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.blackOrWhite }}</template> + + <MkRadios v-model="game.bw"> + <option value="random">{{ i18n.ts.random }}</option> + <option :value="'1'"> + <I18n :src="i18n.ts._reversi.blackIs" tag="span"> + <template #name> + <b><MkUserName :user="game.user1"/></b> + </template> + </I18n> + </option> + <option :value="'2'"> + <I18n :src="i18n.ts._reversi.blackIs" tag="span"> + <template #name> + <b><MkUserName :user="game.user2"/></b> + </template> + </I18n> + </option> + </MkRadios> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.rules }}</template> + + <div class="_gaps_s"> + <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch> + <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch> + <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch> + </div> + </MkFolder> + </div> + </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> + <div style="text-align: center; margin-bottom: 10px;"> + <template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template> + <template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template> + <template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template> + <template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template> + </div> + <div class="_buttonsCenter"> + <MkButton rounded danger @click="exit">{{ i18n.ts.cancel }}</MkButton> + <MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton> + <MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton> + </div> + </MkSpacer> + </div> + </template> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue'; +import * as Misskey from 'misskey-js'; +import * as Reversi from 'misskey-reversi'; +import { i18n } from '@/i18n.js'; +import { signinRequired } from '@/account.js'; +import { deepClone } from '@/scripts/clone.js'; +import MkButton from '@/components/MkButton.vue'; +import MkRadios from '@/components/MkRadios.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import * as os from '@/os.js'; +import { MenuItem } from '@/types/menu.js'; + +const $i = signinRequired(); + +const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category))); + +const props = defineProps<{ + game: Misskey.entities.ReversiGameDetailed; + connection: Misskey.ChannelConnection; +}>(); + +const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game)); +const isLlotheo = ref<boolean>(false); +const mapName = computed(() => { + if (game.value.map == null) return 'Random'; + const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join('')); + return found ? found.name! : '-Custom-'; +}); +const isReady = computed(() => { + if (game.value.user1Id === $i.id && game.value.user1Ready) return true; + if (game.value.user2Id === $i.id && game.value.user2Ready) return true; + return false; +}); +const isOpReady = computed(() => { + if (game.value.user1Id !== $i.id && game.value.user1Ready) return true; + if (game.value.user2Id !== $i.id && game.value.user2Ready) return true; + return false; +}); + +watch(() => game.value.bw, () => { + updateSettings('bw'); +}); + +function chooseMap(ev: MouseEvent) { + const menu: MenuItem[] = []; + + for (const c of mapCategories) { + const maps = Object.values(Reversi.maps).filter(x => x.category === c); + if (maps.length === 0) continue; + if (c != null) { + menu.push({ + type: 'label', + text: c, + }); + } + for (const m of maps) { + menu.push({ + text: m.name!, + action: () => { + game.value.map = m.data; + updateSettings('map'); + }, + }); + } + } + + os.popupMenu(menu, ev.currentTarget ?? ev.target); +} + +function exit() { + props.connection.send('exit', {}); +} + +function ready() { + props.connection.send('ready', true); +} + +function unready() { + props.connection.send('ready', false); +} + +function onChangeReadyStates(states) { + game.value.user1Ready = states.user1; + game.value.user2Ready = states.user2; +} + +function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) { + props.connection.send('updateSettings', { + key: key, + value: game.value[key], + }); +} + +function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) { + if (userId === $i.id) return; + if (game.value[key] === value) return; + game.value[key] = value; +} + +function onMapCellClick(pos: number, pixel: string) { + const x = pos % game.value.map[0].length; + const y = Math.floor(pos / game.value.map[0].length); + const newPixel = + pixel === ' ' ? '-' : + pixel === '-' ? 'b' : + pixel === 'b' ? 'w' : + ' '; + const line = game.value.map[y].split(''); + line[x] = newPixel; + game.value.map[y] = line.join(''); + updateSettings('map'); +} + +props.connection.on('changeReadyStates', onChangeReadyStates); +props.connection.on('updateSettings', onUpdateSettings); + +onUnmounted(() => { + props.connection.off('changeReadyStates', onChangeReadyStates); + props.connection.off('updateSettings', onUpdateSettings); +}); +</script> + +<style lang="scss" module> +.board { + display: grid; + grid-gap: 4px; + width: 300px; + height: 300px; + margin: 0 auto; + color: var(--fg); +} + +.boardCell { + display: grid; + place-items: center; + background: transparent; + border: solid 2px var(--divider); + border-radius: 6px; + overflow: clip; + cursor: pointer; +} +.boardCellNone { + border-color: transparent; +} + +.footer { + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + background: var(--acrylicBg); + border-top: solid 0.5px var(--divider); +} +</style> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div v-if="game == null || connection == null"><MkLoading/></div> +<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection"/> +<GameBoard v-else :game="game" :connection="connection"/> +</template> + +<script lang="ts" setup> +import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue'; +import * as Misskey from 'misskey-js'; +import GameSetting from './game.setting.vue'; +import GameBoard from './game.board.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useStream } from '@/stream.js'; + +const props = defineProps<{ + gameId: string; +}>(); + +const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null); +const connection = shallowRef<Misskey.ChannelConnection | null>(null); + +watch(() => props.gameId, () => { + fetchGame(); +}); + +async function fetchGame() { + const _game = await misskeyApi('reversi/show-game', { + gameId: props.gameId, + }); + + game.value = _game; + + if (connection.value) { + connection.value.dispose(); + } + connection.value = useStream().useChannel('reversiGame', { + gameId: game.value.id, + }); + connection.value.on('started', x => { + game.value = x.game; + }); +} + +onMounted(() => { + fetchGame(); +}); + +onUnmounted(() => { + if (connection.value) { + connection.value.dispose(); + } +}); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata(computed(() => ({ + title: 'Reversi', + icon: 'ti ti-device-gamepad', +}))); +</script> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkSpacer v-if="!matchingAny && !matchingUser" :contentMax="600"> + <div class="_gaps"> + <div> + <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> + </div> + + <div class="_buttonsCenter"> + <MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton> + <MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton> + </div> + + <MkFolder v-if="invitations.length > 0" :defaultOpen="true"> + <template #label>{{ i18n.ts.invitations }}</template> + <div class="_gaps_s"> + <button v-for="user in invitations" :key="user.id" v-panel :class="$style.invitation" class="_button" tabindex="-1" @click="accept(user)"> + <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="user" :showIndicator="true"/> + <span style="margin-right: 8px;"><b><MkUserName :user="user"/></b></span> + <span>@{{ user.username }}</span> + </button> + </div> + </MkFolder> + + <MkFolder v-if="$i" :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.myGames }}</template> + <MkPagination :pagination="myGamesPagination"> + <template #default="{ items }"> + <div :class="$style.gamePreviews"> + <MkA v-for="g in items" :key="g.id" v-panel :class="$style.gamePreview" tabindex="-1" :to="`/reversi/g/${g.id}`"> + <div :class="$style.gamePreviewPlayers"> + <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> + </div> + <div :class="$style.gamePreviewFooter"> + <span :style="!g.isEnded ? 'color: var(--accent);' : ''">{{ g.isEnded ? i18n.ts._reversi.ended : i18n.ts._reversi.playing }}</span> + <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/> + </div> + </MkA> + </div> + </template> + </MkPagination> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.allGames }}</template> + <MkPagination :pagination="gamesPagination"> + <template #default="{ items }"> + <div :class="$style.gamePreviews"> + <MkA v-for="g in items" :key="g.id" v-panel :class="$style.gamePreview" tabindex="-1" :to="`/reversi/g/${g.id}`"> + <div :class="$style.gamePreviewPlayers"> + <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> + </div> + <div :class="$style.gamePreviewFooter"> + <span :style="!g.isEnded ? 'color: var(--accent);' : ''">{{ g.isEnded ? i18n.ts._reversi.ended : i18n.ts._reversi.playing }}</span> + <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/> + </div> + </MkA> + </div> + </template> + </MkPagination> + </MkFolder> + </div> +</MkSpacer> +<MkSpacer v-else :contentMax="600"> + <div :class="$style.waitingScreen"> + <div v-if="matchingUser" :class="$style.waitingScreenTitle"> + <I18n :src="i18n.ts.waitingFor" tag="span"> + <template #x> + <b><MkUserName :user="matchingUser"/></b> + </template> + </I18n> + <MkEllipsis/> + </div> + <div v-else :class="$style.waitingScreenTitle"> + {{ i18n.ts._reversi.lookingForPlayer }}<MkEllipsis/> + </div> + <div class="cancel"> + <MkButton inline rounded @click="cancelMatching">{{ i18n.ts.cancel }}</MkButton> + </div> + </div> +</MkSpacer> +</template> + +<script lang="ts" setup> +import { computed, onMounted, onUnmounted, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useStream } from '@/stream.js'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; +import MkPagination from '@/components/MkPagination.vue'; +import { useRouter } from '@/global/router/supplier.js'; +import * as os from '@/os.js'; +import { useInterval } from '@/scripts/use-interval.js'; + +const myGamesPagination = { + endpoint: 'reversi/games' as const, + limit: 10, + params: { + my: true, + }, +}; + +const gamesPagination = { + endpoint: 'reversi/games' as const, + limit: 10, +}; + +const router = useRouter(); + +if ($i) { + const connection = useStream().useChannel('reversi'); + + connection.on('matched', x => { + startGame(x.game); + }); + + connection.on('invited', invitation => { + if (invitations.value.some(x => x.id === invitation.user.id)) return; + invitations.value.unshift(invitation.user); + }); + + onUnmounted(() => { + connection.dispose(); + }); +} + +const invitations = ref<Misskey.entities.UserLite[]>([]); +const matchingUser = ref<Misskey.entities.UserLite | null>(null); +const matchingAny = ref<boolean>(false); + +function startGame(game: Misskey.entities.ReversiGameDetailed) { + matchingUser.value = null; + matchingAny.value = false; + router.push(`/reversi/g/${game.id}`); +} + +async function matchHeatbeat() { + if (matchingUser.value) { + const res = await misskeyApi('reversi/match', { + userId: matchingUser.value.id, + }); + + if (res != null) { + startGame(res); + } + } else if (matchingAny.value) { + const res = await misskeyApi('reversi/match', { + userId: null, + }); + + if (res != null) { + startGame(res); + } + } +} + +async function matchUser() { + const user = await os.selectUser({ local: true }); + if (user == null) return; + + matchingUser.value = user; + + matchHeatbeat(); +} + +async function matchAny() { + matchingAny.value = true; + + matchHeatbeat(); +} + +function cancelMatching() { + if (matchingUser.value) { + misskeyApi('reversi/cancel-match', { userId: matchingUser.value.id }); + matchingUser.value = null; + } else if (matchingAny.value) { + misskeyApi('reversi/cancel-match', { userId: null }); + matchingAny.value = false; + } +} + +async function accept(user) { + const game = await misskeyApi('reversi/match', { + userId: user.id, + }); + if (game) { + startGame(game); + } +} + +useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true }); + +onMounted(() => { + misskeyApi('reversi/invitations').then(_invitations => { + invitations.value = _invitations; + }); +}); + +definePageMetadata(computed(() => ({ + title: 'Reversi', + icon: 'ti ti-device-gamepad', +}))); +</script> + +<style lang="scss" module> +.invitation { + display: flex; + box-sizing: border-box; + width: 100%; + padding: 16px; + line-height: 32px; + text-align: left; +} + +.gamePreviews { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + grid-gap: var(--margin); +} + +.gamePreview { + font-size: 90%; + border-radius: 8px; + overflow: clip; +} + +.gamePreviewPlayers { + text-align: center; + padding: 16px; + line-height: 32px; +} + +.gamePreviewPlayersAvatar { + width: 32px; + height: 32px; + + &:first-child { + margin-right: 8px; + } + + &:last-child { + margin-left: 8px; + } +} + +.gamePreviewFooter { + display: flex; + align-items: baseline; + border-top: solid 0.5px var(--divider); + padding: 6px 10px; + font-size: 0.9em; +} + +.waitingScreen { + text-align: center; +} + +.waitingScreenTitle { + font-size: 1.5em; + margin-bottom: 16px; + margin-top: 32px; +} +</style> 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<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'reversi/cancel-match', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'reversi/games', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'reversi/match', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request<E extends 'reversi/invitations', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'reversi/show-game', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'reversi/surrender', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; } } 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<string, never>; @@ -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<string, never> | null; + form2: Record<string, never> | 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<string, never> | null; + form2: Record<string, never> | 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' -- GitLab