Skip to content

Commit

Permalink
feat: 予選試合(PreMatch)の実装
Browse files Browse the repository at this point in the history
  • Loading branch information
laminne committed Jul 8, 2024
1 parent a6f3a09 commit 0060356
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 44 deletions.
9 changes: 5 additions & 4 deletions packages/kcms/src/match/model/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ describe('MainMatch', () => {
// 2か4以外は足せない
if (i == 2 || i == 4) {
expect(() => {
mainMatch.setRunResults(
mainMatch.appendRunResults(
[...Array(2)].map((_, i) => {
return RunResult.new({
id: String(i) as RunResultID,
goalTimeSeconds: i * 10,
points: 10 + i,
// ToDo: Team1かTeam2のどちらかを指定する
teamId: i % 2 == 0 ? args.teamId1 : args.teamId2,
finishState: 'FINISHED',
});
Expand All @@ -57,7 +56,7 @@ describe('MainMatch', () => {
continue;
}
expect(() => {
mainMatch.setRunResults(
mainMatch.appendRunResults(
[...Array(i)].map((_, i) => {
return RunResult.new({
id: String(i) as RunResultID,
Expand Down Expand Up @@ -92,7 +91,9 @@ describe('MainMatch', () => {

const mainMatch = MainMatch.new(args);

expect(mainMatch.setWinnerId('2' as EntryID)).toBe(undefined);
expect(() => mainMatch.setWinnerId('2' as EntryID)).not.toThrow(
new Error('WinnerId is already set')
);
});

it('勝者が決まっているときは変更できない', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/kcms/src/match/model/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ export class MainMatch {
return this.runResults;
}

setRunResults(results: RunResult[]) {
appendRunResults(results: RunResult[]) {
// 1チームが2つずつ結果を持つので、2 または 4個
if (results.length !== 4 && results.length !== 2) {
throw new Error('RunResult length must be 2 or 4');
}
this.runResults = results;
this.runResults.concat(results);
}
}
36 changes: 2 additions & 34 deletions packages/kcms/src/match/model/match.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,8 @@
import { Entry, EntryID } from '../../entry/entry.js';
import { SnowflakeID } from '../../id/main.js';

export type MatchID = SnowflakeID<'Match'>;

/*
## 試合の仕様:
### 予選
- タイムトライアル
- 2試合(左/右)必ず行う
- 2回の試合は(同じ人が)連続して行う
- 左右の合計得点の合計とゴールタイムの合計を記録する
- 得点の上位8チーム(小学生部門: 最大16人, オープン部門: 8人)が本選に出場
- 得点が同点のチームが複数ある場合はゴールタイムで順位を決定する
- ゴールタイムでも決まらない場合はじゃんけんで決定する
- コートは3コート
- 部門は混合で行う(同じコートで小学生部門と部門が同時に試合を行う)
- チームのコートへの配分はエントリー順に行う
### 本選
- トーナメント形式で行う
- 2試合行い、合計得点が高いほうが勝ち
- 同点の場合はじゃんけんで決定
- コートは1コート
- 部門ごとにトーナメントを組む
- 対戦相手の決定は順位順に行う
例: 本選トーナメント
※数字は順位
(略)
_|_ _|_ _|_ _|_
| | | | | | | |
1 2 3 4 5 6 7 8
*/
export type MatchID = SnowflakeID<Match>;

// 対戦するチームのペア L左/R右
export type MatchTeams = {
left: Entry | undefined;
Expand Down
120 changes: 120 additions & 0 deletions packages/kcms/src/match/model/pre.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest';
import { PreMatch, PreMatchID } from './pre.js';
import { EntryID } from '../../entry/entry.js';
import { RunResult, RunResultID } from './runResult.js';

describe('PreMatch', () => {
it('正しく初期化できる', () => {
const args = {
id: '1' as PreMatchID,
courseIndex: 1,
matchIndex: 1,
teamId1: '2' as EntryID,
teamId2: '3' as EntryID,
runResults: [],
};
const res = PreMatch.new(args);

expect(res.getId()).toBe(args.id);
expect(res.getCourseIndex()).toBe(args.courseIndex);
expect(res.getMatchIndex()).toBe(args.matchIndex);
expect(res.getTeamId1()).toBe(args.teamId1);
expect(res.getTeamId2()).toBe(args.teamId2);
expect(res.getRunResults()).toBe(args.runResults);
});

it('走行結果を追加できる', () => {
const args = {
id: '1' as PreMatchID,
courseIndex: 1,
matchIndex: 1,
teamId1: '2' as EntryID,
teamId2: '3' as EntryID,
runResults: [...Array(2)].map((_, i) =>
RunResult.new({
id: String(i) as RunResultID,
goalTimeSeconds: i * 10,
points: 10 + i,
teamId: i % 2 == 0 ? ('2' as EntryID) : ('3' as EntryID),
finishState: 'FINISHED',
})
),
};
const res = PreMatch.new(args);

expect(res.getRunResults().length).toBe(2);
});

it('走行結果は0,1,2個になる', () => {
for (let i = 0; i < 100; i++) {
const args = {
id: '1' as PreMatchID,
courseIndex: 1,
matchIndex: 1,
teamId1: '2' as EntryID,
teamId2: '3' as EntryID,
runResults: [],
};

for (let i = 1; i < 100; i++) {
const mainMatch = PreMatch.new(args);
// 0,1,2以外は足せない
if (i == 0 || i == 1 || i == 2) {
expect(() => {
mainMatch.appendRunResults(
[...Array(i)].map((_, i) => {
return RunResult.new({
id: String(i) as RunResultID,
goalTimeSeconds: i * 10,
points: 10 + i,
teamId: i % 2 == 0 ? args.teamId1 : args.teamId2,
finishState: 'FINISHED',
});
})
);
}).not.toThrow(new Error('RunResult length must be 1 or 2'));
continue;
}
expect(() => {
mainMatch.appendRunResults(
[...Array(i)].map((_, i) => {
return RunResult.new({
id: String(i) as RunResultID,
goalTimeSeconds: i * 10,
points: 10 + i,
teamId: i % 2 == 0 ? args.teamId1 : args.teamId2,
finishState: 'FINISHED',
});
})
);
}).toThrow(new Error('RunResult length must be 1 or 2'));
}
}
});

it('走行結果はチーム1またはチーム2のもの', () => {
const args = {
id: '1' as PreMatchID,
courseIndex: 1,
matchIndex: 1,
teamId1: '2' as EntryID,
teamId2: '3' as EntryID,
runResults: [],
};
const res = PreMatch.new(args);

expect(() =>
res.appendRunResults(
[...Array(2)].map((_, i) => {
return RunResult.new({
id: String(i) as RunResultID,
goalTimeSeconds: i * 10,
points: 10 + i,
teamId: i % 2 == 0 ? args.teamId1 : ('999' as EntryID),
finishState: 'FINISHED',
});
})
)
).toThrowError(new Error('RunResult teamId must be teamId1 or teamId2'));
});
});
77 changes: 77 additions & 0 deletions packages/kcms/src/match/model/pre.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 予選試合

import { SnowflakeID } from '../../id/main.js';
import { RunResult } from './runResult.js';
import { EntryID } from '../../entry/entry.js';

export type PreMatchID = SnowflakeID<PreMatch>;

export interface CreatePreMatchArgs {
id: PreMatchID;
courseIndex: number;
matchIndex: number;
teamId1: EntryID;
teamId2?: EntryID;
runResults: RunResult[];
}

export class PreMatch {
private readonly id: PreMatchID;
private readonly courseIndex: number;
private readonly matchIndex: number;
private readonly teamId1: EntryID;
// NOTE: 予選参加者は奇数になる可能性があるので2チーム目はいないことがある
private readonly teamId2?: EntryID;
private runResults: RunResult[];

private constructor(args: CreatePreMatchArgs) {
this.id = args.id;
this.courseIndex = args.courseIndex;
this.matchIndex = args.matchIndex;
this.teamId1 = args.teamId1;
this.teamId2 = args.teamId2;
this.runResults = args.runResults;
}

public static new(args: CreatePreMatchArgs) {
return new PreMatch(args);
}

getId(): PreMatchID {
return this.id;
}

getCourseIndex(): number {
return this.courseIndex;
}

getMatchIndex(): number {
return this.matchIndex;
}

getTeamId1(): EntryID {
return this.teamId1;
}

getTeamId2(): EntryID | undefined {
return this.teamId2;
}

getRunResults(): RunResult[] {
return this.runResults;
}

appendRunResults(runResults: RunResult[]) {
// 1チーム1つずつ結果を持つので,1 or 2個
if (runResults.length !== 1 && runResults.length !== 2) {
throw new Error('RunResult length must be 1 or 2');
}
if (
runResults.some((result) => result.getTeamId() !== this.teamId1) ||
(this.teamId2 && runResults.some((result) => result.getTeamId() !== this.teamId2))
) {
throw new Error('RunResult teamId must be teamId1 or teamId2');
}
this.runResults.concat(runResults);
}
}
6 changes: 3 additions & 3 deletions packages/kcms/src/match/service/generateFinal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class GenerateFinalMatchService {

const matches: Match[] = [];
for (const v of elementaryTournament) {
const id = this.idGenerator.generate<MatchID>();
const id = this.idGenerator.generate<Match>();
if (Result.isErr(id)) {
return Result.err(id[1]);
}
Expand All @@ -75,7 +75,7 @@ export class GenerateFinalMatchService {
);
const matches: Match[] = [];
for (const v of openTournament) {
const id = this.idGenerator.generate<MatchID>();
const id = this.idGenerator.generate<Match>();
if (Result.isErr(id)) {
return Result.err(id[1]);
}
Expand Down Expand Up @@ -185,7 +185,7 @@ export class GenerateFinalMatchService {
// ペアから試合を作る
const newMatches: Match[] = [];
for (const v of teamPair) {
const id = this.idGenerator.generate<MatchID>();
const id = this.idGenerator.generate<Match>();
if (Result.isErr(id)) {
return Result.err(id[1]);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/kcms/src/match/service/generatePrimary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class GeneratePrimaryMatchService {
const gap = Math.floor(courseLength / 2);
const opponentIndex = k + gap >= courseLength ? k + gap - courseLength : k + gap;

const id = this.idGenerator.generate<MatchID>();
const id = this.idGenerator.generate<Match>();
if (Result.isErr(id)) {
return Result.err(id[1]);
}
Expand Down

0 comments on commit 0060356

Please sign in to comment.