wip
This commit is contained in:
18
packages/backend/migration/1705654039457-reversi-2.js
Normal file
18
packages/backend/migration/1705654039457-reversi-2.js
Normal 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"`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}[];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user