enhance(reversi): tweak reversi
This commit is contained in:
parent
7d57487026
commit
fcd7ffe956
6 changed files with 79 additions and 13 deletions
|
@ -181,8 +181,8 @@ export interface ReversiGameEventTypes {
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
log: Reversi.Serializer.Log & { id: string | null };
|
log: Reversi.Serializer.Log & { id: string | null };
|
||||||
syncState: {
|
heatbeat: {
|
||||||
crc32: string;
|
userId: MiUser['id'];
|
||||||
};
|
};
|
||||||
started: {
|
started: {
|
||||||
game: Packed<'ReversiGameDetailed'>;
|
game: Packed<'ReversiGameDetailed'>;
|
||||||
|
|
|
@ -405,6 +405,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
return this.reversiGamesRepository.findOneBy({ id });
|
return this.reversiGamesRepository.findOneBy({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async heatbeat(game: MiReversiGame, user: MiUser) {
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'heatbeat', { userId: user.id });
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ class ReversiGameChannel extends Channel {
|
||||||
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 'putStone': this.putStone(body.pos, body.id); 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,18 +83,24 @@ class ReversiGameChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async syncState(crc32: string | number) {
|
private async heatbeat(crc32?: string | number | null) {
|
||||||
// TODO: キャッシュしたい
|
// TODO: キャッシュしたい
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
||||||
if (game == null) throw new Error('game not found');
|
if (game == null) throw new Error('game not found');
|
||||||
|
|
||||||
if (!game.isStarted) return;
|
if (!game.isStarted) return;
|
||||||
|
|
||||||
|
if (crc32 != null) {
|
||||||
if (crc32.toString() !== game.crc32) {
|
if (crc32.toString() !== game.crc32) {
|
||||||
this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose() {
|
public dispose() {
|
||||||
// Unsubscribe events
|
// Unsubscribe events
|
||||||
|
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="(logPos !== game.logs.length) && turnUser" class="turn">
|
<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"/>
|
<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
||||||
</div>
|
</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="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">
|
<div v-if="game.isEnded && logPos == game.logs.length" class="result">
|
||||||
<template v-if="game.winner">
|
<template v-if="game.winner">
|
||||||
|
@ -139,7 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 CRC32 from 'crc-32';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import * as Reversi from 'misskey-reversi';
|
import * as Reversi from 'misskey-reversi';
|
||||||
|
@ -239,7 +239,7 @@ if (game.value.isStarted && !game.value.isEnded) {
|
||||||
if (game.value.isEnded) return;
|
if (game.value.isEnded) return;
|
||||||
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||||
if (_DEV_) console.log('crc32', crc32);
|
if (_DEV_) console.log('crc32', crc32);
|
||||||
props.connection.send('syncState', {
|
props.connection.send('heatbeat', {
|
||||||
crc32: crc32,
|
crc32: crc32,
|
||||||
});
|
});
|
||||||
}, 10000, { immediate: false, afterMounted: true });
|
}, 10000, { immediate: false, afterMounted: true });
|
||||||
|
@ -339,6 +339,27 @@ function onStreamRescue(_game) {
|
||||||
checkEnd();
|
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() {
|
async function surrender() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -390,12 +411,28 @@ function share() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.connection.on('log', onStreamLog);
|
props.connection.on('log', onStreamLog);
|
||||||
|
props.connection.on('heatbeat', onStreamHeatbeat);
|
||||||
props.connection.on('rescue', onStreamRescue);
|
props.connection.on('rescue', onStreamRescue);
|
||||||
props.connection.on('ended', onStreamEnded);
|
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(() => {
|
onUnmounted(() => {
|
||||||
props.connection.off('log', onStreamLog);
|
props.connection.off('log', onStreamLog);
|
||||||
|
props.connection.off('heatbeat', onStreamHeatbeat);
|
||||||
props.connection.off('rescue', onStreamRescue);
|
props.connection.off('rescue', onStreamRescue);
|
||||||
props.connection.off('ended', onStreamEnded);
|
props.connection.off('ended', onStreamEnded);
|
||||||
});
|
});
|
||||||
|
@ -483,7 +520,7 @@ $gap: 4px;
|
||||||
|
|
||||||
.boardCell {
|
.boardCell {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
transform-style: preserve-3d;
|
transform-style: preserve-3d;
|
||||||
perspective: 150px;
|
perspective: 150px;
|
||||||
|
@ -534,6 +571,6 @@ $gap: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 * as Misskey from 'misskey-js';
|
||||||
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';
|
||||||
|
@ -214,6 +214,14 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
cancelMatching();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cancelMatching();
|
||||||
|
});
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: 'Reversi',
|
title: 'Reversi',
|
||||||
icon: 'ti ti-device-gamepad',
|
icon: 'ti ti-device-gamepad',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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: {
|
export function useInterval(fn: () => void, interval: number, options: {
|
||||||
immediate: boolean;
|
immediate: boolean;
|
||||||
|
@ -28,6 +28,16 @@ export function useInterval(fn: () => void, interval: number, options: {
|
||||||
intervalId = null;
|
intervalId = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (intervalId) return;
|
||||||
|
if (options.immediate) fn();
|
||||||
|
intervalId = window.setInterval(fn, interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue