mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-08 22:23:08 +02:00
enhance(reversi): tweak reversi
This commit is contained in:
parent
4de77784c9
commit
6039f27bd5
7 changed files with 149 additions and 78 deletions
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -9553,6 +9553,10 @@ export interface Locale extends ILocale {
|
||||||
* 対戦相手を探しています
|
* 対戦相手を探しています
|
||||||
*/
|
*/
|
||||||
"lookingForPlayer": string;
|
"lookingForPlayer": string;
|
||||||
|
/**
|
||||||
|
* 対局がキャンセルされました
|
||||||
|
*/
|
||||||
|
"gameCanceled": string;
|
||||||
};
|
};
|
||||||
"_offlineScreen": {
|
"_offlineScreen": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2544,6 +2544,7 @@ _reversi:
|
||||||
timeLimitForEachTurn: "1ターンの時間制限"
|
timeLimitForEachTurn: "1ターンの時間制限"
|
||||||
freeMatch: "フリーマッチ"
|
freeMatch: "フリーマッチ"
|
||||||
lookingForPlayer: "対戦相手を探しています"
|
lookingForPlayer: "対戦相手を探しています"
|
||||||
|
gameCanceled: "対局がキャンセルされました"
|
||||||
|
|
||||||
_offlineScreen:
|
_offlineScreen:
|
||||||
title: "オフライン - サーバーに接続できません"
|
title: "オフライン - サーバーに接続できません"
|
||||||
|
|
|
@ -188,6 +188,9 @@ export interface ReversiGameEventTypes {
|
||||||
winnerId: MiUser['id'] | null;
|
winnerId: MiUser['id'] | null;
|
||||||
game: Packed<'ReversiGameDetailed'>;
|
game: Packed<'ReversiGameDetailed'>;
|
||||||
};
|
};
|
||||||
|
canceled: {
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
|
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async deleteGameCache(gameId: MiReversiGame['id']) {
|
||||||
|
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||||
if (targetUser.id === me.id) {
|
if (targetUser.id === me.id) {
|
||||||
|
@ -239,88 +244,93 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
if (isBothReady) {
|
if (isBothReady) {
|
||||||
// 3秒後、両者readyならゲーム開始
|
// 3秒後、両者readyならゲーム開始
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id });
|
const freshGame = await this.get(game.id);
|
||||||
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
|
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
|
||||||
if (!freshGame.user1Ready || !freshGame.user2Ready) return;
|
if (!freshGame.user1Ready || !freshGame.user2Ready) return;
|
||||||
|
|
||||||
let bw: number;
|
this.startGame(freshGame);
|
||||||
if (freshGame.bw === 'random') {
|
|
||||||
bw = Math.random() > 0.5 ? 1 : 2;
|
|
||||||
} else {
|
|
||||||
bw = parseInt(freshGame.bw, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRandomMap() {
|
|
||||||
const mapCount = Object.entries(Reversi.maps).length;
|
|
||||||
const rnd = Math.floor(Math.random() * mapCount);
|
|
||||||
return Object.values(Reversi.maps)[rnd].data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = freshGame.map != null ? freshGame.map : getRandomMap();
|
|
||||||
|
|
||||||
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
|
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
|
||||||
.set({
|
|
||||||
startedAt: new Date(),
|
|
||||||
isStarted: true,
|
|
||||||
black: bw,
|
|
||||||
map: map,
|
|
||||||
crc32,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
|
||||||
|
|
||||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
|
||||||
const engine = new Reversi.Game(map, {
|
|
||||||
isLlotheo: freshGame.isLlotheo,
|
|
||||||
canPutEverywhere: freshGame.canPutEverywhere,
|
|
||||||
loopedBoard: freshGame.loopedBoard,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (engine.isEnded) {
|
|
||||||
let winner;
|
|
||||||
if (engine.winner === true) {
|
|
||||||
winner = bw === 1 ? freshGame.user1Id : freshGame.user2Id;
|
|
||||||
} else if (engine.winner === false) {
|
|
||||||
winner = bw === 1 ? freshGame.user2Id : freshGame.user1Id;
|
|
||||||
} else {
|
|
||||||
winner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
|
||||||
.set({
|
|
||||||
isEnded: true,
|
|
||||||
endedAt: new Date(),
|
|
||||||
winnerId: winner,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
|
||||||
winnerId: winner,
|
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
|
||||||
});
|
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async startGame(game: MiReversiGame) {
|
||||||
|
let bw: number;
|
||||||
|
if (game.bw === 'random') {
|
||||||
|
bw = Math.random() > 0.5 ? 1 : 2;
|
||||||
|
} else {
|
||||||
|
bw = parseInt(game.bw, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomMap() {
|
||||||
|
const mapCount = Object.entries(Reversi.maps).length;
|
||||||
|
const rnd = Math.floor(Math.random() * mapCount);
|
||||||
|
return Object.values(Reversi.maps)[rnd].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = game.map != null ? game.map : getRandomMap();
|
||||||
|
|
||||||
|
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||||
|
|
||||||
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
startedAt: new Date(),
|
||||||
|
isStarted: true,
|
||||||
|
black: bw,
|
||||||
|
map: map,
|
||||||
|
crc32,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
|
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||||
|
const engine = new Reversi.Game(map, {
|
||||||
|
isLlotheo: game.isLlotheo,
|
||||||
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
|
loopedBoard: game.loopedBoard,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (engine.isEnded) {
|
||||||
|
let winner;
|
||||||
|
if (engine.winner === true) {
|
||||||
|
winner = bw === 1 ? game.user1Id : game.user2Id;
|
||||||
|
} else if (engine.winner === false) {
|
||||||
|
winner = bw === 1 ? game.user2Id : game.user1Id;
|
||||||
|
} else {
|
||||||
|
winner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
isEnded: true,
|
||||||
|
endedAt: new Date(),
|
||||||
|
winnerId: winner,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
|
winnerId: winner,
|
||||||
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||||
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
||||||
const invitations = await this.redisClient.zrange(
|
const invitations = await this.redisClient.zrange(
|
||||||
|
@ -510,6 +520,21 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async cancelGame(gameId: MiReversiGame['id'], user: MiUser) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
|
if (game.isStarted) return;
|
||||||
|
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||||
|
|
||||||
|
await this.reversiGamesRepository.delete(game.id);
|
||||||
|
this.deleteGameCache(game.id);
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'canceled', {
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
||||||
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
||||||
|
|
|
@ -40,6 +40,7 @@ class ReversiGameChannel extends Channel {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
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 'cancel': this.cancelGame(); break;
|
||||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||||
case 'checkState': this.checkState(body.crc32); break;
|
case 'checkState': this.checkState(body.crc32); break;
|
||||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||||
|
@ -60,6 +61,13 @@ class ReversiGameChannel extends Channel {
|
||||||
this.reversiService.gameReady(this.gameId!, this.user, ready);
|
this.reversiService.gameReady(this.gameId!, this.user, ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async cancelGame() {
|
||||||
|
if (this.user == null) return;
|
||||||
|
|
||||||
|
this.reversiService.cancelGame(this.gameId!, this.user);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async putStone(pos: number, id: string) {
|
private async putStone(pos: number, id: string) {
|
||||||
if (this.user == null) return;
|
if (this.user == null) return;
|
||||||
|
|
|
@ -86,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
||||||
</div>
|
</div>
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
<MkButton rounded danger @click="exit">{{ i18n.ts.cancel }}</MkButton>
|
<MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
||||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,9 +109,12 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
|
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -171,8 +174,16 @@ function chooseMap(ev: MouseEvent) {
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exit() {
|
async function cancel() {
|
||||||
props.connection.send('exit', {});
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.areYouSure,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
props.connection.send('cancel', {});
|
||||||
|
|
||||||
|
router.push('/reversi');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ready() {
|
function ready() {
|
||||||
|
|
|
@ -17,6 +17,14 @@ import GameBoard from './game.board.vue';
|
||||||
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';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
import { signinRequired } from '@/account.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
gameId: string;
|
gameId: string;
|
||||||
|
@ -45,6 +53,17 @@ async function fetchGame() {
|
||||||
connection.value.on('started', x => {
|
connection.value.on('started', x => {
|
||||||
game.value = x.game;
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
Loading…
Reference in a new issue