enhance(reversi): 変則なしマッチングを可能に

This commit is contained in:
syuilo 2024-01-24 16:37:06 +09:00
parent 2b6bf074c6
commit 5719a929ad
15 changed files with 159 additions and 77 deletions

8
locales/index.d.ts vendored
View file

@ -9595,6 +9595,14 @@ export interface Locale extends ILocale {
* *
*/ */
"opponentHasSettingsChanged": string; "opponentHasSettingsChanged": string;
/**
* ()
*/
"allowIrregularRules": string;
/**
*
*/
"disallowIrregularRules": string;
}; };
"_offlineScreen": { "_offlineScreen": {
/** /**

View file

@ -2556,6 +2556,8 @@ _reversi:
shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿" shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿"
iStartedAGame: "対局を開始しました! #MisskeyReversi" iStartedAGame: "対局を開始しました! #MisskeyReversi"
opponentHasSettingsChanged: "相手が設定を変更しました" opponentHasSettingsChanged: "相手が設定を変更しました"
allowIrregularRules: "変則許可 (完全フリー)"
disallowIrregularRules: "変則なし"
_offlineScreen: _offlineScreen:
title: "オフライン - サーバーに接続できません" title: "オフライン - サーバーに接続できません"

View file

@ -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"`);
}
}

View file

@ -85,6 +85,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: game.map, map: game.map,
bw: game.bw, bw: game.bw,
crc32: game.crc32, crc32: game.crc32,
noIrregularRules: game.noIrregularRules,
} satisfies Partial<MiReversiGame>; } satisfies Partial<MiReversiGame>;
} }
@ -138,7 +139,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @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) { if (!multiple) {
// 既にマッチしている対局が無いか探す(3分以内) // 既にマッチしている対局が無いか探す(3分以内)
const games = await this.reversiGamesRepository.find({ const games = await this.reversiGamesRepository.find({
@ -177,19 +178,29 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
2, // 自分自身のIDが入っている場合もあるので2つ取得 2, // 自分自身のIDが入っている場合もあるので2つ取得
'REV'); 'REV');
const userIds = matchings.filter(id => id !== me.id); const items = matchings.filter(id => !id.startsWith(me.id));
if (userIds.length > 0) { if (items.length > 0) {
const matchedUserId = userIds[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; return game;
} else { } else {
const redisPipeline = this.redisClient.pipeline(); 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'); redisPipeline.expire('reversi:matchAny', 15, 'NX');
await redisPipeline.exec(); await redisPipeline.exec();
return null; return null;
@ -203,7 +214,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis @bindThis
public async matchAnyUserCancel(user: MiUser) { 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 @bindThis
@ -265,7 +279,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @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({ const game = await this.reversiGamesRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
user1Id: parentId, user1Id: parentId,
@ -278,6 +292,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: Reversi.maps.eighteight.data, map: Reversi.maps.eighteight.data,
bw: 'random', bw: 'random',
isLlotheo: false, isLlotheo: false,
noIrregularRules: options.noIrregularRules,
}).then(x => this.reversiGamesRepository.findOneOrFail({ }).then(x => this.reversiGamesRepository.findOneOrFail({
where: { id: x.identifiers[0].id }, where: { id: x.identifiers[0].id },
relations: ['user1', 'user2'], relations: ['user1', 'user2'],

View file

@ -61,6 +61,7 @@ export class ReversiGameEntityService {
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn, timeLimitForEachTurn: game.timeLimitForEachTurn,
noIrregularRules: game.noIrregularRules,
logs: game.logs, logs: game.logs,
map: game.map, map: game.map,
}); });
@ -105,6 +106,7 @@ export class ReversiGameEntityService {
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn, timeLimitForEachTurn: game.timeLimitForEachTurn,
noIrregularRules: game.noIrregularRules,
}); });
} }

View file

@ -106,6 +106,11 @@ export class MiReversiGame {
}) })
public bw: string; public bw: string;
@Column('boolean', {
default: false,
})
public noIrregularRules: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
noIrregularRules: {
type: 'boolean',
optional: false, nullable: false,
},
isLlotheo: { isLlotheo: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
noIrregularRules: {
type: 'boolean',
optional: false, nullable: false,
},
isLlotheo: { isLlotheo: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -37,6 +37,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id', nullable: true }, userId: { type: 'string', format: 'misskey:id', nullable: true },
noIrregularRules: { type: 'boolean', default: false },
multiple: { type: 'boolean', default: false }, multiple: { type: 'boolean', default: false },
}, },
required: [], required: [],
@ -57,7 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err; throw err;
}) : null; }) : 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; if (game == null) return;

View file

@ -12,69 +12,74 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" :class="{ [$style.disallowInner]: isReady }"> <div class="_gaps" :class="{ [$style.disallowInner]: isReady }">
<div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div> <div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
<div class="_panel"> <template v-if="game.noIrregularRules">
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> <div>{{ i18n.ts._reversi.disallowIrregularRules }}</div>
<div>{{ mapName }}</div> </template>
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton> <template v-else>
</div> <div class="_panel">
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
<div>{{ mapName }}</div>
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
</div>
<div style="padding: 16px;"> <div style="padding: 16px;">
<div v-if="game.map == null"><i class="ti ti-dice"></i></div> <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-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)"> <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> <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>
</div> </div>
</div>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template> <template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
<MkRadios v-model="game.bw"> <MkRadios v-model="game.bw">
<option value="random">{{ i18n.ts.random }}</option> <option value="random">{{ i18n.ts.random }}</option>
<option :value="'1'"> <option :value="'1'">
<I18n :src="i18n.ts._reversi.blackIs" tag="span"> <I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name> <template #name>
<b><MkUserName :user="game.user1"/></b> <b><MkUserName :user="game.user1"/></b>
</template> </template>
</I18n> </I18n>
</option> </option>
<option :value="'2'"> <option :value="'2'">
<I18n :src="i18n.ts._reversi.blackIs" tag="span"> <I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name> <template #name>
<b><MkUserName :user="game.user2"/></b> <b><MkUserName :user="game.user2"/></b>
</template> </template>
</I18n> </I18n>
</option> </option>
</MkRadios> </MkRadios>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template> <template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template> <template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
<MkRadios v-model="game.timeLimitForEachTurn"> <MkRadios v-model="game.timeLimitForEachTurn">
<option :value="5">5{{ i18n.ts._time.second }}</option> <option :value="5">5{{ i18n.ts._time.second }}</option>
<option :value="10">10{{ 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="30">30{{ i18n.ts._time.second }}</option>
<option :value="60">60{{ 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="90">90{{ i18n.ts._time.second }}</option>
<option :value="120">120{{ 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="180">180{{ i18n.ts._time.second }}</option>
<option :value="3600">3600{{ i18n.ts._time.second }}</option> <option :value="3600">3600{{ i18n.ts._time.second }}</option>
</MkRadios> </MkRadios>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.rules }}</template> <template #label>{{ i18n.ts._reversi.rules }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch> <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.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch> <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
</template>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>

View file

@ -157,6 +157,7 @@ if ($i) {
const invitations = ref<Misskey.entities.UserLite[]>([]); const invitations = ref<Misskey.entities.UserLite[]>([]);
const matchingUser = ref<Misskey.entities.UserLite | null>(null); const matchingUser = ref<Misskey.entities.UserLite | null>(null);
const matchingAny = ref<boolean>(false); const matchingAny = ref<boolean>(false);
const noIrregularRules = ref<boolean>(false);
function startGame(game: Misskey.entities.ReversiGameDetailed) { function startGame(game: Misskey.entities.ReversiGameDetailed) {
matchingUser.value = null; matchingUser.value = null;
@ -182,6 +183,7 @@ async function matchHeatbeat() {
} else if (matchingAny.value) { } else if (matchingAny.value) {
const res = await misskeyApi('reversi/match', { const res = await misskeyApi('reversi/match', {
userId: null, userId: null,
noIrregularRules: noIrregularRules.value,
}); });
if (res != null) { if (res != null) {
@ -199,10 +201,22 @@ async function matchUser() {
matchHeatbeat(); matchHeatbeat();
} }
async function matchAny() { function matchAny(ev: MouseEvent) {
matchingAny.value = true; os.popupMenu([{
text: i18n.ts._reversi.allowIrregularRules,
matchHeatbeat(); 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() { function cancelMatching() {

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.4 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-24T01:14:40.901Z * generatedAt: 2024-01-24T07:32:10.455Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.4 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-24T01:14:40.899Z * generatedAt: 2024-01-24T07:32:10.453Z
*/ */
import type { import type {

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.4 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-24T01:14:40.897Z * generatedAt: 2024-01-24T07:32:10.452Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.4 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-24T01:14:40.896Z * generatedAt: 2024-01-24T07:32:10.450Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -2,8 +2,8 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */
/* /*
* version: 2024.2.0-beta.4 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-24T01:14:40.815Z * generatedAt: 2024-01-24T07:32:10.370Z
*/ */
/** /**
@ -4493,6 +4493,7 @@ export type components = {
timeoutUserId: string | null; timeoutUserId: string | null;
black: number | null; black: number | null;
bw: string; bw: string;
noIrregularRules: boolean;
isLlotheo: boolean; isLlotheo: boolean;
canPutEverywhere: boolean; canPutEverywhere: boolean;
loopedBoard: boolean; loopedBoard: boolean;
@ -4528,6 +4529,7 @@ export type components = {
timeoutUserId: string | null; timeoutUserId: string | null;
black: number | null; black: number | null;
bw: string; bw: string;
noIrregularRules: boolean;
isLlotheo: boolean; isLlotheo: boolean;
canPutEverywhere: boolean; canPutEverywhere: boolean;
loopedBoard: boolean; loopedBoard: boolean;
@ -25800,6 +25802,8 @@ export type operations = {
/** Format: misskey:id */ /** Format: misskey:id */
userId?: string | null; userId?: string | null;
/** @default false */ /** @default false */
noIrregularRules?: boolean;
/** @default false */
multiple?: boolean; multiple?: boolean;
}; };
}; };