mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	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…
	
	Add table
		
		Reference in a new issue