enhance(reversi): tweak reversi

This commit is contained in:
syuilo 2024-01-21 12:05:51 +09:00
parent 4de77784c9
commit 6039f27bd5
7 changed files with 149 additions and 78 deletions

4
locales/index.d.ts vendored
View file

@ -9553,6 +9553,10 @@ export interface Locale extends ILocale {
* *
*/ */
"lookingForPlayer": string; "lookingForPlayer": string;
/**
*
*/
"gameCanceled": string;
}; };
"_offlineScreen": { "_offlineScreen": {
/** /**

View file

@ -2544,6 +2544,7 @@ _reversi:
timeLimitForEachTurn: "1ターンの時間制限" timeLimitForEachTurn: "1ターンの時間制限"
freeMatch: "フリーマッチ" freeMatch: "フリーマッチ"
lookingForPlayer: "対戦相手を探しています" lookingForPlayer: "対戦相手を探しています"
gameCanceled: "対局がキャンセルされました"
_offlineScreen: _offlineScreen:
title: "オフライン - サーバーに接続できません" title: "オフライン - サーバーに接続できません"

View file

@ -188,6 +188,9 @@ export interface ReversiGameEventTypes {
winnerId: MiUser['id'] | null; winnerId: MiUser['id'] | null;
game: Packed<'ReversiGameDetailed'>; game: Packed<'ReversiGameDetailed'>;
}; };
canceled: {
userId: MiUser['id'];
};
} }
//#endregion //#endregion

View file

@ -61,6 +61,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game)); await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
} }
@bindThis
private async deleteGameCache(gameId: MiReversiGame['id']) {
await this.redisClient.del(`reversi:game:cache:${gameId}`);
}
@bindThis @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) { if (targetUser.id === me.id) {
@ -239,15 +244,22 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (isBothReady) { if (isBothReady) {
// 3秒後、両者readyならゲーム開始 // 3秒後、両者readyならゲーム開始
setTimeout(async () => { setTimeout(async () => {
const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id }); const freshGame = await this.get(game.id);
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
if (!freshGame.user1Ready || !freshGame.user2Ready) return; if (!freshGame.user1Ready || !freshGame.user2Ready) return;
this.startGame(freshGame);
}, 3000);
}
}
@bindThis
private async startGame(game: MiReversiGame) {
let bw: number; let bw: number;
if (freshGame.bw === 'random') { if (game.bw === 'random') {
bw = Math.random() > 0.5 ? 1 : 2; bw = Math.random() > 0.5 ? 1 : 2;
} else { } else {
bw = parseInt(freshGame.bw, 10); bw = parseInt(game.bw, 10);
} }
function getRandomMap() { function getRandomMap() {
@ -256,9 +268,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
return Object.values(Reversi.maps)[rnd].data; return Object.values(Reversi.maps)[rnd].data;
} }
const map = freshGame.map != null ? freshGame.map : getRandomMap(); const map = game.map != null ? game.map : getRandomMap();
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString(); const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({ .set({
@ -276,17 +288,17 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const engine = new Reversi.Game(map, { const engine = new Reversi.Game(map, {
isLlotheo: freshGame.isLlotheo, isLlotheo: game.isLlotheo,
canPutEverywhere: freshGame.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: freshGame.loopedBoard, loopedBoard: game.loopedBoard,
}); });
if (engine.isEnded) { if (engine.isEnded) {
let winner; let winner;
if (engine.winner === true) { if (engine.winner === true) {
winner = bw === 1 ? freshGame.user1Id : freshGame.user2Id; winner = bw === 1 ? game.user1Id : game.user2Id;
} else if (engine.winner === false) { } else if (engine.winner === false) {
winner = bw === 1 ? freshGame.user2Id : freshGame.user1Id; winner = bw === 1 ? game.user2Id : game.user1Id;
} else { } else {
winner = null; winner = null;
} }
@ -317,8 +329,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishReversiGameStream(game.id, 'started', { this.globalEventService.publishReversiGameStream(game.id, 'started', {
game: await this.reversiGameEntityService.packDetail(game.id), game: await this.reversiGameEntityService.packDetail(game.id),
}); });
}, 3000);
}
} }
@bindThis @bindThis
@ -510,6 +520,21 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
} }
@bindThis
public async cancelGame(gameId: MiReversiGame['id'], user: MiUser) {
const game = await this.get(gameId);
if (game == null) throw new Error('game not found');
if (game.isStarted) return;
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
await this.reversiGamesRepository.delete(game.id);
this.deleteGameCache(game.id);
this.globalEventService.publishReversiGameStream(game.id, 'canceled', {
userId: user.id,
});
}
@bindThis @bindThis
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> { public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
const cached = await this.redisClient.get(`reversi:game:cache:${id}`); const cached = await this.redisClient.get(`reversi:game:cache:${id}`);

View file

@ -40,6 +40,7 @@ class ReversiGameChannel extends Channel {
switch (type) { switch (type) {
case 'ready': this.ready(body); break; case 'ready': this.ready(body); break;
case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'cancel': this.cancelGame(); break;
case 'putStone': this.putStone(body.pos, body.id); break; case 'putStone': this.putStone(body.pos, body.id); break;
case 'checkState': this.checkState(body.crc32); break; case 'checkState': this.checkState(body.crc32); break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
@ -60,6 +61,13 @@ class ReversiGameChannel extends Channel {
this.reversiService.gameReady(this.gameId!, this.user, ready); this.reversiService.gameReady(this.gameId!, this.user, ready);
} }
@bindThis
private async cancelGame() {
if (this.user == null) return;
this.reversiService.cancelGame(this.gameId!, this.user);
}
@bindThis @bindThis
private async putStone(pos: number, id: string) { private async putStone(pos: number, id: string) {
if (this.user == null) return; if (this.user == null) return;

View file

@ -86,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template> <template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
</div> </div>
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton rounded danger @click="exit">{{ i18n.ts.cancel }}</MkButton> <MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton> <MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton> <MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
</div> </div>
@ -109,9 +109,12 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { useRouter } from '@/global/router/supplier.js';
const $i = signinRequired(); const $i = signinRequired();
const router = useRouter();
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category))); const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
const props = defineProps<{ const props = defineProps<{
@ -171,8 +174,16 @@ function chooseMap(ev: MouseEvent) {
os.popupMenu(menu, ev.currentTarget ?? ev.target); os.popupMenu(menu, ev.currentTarget ?? ev.target);
} }
function exit() { async function cancel() {
props.connection.send('exit', {}); const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.areYouSure,
});
if (canceled) return;
props.connection.send('cancel', {});
router.push('/reversi');
} }
function ready() { function ready() {

View file

@ -17,6 +17,14 @@ import GameBoard from './game.board.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { signinRequired } from '@/account.js';
import { useRouter } from '@/global/router/supplier.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
const $i = signinRequired();
const router = useRouter();
const props = defineProps<{ const props = defineProps<{
gameId: string; gameId: string;
@ -45,6 +53,17 @@ async function fetchGame() {
connection.value.on('started', x => { connection.value.on('started', x => {
game.value = x.game; game.value = x.game;
}); });
connection.value.on('canceled', x => {
connection.value?.dispose();
if (x.userId !== $i.id) {
os.alert({
type: 'warning',
text: i18n.ts._reversi.gameCanceled,
});
router.push('/reversi');
}
});
} }
onMounted(() => { onMounted(() => {