summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2024-01-21 10:07:43 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2024-01-21 10:07:43 +0900
commita17251d913c822e3113b47ed8135eecb3f06c445 (patch)
treec90dadc24b723acaa4eba9c0a1f66bc5551c5e4f /packages/backend
parentenhance(frontend): ノート作成画面の添付メニューから直接フ... (diff)
downloadsharkey-a17251d913c822e3113b47ed8135eecb3f06c445.tar.gz
sharkey-a17251d913c822e3113b47ed8135eecb3f06c445.tar.bz2
sharkey-a17251d913c822e3113b47ed8135eecb3f06c445.zip
enhance(reversi): tweak reversi
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/migration/1705793785675-reversi-3.js18
-rw-r--r--packages/backend/migration/1705794768153-reversi-4.js18
-rw-r--r--packages/backend/migration/1705798904141-reversi-5.js16
-rw-r--r--packages/backend/src/core/GlobalEventService.ts3
-rw-r--r--packages/backend/src/core/ReversiService.ts240
-rw-r--r--packages/backend/src/core/entities/ReversiGameEntityService.ts10
-rw-r--r--packages/backend/src/models/ReversiGame.ts20
-rw-r--r--packages/backend/src/models/json-schema/reversi-game.ts32
-rw-r--r--packages/backend/src/server/api/endpoints/reversi/surrender.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts49
10 files changed, 312 insertions, 96 deletions
diff --git a/packages/backend/migration/1705793785675-reversi-3.js b/packages/backend/migration/1705793785675-reversi-3.js
new file mode 100644
index 0000000000..2faf9ae6d5
--- /dev/null
+++ b/packages/backend/migration/1705793785675-reversi-3.js
@@ -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"`);
+ }
+}
diff --git a/packages/backend/migration/1705794768153-reversi-4.js b/packages/backend/migration/1705794768153-reversi-4.js
new file mode 100644
index 0000000000..5b7bacb21e
--- /dev/null
+++ b/packages/backend/migration/1705794768153-reversi-4.js
@@ -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"`);
+ }
+}
diff --git a/packages/backend/migration/1705798904141-reversi-5.js b/packages/backend/migration/1705798904141-reversi-5.js
new file mode 100644
index 0000000000..7ca7221604
--- /dev/null
+++ b/packages/backend/migration/1705798904141-reversi-5.js
@@ -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"`);
+ }
+}
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index e599912e2b..5ddd100e6c 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -181,9 +181,6 @@ export interface ReversiGameEventTypes {
value: any;
};
log: Reversi.Serializer.Log & { id: string | null };
- heatbeat: {
- userId: MiUser['id'];
- };
started: {
game: Packed<'ReversiGameDetailed'>;
};
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index e626cbaf19..b2a4032d4b 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -25,6 +25,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import type { Packed } from '@/misc/json-schema.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
@@ -56,6 +57,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
+ private async cacheGame(game: MiReversiGame) {
+ await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
+ }
+
+ @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) {
throw new Error('You cannot match yourself.');
@@ -83,6 +89,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
+ this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
@@ -125,6 +132,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
+ this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
@@ -160,6 +168,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
+ this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
@@ -182,33 +191,47 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@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;
let isBothReady = false;
if (game.user1Id === user.id) {
- await this.reversiGamesRepository.update(game.id, {
- user1Ready: ready,
- });
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .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', {
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) {
- await this.reversiGamesRepository.update(game.id, {
- user2Ready: ready,
- });
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .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', {
- user1: game.user1Ready,
+ user1: updatedGame.user1Ready,
user2: ready,
});
- if (ready && game.user1Ready) isBothReady = true;
+ if (ready && updatedGame.user1Ready) isBothReady = true;
} else {
return;
}
@@ -237,45 +260,62 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const crc32 = CRC32.str(JSON.stringify(freshGame.logs)).toString();
- await this.reversiGamesRepository.update(game.id, {
- startedAt: new Date(),
- isStarted: true,
- black: bw,
- map: map,
- crc32,
- });
+ 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 o = new Reversi.Game(map, {
+ const engine = new Reversi.Game(map, {
isLlotheo: freshGame.isLlotheo,
canPutEverywhere: freshGame.canPutEverywhere,
loopedBoard: freshGame.loopedBoard,
});
- if (o.isEnded) {
+ if (engine.isEnded) {
let winner;
- if (o.winner === true) {
- winner = freshGame.black === 1 ? freshGame.user1Id : freshGame.user2Id;
- } else if (o.winner === false) {
- winner = freshGame.black === 1 ? freshGame.user2Id : freshGame.user1Id;
+ 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;
}
- await this.reversiGamesRepository.update(game.id, {
- isEnded: true,
- winnerId: winner,
- });
+ 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, user),
+ 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, user),
+ game: await this.reversiGameEntityService.packDetail(game.id),
});
}, 3000);
}
@@ -292,17 +332,27 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@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.user1Id !== user.id) && (game.user2Id !== user.id)) return;
if ((game.user1Id === user.id) && game.user1Ready) 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, {
- [key]: value,
- });
+ // TODO: より厳格なバリデーション
+
+ 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', {
userId: user.id,
@@ -312,7 +362,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@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.isEnded) 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();
- await this.reversiGamesRepository.update(game.id, {
- crc32,
- isEnded: engine.isEnded,
- winnerId: winner,
- logs: serializeLogs,
- });
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .set({
+ crc32,
+ isEnded: engine.isEnded,
+ 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', {
...log,
@@ -376,38 +434,112 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (engine.isEnded) {
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
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
- 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.user1Id !== user.id) && (game.user2Id !== user.id)) return;
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
- await this.reversiGamesRepository.update(game.id, {
- surrendered: user.id,
- isEnded: true,
- winnerId: winnerId,
- });
+ const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
+ .set({
+ isEnded: true,
+ 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', {
winnerId: winnerId,
- game: await this.reversiGameEntityService.packDetail(game.id, user),
+ game: await this.reversiGameEntityService.packDetail(game.id),
});
}
@bindThis
- public async get(id: MiReversiGame['id']) {
- return this.reversiGamesRepository.findOneBy({ id });
+ public async checkTimeout(gameId: MiReversiGame['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
+ public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
+ 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 heatbeat(game: MiReversiGame, user: MiUser) {
- this.globalEventService.publishReversiGameStream(game.id, 'heatbeat', { userId: user.id });
+ 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
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index a7adc681f6..bcb0fd5a6f 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -37,6 +37,7 @@ export class ReversiGameEntityService {
id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(),
startedAt: game.startedAt && game.startedAt.toISOString(),
+ endedAt: game.endedAt && game.endedAt.toISOString(),
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
@@ -49,12 +50,14 @@ export class ReversiGameEntityService {
user2: this.userEntityService.pack(game.user2Id, me),
winnerId: game.winnerId,
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
- surrendered: game.surrendered,
+ surrenderedUserId: game.surrenderedUserId,
+ timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
+ timeLimitForEachTurn: game.timeLimitForEachTurn,
logs: game.logs,
map: game.map,
});
@@ -79,6 +82,7 @@ export class ReversiGameEntityService {
id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(),
startedAt: game.startedAt && game.startedAt.toISOString(),
+ endedAt: game.endedAt && game.endedAt.toISOString(),
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
@@ -91,12 +95,14 @@ export class ReversiGameEntityService {
user2: this.userEntityService.pack(game.user2Id, me),
winnerId: game.winnerId,
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
- surrendered: game.surrendered,
+ surrenderedUserId: game.surrenderedUserId,
+ timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
+ timeLimitForEachTurn: game.timeLimitForEachTurn,
});
}
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index dcaa5c9fa9..11d236e458 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -13,6 +13,12 @@ export class MiReversiGame {
})
public startedAt: Date | null;
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The ended date of the ReversiGame.',
+ })
+ public endedAt: Date | null;
+
@Column(id())
public user1Id: MiUser['id'];
@@ -71,7 +77,19 @@ export class MiReversiGame {
...id(),
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', {
default: [],
diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts
index b94046438b..4ac4d165d8 100644
--- a/packages/backend/src/models/json-schema/reversi-game.ts
+++ b/packages/backend/src/models/json-schema/reversi-game.ts
@@ -21,6 +21,11 @@ export const packedReversiGameLiteSchema = {
optional: false, nullable: true,
format: 'date-time',
},
+ endedAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
isStarted: {
type: 'boolean',
optional: false, nullable: false,
@@ -75,7 +80,12 @@ export const packedReversiGameLiteSchema = {
optional: false, nullable: true,
ref: 'User',
},
- surrendered: {
+ surrenderedUserId: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'id',
+ },
+ timeoutUserId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
@@ -100,6 +110,10 @@ export const packedReversiGameLiteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ timeLimitForEachTurn: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
},
} as const;
@@ -121,6 +135,11 @@ export const packedReversiGameDetailedSchema = {
optional: false, nullable: true,
format: 'date-time',
},
+ endedAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
isStarted: {
type: 'boolean',
optional: false, nullable: false,
@@ -175,7 +194,12 @@ export const packedReversiGameDetailedSchema = {
optional: false, nullable: true,
ref: 'User',
},
- surrendered: {
+ surrenderedUserId: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'id',
+ },
+ timeoutUserId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
@@ -200,6 +224,10 @@ export const packedReversiGameDetailedSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ timeLimitForEachTurn: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
logs: {
type: 'array',
optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts
index c47d36be33..c809142e07 100644
--- a/packages/backend/src/server/api/endpoints/reversi/surrender.ts
+++ b/packages/backend/src/server/api/endpoints/reversi/surrender.ts
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
- await this.reversiService.surrender(game, me);
+ await this.reversiService.surrender(game.id, me);
});
}
}
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 c5d05e5cfb..77eaa6d1d3 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -32,11 +32,6 @@ class ReversiGameChannel extends Channel {
public async init(params: any) {
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);
}
@@ -46,7 +41,8 @@ class ReversiGameChannel extends Channel {
case 'ready': this.ready(body); break;
case 'updateSettings': this.updateSettings(body.key, body.value); 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) {
if (this.user == null) return;
- // TODO: キャッシュしたい
- 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);
+ this.reversiService.updateSettings(this.gameId!, this.user, key, value);
}
@bindThis
private async ready(ready: boolean) {
if (this.user == null) return;
- const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
- if (game == null) throw new Error('game not found');
-
- this.reversiService.gameReady(game, this.user, ready);
+ this.reversiService.gameReady(this.gameId!, this.user, ready);
}
@bindThis
private async putStone(pos: number, id: string) {
if (this.user == null) return;
- // TODO: キャッシュしたい
- 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);
+ this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id);
}
@bindThis
- private async heatbeat(crc32?: string | number | null) {
- // TODO: キャッシュしたい
- const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! });
- if (game == null) throw new Error('game not found');
-
- if (!game.isStarted) return;
+ private async checkState(crc32: string | number) {
+ if (crc32 != null) return;
- if (crc32 != null) {
- if (crc32.toString() !== game.crc32) {
- this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user));
- }
+ const game = await this.reversiService.checkCrc(this.gameId!, crc32);
+ if (game) {
+ this.send('rescue', game);
}
+ }
- if (this.user && (game.user1Id === this.user.id || game.user2Id === this.user.id)) {
- this.reversiService.heatbeat(game, this.user);
- }
+ @bindThis
+ private async claimTimeIsUp() {
+ if (this.user == null) return;
+
+ this.reversiService.checkTimeout(this.gameId!);
}
@bindThis