diff --git a/locales/index.d.ts b/locales/index.d.ts index 63d13f61db473ed0d0998ec87133127218510694..966c2224fb642d85bc0cf9432f12034e54fdb7bb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9595,6 +9595,14 @@ export interface Locale extends ILocale { * 相手ãŒè¨å®šã‚’変更ã—ã¾ã—㟠*/ "opponentHasSettingsChanged": string; + /** + * å¤‰å‰‡è¨±å¯ (完全フリー) + */ + "allowIrregularRules": string; + /** + * 変則ãªã— + */ + "disallowIrregularRules": string; }; "_offlineScreen": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9f17de423272b8a7153d8c018c7b4f9e76d5331d..b9e7ea492267bbbb1ffe105edb1abcda52e4c7d4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2556,6 +2556,8 @@ _reversi: shareToTlTheGameWhenStart: "開始時ã«å¯¾å±€ã‚’タイムラインã«æŠ•ç¨¿" iStartedAGame: "対局を開始ã—ã¾ã—ãŸï¼ #MisskeyReversi" opponentHasSettingsChanged: "相手ãŒè¨å®šã‚’変更ã—ã¾ã—ãŸ" + allowIrregularRules: "å¤‰å‰‡è¨±å¯ (完全フリー)" + disallowIrregularRules: "変則ãªã—" _offlineScreen: title: "オフライン - サーãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã›ã‚“" diff --git a/packages/backend/migration/1706081514499-reversi-6.js b/packages/backend/migration/1706081514499-reversi-6.js new file mode 100644 index 0000000000000000000000000000000000000000..de870be446963c07ab45bb76d7a5d830983a52c0 --- /dev/null +++ b/packages/backend/migration/1706081514499-reversi-6.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Reversi61706081514499 { + name = 'Reversi61706081514499' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`); + } +} diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index f74416a58a8e95dc656a61fbf97e793937b5c26f..84721b2217bbef8cc35bbd13f11a11ce7399088f 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -85,6 +85,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: game.map, bw: game.bw, crc32: game.crc32, + noIrregularRules: game.noIrregularRules, } satisfies Partial<MiReversiGame>; } @@ -138,7 +139,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async matchAnyUser(me: MiUser, multiple = false): Promise<MiReversiGame | null> { + public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> { if (!multiple) { // æ—¢ã«ãƒžãƒƒãƒã—ã¦ã„る対局ãŒç„¡ã„ã‹æŽ¢ã™(3分以内) const games = await this.reversiGamesRepository.find({ @@ -177,19 +178,29 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { 2, // 自分自身ã®IDãŒå…¥ã£ã¦ã„ã‚‹å ´åˆã‚‚ã‚ã‚‹ã®ã§2ã¤å–å¾— 'REV'); - const userIds = matchings.filter(id => id !== me.id); + const items = matchings.filter(id => !id.startsWith(me.id)); - if (userIds.length > 0) { - const matchedUserId = userIds[0]; + if (items.length > 0) { + const [matchedUserId, option] = items[0].split(':'); - await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); + await this.redisClient.zrem('reversi:matchAny', + me.id, + matchedUserId, + me.id + ':noIrregularRules', + matchedUserId + ':noIrregularRules'); - const game = await this.matched(matchedUserId, me.id); + const game = await this.matched(matchedUserId, me.id, { + noIrregularRules: options.noIrregularRules || option === 'noIrregularRules', + }); return game; } else { const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + if (options.noIrregularRules) { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules'); + } else { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + } redisPipeline.expire('reversi:matchAny', 15, 'NX'); await redisPipeline.exec(); return null; @@ -203,7 +214,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis public async matchAnyUserCancel(user: MiUser) { - await this.redisClient.zrem('reversi:matchAny', user.id); + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zrem('reversi:matchAny', user.id); + redisPipeline.zrem('reversi:matchAny', user.id + ':noIrregularRules'); + await redisPipeline.exec(); } @bindThis @@ -265,7 +279,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> { + private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> { const game = await this.reversiGamesRepository.insert({ id: this.idService.gen(), user1Id: parentId, @@ -278,6 +292,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: Reversi.maps.eighteight.data, bw: 'random', isLlotheo: false, + noIrregularRules: options.noIrregularRules, }).then(x => this.reversiGamesRepository.findOneOrFail({ where: { id: x.identifiers[0].id }, relations: ['user1', 'user2'], diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index 6c89a705991ef40fa43c0bba7b84e746a22c2fe3..1a689a7b5395a63a7ca11184fccfca89821f2650 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -61,6 +61,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, logs: game.logs, map: game.map, }); @@ -105,6 +106,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, }); } diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts index 11d236e4588a8358af9eb478a44578b8c24d9704..c03335dd63056831d11613d18cdea898771bb622 100644 --- a/packages/backend/src/models/ReversiGame.ts +++ b/packages/backend/src/models/ReversiGame.ts @@ -106,6 +106,11 @@ export class MiReversiGame { }) public bw: string; + @Column('boolean', { + default: false, + }) + public noIrregularRules: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index f8a5e7451c01c8f7dda7044dfb8d201c21e706ef..ff4c78eeb000ea7b4ab2433b0f540927e5cb05c2 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, @@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 62682cfb50c088ec70c7d604b0ee01a9f25bdd43..f8dee21c4ce34892f634ef5e441be44a29d0a6ee 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -37,6 +37,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id', nullable: true }, + noIrregularRules: { type: 'boolean', default: false }, multiple: { type: 'boolean', default: false }, }, required: [], @@ -57,7 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw err; }) : null; - const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple); + const game = target + ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) + : await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple); if (game == null) return; diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index b1e66e4fdd4521ffd602277c5a4633819890c08b..0fbabfe4defb2ff70eda5846caefb710e3c5ca86 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -12,69 +12,74 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps" :class="{ [$style.disallowInner]: isReady }"> <div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div> - <div class="_panel"> - <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> - <div>{{ mapName }}</div> - <MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton> - </div> + <template v-if="game.noIrregularRules"> + <div>{{ i18n.ts._reversi.disallowIrregularRules }}</div> + </template> + <template v-else> + <div class="_panel"> + <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> + <div>{{ mapName }}</div> + <MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton> + </div> - <div style="padding: 16px;"> - <div v-if="game.map == null"><i class="ti ti-dice"></i></div> - <div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)"> - <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i> + <div style="padding: 16px;"> + <div v-if="game.map == null"><i class="ti ti-dice"></i></div> + <div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> + <div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)"> + <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i> + </div> </div> </div> </div> - </div> - <MkFolder :defaultOpen="true"> - <template #label>{{ i18n.ts._reversi.blackOrWhite }}</template> - - <MkRadios v-model="game.bw"> - <option value="random">{{ i18n.ts.random }}</option> - <option :value="'1'"> - <I18n :src="i18n.ts._reversi.blackIs" tag="span"> - <template #name> - <b><MkUserName :user="game.user1"/></b> - </template> - </I18n> - </option> - <option :value="'2'"> - <I18n :src="i18n.ts._reversi.blackIs" tag="span"> - <template #name> - <b><MkUserName :user="game.user2"/></b> - </template> - </I18n> - </option> - </MkRadios> - </MkFolder> - - <MkFolder :defaultOpen="true"> - <template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template> - <template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template> - - <MkRadios v-model="game.timeLimitForEachTurn"> - <option :value="5">5{{ i18n.ts._time.second }}</option> - <option :value="10">10{{ i18n.ts._time.second }}</option> - <option :value="30">30{{ i18n.ts._time.second }}</option> - <option :value="60">60{{ i18n.ts._time.second }}</option> - <option :value="90">90{{ i18n.ts._time.second }}</option> - <option :value="120">120{{ i18n.ts._time.second }}</option> - <option :value="180">180{{ i18n.ts._time.second }}</option> - <option :value="3600">3600{{ i18n.ts._time.second }}</option> - </MkRadios> - </MkFolder> - - <MkFolder :defaultOpen="true"> - <template #label>{{ i18n.ts._reversi.rules }}</template> - - <div class="_gaps_s"> - <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch> - <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch> - <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch> - </div> - </MkFolder> + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.blackOrWhite }}</template> + + <MkRadios v-model="game.bw"> + <option value="random">{{ i18n.ts.random }}</option> + <option :value="'1'"> + <I18n :src="i18n.ts._reversi.blackIs" tag="span"> + <template #name> + <b><MkUserName :user="game.user1"/></b> + </template> + </I18n> + </option> + <option :value="'2'"> + <I18n :src="i18n.ts._reversi.blackIs" tag="span"> + <template #name> + <b><MkUserName :user="game.user2"/></b> + </template> + </I18n> + </option> + </MkRadios> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template> + <template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template> + + <MkRadios v-model="game.timeLimitForEachTurn"> + <option :value="5">5{{ i18n.ts._time.second }}</option> + <option :value="10">10{{ i18n.ts._time.second }}</option> + <option :value="30">30{{ i18n.ts._time.second }}</option> + <option :value="60">60{{ i18n.ts._time.second }}</option> + <option :value="90">90{{ i18n.ts._time.second }}</option> + <option :value="120">120{{ i18n.ts._time.second }}</option> + <option :value="180">180{{ i18n.ts._time.second }}</option> + <option :value="3600">3600{{ i18n.ts._time.second }}</option> + </MkRadios> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #label>{{ i18n.ts._reversi.rules }}</template> + + <div class="_gaps_s"> + <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch> + <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch> + <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch> + </div> + </MkFolder> + </template> </div> </div> </MkSpacer> diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 7c687e813c10860bb30999120d1a3c18d97a1fdb..8deaead698bb825ea1e09aa0c2212f98280f5050 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -157,6 +157,7 @@ if ($i) { const invitations = ref<Misskey.entities.UserLite[]>([]); const matchingUser = ref<Misskey.entities.UserLite | null>(null); const matchingAny = ref<boolean>(false); +const noIrregularRules = ref<boolean>(false); function startGame(game: Misskey.entities.ReversiGameDetailed) { matchingUser.value = null; @@ -182,6 +183,7 @@ async function matchHeatbeat() { } else if (matchingAny.value) { const res = await misskeyApi('reversi/match', { userId: null, + noIrregularRules: noIrregularRules.value, }); if (res != null) { @@ -199,10 +201,22 @@ async function matchUser() { matchHeatbeat(); } -async function matchAny() { - matchingAny.value = true; - - matchHeatbeat(); +function matchAny(ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts._reversi.allowIrregularRules, + action: () => { + noIrregularRules.value = false; + matchingAny.value = true; + matchHeatbeat(); + }, + }, { + text: i18n.ts._reversi.disallowIrregularRules, + action: () => { + noIrregularRules.value = true; + matchingAny.value = true; + matchHeatbeat(); + }, + }], ev.currentTarget ?? ev.target); } function cancelMatching() { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 4fa42bb91956e26e5faa746c40eed88243645d0b..c97b95e536c182650341bcac819de0750236a60f 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.901Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.455Z */ import type { SwitchCaseResponseType } from '../api.js'; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 6b330c7c116d038c976e595cbc016045f96aae42..e356de345314729a6882a729a4d47cb2bf1bf205 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.899Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.453Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 8eb284ae9ec24ee91b03038d5943f315e3a93711..bfe40dc947faf66e568c5f62b0b86ff864bbee09 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.897Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.452Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 0a6f534aff3403b2766fd4716492a67b126e413b..b7dcbfd951cbc2fb095cdba032c63884c591aaa1 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.896Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.450Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 44421240d0dacb5ee9380dd2433139cab970234d..b5eca12a19d26bd73f23ad1b3e4c90075db4230f 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2024.2.0-beta.4 - * generatedAt: 2024-01-24T01:14:40.815Z + * version: 2024.2.0-beta.6 + * generatedAt: 2024-01-24T07:32:10.370Z */ /** @@ -4493,6 +4493,7 @@ export type components = { timeoutUserId: string | null; black: number | null; bw: string; + noIrregularRules: boolean; isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; @@ -4528,6 +4529,7 @@ export type components = { timeoutUserId: string | null; black: number | null; bw: string; + noIrregularRules: boolean; isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; @@ -25800,6 +25802,8 @@ export type operations = { /** Format: misskey:id */ userId?: string | null; /** @default false */ + noIrregularRules?: boolean; + /** @default false */ multiple?: boolean; }; };