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 df92137f51..820c80006b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -42,7 +42,7 @@ class ReversiGameChannel extends Channel { 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 'resync': this.resync(body.crc32); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @@ -76,12 +76,10 @@ class ReversiGameChannel extends Channel { } @bindThis - private async checkState(crc32: string | number) { - if (crc32 != null) return; - + private async resync(crc32: string | number) { const game = await this.reversiService.checkCrc(this.gameId!, crc32); if (game) { - this.send('rescue', game); + this.send('resynced', game); } } diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 4d4450ed7d..d492296c16 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -163,7 +163,7 @@ const $i = signinRequired(); const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; - connection: Misskey.ChannelConnection; + connection?: Misskey.ChannelConnection | null; }>(); const showBoardLabels = ref(false); @@ -240,10 +240,10 @@ watch(logPos, (v) => { if (game.value.isStarted && !game.value.isEnded) { useInterval(() => { - if (game.value.isEnded) return; + if (game.value.isEnded || props.connection == null) return; const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); if (_DEV_) console.log('crc32', crc32); - props.connection.send('checkState', { + props.connection.send('resync', { crc32: crc32, }); }, 10000, { immediate: false, afterMounted: true }); @@ -267,7 +267,7 @@ function putStone(pos) { }); const id = Math.random().toString(36).slice(2); - props.connection.send('putStone', { + props.connection!.send('putStone', { pos: pos, id, }); @@ -283,22 +283,24 @@ const myTurnTimerRmain = ref(game.value.timeLimitForEachTurn); const opTurnTimerRmain = ref(game.value.timeLimitForEachTurn); const TIMER_INTERVAL_SEC = 3; -useInterval(() => { - if (myTurnTimerRmain.value > 0) { - myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC); - } - if (opTurnTimerRmain.value > 0) { - opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC); - } - - if (iAmPlayer.value) { - if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { - props.connection.send('claimTimeIsUp', {}); +if (!props.game.isEnded) { + useInterval(() => { + if (myTurnTimerRmain.value > 0) { + myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC); + } + if (opTurnTimerRmain.value > 0) { + opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC); } - } -}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); -function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { + if (iAmPlayer.value) { + if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { + props.connection!.send('claimTimeIsUp', {}); + } + } + }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); +} + +async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { game.value.logs = Reversi.Serializer.serializeLogs([ ...Reversi.Serializer.deserializeLogs(game.value.logs), log, @@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { if (log.id == null || !appliedOps.includes(log.id)) { switch (log.operation) { case 'put': { + sound.playUrl('/client-assets/reversi/put.mp3', { + volume: 1, + playbackRate: 1, + }); + + if (log.player !== engine.value.turn) { // = desyncが発生している + const _game = await misskeyApi('reversi/show-game', { + gameId: props.game.id, + }); + restoreGame(_game); + return; + } + engine.value.putStone(log.pos); triggerRef(engine); myTurnTimerRmain.value = game.value.timeLimitForEachTurn; opTurnTimerRmain.value = game.value.timeLimitForEachTurn; - sound.playUrl('/client-assets/reversi/put.mp3', { - volume: 1, - playbackRate: 1, - }); - checkEnd(); break; } @@ -366,9 +376,7 @@ function checkEnd() { } } -function onStreamRescue(_game) { - console.log('rescue'); - +function restoreGame(_game) { game.value = deepClone(_game); engine.value = Reversi.Serializer.restoreGame({ @@ -384,6 +392,12 @@ function onStreamRescue(_game) { checkEnd(); } +function onStreamResynced(_game) { + console.log('resynced'); + + restoreGame(_game); +} + async function surrender() { const { canceled } = await os.confirm({ type: 'warning', @@ -434,27 +448,35 @@ function share() { } onMounted(() => { - props.connection.on('log', onStreamLog); - props.connection.on('rescue', onStreamRescue); - props.connection.on('ended', onStreamEnded); + if (props.connection != null) { + props.connection.on('log', onStreamLog); + props.connection.on('resynced', onStreamResynced); + props.connection.on('ended', onStreamEnded); + } }); onActivated(() => { - props.connection.on('log', onStreamLog); - props.connection.on('rescue', onStreamRescue); - props.connection.on('ended', onStreamEnded); + if (props.connection != null) { + props.connection.on('log', onStreamLog); + props.connection.on('resynced', onStreamResynced); + props.connection.on('ended', onStreamEnded); + } }); onDeactivated(() => { - props.connection.off('log', onStreamLog); - props.connection.off('rescue', onStreamRescue); - props.connection.off('ended', onStreamEnded); + if (props.connection != null) { + props.connection.off('log', onStreamLog); + props.connection.off('resynced', onStreamResynced); + props.connection.off('ended', onStreamEnded); + } }); onUnmounted(() => { - props.connection.off('log', onStreamLog); - props.connection.off('rescue', onStreamRescue); - props.connection.off('ended', onStreamEnded); + if (props.connection != null) { + props.connection.off('log', onStreamLog); + props.connection.off('resynced', onStreamResynced); + props.connection.off('ended', onStreamEnded); + } }); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 0bdbfbcf54..d1e4103919 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -47,23 +47,25 @@ async function fetchGame() { if (connection.value) { connection.value.dispose(); } - connection.value = useStream().useChannel('reversiGame', { - gameId: game.value.id, - }); - connection.value.on('started', x => { - game.value = x.game; - }); - connection.value.on('canceled', x => { - connection.value?.dispose(); + if (!game.value.isEnded) { + connection.value = useStream().useChannel('reversiGame', { + gameId: game.value.id, + }); + 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'); - } - }); + if (x.userId !== $i.id) { + os.alert({ + type: 'warning', + text: i18n.ts._reversi.gameCanceled, + }); + router.push('/reversi'); + } + }); + } } onMounted(() => {