From 2a9db983fcd79e1993d5ea5b03e4979c1a578d7d Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 7 Jan 2024 02:35:58 +0100 Subject: feat: export clips (#12931) * feat: export clips * Update CHANGELOG.md --- packages/backend/src/server/api/EndpointsModule.ts | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 86a64d7121..a3a9805444 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -569,6 +570,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default }; const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; @@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, -- cgit v1.2.3-freya From cf54c2ba4750c307d840016828f837a61f886726 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Jan 2024 18:13:39 +0900 Subject: feat: ranking system of bubble game Resolve #12961 --- locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../migration/1704959805077-bubble-game-record.js | 24 ++++++ packages/backend/src/di-symbols.ts | 1 + packages/backend/src/models/BubbleGameRecord.ts | 57 ++++++++++++++ packages/backend/src/models/RepositoryModule.ts | 10 ++- packages/backend/src/models/_.ts | 3 + packages/backend/src/postgres.ts | 2 + packages/backend/src/server/api/EndpointsModule.ts | 8 ++ packages/backend/src/server/api/endpoints.ts | 4 + .../server/api/endpoints/bubble-game/ranking.ts | 75 +++++++++++++++++++ .../server/api/endpoints/bubble-game/register.ts | 86 ++++++++++++++++++++++ .../frontend/src/pages/drop-and-fusion.game.vue | 25 ++++++- packages/frontend/src/pages/drop-and-fusion.vue | 32 +++++++- .../frontend/src/scripts/drop-and-fusion-engine.ts | 71 +++++++++++++++++- packages/frontend/src/scripts/sound.ts | 1 - 16 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 packages/backend/migration/1704959805077-bubble-game-record.js create mode 100644 packages/backend/src/models/BubbleGameRecord.ts create mode 100644 packages/backend/src/server/api/endpoints/bubble-game/ranking.ts create mode 100644 packages/backend/src/server/api/endpoints/bubble-game/register.ts (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/locales/index.d.ts b/locales/index.d.ts index 852cbdd27d..317a474dba 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1199,6 +1199,7 @@ export interface Locale { "showReplay": string; "replay": string; "replaying": string; + "ranking": string; "_bubbleGame": { "howToPlay": string; "_howToPlay": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f85dc0fcf8..d3c2b4d312 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1196,6 +1196,7 @@ soundWillBePlayed: "サウンドが再生されます" showReplay: "リプレイを見る" replay: "リプレイ" replaying: "リプレイ中" +ranking: "ランキング" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/backend/migration/1704959805077-bubble-game-record.js b/packages/backend/migration/1704959805077-bubble-game-record.js new file mode 100644 index 0000000000..cc45b09c82 --- /dev/null +++ b/packages/backend/migration/1704959805077-bubble-game-record.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class BubbleGameRecord1704959805077 { + name = 'BubbleGameRecord1704959805077' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `); + await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `); + await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`); + await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`); + await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`); + await queryRunner.query(`DROP TABLE "bubble_game_record"`); + } +} diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 8411cb8229..e29fee3f96 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -78,5 +78,6 @@ export const DI = { flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), + bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), //#endregion }; diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts new file mode 100644 index 0000000000..4b483ed4d3 --- /dev/null +++ b/packages/backend/src/models/BubbleGameRecord.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('bubble_game_record') +export class MiBubbleGameRecord { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column('timestamp with time zone') + public seededAt: Date; + + @Column('varchar', { + length: 1024, + }) + public seed: string; + + @Column('integer') + public gameVersion: number; + + @Column('varchar', { + length: 128, + }) + public gameMode: string; + + @Index() + @Column('integer') + public score: number; + + @Column('jsonb', { + default: [], + }) + public logs: any[]; + + @Column('boolean', { + default: false, + }) + public isVerified: boolean; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 866fdfe6d4..0399536c3e 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 } 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 } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -399,6 +399,12 @@ const $userMemosRepository: Provider = { inject: [DI.db], }; +export const $bubbleGameRecordsRepository: Provider = { + provide: DI.bubbleGameRecordsRepository, + useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -468,6 +474,7 @@ const $userMemosRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $bubbleGameRecordsRepository, ], exports: [ $usersRepository, @@ -535,6 +542,7 @@ const $userMemosRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $bubbleGameRecordsRepository, ], }) export class RepositoryModule {} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index d7c327f164..a1c4b0743e 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -68,6 +68,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; 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 type { Repository } from 'typeorm'; export { @@ -136,6 +137,7 @@ export { MiFlash, MiFlashLike, MiUserMemo, + MiBubbleGameRecord, }; export type AbuseUserReportsRepository = Repository; @@ -203,3 +205,4 @@ export type RoleAssignmentsRepository = Repository; export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; export type UserMemoRepository = Repository; +export type BubbleGameRecordsRepository = Repository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index cd611839a4..0430e9ca19 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -76,6 +76,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; 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 { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -190,6 +191,7 @@ export const entities = [ MiFlash, MiFlashLike, MiUserMemo, + MiBubbleGameRecord, ...charts, ]; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index a3a9805444..781332d349 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -364,6 +364,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; 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 { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -726,6 +728,8 @@ const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default }; 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 }; @Module({ imports: [ @@ -1092,6 +1096,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $fetchRss, $fetchExternalResources, $retention, + $bubbleGame_register, + $bubbleGame_ranking, ], exports: [ $admin_meta, @@ -1449,6 +1455,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $fetchRss, $fetchExternalResources, $retention, + $bubbleGame_register, + $bubbleGame_ranking, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index bd8aa4af72..f17db41a5d 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -365,6 +365,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; 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'; const eps = [ ['admin/meta', ep___admin_meta], @@ -725,6 +727,8 @@ const eps = [ ['fetch-rss', ep___fetchRss], ['fetch-external-resources', ep___fetchExternalResources], ['retention', ep___retention], + ['bubble-game/register', ep___bubbleGame_register], + ['bubble-game/ranking', ep___bubbleGame_ranking], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts new file mode 100644 index 0000000000..0cba129a09 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { BubbleGameRecordsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; + +export const meta = { + tags: [], + + allowGet: true, + cacheSec: 60, + + errors: { + }, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { type: 'string', format: 'misskey:id' }, + score: { type: 'integer' }, + user: { ref: 'UserLite' }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameMode: { type: 'string' }, + }, + required: ['gameMode'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.bubbleGameRecordsRepository) + private bubbleGameRecordsRepository: BubbleGameRecordsRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps) => { + const records = await this.bubbleGameRecordsRepository.find({ + where: { + gameMode: ps.gameMode, + seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)), + }, + order: { + score: 'DESC', + }, + take: 10, + relations: ['user'], + }); + + const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false }); + + return records.map(r => ({ + id: r.id, + score: r.score, + user: users.find(u => u.id === r.user!.id), + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/bubble-game/register.ts b/packages/backend/src/server/api/endpoints/bubble-game/register.ts new file mode 100644 index 0000000000..af0f69e4ad --- /dev/null +++ b/packages/backend/src/server/api/endpoints/bubble-game/register.ts @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import type { BubbleGameRecordsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: [], + + requireCredential: true, + + kind: 'write:account', + + limit: { + duration: ms('1hour'), + max: 120, + minInterval: ms('30sec'), + }, + + errors: { + invalidSeed: { + message: 'Provided seed is invalid.', + code: 'INVALID_SEED', + id: 'eb627bc7-574b-4a52-a860-3c3eae772b88', + }, + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + score: { type: 'integer', minimum: 0 }, + seed: { type: 'string', minLength: 1, maxLength: 1024 }, + logs: { type: 'array' }, + gameMode: { type: 'string' }, + gameVersion: { type: 'integer' }, + }, + required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.bubbleGameRecordsRepository) + private bubbleGameRecordsRepository: BubbleGameRecordsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const seedDate = new Date(parseInt(ps.seed, 10)); + const now = new Date(); + + // シードが未来なのは通常のプレイではありえないので弾く + if (seedDate.getTime() > now.getTime()) { + throw new ApiError(meta.errors.invalidSeed); + } + + // シードが古すぎる(1時間以上前)のも弾く + if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) { + throw new ApiError(meta.errors.invalidSeed); + } + + await this.bubbleGameRecordsRepository.insert({ + id: this.idService.gen(now.getTime()), + seed: ps.seed, + seededAt: seedDate, + userId: me.id, + score: ps.score, + logs: ps.logs, + gameMode: ps.gameMode, + gameVersion: ps.gameVersion, + isVerified: false, + }); + }); + } +} diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 3fefb49fae..c222fdeb40 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -679,9 +679,11 @@ function endReplay() { function exportLog() { if (!logs) return; const data = JSON.stringify({ - seed: seed, - date: new Date().toISOString(), - logs: logs, + v: game.GAME_VERSION, + m: props.gameMode, + s: seed, + d: new Date().toISOString(), + l: DropAndFusionGame.serializeLogs(logs), }); copyToClipboard(data); os.success(); @@ -723,8 +725,15 @@ function getGameImageDriveFile() { const [frame, logo] = images; ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, game.GAME_WIDTH, game.GAME_HEIGHT); + ctx.drawImage(frame, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT); ctx.drawImage(canvasEl.value!, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT); + + ctx.fillStyle = '#000'; + ctx.font = '16px bold sans-serif'; + ctx.textBaseline = 'top'; + ctx.fillText(`SCORE: ${score.value.toLocaleString()}`, 10, 10); + ctx.globalAlpha = 0.7; ctx.drawImage(logo, game.GAME_WIDTH * 0.55, 6, game.GAME_WIDTH * 0.45, game.GAME_WIDTH * 0.45 * (logo.height / logo.width)); ctx.globalAlpha = 1; @@ -765,7 +774,7 @@ async function share() { os.post({ initialText: `#BubbleGame MODE: ${props.gameMode} -SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})`, +SCORE: ${score.value.toLocaleString()} (MAX CHAIN: ${maxCombo.value})`, initialFiles: [file], instant: true, }); @@ -859,6 +868,14 @@ function attachGameEvents() { dropReady.value = false; isGameOver.value = true; + misskeyApi('bubble-game/register', { + seed, + score: score.value, + gameMode: props.gameMode, + gameVersion: game.GAME_VERSION, + logs: DropAndFusionGame.serializeLogs(logs), + }); + if (score.value > (highScore.value ?? 0)) { highScore.value = score.value; diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 7bd0eef000..0938ca6a87 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -39,6 +39,21 @@ SPDX-License-Identifier: AGPL-3.0-only +
+
+
+
{{ i18n.ts.ranking }} ({{ gameMode }})
+
+
+ + + {{ r.score.toLocaleString() }} pt +
+
+
{{ i18n.ts.loading }}
+
+
+
{{ i18n.ts._bubbleGame.howToPlay }}
@@ -70,17 +85,23 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue index 5d2482ded1..45a135a459 100644 --- a/packages/frontend/src/pages/games.vue +++ b/packages/frontend/src/pages/games.vue @@ -7,10 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only -
- - - +
+
+ + + +
+
+ + + +
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue new file mode 100644 index 0000000000..18fd74427c --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -0,0 +1,428 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue new file mode 100644 index 0000000000..301a177de1 --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -0,0 +1,236 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue new file mode 100644 index 0000000000..dbbeb20f42 --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue new file mode 100644 index 0000000000..c483e36c24 --- /dev/null +++ b/packages/frontend/src/pages/reversi/index.vue @@ -0,0 +1,271 @@ + + + + + + + diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 98fe0043c1..8cdc7b59c6 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -103,7 +103,7 @@ export function getConfig(): UserConfig { // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies optimizeDeps: { - include: ['misskey-js'], + include: ['misskey-js', 'misskey-reversi'], }, build: { @@ -135,7 +135,7 @@ export function getConfig(): UserConfig { // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies commonjsOptions: { - include: [/misskey-js/, /node_modules/], + include: [/misskey-js/, /misskey-reversi/, /node_modules/], }, }, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f955cc5cc1..2b95e01533 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1623,6 +1623,16 @@ declare namespace entities { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -1659,7 +1669,9 @@ declare namespace entities { Flash, Signin, RoleLite, - Role + Role, + ReversiGameLite, + ReversiGameDetailed } } export { entities } @@ -2596,6 +2608,42 @@ type ResetPasswordRequest = operations['reset-password']['requestBody']['content // @public (undocumented) type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; + +// @public (undocumented) +type ReversiGameLite = components['schemas']['ReversiGameLite']; + +// @public (undocumented) +type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; + // @public (undocumented) type Role = components['schemas']['Role']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index b60f449a71..e4e7d13668 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.782Z + * generatedAt: 2024-01-19T11:00:07.160Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -4007,5 +4007,71 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; } } diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index dc591a7046..671abd78ce 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.778Z + * generatedAt: 2024-01-19T11:00:07.158Z */ import type { @@ -544,6 +544,16 @@ import type { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, } from './entities.js'; export type Endpoints = { @@ -907,4 +917,10 @@ export type Endpoints = { 'retention': { req: EmptyRequest; res: RetentionResponse }; 'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; + 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: ReversiCancelMatchResponse }; + 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse }; + 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse }; + 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; + 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; + 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; } diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index dfe24ce0d8..c14876c0e3 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.775Z + * generatedAt: 2024-01-19T11:00:07.156Z */ import { operations } from './types.js'; @@ -546,3 +546,13 @@ export type BubbleGameRegisterRequest = operations['bubble-game/register']['requ export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; +export type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; +export type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; +export type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; +export type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; +export type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; +export type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; +export type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; +export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; +export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; +export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 5c6bebf2fd..78f14d2250 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.773Z + * generatedAt: 2024-01-19T11:00:07.155Z */ import { components } from './types.js'; @@ -41,3 +41,5 @@ export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; +export type ReversiGameLite = components['schemas']['ReversiGameLite']; +export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 76e2b5309c..36facf6e28 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.633Z + * generatedAt: 2024-01-19T11:00:07.077Z */ /** @@ -3472,6 +3472,60 @@ export type paths = { */ post: operations['bubble-game/ranking']; }; + '/reversi/cancel-match': { + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/cancel-match']; + }; + '/reversi/games': { + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/games']; + }; + '/reversi/match': { + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/match']; + }; + '/reversi/invitations': { + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['reversi/invitations']; + }; + '/reversi/show-game': { + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/show-game']; + }; + '/reversi/surrender': { + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/surrender']; + }; }; export type webhooks = Record; @@ -4404,6 +4458,72 @@ export type components = { }; usersCount: number; }); + ReversiGameLite: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Ready: boolean; + user2Ready: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + }; + ReversiGameDetailed: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Ready: boolean; + user2Ready: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + logs: { + at: number; + color: boolean; + pos: number; + }[]; + map: string[]; + }; }; responses: never; parameters: never; @@ -25542,5 +25662,325 @@ export type operations = { }; }; }; + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/cancel-match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/games': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default false */ + my?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGameLite'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'reversi/invitations': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/show-game': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGameDetailed']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/surrender': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; }; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json new file mode 100644 index 0000000000..8d3ca30166 --- /dev/null +++ b/packages/misskey-reversi/package.json @@ -0,0 +1,26 @@ +{ + "name": "misskey-reversi", + "version": "0.0.1", + "main": "./built/index.js", + "types": "./built/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", + "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@misskey-dev/eslint-plugin": "1.0.0", + "@types/node": "20.11.5", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", + "eslint": "8.56.0", + "typescript": "5.3.3" + }, + "files": [ + "built" + ], + "dependencies": { + } +} diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts new file mode 100644 index 0000000000..55d0b84da7 --- /dev/null +++ b/packages/misskey-reversi/src/game.ts @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * true ... 黒 + * false ... 白 + */ +export type Color = boolean; +const BLACK = true; +const WHITE = false; + +export type MapCell = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; +}; + +export type Undo = { + color: Color; + pos: number; + + /** + * 反転した石の位置の配列 + */ + effects: number[]; + + turn: Color | null; +}; + +export class Game { + public map: MapCell[]; + public mapWidth: number; + public mapHeight: number; + public board: (Color | null | undefined)[]; + public turn: Color | null = BLACK; + public opts: Options; + + public prevPos = -1; + public prevColor: Color | null = null; + + private logs: Undo[] = []; + + constructor(map: string[], opts: Options) { + //#region binds + this.put = this.put.bind(this); + //#endregion + + //#region Options + this.opts = opts; + if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; + if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; + if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + //#endregion + + //#region Parse map data + this.mapWidth = map[0].length; + this.mapHeight = map.length; + const mapData = map.join(''); + + this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined); + + this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null'); + //#endregion + + // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある + if (!this.canPutSomewhere(BLACK)) + this.turn = this.canPutSomewhere(WHITE) ? WHITE : null; + } + + public get blackCount() { + return this.board.filter(x => x === BLACK).length; + } + + public get whiteCount() { + return this.board.filter(x => x === WHITE).length; + } + + public posToXy(pos: number): number[] { + const x = pos % this.mapWidth; + const y = Math.floor(pos / this.mapWidth); + return [x, y]; + } + + public xyToPos(x: number, y: number): number { + return x + (y * this.mapWidth); + } + + public put(color: Color, pos: number) { + this.prevPos = pos; + this.prevColor = color; + + this.board[pos] = color; + + // 反転させられる石を取得 + const effects = this.effects(color, pos); + + // 反転させる + for (const pos of effects) { + this.board[pos] = color; + } + + const turn = this.turn; + + this.logs.push({ + color, + pos, + effects, + turn + }); + + this.calcTurn(); + } + + private calcTurn() { + // ターン計算 + this.turn = + this.canPutSomewhere(!this.prevColor) ? !this.prevColor : + this.canPutSomewhere(this.prevColor!) ? this.prevColor : + null; + } + + public undo() { + const undo = this.logs.pop()!; + this.prevColor = undo.color; + this.prevPos = undo.pos; + this.board[undo.pos] = null; + for (const pos of undo.effects) { + const color = this.board[pos]; + this.board[pos] = !color; + } + this.turn = undo.turn; + } + + public mapDataGet(pos: number): MapCell { + const [x, y] = this.posToXy(pos); + return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos]; + } + + public getPuttablePlaces(color: Color): number[] { + return Array.from(this.board.keys()).filter(i => this.canPut(color, i)); + } + + public canPutSomewhere(color: Color): boolean { + return this.getPuttablePlaces(color).length > 0; + } + + public canPut(color: Color, pos: number): boolean { + return ( + this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない + this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード + this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param initPos 位置 + */ + public effects(color: Color, initPos: number): number[] { + const enemyColor = !color; + + const diffVectors: [number, number][] = [ + [ 0, -1], // 上 + [+1, -1], // 右上 + [+1, 0], // 右 + [+1, +1], // 右下 + [ 0, +1], // 下 + [-1, +1], // 左下 + [-1, 0], // 左 + [-1, -1] // 左上 + ]; + + const effectsInLine = ([dx, dy]: [number, number]): number[] => { + const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy]; + + const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列 + let [x, y] = this.posToXy(initPos); + while (true) { + [x, y] = nextPos(x, y); + + // 座標が指し示す位置がボード外に出たとき + if (this.opts.loopedBoard && this.xyToPos( + (x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), + (y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) + // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) + return found; + else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) + return []; // 挟めないことが確定 (盤面外に到達) + + const pos = this.xyToPos(x, y); + if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) + const stone = this.board[pos]; + if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達) + if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見) + if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見) + } + }; + + return ([] as number[]).concat(...diffVectors.map(effectsInLine)); + } + + public get isEnded(): boolean { + return this.turn === null; + } + + public get winner(): Color | null { + return this.isEnded ? + this.blackCount == this.whiteCount ? null : + this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : + undefined as never; + } +} diff --git a/packages/misskey-reversi/src/index.ts b/packages/misskey-reversi/src/index.ts new file mode 100644 index 0000000000..20ed36f208 --- /dev/null +++ b/packages/misskey-reversi/src/index.ts @@ -0,0 +1,7 @@ +import { Game } from './game.js'; + +export { + Game, +}; + +export * as maps from './maps.js'; diff --git a/packages/misskey-reversi/src/maps.ts b/packages/misskey-reversi/src/maps.ts new file mode 100644 index 0000000000..85cf1a0485 --- /dev/null +++ b/packages/misskey-reversi/src/maps.ts @@ -0,0 +1,715 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + category?: string; + author?: string; + data: string[]; +}; + +export const fourfour: Map = { + name: '4x4', + category: '4x4', + data: [ + '----', + '-wb-', + '-bw-', + '----' + ] +}; + +export const sixsix: Map = { + name: '6x6', + category: '6x6', + data: [ + '------', + '------', + '--wb--', + '--bw--', + '------', + '------' + ] +}; + +export const roundedSixsix: Map = { + name: '6x6 rounded', + category: '6x6', + author: 'syuilo', + data: [ + ' ---- ', + '------', + '--wb--', + '--bw--', + '------', + ' ---- ' + ] +}; + +export const roundedSixsix2: Map = { + name: '6x6 rounded 2', + category: '6x6', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + '--wb--', + '--bw--', + ' ---- ', + ' -- ' + ] +}; + +export const eighteight: Map = { + name: '8x8', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH28: Map = { + name: '8x8 handicap 28', + category: '8x8', + data: [ + 'bbbbbbbb', + 'b------b', + 'b------b', + 'b--wb--b', + 'b--bw--b', + 'b------b', + 'b------b', + 'bbbbbbbb' + ] +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + ' ------ ' + ] +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + category: '8x8', + author: 'syuilo', + data: [ + ' ---- ', + ' ------ ', + '--------', + '---wb---', + '---bw---', + '--------', + ' ------ ', + ' ---- ' + ] +}; + +export const roundedEighteight3: Map = { + name: '8x8 rounded 3', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ---- ', + ' -- ' + ] +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + category: '8x8', + author: 'syuilo', + data: [ + '--- ---', + '--------', + '--------', + ' --wb-- ', + ' --bw-- ', + '--------', + '--------', + '--- ---' + ] +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + category: '8x8', + author: 'syuilo', + data: [ + '--- ----', + '----- --', + '-- -----', + '---wb---', + '---bw- -', + ' -------', + '--- ----', + '--------' + ] +}; + +export const circle: Map = { + name: 'Circle', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ------ ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ------ ', + ' -- ' + ] +}; + +export const smile: Map = { + name: 'Smile', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '-- -- --', + '---wb---', + '-- bw --', + '--- ---', + '--------', + ' ------ ' + ] +}; + +export const window: Map = { + name: 'Window', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '- -- -', + '- -- -', + '---wb---', + '---bw---', + '- -- -', + '- -- -', + '--------' + ] +}; + +export const reserved: Map = { + name: 'Reserved', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------w' + ] +}; + +export const x: Map = { + name: 'X', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '-w----b-', + '--w--b--', + '---wb---', + '---bw---', + '--b--w--', + '-b----w-', + 'b------w' + ] +}; + +export const parallel: Map = { + name: 'Parallel', + category: '8x8', + author: 'Aya', + data: [ + '--------', + '--------', + '--------', + '---bb---', + '---ww---', + '--------', + '--------', + '--------' + ] +}; + +export const lackOfBlack: Map = { + name: 'Lack of Black', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---w----', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const squareParty: Map = { + name: 'Square Party', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '-wwwbbb-', + '-w-wb-b-', + '-wwwbbb-', + '-bbbwww-', + '-b-bw-w-', + '-bbbwww-', + '--------' + ] +}; + +export const minesweeper: Map = { + name: 'Minesweeper', + category: '8x8', + author: 'syuilo', + data: [ + 'b-b--w-w', + '-w-wb-b-', + 'w-b--w-b', + '-b-wb-w-', + '-w-bw-b-', + 'b-w--b-w', + '-b-bw-w-', + 'w-w--b-b' + ] +}; + +export const tenthtenth: Map = { + name: '10x10', + category: '10x10', + data: [ + '----------', + '----------', + '----------', + '----------', + '----wb----', + '----bw----', + '----------', + '----------', + '----------', + '----------' + ] +}; + +export const hole: Map = { + name: 'The Hole', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '----------', + '--wb--wb--', + '--bw--bw--', + '---- ----', + '---- ----', + '--wb--wb--', + '--bw--bw--', + '----------', + '----------' + ] +}; + +export const grid: Map = { + name: 'Grid', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '- - -- - -', + '----------', + '- - -- - -', + '----wb----', + '----bw----', + '- - -- - -', + '----------', + '- - -- - -', + '----------' + ] +}; + +export const cross: Map = { + name: 'Cross', + category: '10x10', + author: 'Aya', + data: [ + ' ---- ', + ' ---- ', + ' ---- ', + '----------', + '----wb----', + '----bw----', + '----------', + ' ---- ', + ' ---- ', + ' ---- ' + ] +}; + +export const charX: Map = { + name: 'Char X', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + '----------', + '---- ----', + '--- ---' + ] +}; + +export const charY: Map = { + name: 'Char Y', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' ------ ', + ' ------ ', + ' ------ ', + ' ------ ' + ] +}; + +export const walls: Map = { + name: 'Walls', + category: '10x10', + author: 'Aya', + data: [ + ' bbbbbbbb ', + 'w--------w', + 'w--------w', + 'w--------w', + 'w---wb---w', + 'w---bw---w', + 'w--------w', + 'w--------w', + 'w--------w', + ' bbbbbbbb ' + ] +}; + +export const cpu: Map = { + name: 'CPU', + category: '10x10', + author: 'syuilo', + data: [ + ' b b b b ', + 'w--------w', + ' -------- ', + 'w--------w', + ' ---wb--- ', + ' ---bw--- ', + 'w--------w', + ' -------- ', + 'w--------w', + ' b b b b ' + ] +}; + +export const checker: Map = { + name: 'Checker', + category: '10x10', + author: 'Aya', + data: [ + '----------', + '----------', + '----------', + '---wbwb---', + '---bwbw---', + '---wbwb---', + '---bwbw---', + '----------', + '----------', + '----------' + ] +}; + +export const japaneseCurry: Map = { + name: 'Japanese curry', + category: '10x10', + author: 'syuilo', + data: [ + 'w-b-b-b-b-', + '-w-b-b-b-b', + 'w-w-b-b-b-', + '-w-w-b-b-b', + 'w-w-wwb-b-', + '-w-wbb-b-b', + 'w-w-w-b-b-', + '-w-w-w-b-b', + 'w-w-w-w-b-', + '-w-w-w-w-b' + ] +}; + +export const mosaic: Map = { + name: 'Mosaic', + category: '10x10', + author: 'syuilo', + data: [ + '- - - - - ', + ' - - - - -', + '- - - - - ', + ' - w w - -', + '- - b b - ', + ' - w w - -', + '- - b b - ', + ' - - - - -', + '- - - - - ', + ' - - - - -', + ] +}; + +export const arena: Map = { + name: 'Arena', + category: '10x10', + author: 'syuilo', + data: [ + '- - -- - -', + ' - - - - ', + '- ------ -', + ' -------- ', + '- --wb-- -', + '- --bw-- -', + ' -------- ', + '- ------ -', + ' - - - - ', + '- - -- - -' + ] +}; + +export const reactor: Map = { + name: 'Reactor', + category: '10x10', + author: 'syuilo', + data: [ + '-w------b-', + 'b- - - -w', + '- --wb-- -', + '---b w---', + '- b wb w -', + '- w bw b -', + '---w b---', + '- --bw-- -', + 'w- - - -b', + '-b------w-' + ] +}; + +export const sixeight: Map = { + name: '6x8', + category: 'Special', + data: [ + '------', + '------', + '------', + '--wb--', + '--bw--', + '------', + '------', + '------' + ] +}; + +export const spark: Map = { + name: 'Spark', + category: 'Special', + author: 'syuilo', + data: [ + ' - - ', + '----------', + ' -------- ', + ' -------- ', + ' ---wb--- ', + ' ---bw--- ', + ' -------- ', + ' -------- ', + '----------', + ' - - ' + ] +}; + +export const islands: Map = { + name: 'Islands', + category: 'Special', + author: 'syuilo', + data: [ + '-------- ', + '---wb--- ', + '---bw--- ', + '-------- ', + ' - - ', + ' - - ', + ' --------', + ' --------', + ' --------', + ' --------' + ] +}; + +export const galaxy: Map = { + name: 'Galaxy', + category: 'Special', + author: 'syuilo', + data: [ + ' ------ ', + ' --www--- ', + ' ------w--- ', + '---bbb--w---', + '--b---b-w-b-', + '-b--wwb-w-b-', + '-b-w-bww--b-', + '-b-w-b---b--', + '---w--bbb---', + ' ---w------ ', + ' ---www-- ', + ' ------ ' + ] +}; + +export const triangle: Map = { + name: 'Triangle', + category: 'Special', + author: 'syuilo', + data: [ + ' -- ', + ' -- ', + ' ---- ', + ' ---- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + ' -------- ', + '----------', + '----------' + ] +}; + +export const iphonex: Map = { + name: 'iPhone X', + category: 'Special', + author: 'syuilo', + data: [ + ' -- -- ', + '--------', + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------', + '--------', + ' ------ ' + ] +}; + +export const dealWithIt: Map = { + name: 'Deal with it!', + category: 'Special', + author: 'syuilo', + data: [ + '------------', + '--w-b-------', + ' --b-w------', + ' --w-b---- ', + ' ------- ' + ] +}; + +export const bigBoard: Map = { + name: 'Big board', + category: 'Special', + data: [ + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '-------wb-------', + '-------bw-------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------' + ] +}; + +export const twoBoard: Map = { + name: 'Two board', + category: 'Special', + author: 'Aya', + data: [ + '-------- --------', + '-------- --------', + '-------- --------', + '---wb--- ---wb---', + '---bw--- ---bw---', + '-------- --------', + '-------- --------', + '-------- --------' + ] +}; diff --git a/packages/misskey-reversi/tsconfig.json b/packages/misskey-reversi/tsconfig.json new file mode 100644 index 0000000000..f56b65e868 --- /dev/null +++ b/packages/misskey-reversi/tsconfig.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./built/", + "removeComments": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 825a7ab860..31394eb081 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: content-disposition: specifier: 0.5.4 version: 0.5.4 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -263,6 +266,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi ms: specifier: 3.0.0-canary.1 version: 3.0.0-canary.1 @@ -736,6 +742,9 @@ importers: compare-versions: specifier: 6.1.0 version: 6.1.0 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 cropperjs: specifier: 2.0.0-beta.4 version: 2.0.0-beta.4 @@ -772,6 +781,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi photoswipe: specifier: 5.4.3 version: 5.4.3 @@ -1114,6 +1126,27 @@ importers: specifier: 5.3.3 version: 5.3.3 + packages/misskey-reversi: + devDependencies: + '@misskey-dev/eslint-plugin': + specifier: 1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + '@types/node': + specifier: 20.11.5 + version: 20.11.5 + '@typescript-eslint/eslint-plugin': + specifier: 6.19.0 + version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: 6.19.0 + version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: + specifier: 8.56.0 + version: 8.56.0 + typescript: + specifier: 5.3.3 + version: 5.3.3 + packages/sw: dependencies: esbuild: @@ -1128,7 +1161,7 @@ importers: devDependencies: '@misskey-dev/eslint-plugin': specifier: ^1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) '@typescript-eslint/parser': specifier: 6.14.0 version: 6.14.0(eslint@8.56.0)(typescript@5.3.3) @@ -1812,7 +1845,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.17 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1835,7 +1868,7 @@ packages: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1937,7 +1970,7 @@ packages: '@babel/core': 7.23.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -3336,7 +3369,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.22.17 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3354,7 +3387,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.23.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4233,7 +4266,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4250,7 +4283,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4515,7 +4548,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4571,7 +4604,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4592,14 +4625,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.5) + jest-config: 29.7.0(@types/node@20.11.5) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4634,7 +4667,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 dev: true @@ -4661,7 +4694,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4694,7 +4727,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4788,7 +4821,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4800,7 +4833,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true @@ -4992,6 +5025,34 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) dev: true + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) + dev: true + + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0) + dev: true + /@misskey-dev/sharp-read-bmp@1.1.1: resolution: {integrity: sha512-X52BQYL/I9mafypQ+wBhst+BUlYiPWnHhKGcF6ybcYSLl+zhcV0q5mezIXHozhM0Sv0A7xCdrWmR7TCNxHLrtQ==} dependencies: @@ -5089,7 +5150,7 @@ packages: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) headers-polyfill: 3.2.5 outvariant: 1.4.0 strict-event-emitter: 0.2.8 @@ -7992,7 +8053,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/responselike': 1.0.0 dev: false @@ -8025,7 +8086,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/content-disposition@0.5.8: @@ -8039,7 +8100,7 @@ packages: /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/debug@4.1.7: @@ -8097,7 +8158,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -8125,13 +8186,13 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/http-cache-semantics@4.0.1: @@ -8212,7 +8273,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/lodash@4.14.191: @@ -8261,7 +8322,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -8279,6 +8340,11 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 + /@types/node@20.9.1: resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} dependencies: @@ -8381,7 +8447,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/rename@1.0.7: @@ -8395,7 +8461,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/sanitize-html@2.9.5: @@ -8421,7 +8487,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/serviceworker@0.0.67: @@ -8431,7 +8497,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/sharp@0.32.0: @@ -8534,7 +8600,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true optional: true @@ -8555,7 +8621,7 @@ packages: '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8584,7 +8650,65 @@ packages: '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8610,7 +8734,7 @@ packages: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8631,7 +8755,28 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8654,6 +8799,14 @@ packages: '@typescript-eslint/visitor-keys': 6.14.0 dev: true + /@typescript-eslint/scope-manager@6.19.0: + resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8666,7 +8819,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8686,7 +8839,27 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.56.0 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8704,6 +8877,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@6.19.0: + resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3): resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8715,7 +8893,7 @@ packages: dependencies: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8736,7 +8914,7 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8746,6 +8924,28 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3): + resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8784,6 +8984,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@6.11.0: resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8800,6 +9019,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.19.0: + resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -9193,7 +9420,7 @@ packages: engines: {node: '>= 6.0.0'} requiresBuild: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -9201,7 +9428,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -9587,7 +9814,7 @@ packages: resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} dependencies: archy: 1.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fastq: 1.15.0 transitivePeerDependencies: - supports-color @@ -11036,7 +11263,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: true /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -11049,6 +11275,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -11265,7 +11492,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -11589,7 +11816,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -11806,6 +12033,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0)(eslint@8.53.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} @@ -11876,6 +12132,41 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-vue@9.19.2(eslint@8.56.0): resolution: {integrity: sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==} engines: {node: ^14.17.0 || >=16.0.0} @@ -11927,7 +12218,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11974,7 +12265,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -12604,7 +12895,7 @@ packages: debug: optional: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -13160,7 +13451,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -13298,7 +13588,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13360,7 +13650,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -13370,7 +13660,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -13379,7 +13669,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13389,7 +13679,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13549,7 +13839,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -13990,7 +14280,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -14044,7 +14334,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -14133,6 +14423,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@20.11.5): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.11 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.5 + babel-jest: 29.7.0(@babel/core@7.22.11) + chalk: 4.1.2 + ci-info: 3.7.1 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@28.1.3: resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -14188,7 +14518,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -14218,7 +14548,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.10.5 + '@types/node': 20.11.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14279,7 +14609,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /jest-mock@29.7.0: @@ -14342,7 +14672,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -14373,7 +14703,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -14425,7 +14755,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -14450,7 +14780,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -14469,7 +14799,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -14667,7 +14997,7 @@ packages: resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) rfdc: 1.3.0 uri-js: 4.4.1 transitivePeerDependencies: @@ -17278,7 +17608,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -18275,7 +18605,7 @@ packages: dependencies: '@hapi/hoek': 10.0.1 '@hapi/wreck': 18.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) joi: 17.7.0 transitivePeerDependencies: - supports-color @@ -18475,7 +18805,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -18628,7 +18958,7 @@ packages: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -18892,7 +19222,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -19515,7 +19844,7 @@ packages: chalk: 4.1.2 cli-highlight: 2.1.11 date-fns: 2.30.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) dotenv: 16.0.3 glob: 8.1.0 ioredis: 5.3.2 @@ -19880,7 +20209,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -19992,7 +20321,7 @@ packages: acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.10 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) happy-dom: 10.0.3 local-pkg: 0.4.3 magic-string: 0.30.3 @@ -20074,7 +20403,7 @@ packages: peerDependencies: eslint: '>=6.0.0' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3b2ecec7fd..3a03a58253 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'packages/sw' - 'packages/misskey-js' - 'packages/misskey-js/generator' + - 'packages/misskey-reversi' -- cgit v1.2.3-freya From e8ba0b3f54c2f25566f467d27c45c66139cbf102 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 23 Jan 2024 10:51:59 +0900 Subject: enhance(reversi): improve desync handling --- packages/backend/package.json | 1 - packages/backend/src/core/ReversiService.ts | 19 +++--- packages/backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/reversi/verify.ts | 64 +++++++++++++++++++ .../src/server/api/stream/channels/reversi-game.ts | 16 +---- packages/frontend/package.json | 1 - packages/frontend/src/pages/reversi/game.board.vue | 25 +++----- packages/misskey-js/etc/misskey-js.api.md | 8 +++ packages/misskey-js/src/autogen/apiClientJSDoc.ts | 15 ++++- packages/misskey-js/src/autogen/endpoint.ts | 7 ++- packages/misskey-js/src/autogen/entities.ts | 6 +- packages/misskey-js/src/autogen/models.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 71 +++++++++++++++++++++- packages/misskey-reversi/package.json | 3 +- packages/misskey-reversi/src/game.ts | 9 +++ pnpm-lock.yaml | 11 ++-- 17 files changed, 206 insertions(+), 60 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/reversi/verify.ts (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/packages/backend/package.json b/packages/backend/package.json index 412c7ab8e4..9551991b34 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -107,7 +107,6 @@ "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.25.2", diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 0d5f989c11..66296f1ed4 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; import { IsNull } from 'typeorm'; @@ -255,7 +254,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw = parseInt(game.bw, 10); } - const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); + const engine = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + const crc32 = engine.calcCrc32().toString(); const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() .set({ @@ -276,12 +281,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const engine = new Reversi.Game(updatedGame.map, { - isLlotheo: updatedGame.isLlotheo, - canPutEverywhere: updatedGame.canPutEverywhere, - loopedBoard: updatedGame.loopedBoard, - }); - if (engine.isEnded) { let winnerId; if (engine.winner === true) { @@ -406,7 +405,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const serializeLogs = Reversi.Serializer.serializeLogs(logs); - const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); + const crc32 = engine.calcCrc32().toString(); const updatedGame = { ...game, @@ -536,7 +535,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (game == null) throw new Error('game not found'); if (crc32.toString() !== game.crc32) { - return await this.reversiGameEntityService.packDetail(game); + return game; } else { return null; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index df69ce2385..e74441834e 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -372,6 +372,7 @@ 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 * as ep___reversi_verify from './endpoints/reversi/verify.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -742,6 +743,7 @@ const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___r 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 }; +const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default }; @Module({ imports: [ @@ -1116,6 +1118,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], exports: [ $admin_meta, @@ -1481,6 +1484,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0f2c8cb754..4a88216d06 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -373,6 +373,7 @@ 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 * as ep___reversi_verify from './endpoints/reversi/verify.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -741,6 +742,7 @@ const eps = [ ['reversi/invitations', ep___reversi_invitations], ['reversi/show-game', ep___reversi_showGame], ['reversi/surrender', ep___reversi_surrender], + ['reversi/verify', ep___reversi_verify], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts new file mode 100644 index 0000000000..5f5af6ce67 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/verify.ts @@ -0,0 +1,64 @@ +/* + * 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 = { + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: '8fb05624-b525-43dd-90f7-511852bdfeee', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + desynced: { type: 'boolean' }, + game: { + type: 'object', + optional: true, nullable: true, + ref: 'ReversiGameDetailed', + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + crc32: { type: 'string' }, + }, + required: ['gameId', 'crc32'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32); + if (game) { + return { + desynced: true, + game: await this.reversiGameEntityService.packDetail(game), + }; + } else { + return { + desynced: false, + }; + } + }); + } +} diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index 820c80006b..fb24a29b75 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; +import type { MiReversiGame } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; @@ -19,7 +19,6 @@ class ReversiGameChannel extends Channel { constructor( private reversiService: ReversiService, - private reversiGamesRepository: ReversiGamesRepository, private reversiGameEntityService: ReversiGameEntityService, id: string, @@ -42,7 +41,6 @@ class ReversiGameChannel extends Channel { case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'cancel': this.cancelGame(); break; case 'putStone': this.putStone(body.pos, body.id); break; - case 'resync': this.resync(body.crc32); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @@ -75,14 +73,6 @@ class ReversiGameChannel extends Channel { this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id); } - @bindThis - private async resync(crc32: string | number) { - const game = await this.reversiService.checkCrc(this.gameId!, crc32); - if (game) { - this.send('resynced', game); - } - } - @bindThis private async claimTimeIsUp() { if (this.user == null) return; @@ -104,9 +94,6 @@ export class ReversiGameChannelService implements MiChannelService { public readonly kind = ReversiGameChannel.kind; constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - private reversiService: ReversiService, private reversiGameEntityService: ReversiGameEntityService, ) { @@ -116,7 +103,6 @@ export class ReversiGameChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): ReversiGameChannel { return new ReversiGameChannel( this.reversiService, - this.reversiGamesRepository, this.reversiGameEntityService, id, connection, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a93679e359..a926daa17d 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -41,7 +41,6 @@ "chartjs-plugin-zoom": "2.0.1", "chromatic": "10.3.1", "compare-versions": "6.1.0", - "crc-32": "^1.2.2", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", "defu": "^6.1.4", diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index d492296c16..8b59df06f7 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -143,7 +143,6 @@ SPDX-License-Identifier: AGPL-3.0-only