summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2024-01-20 21:23:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2024-01-20 21:23:33 +0900
commitfcd7ffe95639ac74117e438ba68c5e3df7560dcb (patch)
treeefdcfe4387f6fd50d6ac5400a0835cbd2429acfc /packages
parentUpdate Dockerfile (diff)
downloadmisskey-fcd7ffe95639ac74117e438ba68c5e3df7560dcb.tar.gz
misskey-fcd7ffe95639ac74117e438ba68c5e3df7560dcb.tar.bz2
misskey-fcd7ffe95639ac74117e438ba68c5e3df7560dcb.zip
enhance(reversi): tweak reversi
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/core/GlobalEventService.ts4
-rw-r--r--packages/backend/src/core/ReversiService.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts14
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue47
-rw-r--r--packages/frontend/src/pages/reversi/index.vue10
-rw-r--r--packages/frontend/src/scripts/use-interval.ts12
6 files changed, 79 insertions, 13 deletions
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 896149f238..e599912e2b 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -181,8 +181,8 @@ export interface ReversiGameEventTypes {
value: any;
};
log: Reversi.Serializer.Log & { id: string | null };
- syncState: {
- crc32: string;
+ heatbeat: {
+ userId: MiUser['id'];
};
started: {
game: Packed<'ReversiGameDetailed'>;
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 9fe7255e48..e626cbaf19 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -406,6 +406,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
+ public async heatbeat(game: MiReversiGame, user: MiUser) {
+ this.globalEventService.publishReversiGameStream(game.id, 'heatbeat', { userId: user.id });
+ }
+
+ @bindThis
public dispose(): void {
}
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 2d8c396db9..c5d05e5cfb 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -46,7 +46,7 @@ class ReversiGameChannel extends Channel {
case 'ready': this.ready(body); break;
case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'putStone': this.putStone(body.pos, body.id); break;
- case 'syncState': this.syncState(body.crc32); break;
+ case 'heatbeat': this.heatbeat(body.crc32); break;
}
}
@@ -83,15 +83,21 @@ class ReversiGameChannel extends Channel {
}
@bindThis
- private async syncState(crc32: string | number) {
+ private async heatbeat(crc32?: string | number | null) {
// TODO: キャッシュしたい
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
if (game == null) throw new Error('game not found');
if (!game.isStarted) return;
- if (crc32.toString() !== game.crc32) {
- this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user));
+ if (crc32 != null) {
+ if (crc32.toString() !== game.crc32) {
+ this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user));
+ }
+ }
+
+ if (this.user && (game.user1Id === this.user.id || game.user2Id === this.user.id)) {
+ this.reversiService.heatbeat(game, this.user);
}
}
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 3a24777db8..2f09cf39e8 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="(logPos !== game.logs.length) && turnUser" class="turn">
<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
</div>
- <div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div>
+ <div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/><soan v-if="opponentNotResponding" style="margin-left: 8px;">({{ i18n.ts.notResponding }})</soan></div>
<div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div>
<div v-if="game.isEnded && logPos == game.logs.length" class="result">
<template v-if="game.winner">
@@ -139,7 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
+import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
import * as CRC32 from 'crc-32';
import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi';
@@ -239,7 +239,7 @@ if (game.value.isStarted && !game.value.isEnded) {
if (game.value.isEnded) return;
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
if (_DEV_) console.log('crc32', crc32);
- props.connection.send('syncState', {
+ props.connection.send('heatbeat', {
crc32: crc32,
});
}, 10000, { immediate: false, afterMounted: true });
@@ -339,6 +339,27 @@ function onStreamRescue(_game) {
checkEnd();
}
+const opponentLastHeatbeatedAt = ref<number>(Date.now());
+const opponentNotResponding = ref<boolean>(false);
+
+useInterval(() => {
+ if (game.value.isEnded) return;
+ if (!iAmPlayer.value) return;
+
+ if (Date.now() - opponentLastHeatbeatedAt.value > 20000) {
+ opponentNotResponding.value = true;
+ } else {
+ opponentNotResponding.value = false;
+ }
+}, 1000, { immediate: false, afterMounted: true });
+
+function onStreamHeatbeat({ userId }) {
+ if ($i.id === userId) return;
+
+ opponentNotResponding.value = false;
+ opponentLastHeatbeatedAt.value = Date.now();
+}
+
async function surrender() {
const { canceled } = await os.confirm({
type: 'warning',
@@ -390,12 +411,28 @@ function share() {
onMounted(() => {
props.connection.on('log', onStreamLog);
+ props.connection.on('heatbeat', onStreamHeatbeat);
props.connection.on('rescue', onStreamRescue);
props.connection.on('ended', onStreamEnded);
});
+onActivated(() => {
+ props.connection.on('log', onStreamLog);
+ props.connection.on('heatbeat', onStreamHeatbeat);
+ props.connection.on('rescue', onStreamRescue);
+ props.connection.on('ended', onStreamEnded);
+});
+
+onDeactivated(() => {
+ props.connection.off('log', onStreamLog);
+ props.connection.off('heatbeat', onStreamHeatbeat);
+ props.connection.off('rescue', onStreamRescue);
+ props.connection.off('ended', onStreamEnded);
+});
+
onUnmounted(() => {
props.connection.off('log', onStreamLog);
+ props.connection.off('heatbeat', onStreamHeatbeat);
props.connection.off('rescue', onStreamRescue);
props.connection.off('ended', onStreamEnded);
});
@@ -483,7 +520,7 @@ $gap: 4px;
.boardCell {
background: transparent;
- border-radius: 6px;
+ border-radius: 100%;
aspect-ratio: 1;
transform-style: preserve-3d;
perspective: 150px;
@@ -534,6 +571,6 @@ $gap: 4px;
display: block;
width: 100%;
height: 100%;
- border-radius: 6px;
+ border-radius: 100%;
}
</style>
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 5fbbbef2c5..796e732208 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref } from 'vue';
+import { computed, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -214,6 +214,14 @@ onMounted(() => {
});
});
+onDeactivated(() => {
+ cancelMatching();
+});
+
+onUnmounted(() => {
+ cancelMatching();
+});
+
definePageMetadata(computed(() => ({
title: 'Reversi',
icon: 'ti ti-device-gamepad',
diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts
index b8c5431fb6..d8ffb2205b 100644
--- a/packages/frontend/src/scripts/use-interval.ts
+++ b/packages/frontend/src/scripts/use-interval.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { onMounted, onUnmounted } from 'vue';
+import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
export function useInterval(fn: () => void, interval: number, options: {
immediate: boolean;
@@ -28,6 +28,16 @@ export function useInterval(fn: () => void, interval: number, options: {
intervalId = null;
};
+ onActivated(() => {
+ if (intervalId) return;
+ if (options.immediate) fn();
+ intervalId = window.setInterval(fn, interval);
+ });
+
+ onDeactivated(() => {
+ clear();
+ });
+
onUnmounted(() => {
clear();
});