diff --git a/packages/kcms/src/match/model/main.test.ts b/packages/kcms/src/match/model/main.test.ts index 33de9a83..f6c523d7 100644 --- a/packages/kcms/src/match/model/main.test.ts +++ b/packages/kcms/src/match/model/main.test.ts @@ -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', }); @@ -57,7 +56,7 @@ describe('MainMatch', () => { continue; } expect(() => { - mainMatch.setRunResults( + mainMatch.appendRunResults( [...Array(i)].map((_, i) => { return RunResult.new({ id: String(i) as RunResultID, @@ -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('勝者が決まっているときは変更できない', () => { diff --git a/packages/kcms/src/match/model/main.ts b/packages/kcms/src/match/model/main.ts index c49247e8..40177630 100644 --- a/packages/kcms/src/match/model/main.ts +++ b/packages/kcms/src/match/model/main.ts @@ -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); } } diff --git a/packages/kcms/src/match/model/match.ts b/packages/kcms/src/match/model/match.ts index 997fa368..c3403182 100644 --- a/packages/kcms/src/match/model/match.ts +++ b/packages/kcms/src/match/model/match.ts @@ -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; + // 対戦するチームのペア L左/R右 export type MatchTeams = { left: Entry | undefined; diff --git a/packages/kcms/src/match/model/pre.test.ts b/packages/kcms/src/match/model/pre.test.ts new file mode 100644 index 00000000..2ec04718 --- /dev/null +++ b/packages/kcms/src/match/model/pre.test.ts @@ -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')); + }); +}); diff --git a/packages/kcms/src/match/model/pre.ts b/packages/kcms/src/match/model/pre.ts new file mode 100644 index 00000000..5a2d8ac3 --- /dev/null +++ b/packages/kcms/src/match/model/pre.ts @@ -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; + +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); + } +} diff --git a/packages/kcms/src/match/service/generateFinal.ts b/packages/kcms/src/match/service/generateFinal.ts index 7b0f5aff..79849151 100644 --- a/packages/kcms/src/match/service/generateFinal.ts +++ b/packages/kcms/src/match/service/generateFinal.ts @@ -48,7 +48,7 @@ export class GenerateFinalMatchService { const matches: Match[] = []; for (const v of elementaryTournament) { - const id = this.idGenerator.generate(); + const id = this.idGenerator.generate(); if (Result.isErr(id)) { return Result.err(id[1]); } @@ -75,7 +75,7 @@ export class GenerateFinalMatchService { ); const matches: Match[] = []; for (const v of openTournament) { - const id = this.idGenerator.generate(); + const id = this.idGenerator.generate(); if (Result.isErr(id)) { return Result.err(id[1]); } @@ -185,7 +185,7 @@ export class GenerateFinalMatchService { // ペアから試合を作る const newMatches: Match[] = []; for (const v of teamPair) { - const id = this.idGenerator.generate(); + const id = this.idGenerator.generate(); if (Result.isErr(id)) { return Result.err(id[1]); } diff --git a/packages/kcms/src/match/service/generatePrimary.ts b/packages/kcms/src/match/service/generatePrimary.ts index 12c88c6a..508eeb6d 100644 --- a/packages/kcms/src/match/service/generatePrimary.ts +++ b/packages/kcms/src/match/service/generatePrimary.ts @@ -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(); + const id = this.idGenerator.generate(); if (Result.isErr(id)) { return Result.err(id[1]); }