enhance(reversi): more robust matching process
This commit is contained in:
parent
cc420c245f
commit
65557d5f27
9 changed files with 74 additions and 25 deletions
|
@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import * as Reversi from 'misskey-reversi';
|
import * as Reversi from 'misskey-reversi';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull, LessThan } from 'typeorm';
|
||||||
import type {
|
import type {
|
||||||
MiReversiGame,
|
MiReversiGame,
|
||||||
ReversiGamesRepository,
|
ReversiGamesRepository,
|
||||||
|
@ -24,7 +24,7 @@ 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';
|
||||||
|
|
||||||
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
|
const MATCHING_TIMEOUT_MS = 1000 * 10; // 10sec
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
@ -89,11 +89,27 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (targetUser.id === me.id) {
|
||||||
throw new Error('You cannot match yourself.');
|
throw new Error('You cannot match yourself.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!multiple) {
|
||||||
|
// 既にマッチしている対局が無いか探す(3分以内)
|
||||||
|
const games = await this.reversiGamesRepository.find({
|
||||||
|
where: [
|
||||||
|
{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
|
||||||
|
{ id: LessThan(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(
|
const invitations = await this.redisClient.zrange(
|
||||||
`reversi:matchSpecific:${me.id}`,
|
`reversi:matchSpecific:${me.id}`,
|
||||||
Date.now() - MATCHING_TIMEOUT_MS,
|
Date.now() - MATCHING_TIMEOUT_MS,
|
||||||
|
@ -106,19 +122,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
const game = await this.matched(targetUser.id, me.id);
|
const game = await this.matched(targetUser.id, me.id);
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
} else {
|
|
||||||
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
|
||||||
user: await this.userEntityService.pack(me, targetUser),
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||||
|
user: await this.userEntityService.pack(me, targetUser),
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
|
public async matchAnyUser(me: MiUser, multiple = false): Promise<MiReversiGame | null> {
|
||||||
|
if (!multiple) {
|
||||||
|
// 既にマッチしている対局が無いか探す(3分以内)
|
||||||
|
const games = await this.reversiGamesRepository.find({
|
||||||
|
where: [
|
||||||
|
{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
|
||||||
|
{ id: LessThan(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 まず自分宛ての招待を探す
|
//#region まず自分宛ての招待を探す
|
||||||
const invitations = await this.redisClient.zrange(
|
const invitations = await this.redisClient.zrange(
|
||||||
`reversi:matchSpecific:${me.id}`,
|
`reversi:matchSpecific:${me.id}`,
|
||||||
|
@ -169,6 +201,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
await this.redisClient.zrem('reversi:matchAny', user.id);
|
await this.redisClient.zrem('reversi:matchAny', user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async cleanOutdatedGames() {
|
||||||
|
await this.reversiGamesRepository.delete({
|
||||||
|
id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
|
||||||
|
isStarted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
|
public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
|
||||||
const game = await this.get(gameId);
|
const game = await this.get(gameId);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { ReversiService } from '@/core/ReversiService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ export class CleanProcessorService {
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private reversiService: ReversiService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('clean');
|
this.logger = this.queueLoggerService.logger.createSubLogger('clean');
|
||||||
|
@ -65,6 +67,8 @@ export class CleanProcessorService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reversiService.cleanOutdatedGames();
|
||||||
|
|
||||||
this.logger.succ('Cleaned.');
|
this.logger.succ('Cleaned.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
multiple: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -56,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
|
const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple);
|
||||||
|
|
||||||
if (game == null) return;
|
if (game == null) return;
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,9 @@ if ($i) {
|
||||||
const connection = useStream().useChannel('reversi');
|
const connection = useStream().useChannel('reversi');
|
||||||
|
|
||||||
connection.on('matched', x => {
|
connection.on('matched', x => {
|
||||||
startGame(x.game);
|
if (matchingUser.value != null || matchingAny.value) {
|
||||||
|
startGame(x.game);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.on('invited', invitation => {
|
connection.on('invited', invitation => {
|
||||||
|
@ -222,7 +224,7 @@ async function accept(user) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true });
|
useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
misskeyApi('reversi/invitations').then(_invitations => {
|
misskeyApi('reversi/invitations').then(_invitations => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.3
|
* version: 2024.2.0-beta.4
|
||||||
* generatedAt: 2024-01-23T01:22:13.177Z
|
* generatedAt: 2024-01-24T01:14:40.901Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.3
|
* version: 2024.2.0-beta.4
|
||||||
* generatedAt: 2024-01-23T01:22:13.175Z
|
* generatedAt: 2024-01-24T01:14:40.899Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.3
|
* version: 2024.2.0-beta.4
|
||||||
* generatedAt: 2024-01-23T01:22:13.173Z
|
* generatedAt: 2024-01-24T01:14:40.897Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.3
|
* version: 2024.2.0-beta.4
|
||||||
* generatedAt: 2024-01-23T01:22:13.172Z
|
* generatedAt: 2024-01-24T01:14:40.896Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.3
|
* version: 2024.2.0-beta.4
|
||||||
* generatedAt: 2024-01-23T01:22:13.093Z
|
* generatedAt: 2024-01-24T01:14:40.815Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25799,6 +25799,8 @@ export type operations = {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
/** Format: misskey:id */
|
/** Format: misskey:id */
|
||||||
userId?: string | null;
|
userId?: string | null;
|
||||||
|
/** @default false */
|
||||||
|
multiple?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue