Commit d4adb53d authored by nanahira's avatar nanahira

update seq things again

parent 746351da
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
} from '../tournament/entities/Tournament.entity'; } from '../tournament/entities/Tournament.entity';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { TournamentService } from '../tournament/tournament.service'; import { TournamentService } from '../tournament/tournament.service';
import { normalSeq } from '../utility/normal-seq';
@Injectable() @Injectable()
export class ParticipantService extends CrudService(Participant, { export class ParticipantService extends CrudService(Participant, {
...@@ -97,6 +98,7 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -97,6 +98,7 @@ export class ParticipantService extends CrudService(Participant, {
participants: Participant[], participants: Participant[],
user: MycardUser | number, user: MycardUser | number,
) { ) {
normalSeq(participants);
return this.importEntities(participants, async (p) => { return this.importEntities(participants, async (p) => {
try { try {
await this.tournamentService.canModifyParticipants( await this.tournamentService.canModifyParticipants(
......
import { IsInt, IsPositive } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class DragParticipantDto {
@IsInt()
@IsPositive()
@ApiProperty({
description: '正在拖动的参赛者 ID',
required: true,
})
draggingParticipantId: number;
@IsInt()
@ApiProperty({
description: '放置在哪个参赛者后面,0 代表『放在最前面』',
})
placeAfterParticipantId: number;
}
...@@ -16,6 +16,14 @@ export class ParticipantSourceDto { ...@@ -16,6 +16,14 @@ export class ParticipantSourceDto {
}) })
tournamentId?: number; tournamentId?: number;
@IsOptional()
@IsInt()
@IsPositive()
@ApiProperty({
description: '导入瑞士轮比赛的时候,取前 n 名选手(必须是 2 的幂)',
})
swissMaxPlayers?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
......
import { PickType } from '@nestjs/swagger';
import { Participant } from '../../participant/entities/participant.entity';
export class UpdateParticipantSeqResultDto extends PickType(Participant, [
'id',
'seq',
]) {}
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
import { TournamentService } from './tournament.service'; import { TournamentService } from './tournament.service';
import { import {
ApiError, ApiError,
ApiTypeResponse,
BlankReturnMessageDto, BlankReturnMessageDto,
DataBody, DataBody,
RestfulFactory, RestfulFactory,
...@@ -28,6 +29,8 @@ import { ParticipantService } from '../participant/participant.service'; ...@@ -28,6 +29,8 @@ import { ParticipantService } from '../participant/participant.service';
import { FilesInterceptor } from '@nestjs/platform-express'; import { FilesInterceptor } from '@nestjs/platform-express';
import { multerToParticipant } from '../utility/multer-to-participant'; import { multerToParticipant } from '../utility/multer-to-participant';
import { ParticipantSourceDto } from './dto/participant-source.dto'; import { ParticipantSourceDto } from './dto/participant-source.dto';
import { UpdateParticipantSeqResultDto } from './dto/update-participant-seq-result.dto';
import { DragParticipantDto } from './dto/drag-participant.dto';
const factory = new RestfulFactory(Tournament, { const factory = new RestfulFactory(Tournament, {
relations: [ relations: [
...@@ -201,11 +204,24 @@ export class TournamentController { ...@@ -201,11 +204,24 @@ export class TournamentController {
@HttpCode(200) @HttpCode(200)
@ApiOperation({ summary: '打乱比赛选手' }) @ApiOperation({ summary: '打乱比赛选手' })
@ApiParam({ name: 'id', description: 'Tournament ID' }) @ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto }) @ApiTypeResponse(UpdateParticipantSeqResultDto)
async shuffleParticipants( async shuffleParticipants(
@factory.idParam() id: number, @factory.idParam() id: number,
@PutMycardUser() user: MycardUser, @PutMycardUser() user: MycardUser,
) { ) {
return this.tournamentService.shuffleParticipants(id, user); return this.tournamentService.shuffleParticipants(id, user);
} }
@Post(':id/drag-participant')
@HttpCode(200)
@ApiOperation({ summary: '拖动比赛选手' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiTypeResponse(UpdateParticipantSeqResultDto)
async dragParticipant(
@factory.idParam() id: number,
@DataBody() dto: DragParticipantDto,
@PutMycardUser() user: MycardUser,
) {
return this.tournamentService.dragParticipant(id, dto, user);
}
} }
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { BlankReturnMessageDto, CrudService } from 'nicot'; import {
import { Tournament, TournamentStatus } from './entities/Tournament.entity'; BlankReturnMessageDto,
CrudService,
GenericReturnMessageDto,
} from 'nicot';
import {
Tournament,
TournamentRule,
TournamentStatus,
} from './entities/Tournament.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { Match, MatchStatus } from '../match/entities/match.entity'; import { Match, MatchStatus } from '../match/entities/match.entity';
...@@ -9,13 +17,17 @@ import { Participant } from '../participant/entities/participant.entity'; ...@@ -9,13 +17,17 @@ import { Participant } from '../participant/entities/participant.entity';
import { ParticipantService } from '../participant/participant.service'; import { ParticipantService } from '../participant/participant.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { ParticipantSourceDto } from './dto/participant-source.dto'; import { ParticipantSourceDto } from './dto/participant-source.dto';
import { pick } from 'lodash'; import _, { pick } from 'lodash';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { filter, lastValueFrom, mergeMap, tap } from 'rxjs'; import { filter, lastValueFrom, mergeMap, tap } from 'rxjs';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { parseZipStream } from '../utility/parse-zip-stream'; import { parseZipStream } from '../utility/parse-zip-stream';
import YGOProDeck from 'ygopro-deck-encode'; import YGOProDeck from 'ygopro-deck-encode';
import { shuffleArray } from '../utility/shuffle-array'; import { shuffleArray } from '../utility/shuffle-array';
import { DragParticipantDto } from './dto/drag-participant.dto';
import { dragRearrange } from '../utility/drag-rearrange';
import { normalSeq } from '../utility/normal-seq';
import { sortAfterSwiss } from '../utility/soft-after-swiss';
@Injectable() @Injectable()
export class TournamentService extends CrudService(Tournament, { export class TournamentService extends CrudService(Tournament, {
...@@ -251,14 +263,39 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -251,14 +263,39 @@ export class TournamentService extends CrudService(Tournament, {
) { ) {
let participants: Participant[] = []; let participants: Participant[] = [];
if (dto.tournamentId) { if (dto.tournamentId) {
const tournament = await this.getTournament(dto.tournamentId, user); const { data: tournament } = await this.getTournament(
participants = tournament.data.participants.map((p) => dto.tournamentId,
user,
);
participants = tournament.participants.map((p) =>
Object.assign(new Participant(), { Object.assign(new Participant(), {
...pick(p, 'name', 'deckbuf'), ...pick(p, 'name', 'deckbuf', 'seq'),
tournamentId: id, tournamentId: id,
quit: false, quit: false,
} as Partial<Participant>), } as Partial<Participant>),
); );
if (dto.swissMaxPlayers) {
// check if swissMaxPlayers is power of 2
if (
dto.swissMaxPlayers < 2 ||
(dto.swissMaxPlayers & (dto.swissMaxPlayers - 1)) !== 0
) {
throw new BlankReturnMessageDto(
400,
'瑞士轮比赛的选手数量必须是 2 的幂。',
).toException();
}
if (
tournament.rule !== TournamentRule.Swiss &&
tournament.status === TournamentStatus.Ready
) {
throw new BlankReturnMessageDto(
404,
'只能从瑞士轮比赛导入选手。',
).toException();
}
participants = sortAfterSwiss(participants.slice(dto.swissMaxPlayers));
}
} else if (dto.ygobbsCompt) { } else if (dto.ygobbsCompt) {
const comptId = dto.getYgobbsComptId(); const comptId = dto.getYgobbsComptId();
if (!comptId) { if (!comptId) {
...@@ -298,9 +335,6 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -298,9 +335,6 @@ export class TournamentService extends CrudService(Tournament, {
.pop() .pop()
.replace(/(\.ydk)+$/i, ''); .replace(/(\.ydk)+$/i, '');
participant.tournamentId = id; participant.tournamentId = id;
participant.quit = false;
participant.seq = 1000;
participant.deckbuf = undefined; participant.deckbuf = undefined;
participant['_invalid'] = reason; participant['_invalid'] = reason;
participants.push(participant); participants.push(participant);
...@@ -340,8 +374,6 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -340,8 +374,6 @@ export class TournamentService extends CrudService(Tournament, {
const text = file.content.toString('utf-8'); const text = file.content.toString('utf-8');
const participant = new Participant(); const participant = new Participant();
participant.quit = false; participant.quit = false;
participant.seq = 1000;
participant.name = file.path participant.name = file.path
.split('/') .split('/')
.pop() .pop()
...@@ -361,6 +393,31 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -361,6 +393,31 @@ export class TournamentService extends CrudService(Tournament, {
return this.participantService.importParticipants(participants, user); return this.participantService.importParticipants(participants, user);
} }
private async updateParticipantSeq<T extends number | string>(
updates: { id: T; seq: number }[],
) {
if (updates.length) {
await this.repo.manager
.createQueryBuilder()
.update(Participant)
.set({
seq: () => `
CASE id
${updates.map((p) => `WHEN ${p.id} THEN ${p.seq}`).join('\n')}
END
`,
})
.whereInIds(updates.map((p) => p.id))
.execute();
}
return new GenericReturnMessageDto(
200,
'success',
updates.map((p) => pick(p, 'id', 'seq')),
);
}
async shuffleParticipants(id: number, user: MycardUser | number) { async shuffleParticipants(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament( const tournament = await this.checkPermissionOfTournament(
id, id,
...@@ -374,7 +431,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -374,7 +431,7 @@ export class TournamentService extends CrudService(Tournament, {
'比赛已经开始,不能打乱参赛者。', '比赛已经开始,不能打乱参赛者。',
).toException(); ).toException();
} }
if (!tournament.participants.length) { if (tournament.participants.length < 2) {
throw new BlankReturnMessageDto( throw new BlankReturnMessageDto(
404, 404,
'没有选手,不能打乱参赛者。', '没有选手,不能打乱参赛者。',
...@@ -383,25 +440,44 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -383,25 +440,44 @@ export class TournamentService extends CrudService(Tournament, {
const participants = [...tournament.participants]; const participants = [...tournament.participants];
// shuffle // shuffle
shuffleArray(participants); shuffleArray(participants);
participants.forEach((p, i) => { normalSeq(participants);
p.seq = 1000 + 100 * i; return this.updateParticipantSeq(participants);
}); }
const ids = participants.map((p) => p.id);
const cases = participants async dragParticipant(
.map((p) => `WHEN ${p.id} THEN ${p.seq}`) id: number,
.join('\n'); dto: DragParticipantDto,
await this.repo.manager user: MycardUser | number,
.createQueryBuilder() ) {
.update(Participant) const tournament = await this.checkPermissionOfTournament(
.set({ id,
seq: () => ` user,
CASE id ['status'],
${cases} ['participants'],
END );
`, if (tournament.status !== TournamentStatus.Ready) {
}) throw new BlankReturnMessageDto(
.whereInIds(ids) 403,
.execute(); '比赛已经开始,不能拖动参赛者。',
return new BlankReturnMessageDto(200, 'success'); ).toException();
}
const participants = _.sortBy(
tournament.participants,
(p) => p.seq,
(p) => p.id,
);
const draggingIndex = participants.findIndex(
(p) => p.id === dto.draggingParticipantId,
);
const insertAfter =
dto.placeAfterParticipantId === 0
? 'front'
: participants.findIndex((p) => p.id === dto.placeAfterParticipantId);
const { updates } = dragRearrange(participants, draggingIndex, insertAfter);
return this.updateParticipantSeq(updates);
} }
} }
export function dragRearrange<T extends { id: any; seq: number }>(
objs: T[],
draggingIndex: number,
insertAfter: number | 'front',
): {
newList: T[];
updates: { id: T['id']; seq: number }[];
} {
const updates: { id: T['id']; seq: number }[] = [];
if (
draggingIndex < 0 ||
draggingIndex >= objs.length ||
draggingIndex === insertAfter
) {
return { newList: objs, updates };
}
const draggingItem = objs[draggingIndex];
const newList = [...objs];
newList.splice(draggingIndex, 1); // 移除拖动项
// 计算插入索引
const insertIndex =
insertAfter === 'front'
? 0
: draggingIndex < insertAfter
? insertAfter
: insertAfter + 1;
newList.splice(insertIndex, 0, draggingItem); // 插入新位置
// 这是一个无效的拖动操作
if (draggingIndex === insertIndex) {
return { newList, updates };
}
// 优先:插到最前面
if (insertIndex === 0) {
const next = newList[1];
const newSeq = next.seq - 100;
draggingItem.seq = newSeq;
updates.push({ id: draggingItem.id, seq: newSeq });
return { newList, updates };
}
// 优先:插到最后面
if (insertIndex === newList.length - 1) {
const prev = newList[newList.length - 2];
const newSeq = prev.seq + 100;
draggingItem.seq = newSeq;
updates.push({ id: draggingItem.id, seq: newSeq });
return { newList, updates };
}
// 其次:相邻交换
const isAdjacent =
insertIndex === draggingIndex - 1 || insertIndex === draggingIndex + 1;
if (isAdjacent) {
const target =
newList[
insertIndex === draggingIndex - 1 ? insertIndex + 1 : insertIndex - 1
];
const tempSeq = draggingItem.seq;
draggingItem.seq = target.seq;
target.seq = tempSeq;
updates.push({ id: draggingItem.id, seq: draggingItem.seq });
updates.push({ id: target.id, seq: target.seq });
return { newList, updates };
}
// 普通插入中间位置:尝试取平均
const prev = newList[insertIndex - 1];
const next = newList[insertIndex + 1];
const gap = next.seq - prev.seq;
if (gap <= 1) {
// 局部重排:按方向只重排一侧
if (insertIndex > newList.length / 2) {
// 插入在后半段:向后重排,从 prev 开始
let baseSeq = prev.seq;
for (let i = insertIndex; i < newList.length; i++) {
baseSeq += 100;
if (newList[i].seq !== baseSeq) {
newList[i].seq = baseSeq;
updates.push({ id: newList[i].id, seq: baseSeq });
}
}
} else {
// 插入在前半段:向前重排,从 next 开始
let baseSeq = next.seq;
for (let i = insertIndex; i >= 0; i--) {
baseSeq -= 100;
if (newList[i].seq !== baseSeq) {
newList[i].seq = baseSeq;
updates.push({ id: newList[i].id, seq: baseSeq });
}
}
}
return { newList, updates };
} else {
const newSeq = Math.floor((prev.seq + next.seq) / 2);
draggingItem.seq = newSeq;
updates.push({ id: draggingItem.id, seq: newSeq });
return { newList, updates };
}
}
export function normalSeq<T extends { seq: number }>(
arr: T[],
initial = 1000,
step = 100,
) {
arr.forEach((item, i) => {
item.seq = initial + step * i;
});
return arr;
}
export function sortAfterSwiss<T>(arr: T[]): T[] {
const n = arr.length;
// 生成种子对阵位置索引(经典对阵排序)
const buildBracket = (seeds: number[]): number[] => {
if (seeds.length <= 2) return seeds;
const half = seeds.length / 2;
const left = buildBracket(seeds.slice(0, half));
const right = buildBracket(seeds.slice(half)).reverse();
return left.flatMap((val, i) => [val, right[i]]);
};
// 创建位置索引,然后用原数组元素替换对应位置
const indices = buildBracket([...Array(n)].map((_, i) => i));
return indices.map((i) => arr[i]);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment