enhance(reversi): 変則なしマッチングを可能に
This commit is contained in:
parent
2b6bf074c6
commit
5719a929ad
15 changed files with 159 additions and 77 deletions
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -9595,6 +9595,14 @@ export interface Locale extends ILocale {
|
|||
* 相手が設定を変更しました
|
||||
*/
|
||||
"opponentHasSettingsChanged": string;
|
||||
/**
|
||||
* 変則許可 (完全フリー)
|
||||
*/
|
||||
"allowIrregularRules": string;
|
||||
/**
|
||||
* 変則なし
|
||||
*/
|
||||
"disallowIrregularRules": string;
|
||||
};
|
||||
"_offlineScreen": {
|
||||
/**
|
||||
|
|
|
@ -2556,6 +2556,8 @@ _reversi:
|
|||
shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿"
|
||||
iStartedAGame: "対局を開始しました! #MisskeyReversi"
|
||||
opponentHasSettingsChanged: "相手が設定を変更しました"
|
||||
allowIrregularRules: "変則許可 (完全フリー)"
|
||||
disallowIrregularRules: "変則なし"
|
||||
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できません"
|
||||
|
|
16
packages/backend/migration/1706081514499-reversi-6.js
Normal file
16
packages/backend/migration/1706081514499-reversi-6.js
Normal 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"`);
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,11 @@ export class MiReversiGame {
|
|||
})
|
||||
public bw: string;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public noIrregularRules: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
|
||||
<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
|
||||
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<div 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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue