diff --git a/package.json b/package.json index 840fe5a73b..e3ce2f4d2d 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "compression": "1.7.2", "cookie": "0.3.1", "cors": "2.8.4", + "crc-32": "^1.2.0", "css-loader": "0.28.10", "debug": "3.1.0", "deep-equal": "1.0.1", diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts index ab90cffa44..788fb5cba6 100644 --- a/src/api/models/othello-game.ts +++ b/src/api/models/othello-game.ts @@ -35,6 +35,9 @@ export interface IGame { }; form1: any; form2: any; + + // ログのposを文字列としてすべて連結したもののCRC32値 + crc32: string; } /** diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts index 888c599338..e2ecce38ea 100644 --- a/src/api/stream/othello-game.ts +++ b/src/api/stream/othello-game.ts @@ -1,5 +1,6 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; +import * as CRC32 from 'crc-32'; import Game, { pack } from '../models/othello-game'; import { publishOthelloGameStream } from '../event'; import Othello from '../../common/othello/core'; @@ -50,6 +51,11 @@ export default function(request: websocket.request, connection: websocket.connec if (msg.pos == null) return; set(msg.pos); break; + + case 'check': + if (msg.crc32 == null) return; + check(msg.crc32); + break; } }); @@ -231,11 +237,12 @@ export default function(request: websocket.request, connection: websocket.connec } //#endregion - publishOthelloGameStream(gameId, 'started', await pack(gameId)); + publishOthelloGameStream(gameId, 'started', await pack(gameId, user)); }, 3000); } } + // 石を打つ async function set(pos) { const game = await Game.findOne({ _id: gameId }); @@ -278,10 +285,13 @@ export default function(request: websocket.request, connection: websocket.connec pos }; + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + await Game.update({ _id: gameId }, { $set: { + crc32, is_ended: o.isEnded, winner_id: winner }, @@ -300,4 +310,20 @@ export default function(request: websocket.request, connection: websocket.connec }); } } + + async function check(crc32) { + const game = await Game.findOne({ _id: gameId }); + + if (!game.is_started) return; + + // 互換性のため + if (game.crc32 == null) return; + + if (crc32 !== game.crc32) { + connection.send(JSON.stringify({ + type: 'rescue', + body: await pack(game, user) + })); + } + } } diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue index 77be458879..01148f193e 100644 --- a/src/web/app/common/views/components/othello.game.vue +++ b/src/web/app/common/views/components/othello.game.vue @@ -37,17 +37,20 @@ <script lang="ts"> import Vue from 'vue'; +import * as CRC32 from 'crc-32'; import Othello, { Color } from '../../../../../common/othello/core'; import { url } from '../../../config'; export default Vue.extend({ - props: ['game', 'connection'], + props: ['initGame', 'connection'], data() { return { + game: null, o: null as Othello, logs: [], - logPos: 0 + logPos: 0, + pollingClock: null }; }, @@ -104,6 +107,8 @@ export default Vue.extend({ }, created() { + this.game = this.initGame; + this.o = new Othello(this.game.settings.map, { isLlotheo: this.game.settings.is_llotheo, canPutEverywhere: this.game.settings.can_put_everywhere, @@ -116,14 +121,29 @@ export default Vue.extend({ this.logs = this.game.logs; this.logPos = this.logs.length; + + // 通信を取りこぼしてもいいように定期的にポーリングさせる + if (this.game.is_started && !this.game.is_ended) { + this.pollingClock = setInterval(() => { + const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); + this.connection.send({ + type: 'check', + crc32 + }); + }, 10000); + } }, mounted() { this.connection.on('set', this.onSet); + this.connection.on('rescue', this.onRescue); }, beforeDestroy() { this.connection.off('set', this.onSet); + this.connection.off('rescue', this.onRescue); + + clearInterval(this.pollingClock); }, methods: { @@ -181,6 +201,27 @@ export default Vue.extend({ this.game.winner = null; } } + }, + + // 正しいゲーム情報が送られてきたとき + onRescue(game) { + this.game = game; + + this.o = new Othello(this.game.settings.map, { + isLlotheo: this.game.settings.is_llotheo, + canPutEverywhere: this.game.settings.can_put_everywhere, + loopedBoard: this.game.settings.looped_board + }); + + this.game.logs.forEach(log => { + this.o.put(log.color, log.pos); + }); + + this.logs = this.game.logs; + this.logPos = this.logs.length; + + this.checkEnd(); + this.$forceUpdate(); } } }); diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/web/app/common/views/components/othello.gameroom.vue index 9f4037515a..9df458f644 100644 --- a/src/web/app/common/views/components/othello.gameroom.vue +++ b/src/web/app/common/views/components/othello.gameroom.vue @@ -1,7 +1,7 @@ <template> <div> <x-room v-if="!g.is_started" :game="g" :connection="connection"/> - <x-game v-else :game="g" :connection="connection"/> + <x-game v-else :init-game="g" :connection="connection"/> </div> </template>