summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/ReversiService.ts
diff options
context:
space:
mode:
authorMarie <marie@kaifa.ch>2024-01-25 14:21:42 +0100
committerMarie <marie@kaifa.ch>2024-01-25 14:21:42 +0100
commit913dd581efa3e0afe6da57131d213965a376f90e (patch)
tree7d6241b02e299d86c02db9305c23a5142390e044 /packages/backend/src/core/ReversiService.ts
parentfix: notes/versions not checking visibility (diff)
parent2024.2.0-beta.7 (diff)
downloadsharkey-913dd581efa3e0afe6da57131d213965a376f90e.tar.gz
sharkey-913dd581efa3e0afe6da57131d213965a376f90e.tar.bz2
sharkey-913dd581efa3e0afe6da57131d213965a376f90e.zip
merge: upstream
Diffstat (limited to 'packages/backend/src/core/ReversiService.ts')
-rw-r--r--packages/backend/src/core/ReversiService.ts136
1 files changed, 98 insertions, 38 deletions
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 4f68fabc4c..50bddb0c2b 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import CRC32 from 'crc-32';
import { ModuleRef } from '@nestjs/core';
import * as Reversi from 'misskey-reversi';
-import { IsNull } from 'typeorm';
+import { IsNull, LessThan, MoreThan } from 'typeorm';
import type {
MiReversiGame,
ReversiGamesRepository,
@@ -25,7 +24,7 @@ import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
-const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
+const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
@Injectable()
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@@ -86,44 +85,82 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: game.map,
bw: game.bw,
crc32: game.crc32,
+ noIrregularRules: game.noIrregularRules,
} satisfies Partial<MiReversiGame>;
}
@bindThis
- public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
+ public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) {
throw new Error('You cannot match yourself.');
}
+ if (!multiple) {
+ // 既にマッチしている対局が無いか探す(3分以内)
+ const games = await this.reversiGamesRepository.find({
+ where: [
+ { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
+ { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
+ ],
+ relations: ['user1', 'user2'],
+ order: { id: 'DESC' },
+ });
+ if (games.length > 0) {
+ return games[0];
+ }
+ }
+
+ //#region 相手から既に招待されてないか確認
const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${me.id}`,
- Date.now() - MATCHING_TIMEOUT_MS,
+ Date.now() - INVITATION_TIMEOUT_MS,
'+inf',
'BYSCORE');
if (invitations.includes(targetUser.id)) {
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
- const game = await this.matched(targetUser.id, me.id);
+ const game = await this.matched(targetUser.id, me.id, {
+ noIrregularRules: false,
+ });
return game;
- } else {
- this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
+ }
+ //#endregion
- this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
- user: await this.userEntityService.pack(me, targetUser),
- });
+ const redisPipeline = this.redisClient.pipeline();
+ redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
+ redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX');
+ await redisPipeline.exec();
- return null;
- }
+ this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
+ user: await this.userEntityService.pack(me, targetUser),
+ });
+
+ return null;
}
@bindThis
- public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
+ public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> {
+ if (!multiple) {
+ // 既にマッチしている対局が無いか探す(3分以内)
+ const games = await this.reversiGamesRepository.find({
+ where: [
+ { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
+ { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
+ ],
+ relations: ['user1', 'user2'],
+ order: { id: 'DESC' },
+ });
+ if (games.length > 0) {
+ return games[0];
+ }
+ }
+
//#region まず自分宛ての招待を探す
const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${me.id}`,
- Date.now() - MATCHING_TIMEOUT_MS,
+ Date.now() - INVITATION_TIMEOUT_MS,
'+inf',
'BYSCORE');
@@ -131,7 +168,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
- const game = await this.matched(invitorId, me.id);
+ const game = await this.matched(invitorId, me.id, {
+ noIrregularRules: false,
+ });
return game;
}
@@ -139,23 +178,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const matchings = await this.redisClient.zrange(
'reversi:matchAny',
- Date.now() - MATCHING_TIMEOUT_MS,
- '+inf',
- 'BYSCORE');
+ 0,
+ 2, // 自分自身のIDが入っている場合もあるので2つ取得
+ 'REV');
- const userIds = matchings.filter(id => id !== me.id);
+ const items = matchings.filter(id => !id.startsWith(me.id));
- if (userIds.length > 0) {
- // pick random
- const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
+ if (items.length > 0) {
+ const [matchedUserId, option] = items[0].split(':');
- await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
+ await this.redisClient.zrem('reversi:matchAny',
+ me.id,
+ matchedUserId,
+ me.id + ':noIrregularRules',
+ matchedUserId + ':noIrregularRules');
- const game = await this.matched(matchedUserId, me.id);
+ const game = await this.matched(matchedUserId, me.id, {
+ noIrregularRules: options.noIrregularRules || option === 'noIrregularRules',
+ });
return game;
} else {
- await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
+ const redisPipeline = this.redisClient.pipeline();
+ if (options.noIrregularRules) {
+ redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules');
+ } else {
+ redisPipeline.zadd('reversi:matchAny', Date.now(), me.id);
+ }
+ redisPipeline.expire('reversi:matchAny', 15, 'NX');
+ await redisPipeline.exec();
return null;
}
}
@@ -167,7 +218,15 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async matchAnyUserCancel(user: MiUser) {
- await this.redisClient.zrem('reversi:matchAny', user.id);
+ await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules');
+ }
+
+ @bindThis
+ public async cleanOutdatedGames() {
+ await this.reversiGamesRepository.delete({
+ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
+ isStarted: false,
+ });
}
@bindThis
@@ -221,7 +280,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
+ private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
const game = await this.reversiGamesRepository.insert({
id: this.idService.gen(),
user1Id: parentId,
@@ -234,6 +293,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: Reversi.maps.eighteight.data,
bw: 'random',
isLlotheo: false,
+ noIrregularRules: options.noIrregularRules,
}).then(x => this.reversiGamesRepository.findOneOrFail({
where: { id: x.identifiers[0].id },
relations: ['user1', 'user2'],
@@ -255,7 +315,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw = parseInt(game.bw, 10);
}
- const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
+ const engine = new Reversi.Game(game.map, {
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard,
+ });
+
+ const crc32 = engine.calcCrc32().toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({
@@ -276,12 +342,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.cacheGame(updatedGame);
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
- const engine = new Reversi.Game(updatedGame.map, {
- isLlotheo: updatedGame.isLlotheo,
- canPutEverywhere: updatedGame.canPutEverywhere,
- loopedBoard: updatedGame.loopedBoard,
- });
-
if (engine.isEnded) {
let winnerId;
if (engine.winner === true) {
@@ -335,7 +395,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${user.id}`,
- Date.now() - MATCHING_TIMEOUT_MS,
+ Date.now() - INVITATION_TIMEOUT_MS,
'+inf',
'BYSCORE');
return invitations;
@@ -406,7 +466,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const serializeLogs = Reversi.Serializer.serializeLogs(logs);
- const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
+ const crc32 = engine.calcCrc32().toString();
const updatedGame = {
...game,
@@ -538,7 +598,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (game == null) throw new Error('game not found');
if (crc32.toString() !== game.crc32) {
- return await this.reversiGameEntityService.packDetail(game);
+ return game;
} else {
return null;
}