summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--locales/index.d.ts4
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/backend/src/core/GlobalEventService.ts3
-rw-r--r--packages/backend/src/core/ReversiService.ts157
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts8
-rw-r--r--packages/frontend/src/pages/reversi/game.setting.vue17
-rw-r--r--packages/frontend/src/pages/reversi/game.vue19
7 files changed, 140 insertions, 69 deletions
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 910b1edad8..5e00e539f2 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9553,6 +9553,10 @@ export interface Locale extends ILocale {
* 対戦相手を探しています
*/
"lookingForPlayer": string;
+ /**
+ * 対局がキャンセルされました
+ */
+ "gameCanceled": string;
};
"_offlineScreen": {
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6460397db7..915b9a2080 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2544,6 +2544,7 @@ _reversi:
timeLimitForEachTurn: "1ターンの時間制限"
freeMatch: "フリーマッチ"
lookingForPlayer: "対戦相手を探しています"
+ gameCanceled: "対局がキャンセルされました"
_offlineScreen:
title: "オフライン - サーバーに接続できません"
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 5ddd100e6c..5b4c8cb44f 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -188,6 +188,9 @@ export interface ReversiGameEventTypes {
winnerId: MiUser['id'] | null;
game: Packed<'ReversiGameDetailed'>;
};
+ canceled: {
+ userId: MiUser['id'];
+ };
}
//#endregion
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index b2a4032d4b..f97f71eb43 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -62,6 +62,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
+ private async deleteGameCache(gameId: MiReversiGame['id']) {
+ await this.redisClient.del(`reversi:game:cache:${gameId}`);
+ }
+
+ @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) {
throw new Error('You cannot match yourself.');
@@ -239,86 +244,91 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (isBothReady) {
// 3秒後、両者readyならゲーム開始
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.user1Ready || !freshGame.user2Ready) return;
- let bw: number;
- if (freshGame.bw === 'random') {
- bw = Math.random() > 0.5 ? 1 : 2;
- } else {
- bw = parseInt(freshGame.bw, 10);
- }
-
- function getRandomMap() {
- const mapCount = Object.entries(Reversi.maps).length;
- const rnd = Math.floor(Math.random() * mapCount);
- return Object.values(Reversi.maps)[rnd].data;
- }
+ this.startGame(freshGame);
+ }, 3000);
+ }
+ }
- const map = freshGame.map != null ? freshGame.map : getRandomMap();
+ @bindThis
+ private async startGame(game: MiReversiGame) {
+ let bw: number;
+ if (game.bw === 'random') {
+ bw = Math.random() > 0.5 ? 1 : 2;
+ } else {
+ bw = parseInt(game.bw, 10);
+ }
- const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
+ function getRandomMap() {
+ const mapCount = Object.entries(Reversi.maps).length;
+ const rnd = Math.floor(Math.random() * mapCount);
+ return Object.values(Reversi.maps)[rnd].data;
+ }
- const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
- .set({
- startedAt: new Date(),
- isStarted: true,
- black: bw,
- map: map,
- crc32,
- })
- .where('id = :id', { id: game.id })
- .returning('*')
- .execute()
- .then((response) => response.raw[0]);
- this.cacheGame(updatedGame);
+ const map = game.map != null ? game.map : getRandomMap();
- //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
- const engine = new Reversi.Game(map, {
- isLlotheo: freshGame.isLlotheo,
- canPutEverywhere: freshGame.canPutEverywhere,
- loopedBoard: freshGame.loopedBoard,
- });
+ const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
- if (engine.isEnded) {
- let winner;
- if (engine.winner === true) {
- winner = bw === 1 ? freshGame.user1Id : freshGame.user2Id;
- } else if (engine.winner === false) {
- winner = bw === 1 ? freshGame.user2Id : freshGame.user1Id;
- } else {
- winner = null;
- }
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .set({
+ startedAt: new Date(),
+ isStarted: true,
+ black: bw,
+ map: map,
+ crc32,
+ })
+ .where('id = :id', { id: game.id })
+ .returning('*')
+ .execute()
+ .then((response) => response.raw[0]);
+ this.cacheGame(updatedGame);
- const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
- .set({
- isEnded: true,
- endedAt: new Date(),
- winnerId: winner,
- })
- .where('id = :id', { id: game.id })
- .returning('*')
- .execute()
- .then((response) => response.raw[0]);
- this.cacheGame(updatedGame);
+ //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
+ const engine = new Reversi.Game(map, {
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard,
+ });
- this.globalEventService.publishReversiGameStream(game.id, 'ended', {
- winnerId: winner,
- game: await this.reversiGameEntityService.packDetail(game.id),
- });
+ if (engine.isEnded) {
+ let winner;
+ if (engine.winner === true) {
+ winner = bw === 1 ? game.user1Id : game.user2Id;
+ } else if (engine.winner === false) {
+ winner = bw === 1 ? game.user2Id : game.user1Id;
+ } else {
+ winner = null;
+ }
- return;
- }
- //#endregion
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .set({
+ isEnded: true,
+ endedAt: new Date(),
+ winnerId: winner,
+ })
+ .where('id = :id', { id: game.id })
+ .returning('*')
+ .execute()
+ .then((response) => response.raw[0]);
+ this.cacheGame(updatedGame);
- this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
+ this.globalEventService.publishReversiGameStream(game.id, 'ended', {
+ winnerId: winner,
+ game: await this.reversiGameEntityService.packDetail(game.id),
+ });
- this.globalEventService.publishReversiGameStream(game.id, 'started', {
- game: await this.reversiGameEntityService.packDetail(game.id),
- });
- }, 3000);
+ return;
}
+ //#endregion
+
+ this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
+
+ this.globalEventService.publishReversiGameStream(game.id, 'started', {
+ game: await this.reversiGameEntityService.packDetail(game.id),
+ });
}
@bindThis
@@ -511,6 +521,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
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
if (cached != null) {
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 77eaa6d1d3..df92137f51 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -40,6 +40,7 @@ class ReversiGameChannel extends Channel {
switch (type) {
case 'ready': this.ready(body); 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 'checkState': this.checkState(body.crc32); break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
@@ -61,6 +62,13 @@ class ReversiGameChannel extends Channel {
}
@bindThis
+ private async cancelGame() {
+ if (this.user == null) return;
+
+ this.reversiService.cancelGame(this.gameId!, this.user);
+ }
+
+ @bindThis
private async putStone(pos: number, id: string) {
if (this.user == null) return;
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 360b75745c..9ca107278b 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -86,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
</div>
<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 @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
</div>
@@ -109,9 +109,12 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import { MenuItem } from '@/types/menu.js';
+import { useRouter } from '@/global/router/supplier.js';
const $i = signinRequired();
+const router = useRouter();
+
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
const props = defineProps<{
@@ -171,8 +174,16 @@ function chooseMap(ev: MouseEvent) {
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
-function exit() {
- props.connection.send('exit', {});
+async function cancel() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ });
+ if (canceled) return;
+
+ props.connection.send('cancel', {});
+
+ router.push('/reversi');
}
function ready() {
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index dbbeb20f42..0bdbfbcf54 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -17,6 +17,14 @@ import GameBoard from './game.board.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.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<{
gameId: string;
@@ -45,6 +53,17 @@ async function fetchGame() {
connection.value.on('started', x => {
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(() => {