mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-12-22 08:20:10 +00:00
enhance(reversi): tweak reversi
This commit is contained in:
parent
576484835e
commit
a17251d913
19 changed files with 395 additions and 135 deletions
11
locales/index.d.ts
vendored
11
locales/index.d.ts
vendored
|
@ -536,6 +536,9 @@ export interface Locale extends ILocale {
|
||||||
* 添付取り消し
|
* 添付取り消し
|
||||||
*/
|
*/
|
||||||
"attachCancel": string;
|
"attachCancel": string;
|
||||||
|
/**
|
||||||
|
* ファイルを削除
|
||||||
|
*/
|
||||||
"deleteFile": string;
|
"deleteFile": string;
|
||||||
/**
|
/**
|
||||||
* センシティブとして設定
|
* センシティブとして設定
|
||||||
|
@ -9482,6 +9485,10 @@ export interface Locale extends ILocale {
|
||||||
* 投了により
|
* 投了により
|
||||||
*/
|
*/
|
||||||
"surrendered": string;
|
"surrendered": string;
|
||||||
|
/**
|
||||||
|
* 時間切れ
|
||||||
|
*/
|
||||||
|
"timeout": string;
|
||||||
/**
|
/**
|
||||||
* 引き分け
|
* 引き分け
|
||||||
*/
|
*/
|
||||||
|
@ -9534,6 +9541,10 @@ export interface Locale extends ILocale {
|
||||||
* どこでも置けるモード
|
* どこでも置けるモード
|
||||||
*/
|
*/
|
||||||
"canPutEverywhere": string;
|
"canPutEverywhere": string;
|
||||||
|
/**
|
||||||
|
* 1ターンの時間制限
|
||||||
|
*/
|
||||||
|
"timeLimitForEachTurn": string;
|
||||||
/**
|
/**
|
||||||
* フリーマッチ
|
* フリーマッチ
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2527,6 +2527,7 @@ _reversi:
|
||||||
pastTurnOf: "{name}のターン"
|
pastTurnOf: "{name}のターン"
|
||||||
surrender: "投了"
|
surrender: "投了"
|
||||||
surrendered: "投了により"
|
surrendered: "投了により"
|
||||||
|
timeout: "時間切れ"
|
||||||
drawn: "引き分け"
|
drawn: "引き分け"
|
||||||
won: "{name}の勝ち"
|
won: "{name}の勝ち"
|
||||||
black: "黒"
|
black: "黒"
|
||||||
|
@ -2540,5 +2541,6 @@ _reversi:
|
||||||
isLlotheo: "石の少ない方が勝ち(ロセオ)"
|
isLlotheo: "石の少ない方が勝ち(ロセオ)"
|
||||||
loopedMap: "ループマップ"
|
loopedMap: "ループマップ"
|
||||||
canPutEverywhere: "どこでも置けるモード"
|
canPutEverywhere: "どこでも置けるモード"
|
||||||
|
timeLimitForEachTurn: "1ターンの時間制限"
|
||||||
freeMatch: "フリーマッチ"
|
freeMatch: "フリーマッチ"
|
||||||
lookingForPlayer: "対戦相手を探しています"
|
lookingForPlayer: "対戦相手を探しています"
|
||||||
|
|
18
packages/backend/migration/1705793785675-reversi-3.js
Normal file
18
packages/backend/migration/1705793785675-reversi-3.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Reversi31705793785675 {
|
||||||
|
name = 'Reversi31705793785675'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrendered" TO "surrenderedUserId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeoutUserId" character varying(32)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeoutUserId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrenderedUserId" TO "surrendered"`);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1705794768153-reversi-4.js
Normal file
18
packages/backend/migration/1705794768153-reversi-4.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Reversi41705794768153 {
|
||||||
|
name = 'Reversi41705794768153'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "endedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "endedAt"`);
|
||||||
|
}
|
||||||
|
}
|
16
packages/backend/migration/1705798904141-reversi-5.js
Normal file
16
packages/backend/migration/1705798904141-reversi-5.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Reversi51705798904141 {
|
||||||
|
name = 'Reversi51705798904141'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeLimitForEachTurn"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -181,9 +181,6 @@ export interface ReversiGameEventTypes {
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
log: Reversi.Serializer.Log & { id: string | null };
|
log: Reversi.Serializer.Log & { id: string | null };
|
||||||
heatbeat: {
|
|
||||||
userId: MiUser['id'];
|
|
||||||
};
|
|
||||||
started: {
|
started: {
|
||||||
game: Packed<'ReversiGameDetailed'>;
|
game: Packed<'ReversiGameDetailed'>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import { Serialized } from '@/types.js';
|
||||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -55,6 +56,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async cacheGame(game: MiReversiGame) {
|
||||||
|
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
|
||||||
|
}
|
||||||
|
|
||||||
@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) {
|
||||||
|
@ -83,6 +89,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
bw: 'random',
|
bw: 'random',
|
||||||
isLlotheo: false,
|
isLlotheo: false,
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
this.cacheGame(game);
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
||||||
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
||||||
|
@ -125,6 +132,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
bw: 'random',
|
bw: 'random',
|
||||||
isLlotheo: false,
|
isLlotheo: false,
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
this.cacheGame(game);
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
||||||
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
||||||
|
@ -160,6 +168,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
bw: 'random',
|
bw: 'random',
|
||||||
isLlotheo: false,
|
isLlotheo: false,
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
this.cacheGame(game);
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
||||||
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
||||||
|
@ -182,33 +191,47 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async gameReady(game: MiReversiGame, user: MiUser, ready: boolean) {
|
public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
if (game.isStarted) return;
|
if (game.isStarted) return;
|
||||||
|
|
||||||
let isBothReady = false;
|
let isBothReady = false;
|
||||||
|
|
||||||
if (game.user1Id === user.id) {
|
if (game.user1Id === user.id) {
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
user1Ready: ready,
|
.set({
|
||||||
});
|
user1Ready: ready,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||||
user1: ready,
|
user1: ready,
|
||||||
user2: game.user2Ready,
|
user2: updatedGame.user2Ready,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ready && game.user2Ready) isBothReady = true;
|
if (ready && updatedGame.user2Ready) isBothReady = true;
|
||||||
} else if (game.user2Id === user.id) {
|
} else if (game.user2Id === user.id) {
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
user2Ready: ready,
|
.set({
|
||||||
});
|
user2Ready: ready,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||||
user1: game.user1Ready,
|
user1: updatedGame.user1Ready,
|
||||||
user2: ready,
|
user2: ready,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ready && game.user1Ready) isBothReady = true;
|
if (ready && updatedGame.user1Ready) isBothReady = true;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -237,45 +260,62 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
startedAt: new Date(),
|
.set({
|
||||||
isStarted: true,
|
startedAt: new Date(),
|
||||||
black: bw,
|
isStarted: true,
|
||||||
map: map,
|
black: bw,
|
||||||
crc32,
|
map: map,
|
||||||
});
|
crc32,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||||
const o = new Reversi.Game(map, {
|
const engine = new Reversi.Game(map, {
|
||||||
isLlotheo: freshGame.isLlotheo,
|
isLlotheo: freshGame.isLlotheo,
|
||||||
canPutEverywhere: freshGame.canPutEverywhere,
|
canPutEverywhere: freshGame.canPutEverywhere,
|
||||||
loopedBoard: freshGame.loopedBoard,
|
loopedBoard: freshGame.loopedBoard,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (o.isEnded) {
|
if (engine.isEnded) {
|
||||||
let winner;
|
let winner;
|
||||||
if (o.winner === true) {
|
if (engine.winner === true) {
|
||||||
winner = freshGame.black === 1 ? freshGame.user1Id : freshGame.user2Id;
|
winner = bw === 1 ? freshGame.user1Id : freshGame.user2Id;
|
||||||
} else if (o.winner === false) {
|
} else if (engine.winner === false) {
|
||||||
winner = freshGame.black === 1 ? freshGame.user2Id : freshGame.user1Id;
|
winner = bw === 1 ? freshGame.user2Id : freshGame.user1Id;
|
||||||
} else {
|
} else {
|
||||||
winner = null;
|
winner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
isEnded: true,
|
.set({
|
||||||
winnerId: winner,
|
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', {
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
winnerId: winner,
|
winnerId: winner,
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
});
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
@ -292,17 +332,27 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) {
|
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
if (game.isStarted) return;
|
if (game.isStarted) return;
|
||||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||||
if ((game.user1Id === user.id) && game.user1Ready) return;
|
if ((game.user1Id === user.id) && game.user1Ready) return;
|
||||||
if ((game.user2Id === user.id) && game.user2Ready) return;
|
if ((game.user2Id === user.id) && game.user2Ready) return;
|
||||||
|
|
||||||
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
|
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
// TODO: より厳格なバリデーション
|
||||||
[key]: value,
|
|
||||||
});
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
[key]: value,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -312,7 +362,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number, id?: string | null) {
|
public async putStoneToGame(gameId: MiReversiGame['id'], user: MiUser, pos: number, id?: string | null) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
if (!game.isStarted) return;
|
if (!game.isStarted) return;
|
||||||
if (game.isEnded) return;
|
if (game.isEnded) return;
|
||||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||||
|
@ -361,12 +413,18 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
crc32,
|
.set({
|
||||||
isEnded: engine.isEnded,
|
crc32,
|
||||||
winnerId: winner,
|
isEnded: engine.isEnded,
|
||||||
logs: serializeLogs,
|
winnerId: winner,
|
||||||
});
|
logs: serializeLogs,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||||
...log,
|
...log,
|
||||||
|
@ -376,38 +434,112 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
if (engine.isEnded) {
|
if (engine.isEnded) {
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
winnerId: winner ?? null,
|
winnerId: winner ?? null,
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async surrender(game: MiReversiGame, user: MiUser) {
|
public async surrender(gameId: MiReversiGame['id'], user: MiUser) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
if (game.isEnded) return;
|
if (game.isEnded) return;
|
||||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||||
|
|
||||||
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
||||||
|
|
||||||
await this.reversiGamesRepository.update(game.id, {
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
surrendered: user.id,
|
.set({
|
||||||
isEnded: true,
|
isEnded: true,
|
||||||
winnerId: winnerId,
|
endedAt: new Date(),
|
||||||
});
|
winnerId: winnerId,
|
||||||
|
surrenderedUserId: user.id,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
winnerId: winnerId,
|
winnerId: winnerId,
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async get(id: MiReversiGame['id']) {
|
public async checkTimeout(gameId: MiReversiGame['id']) {
|
||||||
return this.reversiGamesRepository.findOneBy({ id });
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
|
if (game.isEnded) return;
|
||||||
|
|
||||||
|
const engine = Reversi.Serializer.restoreGame({
|
||||||
|
map: game.map,
|
||||||
|
isLlotheo: game.isLlotheo,
|
||||||
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
|
loopedBoard: game.loopedBoard,
|
||||||
|
logs: game.logs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (engine.turn == null) return;
|
||||||
|
|
||||||
|
const timer = await this.redisClient.exists(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`);
|
||||||
|
|
||||||
|
if (timer === 0) {
|
||||||
|
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
|
||||||
|
|
||||||
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
isEnded: true,
|
||||||
|
endedAt: new Date(),
|
||||||
|
winnerId: winnerId,
|
||||||
|
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
|
winnerId: winnerId,
|
||||||
|
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async heatbeat(game: MiReversiGame, user: MiUser) {
|
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'heatbeat', { userId: user.id });
|
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
||||||
|
if (cached != null) {
|
||||||
|
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
||||||
|
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const game = await this.reversiGamesRepository.findOneBy({ id });
|
||||||
|
if (game == null) return null;
|
||||||
|
|
||||||
|
this.cacheGame(game);
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async checkCrc(gameId: MiReversiGame['id'], crc32: string | number) {
|
||||||
|
const game = await this.get(gameId);
|
||||||
|
if (game == null) throw new Error('game not found');
|
||||||
|
|
||||||
|
if (crc32.toString() !== game.crc32) {
|
||||||
|
return await this.reversiGameEntityService.packDetail(game);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -37,6 +37,7 @@ export class ReversiGameEntityService {
|
||||||
id: game.id,
|
id: game.id,
|
||||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||||
|
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||||
isStarted: game.isStarted,
|
isStarted: game.isStarted,
|
||||||
isEnded: game.isEnded,
|
isEnded: game.isEnded,
|
||||||
form1: game.form1,
|
form1: game.form1,
|
||||||
|
@ -49,12 +50,14 @@ export class ReversiGameEntityService {
|
||||||
user2: this.userEntityService.pack(game.user2Id, me),
|
user2: this.userEntityService.pack(game.user2Id, me),
|
||||||
winnerId: game.winnerId,
|
winnerId: game.winnerId,
|
||||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||||
surrendered: game.surrendered,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
bw: game.bw,
|
bw: game.bw,
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
|
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||||
logs: game.logs,
|
logs: game.logs,
|
||||||
map: game.map,
|
map: game.map,
|
||||||
});
|
});
|
||||||
|
@ -79,6 +82,7 @@ export class ReversiGameEntityService {
|
||||||
id: game.id,
|
id: game.id,
|
||||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||||
|
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||||
isStarted: game.isStarted,
|
isStarted: game.isStarted,
|
||||||
isEnded: game.isEnded,
|
isEnded: game.isEnded,
|
||||||
form1: game.form1,
|
form1: game.form1,
|
||||||
|
@ -91,12 +95,14 @@ export class ReversiGameEntityService {
|
||||||
user2: this.userEntityService.pack(game.user2Id, me),
|
user2: this.userEntityService.pack(game.user2Id, me),
|
||||||
winnerId: game.winnerId,
|
winnerId: game.winnerId,
|
||||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||||
surrendered: game.surrendered,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
bw: game.bw,
|
bw: game.bw,
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
|
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,12 @@ export class MiReversiGame {
|
||||||
})
|
})
|
||||||
public startedAt: Date | null;
|
public startedAt: Date | null;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
comment: 'The ended date of the ReversiGame.',
|
||||||
|
})
|
||||||
|
public endedAt: Date | null;
|
||||||
|
|
||||||
@Column(id())
|
@Column(id())
|
||||||
public user1Id: MiUser['id'];
|
public user1Id: MiUser['id'];
|
||||||
|
|
||||||
|
@ -71,7 +77,19 @@ export class MiReversiGame {
|
||||||
...id(),
|
...id(),
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public surrendered: MiUser['id'] | null;
|
public surrenderedUserId: MiUser['id'] | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public timeoutUserId: MiUser['id'] | null;
|
||||||
|
|
||||||
|
// in sec
|
||||||
|
@Column('smallint', {
|
||||||
|
default: 90,
|
||||||
|
})
|
||||||
|
public timeLimitForEachTurn: number;
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -21,6 +21,11 @@ export const packedReversiGameLiteSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
endedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
isStarted: {
|
isStarted: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -75,7 +80,12 @@ export const packedReversiGameLiteSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
ref: 'User',
|
ref: 'User',
|
||||||
},
|
},
|
||||||
surrendered: {
|
surrenderedUserId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
timeoutUserId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
|
@ -100,6 +110,10 @@ export const packedReversiGameLiteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
timeLimitForEachTurn: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -121,6 +135,11 @@ export const packedReversiGameDetailedSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
endedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
isStarted: {
|
isStarted: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -175,7 +194,12 @@ export const packedReversiGameDetailedSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
ref: 'User',
|
ref: 'User',
|
||||||
},
|
},
|
||||||
surrendered: {
|
surrenderedUserId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
timeoutUserId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
|
@ -200,6 +224,10 @@ export const packedReversiGameDetailedSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
timeLimitForEachTurn: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
logs: {
|
logs: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.accessDenied);
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.reversiService.surrender(game, me);
|
await this.reversiService.surrender(game.id, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,11 +32,6 @@ class ReversiGameChannel extends Channel {
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
this.gameId = params.gameId as string;
|
this.gameId = params.gameId as string;
|
||||||
|
|
||||||
const game = await this.reversiGamesRepository.findOneBy({
|
|
||||||
id: this.gameId,
|
|
||||||
});
|
|
||||||
if (game == null) return;
|
|
||||||
|
|
||||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
|
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +41,8 @@ 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 'heatbeat': this.heatbeat(body.crc32); break;
|
case 'checkState': this.checkState(body.crc32); break;
|
||||||
|
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,51 +50,38 @@ class ReversiGameChannel extends Channel {
|
||||||
private async updateSettings(key: string, value: any) {
|
private async updateSettings(key: string, value: any) {
|
||||||
if (this.user == null) return;
|
if (this.user == null) return;
|
||||||
|
|
||||||
// TODO: キャッシュしたい
|
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
|
||||||
if (game == null) throw new Error('game not found');
|
|
||||||
|
|
||||||
this.reversiService.updateSettings(game, this.user, key, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async ready(ready: boolean) {
|
private async ready(ready: boolean) {
|
||||||
if (this.user == null) return;
|
if (this.user == null) return;
|
||||||
|
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
this.reversiService.gameReady(this.gameId!, this.user, ready);
|
||||||
if (game == null) throw new Error('game not found');
|
|
||||||
|
|
||||||
this.reversiService.gameReady(game, this.user, ready);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
|
||||||
// TODO: キャッシュしたい
|
this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id);
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
|
||||||
if (game == null) throw new Error('game not found');
|
|
||||||
|
|
||||||
this.reversiService.putStoneToGame(game, this.user, pos, id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async heatbeat(crc32?: string | number | null) {
|
private async checkState(crc32: string | number) {
|
||||||
// TODO: キャッシュしたい
|
if (crc32 != null) return;
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
|
|
||||||
if (game == null) throw new Error('game not found');
|
|
||||||
|
|
||||||
if (!game.isStarted) return;
|
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||||
|
if (game) {
|
||||||
if (crc32 != null) {
|
this.send('rescue', game);
|
||||||
if (crc32.toString() !== game.crc32) {
|
|
||||||
this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.user && (game.user1Id === this.user.id || game.user2Id === this.user.id)) {
|
@bindThis
|
||||||
this.reversiService.heatbeat(game, this.user);
|
private async claimTimeIsUp() {
|
||||||
}
|
if (this.user == null) return;
|
||||||
|
|
||||||
|
this.reversiService.checkTimeout(this.gameId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -15,19 +15,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="overflow: clip; line-height: 28px;">
|
<div style="overflow: clip; line-height: 28px;">
|
||||||
<div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn">
|
<div v-if="!iAmPlayer && !game.isEnded && turnUser">
|
||||||
<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
|
||||||
<MkEllipsis/>
|
<MkEllipsis/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(logPos !== game.logs.length) && turnUser" class="turn">
|
<div v-if="(logPos !== game.logs.length) && turnUser">
|
||||||
<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/><soan v-if="opponentNotResponding" style="margin-left: 8px;">({{ i18n.ts.notResponding }})</soan></div>
|
<div v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/><span style="margin-left: 1em; opacity: 0.7;">({{ i18n.tsx.remainingN({ n: opTurnTimerRmain }) }})</span></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"><span style="display: inline-block; font-weight: bold; animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</span><span style="margin-left: 1em; opacity: 0.7;">({{ i18n.tsx.remainingN({ n: myTurnTimerRmain }) }})</span></div>
|
||||||
<div v-if="game.isEnded && logPos == game.logs.length" class="result">
|
<div v-if="game.isEnded && logPos == game.logs.length">
|
||||||
<template v-if="game.winner">
|
<template v-if="game.winner">
|
||||||
<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
|
<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
|
||||||
<span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span>
|
<span v-if="game.surrenderedUserId != null"> ({{ i18n.ts._reversi.surrendered }})</span>
|
||||||
|
<span v-if="game.timeoutUserId != null"> ({{ i18n.ts._reversi.timeout }})</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>{{ i18n.ts._reversi.drawn }}</template>
|
<template v-else>{{ i18n.ts._reversi.drawn }}</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -239,7 +240,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('heatbeat', {
|
props.connection.send('checkState', {
|
||||||
crc32: crc32,
|
crc32: crc32,
|
||||||
});
|
});
|
||||||
}, 10000, { immediate: false, afterMounted: true });
|
}, 10000, { immediate: false, afterMounted: true });
|
||||||
|
@ -269,9 +270,31 @@ function putStone(pos) {
|
||||||
});
|
});
|
||||||
appliedOps.push(id);
|
appliedOps.push(id);
|
||||||
|
|
||||||
|
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
|
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
|
|
||||||
checkEnd();
|
checkEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
||||||
|
const opTurnTimerRmain = ref<number>(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', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||||
|
|
||||||
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||||
game.value.logs = Reversi.Serializer.serializeLogs([
|
game.value.logs = Reversi.Serializer.serializeLogs([
|
||||||
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
||||||
|
@ -286,6 +309,9 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||||
engine.value.putStone(log.pos);
|
engine.value.putStone(log.pos);
|
||||||
triggerRef(engine);
|
triggerRef(engine);
|
||||||
|
|
||||||
|
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
|
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
|
|
||||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||||
volume: 1,
|
volume: 1,
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
|
@ -339,27 +365,6 @@ 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',
|
||||||
|
@ -411,28 +416,24 @@ 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(() => {
|
onActivated(() => {
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,6 +49,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||||
|
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||||
|
|
||||||
|
<MkRadios v-model="game.timeLimitForEachTurn">
|
||||||
|
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
||||||
|
|
||||||
|
@ -125,6 +141,10 @@ watch(() => game.value.bw, () => {
|
||||||
updateSettings('bw');
|
updateSettings('bw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => game.value.timeLimitForEachTurn, () => {
|
||||||
|
updateSettings('timeLimitForEachTurn');
|
||||||
|
});
|
||||||
|
|
||||||
function chooseMap(ev: MouseEvent) {
|
function chooseMap(ev: MouseEvent) {
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-20T04:59:59.768Z
|
* generatedAt: 2024-01-21T01:01:12.332Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-20T04:59:59.766Z
|
* generatedAt: 2024-01-21T01:01:12.330Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-20T04:59:59.765Z
|
* generatedAt: 2024-01-21T01:01:12.328Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-20T04:59:59.764Z
|
* generatedAt: 2024-01-21T01:01:12.327Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-20T04:59:59.681Z
|
* generatedAt: 2024-01-21T01:01:12.246Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4465,6 +4465,8 @@ export type components = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
startedAt: string | null;
|
startedAt: string | null;
|
||||||
|
/** Format: date-time */
|
||||||
|
endedAt: string | null;
|
||||||
isStarted: boolean;
|
isStarted: boolean;
|
||||||
isEnded: boolean;
|
isEnded: boolean;
|
||||||
form1: Record<string, never> | null;
|
form1: Record<string, never> | null;
|
||||||
|
@ -4481,12 +4483,15 @@ export type components = {
|
||||||
winnerId: string | null;
|
winnerId: string | null;
|
||||||
winner: components['schemas']['User'] | null;
|
winner: components['schemas']['User'] | null;
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
surrendered: string | null;
|
surrenderedUserId: string | null;
|
||||||
|
/** Format: id */
|
||||||
|
timeoutUserId: string | null;
|
||||||
black: number | null;
|
black: number | null;
|
||||||
bw: string;
|
bw: string;
|
||||||
isLlotheo: boolean;
|
isLlotheo: boolean;
|
||||||
canPutEverywhere: boolean;
|
canPutEverywhere: boolean;
|
||||||
loopedBoard: boolean;
|
loopedBoard: boolean;
|
||||||
|
timeLimitForEachTurn: number;
|
||||||
};
|
};
|
||||||
ReversiGameDetailed: {
|
ReversiGameDetailed: {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
|
@ -4495,6 +4500,8 @@ export type components = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
startedAt: string | null;
|
startedAt: string | null;
|
||||||
|
/** Format: date-time */
|
||||||
|
endedAt: string | null;
|
||||||
isStarted: boolean;
|
isStarted: boolean;
|
||||||
isEnded: boolean;
|
isEnded: boolean;
|
||||||
form1: Record<string, never> | null;
|
form1: Record<string, never> | null;
|
||||||
|
@ -4511,12 +4518,15 @@ export type components = {
|
||||||
winnerId: string | null;
|
winnerId: string | null;
|
||||||
winner: components['schemas']['User'] | null;
|
winner: components['schemas']['User'] | null;
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
surrendered: string | null;
|
surrenderedUserId: string | null;
|
||||||
|
/** Format: id */
|
||||||
|
timeoutUserId: string | null;
|
||||||
black: number | null;
|
black: number | null;
|
||||||
bw: string;
|
bw: string;
|
||||||
isLlotheo: boolean;
|
isLlotheo: boolean;
|
||||||
canPutEverywhere: boolean;
|
canPutEverywhere: boolean;
|
||||||
loopedBoard: boolean;
|
loopedBoard: boolean;
|
||||||
|
timeLimitForEachTurn: number;
|
||||||
logs: unknown[][];
|
logs: unknown[][];
|
||||||
map: string[];
|
map: string[];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue