Commit 80632e78 authored by nanahira's avatar nanahira

swiss

parent 485d2019
Pipeline #19577 passed with stages
in 5 minutes and 21 seconds
...@@ -124,6 +124,10 @@ export class Match extends IdBase() { ...@@ -124,6 +124,10 @@ export class Match extends IdBase() {
return this.player1Id === id || this.player2Id === id; return this.player1Id === id || this.player2Id === id;
} }
opponentId(id: number) {
return this.player1Id === id ? this.player2Id : this.player1Id;
}
clone() { clone() {
const match = new Match(); const match = new Match();
Object.assign(match, this); Object.assign(match, this);
......
...@@ -5,9 +5,9 @@ import { InjectRepository } from '@nestjs/typeorm'; ...@@ -5,9 +5,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { import {
Tournament, Tournament,
TournamentRule,
TournamentStatus, TournamentStatus,
} from '../tournament/entities/Tournament.entity'; } from '../tournament/entities/Tournament.entity';
import { Participant } from '../participant/entities/participant.entity';
import { TournamentService } from '../tournament/tournament.service'; import { TournamentService } from '../tournament/tournament.service';
import { MoreThan } from 'typeorm'; import { MoreThan } from 'typeorm';
...@@ -75,6 +75,15 @@ export class MatchService extends CrudService(Match, { ...@@ -75,6 +75,15 @@ export class MatchService extends CrudService(Match, {
'比赛已结束,无法修改。', '比赛已结束,无法修改。',
).toException(); ).toException();
} }
if (
dto.winnerId === null &&
match.tournament.rule !== TournamentRule.Swiss
) {
throw new BlankReturnMessageDto(
400,
'非瑞士轮对局胜者不能为空。',
).toException();
}
if (dto.winnerId !== undefined) { if (dto.winnerId !== undefined) {
dto.status = MatchStatus.Finished; dto.status = MatchStatus.Finished;
} }
......
...@@ -42,7 +42,7 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -42,7 +42,7 @@ export class ParticipantService extends CrudService(Participant, {
dto: Partial<Participant>, dto: Partial<Participant>,
user: MycardUser, user: MycardUser,
) { ) {
await this.checkPermissionOfParticipant(id, user); await this.checkPermissionOfParticipant(id, user, true);
return this.update(id, dto); return this.update(id, dto);
} }
...@@ -51,7 +51,11 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -51,7 +51,11 @@ export class ParticipantService extends CrudService(Participant, {
return this.delete(id); return this.delete(id);
} }
async checkPermissionOfParticipant(id: number, user: MycardUser) { async checkPermissionOfParticipant(
id: number,
user: MycardUser,
allowRunning = false,
) {
const participant = await this.repo.findOne({ const participant = await this.repo.findOne({
where: { id }, where: { id },
select: { select: {
...@@ -68,7 +72,16 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -68,7 +72,16 @@ export class ParticipantService extends CrudService(Participant, {
if (!participant?.tournament) { if (!participant?.tournament) {
throw new BlankReturnMessageDto(404, '未找到该参赛者。').toException(); throw new BlankReturnMessageDto(404, '未找到该参赛者。').toException();
} }
if (participant.tournament.status !== TournamentStatus.Ready) { if (participant.tournament.status === TournamentStatus.Finished) {
throw new BlankReturnMessageDto(
400,
'比赛已结束,无法修改参赛者。',
).toException();
}
if (
!allowRunning &&
participant.tournament.status !== TournamentStatus.Ready
) {
throw new BlankReturnMessageDto( throw new BlankReturnMessageDto(
400, 400,
'比赛已开始,无法修改参赛者。', '比赛已开始,无法修改参赛者。',
...@@ -80,7 +93,7 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -80,7 +93,7 @@ export class ParticipantService extends CrudService(Participant, {
async importParticipants(participants: Participant[], user: MycardUser) { async importParticipants(participants: Participant[], user: MycardUser) {
return this.importEntities(participants, async (p) => { return this.importEntities(participants, async (p) => {
try { try {
await this.tournamentService.checkPermissionOfTournament( await this.tournamentService.canModifyParticipants(
p.tournamentId, p.tournamentId,
user, user,
); );
......
...@@ -7,10 +7,16 @@ import { ...@@ -7,10 +7,16 @@ import {
} from '../participant/entities/participant.entity'; } from '../participant/entities/participant.entity';
export class TournamentRuleBase { export class TournamentRuleBase {
protected participantMap = new Map<number, Participant>();
constructor( constructor(
protected tournament: Tournament, protected tournament: Tournament,
protected repo?: Repository<Match>, protected repo?: Repository<Match>,
) {} ) {
this.tournament.participants?.forEach((p) =>
this.participantMap.set(p.id, p),
);
}
async saveMatch(matches: Match[]) { async saveMatch(matches: Match[]) {
matches.forEach((m) => (m.tournamentId = this.tournament.id)); matches.forEach((m) => (m.tournamentId = this.tournament.id));
...@@ -20,7 +26,11 @@ export class TournamentRuleBase { ...@@ -20,7 +26,11 @@ export class TournamentRuleBase {
currentRoundCount() { currentRoundCount() {
return Math.max( return Math.max(
...this.tournament.matches ...this.tournament.matches
.filter((m) => m.status === MatchStatus.Finished) .filter(
(m) =>
m.status === MatchStatus.Finished ||
m.status === MatchStatus.Running,
)
.map((m) => m.round), .map((m) => m.round),
); );
} }
...@@ -53,4 +63,8 @@ export class TournamentRuleBase { ...@@ -53,4 +63,8 @@ export class TournamentRuleBase {
).length, ).length,
}; };
} }
participantScoreAfter(participant: Participant): Partial<ParticipantScore> {
return {};
}
} }
import { TournamentRule } from '../tournament/entities/Tournament.entity'; import { TournamentRule } from '../tournament/entities/Tournament.entity';
import { SingleElimination } from './rules/single-elimination'; import { SingleElimination } from './rules/single-elimination';
import { TournamentRuleBase } from './base'; import { TournamentRuleBase } from './base';
import { Swiss } from './rules/swiss';
export const TournamentRules = new Map< export const TournamentRules = new Map<
TournamentRule, TournamentRule,
...@@ -8,3 +9,4 @@ export const TournamentRules = new Map< ...@@ -8,3 +9,4 @@ export const TournamentRules = new Map<
>(); >();
TournamentRules.set(<TournamentRule>'SingleElimination', SingleElimination); TournamentRules.set(<TournamentRule>'SingleElimination', SingleElimination);
TournamentRules.set(<TournamentRule>'Swiss', Swiss);
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
Participant, Participant,
ParticipantScore, ParticipantScore,
} from '../../participant/entities/participant.entity'; } from '../../participant/entities/participant.entity';
import _ from 'lodash';
export class SingleElimination extends TournamentRuleBase { export class SingleElimination extends TournamentRuleBase {
totalRoundCount() { totalRoundCount() {
...@@ -24,14 +25,16 @@ export class SingleElimination extends TournamentRuleBase { ...@@ -24,14 +25,16 @@ export class SingleElimination extends TournamentRuleBase {
]); ]);
if (i === 1) { if (i === 1) {
// add participants to first round // add participants to first round
const { participants } = this.tournament; const participants = _.sortBy(
this.tournament.participants,
(p) => -p.id,
);
const neededMatchesCount = const neededMatchesCount =
participants.length - nextRoundMatches.length * 2; participants.length - nextRoundMatches.length * 2;
matches = matches.slice(0, neededMatchesCount); matches = matches.slice(0, neededMatchesCount);
const participantsToAdd = [...participants].reverse();
for (const match of matches) { for (const match of matches) {
match.player1Id = participantsToAdd.pop().id; match.player1Id = participants.pop().id;
match.player2Id = participantsToAdd.pop().id; match.player2Id = participants.pop().id;
match.status = MatchStatus.Running; match.status = MatchStatus.Running;
} }
} }
...@@ -49,7 +52,7 @@ export class SingleElimination extends TournamentRuleBase { ...@@ -49,7 +52,7 @@ export class SingleElimination extends TournamentRuleBase {
const thirdPlaceMatch = new Match(); const thirdPlaceMatch = new Match();
thirdPlaceMatch.isThirdPlaceMatch = true; thirdPlaceMatch.isThirdPlaceMatch = true;
thirdPlaceMatch.round = roundCount; thirdPlaceMatch.round = roundCount;
await this.repo.save(thirdPlaceMatch); await this.saveMatch([thirdPlaceMatch]);
} }
} }
...@@ -65,7 +68,7 @@ export class SingleElimination extends TournamentRuleBase { ...@@ -65,7 +68,7 @@ export class SingleElimination extends TournamentRuleBase {
.reverse(); .reverse();
const nextRoundCount = this.nextRoundCount(); const nextRoundCount = this.nextRoundCount();
const matches = this.specificMatches(MatchStatus.Pending).filter( const matches = this.specificMatches(MatchStatus.Pending).filter(
(m) => m.round === nextRoundCount, (m) => m.round === nextRoundCount && !m.isThirdPlaceMatch,
); );
for (const match of matches) { for (const match of matches) {
match.player1Id = survivedParticipants.pop().id; match.player1Id = survivedParticipants.pop().id;
......
import { TournamentRuleBase } from '../base';
import { RuleSettings } from '../../tournament/entities/Tournament.entity';
import { Match, MatchStatus } from '../../match/entities/match.entity';
import _ from 'lodash';
import {
Participant,
ParticipantScore,
} from '../../participant/entities/participant.entity';
export class Swiss extends TournamentRuleBase {
private settings: Partial<RuleSettings> = {
rounds:
this.tournament.ruleSettings?.rounds ??
Math.ceil(Math.log2(this.tournament.participants?.length || 2)),
winScore: this.tournament.ruleSettings?.winScore ?? 3,
drawScore: this.tournament.ruleSettings?.drawScore ?? 1,
byeScore: this.tournament.ruleSettings?.byeScore ?? 3,
};
async initialize() {
const matchCountPerRound = Math.floor(
this.tournament.participants.length / 2,
);
const allMatches: Match[] = [];
for (let r = 1; r < this.settings.rounds; ++r) {
const matches: Match[] = [];
for (let i = 0; i < matchCountPerRound; ++i) {
const match = new Match();
match.round = r;
match.status = MatchStatus.Pending;
matches.push(match);
}
if (r === 1) {
const participants = _.sortBy(
this.tournament.participants,
(p) => -p.id,
);
for (const match of matches) {
match.status = MatchStatus.Running;
match.player1Id = participants.pop().id;
match.player2Id = participants.pop().id;
}
}
allMatches.push(...matches);
}
await this.saveMatch(allMatches);
}
nextRound(): Partial<Match[]> {
this.tournament.calculateScore();
const participants = this.tournament.participants
.filter((p) => !p.quit)
.reverse();
const nextRoundCount = this.nextRoundCount();
const matches = this.tournament.matches.filter(
(m) => m.round === nextRoundCount,
);
for (const match of matches) {
match.status = MatchStatus.Running;
match.player1Id = participants.pop()?.id;
match.player2Id = participants.pop()?.id;
if (!match.player1Id || !match.player2Id) {
match.status = MatchStatus.Abandoned;
match.player1Id = null;
match.player2Id = null;
}
}
return matches;
}
participantScore(participant: Participant): Partial<ParticipantScore> {
const data = super.participantScore(participant);
let bye = 0;
for (let i = 1; i <= this.currentRoundCount(); ++i) {
if (
!this.tournament.matches.some(
(m) => m.round === i && m.participated(participant.id),
)
) {
++bye;
}
}
return {
...data,
bye,
score:
data.win * this.settings.winScore +
data.draw * this.settings.drawScore +
bye * this.settings.byeScore,
};
}
participantScoreAfter(participant: Participant): Partial<ParticipantScore> {
const opponentIds = this.specificMatches(MatchStatus.Finished)
.filter((m) => m.participated(participant.id))
.map((m) => m.opponentId(participant.id));
const opponents = opponentIds.map((id) => this.participantMap.get(id));
return {
tieBreaker: _.sumBy(opponents, (p) => p.score.score),
};
}
}
...@@ -194,6 +194,10 @@ export class Tournament extends DescBase { ...@@ -194,6 +194,10 @@ export class Tournament extends DescBase {
p.score = new ParticipantScore(); p.score = new ParticipantScore();
Object.assign(p.score, rule.participantScore(p)); Object.assign(p.score, rule.participantScore(p));
}); });
this.participants.forEach((p) => {
const editScore = rule.participantScoreAfter(p);
Object.assign(p.score, editScore);
});
this.participants = _.sortBy( this.participants = _.sortBy(
this.participants, this.participants,
(p) => -p.score.score, (p) => -p.score.score,
......
...@@ -112,6 +112,12 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -112,6 +112,12 @@ export class TournamentService extends CrudService(Tournament, {
'比赛已经开始,不能重复开始。', '比赛已经开始,不能重复开始。',
).toException(); ).toException();
} }
if (tournament.participants.length < 2) {
throw new BlankReturnMessageDto(
403,
'参赛者数量不足,不能开始。',
).toException();
}
await this.repo.manager.transaction(async (edb) => { await this.repo.manager.transaction(async (edb) => {
await tournament.getRuleHandler(edb.getRepository(Match)).initialize(); await tournament.getRuleHandler(edb.getRepository(Match)).initialize();
await edb.update(Tournament, id, { status: TournamentStatus.Running }); await edb.update(Tournament, id, { status: TournamentStatus.Running });
......
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