This commit is contained in:
syuilo
2024-01-19 18:35:53 +09:00
parent 4c43ee4b53
commit 353098f576
27 changed files with 872 additions and 822 deletions

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Reversi21705654039457 {
name = 'Reversi21705654039457'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`);
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`);
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`);
}
}

View File

@@ -170,9 +170,7 @@ export interface ReversiEventTypes {
}
export interface ReversiGameEventTypes {
accept: boolean;
cancelAccept: undefined;
changeAcceptingStates: {
changeReadyStates: {
user1: boolean;
user2: boolean;
};
@@ -181,7 +179,7 @@ export interface ReversiGameEventTypes {
value: any;
};
putStone: {
at: Date;
at: number;
color: boolean;
pos: number;
next: boolean;

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import * as CRC32 from 'crc-32';
import CRC32 from 'crc-32';
import { ModuleRef } from '@nestjs/core';
import * as Reversi from 'misskey-reversi';
import { IsNull } from 'typeorm';
@@ -28,7 +28,7 @@ import { NotificationService } from '@/core/NotificationService.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
const MATCHING_TIMEOUT_MS = 15 * 1000; // 15sec
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
@Injectable()
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@@ -61,7 +61,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
throw new Error('You cannot match yourself.');
}
const invitations = await this.redisClient.zrange(`reversi:matchSpecific:${me.id}`, Date.now() - MATCHING_TIMEOUT_MS, '+inf');
const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${me.id}`,
Date.now() - MATCHING_TIMEOUT_MS,
'+inf',
'BYSCORE');
if (invitations.includes(targetUser.id)) {
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
@@ -70,8 +74,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
id: this.idService.gen(),
user1Id: targetUser.id,
user2Id: me.id,
user1Accepted: false,
user2Accepted: false,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
@@ -97,21 +101,26 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
const scanRes = await this.redisClient.scan(0, 'MATCH', 'reversi:matchAny:*', 'COUNT', 10);
const userIds = scanRes[1].map(key => key.split(':')[2]).filter(id => id !== me.id);
const matchings = await this.redisClient.zrange(
'reversi:matchAny',
Date.now() - MATCHING_TIMEOUT_MS,
'+inf',
'BYSCORE');
const userIds = matchings.filter(id => id !== me.id);
if (userIds.length > 0) {
// pick random
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
await this.redisClient.del(`reversi:matchAny:${matchedUserId}`);
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
const game = await this.reversiGamesRepository.insert({
id: this.idService.gen(),
user1Id: matchedUserId,
user2Id: me.id,
user1Accepted: false,
user2Accepted: false,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
@@ -125,7 +134,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
return game;
} else {
await this.redisClient.setex(`reversi:matchAny:${me.id}`, MATCHING_TIMEOUT_MS / 1000, '');
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
return null;
}
}
@@ -137,47 +146,47 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async matchAnyUserCancel(user: MiUser) {
await this.redisClient.del(`reversi:matchAny:${user.id}`);
await this.redisClient.zrem('reversi:matchAny', user.id);
}
@bindThis
public async matchAccept(game: MiReversiGame, user: MiUser, isAccepted: boolean) {
public async gameReady(game: MiReversiGame, user: MiUser, ready: boolean) {
if (game.isStarted) return;
let bothAccepted = false;
let isBothReady = false;
if (game.user1Id === user.id) {
await this.reversiGamesRepository.update(game.id, {
user1Accepted: isAccepted,
user1Ready: ready,
});
this.globalEventService.publishReversiGameStream(game.id, 'changeAcceptingStates', {
user1: isAccepted,
user2: game.user2Accepted,
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
user1: ready,
user2: game.user2Ready,
});
if (isAccepted && game.user2Accepted) bothAccepted = true;
if (ready && game.user2Ready) isBothReady = true;
} else if (game.user2Id === user.id) {
await this.reversiGamesRepository.update(game.id, {
user2Accepted: isAccepted,
user2Ready: ready,
});
this.globalEventService.publishReversiGameStream(game.id, 'changeAcceptingStates', {
user1: game.user1Accepted,
user2: isAccepted,
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
user1: game.user1Ready,
user2: ready,
});
if (isAccepted && game.user1Accepted) bothAccepted = true;
if (ready && game.user1Ready) isBothReady = true;
} else {
return;
}
if (bothAccepted) {
// 3秒後、まだacceptされていたらゲーム開始
if (isBothReady) {
// 3秒後、両者readyならゲーム開始
setTimeout(async () => {
const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id });
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
if (!freshGame.user1Ready || !freshGame.user2Ready) return;
let bw: number;
if (freshGame.bw === 'random') {
@@ -239,7 +248,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
const invitations = await this.redisClient.zrange(`reversi:matchSpecific:${user.id}`, Date.now() - MATCHING_TIMEOUT_MS, '+inf');
const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${user.id}`,
Date.now() - MATCHING_TIMEOUT_MS,
'+inf',
'BYSCORE');
return invitations;
}
@@ -247,8 +260,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) {
if (game.isStarted) return;
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
if ((game.user1Id === user.id) && game.user1Accepted) return;
if ((game.user2Id === user.id) && game.user2Accepted) 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;
@@ -301,7 +314,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
const log = {
at: new Date(),
at: Date.now(),
color: myColor,
pos,
};
@@ -317,9 +330,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
logs: game.logs,
});
this.globalEventService.publishReversiGameStream(game.id, 'putStone', Object.assign(log, {
this.globalEventService.publishReversiGameStream(game.id, 'putStone', {
...log,
next: o.turn,
}));
});
if (o.isEnded) {
this.globalEventService.publishReversiGameStream(game.id, 'ended', {

View File

@@ -41,8 +41,8 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Accepted: game.user1Accepted,
user2Accepted: game.user2Accepted,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me),
@@ -56,7 +56,7 @@ export class ReversiGameEntityService {
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
logs: game.logs.map(log => ({
at: log.at.toISOString(),
at: log.at,
color: log.color,
pos: log.pos,
})),
@@ -87,8 +87,8 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Accepted: game.user1Accepted,
user2Accepted: game.user2Accepted,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me),

View File

@@ -34,12 +34,12 @@ export class MiReversiGame {
@Column('boolean', {
default: false,
})
public user1Accepted: boolean;
public user1Ready: boolean;
@Column('boolean', {
default: false,
})
public user2Accepted: boolean;
public user2Ready: boolean;
/**
* どちらのプレイヤーが先行(黒)か
@@ -77,7 +77,7 @@ export class MiReversiGame {
default: [],
})
public logs: {
at: Date;
at: number;
color: boolean;
pos: number;
}[];

View File

@@ -37,11 +37,11 @@ export const packedReversiGameLiteSchema = {
type: 'any',
optional: false, nullable: true,
},
user1Accepted: {
user1Ready: {
type: 'boolean',
optional: false, nullable: false,
},
user2Accepted: {
user2Ready: {
type: 'boolean',
optional: false, nullable: false,
},
@@ -137,11 +137,11 @@ export const packedReversiGameDetailedSchema = {
type: 'any',
optional: false, nullable: true,
},
user1Accepted: {
user1Ready: {
type: 'boolean',
optional: false, nullable: false,
},
user2Accepted: {
user2Ready: {
type: 'boolean',
optional: false, nullable: false,
},
@@ -208,9 +208,8 @@ export const packedReversiGameDetailedSchema = {
optional: false, nullable: false,
properties: {
at: {
type: 'string',
type: 'number',
optional: false, nullable: false,
format: 'date-time',
},
color: {
type: 'boolean',

View File

@@ -22,9 +22,13 @@ import { SigninApiService } from './api/SigninApiService.js';
import { SigninService } from './api/SigninService.js';
import { SignupApiService } from './api/SignupApiService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import { MainChannelService } from './api/stream/channels/main.js';
import { AdminChannelService } from './api/stream/channels/admin.js';
import { AntennaChannelService } from './api/stream/channels/antenna.js';
@@ -38,10 +42,9 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
@Module({
imports: [
@@ -77,6 +80,8 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
GlobalTimelineChannelService,
HashtagChannelService,
RoleTimelineChannelService,
ReversiChannelService,
ReversiGameChannelService,
HomeTimelineChannelService,
HybridTimelineChannelService,
LocalTimelineChannelService,

View File

@@ -43,8 +43,7 @@ class ReversiGameChannel extends Channel {
@bindThis
public onMessage(type: string, body: any) {
switch (type) {
case 'accept': this.accept(true); break;
case 'cancelAccept': this.accept(false); break;
case 'ready': this.ready(body); break;
case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'putStone': this.putStone(body.pos); break;
case 'syncState': this.syncState(body.crc32); break;
@@ -63,13 +62,13 @@ class ReversiGameChannel extends Channel {
}
@bindThis
private async accept(accept: boolean) {
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.matchAccept(game, this.user, accept);
this.reversiService.gameReady(game, this.user, ready);
}
@bindThis