Skip to content

Commit

Permalink
feat!: 本戦試合生成をトーナメント形式に対応させた
Browse files Browse the repository at this point in the history
  • Loading branch information
laminne committed Jan 10, 2025
1 parent 9068b42 commit ed4bafe
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 60 deletions.
29 changes: 12 additions & 17 deletions packages/kcms/src/match/adaptor/controller/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,29 +125,24 @@ export class MatchController {

async generateMatchManual(
departmentType: DepartmentType,
team1ID: string,
team2ID: string
teamIDs: string[]
): Promise<Result.Result<Error, z.infer<typeof PostMatchGenerateManualResponseSchema>>> {
const res = await this.generateMainMatchService.handle(
departmentType,
team1ID as TeamID,
team2ID as TeamID
);
const res = await this.generateMainMatchService.handle(departmentType, teamIDs as TeamID[]);
if (Result.isErr(res)) return res;

const match = Result.unwrap(res);
return Result.ok<z.infer<typeof ShortMainSchema>[]>([
{
id: match.getID(),
matchCode: `${match.getCourseIndex()}-${match.getMatchIndex()}`,
return Result.ok<z.infer<typeof ShortMainSchema>[]>(
match.map((v) => ({
id: v.getID(),
matchCode: `${v.getCourseIndex()}-${v.getMatchIndex()}`,
matchType: 'main',
departmentType,
team1ID: match.getTeamID1(),
team2ID: match.getTeamID2(),
departmentType: v.getDepartmentType(),
team1ID: v.getTeamID1(),
team2ID: v.getTeamID2(),
runResults: [],
winnerID: match.getWinnerID() ?? '',
},
]);
winnerID: v.getWinnerID() ?? '',
}))
);
}

async getMatchByID<T extends MatchType>(
Expand Down
3 changes: 1 addition & 2 deletions packages/kcms/src/match/adaptor/validator/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ export const PostMatchGenerateManualParamsSchema = z.object({
departmentType: DepartmentTypeSchema,
});
export const PostMatchGenerateManualRequestSchema = z.object({
team1ID: z.string().openapi({ example: '45098607' }),
team2ID: z.string().openapi({ example: '2230392' }),
teamIDs: z.array(z.string().openapi({ example: '45098607' })).min(2),
});
export const PostMatchGenerateManualResponseSchema = z.array(ShortMainSchema);

Expand Down
9 changes: 7 additions & 2 deletions packages/kcms/src/match/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { Result } from '@mikuroxina/mini-fn';
import { apiReference } from '@scalar/hono-api-reference';
import { config } from 'config';
import { prismaClient } from '../adaptor';
import { SnowflakeIDGenerator } from '../id/main';
import { errorToCode } from '../team/adaptor/errors';
Expand Down Expand Up @@ -56,7 +57,11 @@ const generatePreMatchService = new GeneratePreMatchService(
);
const generateRankingService = new GenerateRankingService(preMatchRepository, mainMatchRepository);
const fetchRunResultService = new FetchRunResultService(mainMatchRepository, preMatchRepository);
const generateMainMatchService = new GenerateMainMatchService(mainMatchRepository, idGenerator);
const generateMainMatchService = new GenerateMainMatchService(
mainMatchRepository,
idGenerator,
config.match.main.requiredTeams
);
const matchController = new MatchController(
getMatchService,
fetchTeamService,
Expand Down Expand Up @@ -115,7 +120,7 @@ matchHandler.openapi(PostMatchGenerateManualRoute, async (c) => {
const { departmentType } = c.req.valid('param');
const req = c.req.valid('json');

const res = await matchController.generateMatchManual(departmentType, req.team1ID, req.team2ID);
const res = await matchController.generateMatchManual(departmentType, req.teamIDs);
if (Result.isErr(res)) {
return c.json({ description: res[1].message }, 400);
}
Expand Down
18 changes: 16 additions & 2 deletions packages/kcms/src/match/model/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export class MainMatch {
private readonly courseIndex: number;
private readonly matchIndex: number;
private readonly departmentType: DepartmentType;
private readonly teamID1?: TeamID;
private readonly teamID2?: TeamID;
private teamID1?: TeamID;
private teamID2?: TeamID;

/**
* トーナメントで自分より後に行われる1試合のID\
Expand Down Expand Up @@ -95,10 +95,24 @@ export class MainMatch {
return this.teamID1;
}

setTeamID1(teamID: TeamID) {
if (this.teamID1 !== undefined) {
throw new Error('TeamID1 is already set');
}
this.teamID1 = teamID;
}

getTeamID2(): TeamID | undefined {
return this.teamID2;
}

setTeamID2(teamID: TeamID) {
if (this.teamID2 !== undefined) {
throw new Error('TeamID2 is already set');
}
this.teamID2 = teamID;
}

getWinnerID(): TeamID | undefined {
return this.winnerID;
}
Expand Down
103 changes: 95 additions & 8 deletions packages/kcms/src/match/service/generateMain.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Result } from '@mikuroxina/mini-fn';
import { config } from 'config';
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it } from 'vitest';
import { SnowflakeIDGenerator } from '../../id/main';
import { TeamID } from '../../team/models/team';
import { DummyMainMatchRepository } from '../adaptor/dummy/mainMatchRepository';
Expand All @@ -11,18 +11,105 @@ describe('GenerateMainMatchService', () => {
BigInt(new Date('2024/01/01 00:00:00 UTC').getTime())
);
const mainMatchRepository = new DummyMainMatchRepository([]);
const service = new GenerateMainMatchService(mainMatchRepository, idGenerator);
const service = new GenerateMainMatchService(
mainMatchRepository,
idGenerator,
config.match.main.requiredTeams
);

it.todo('必要なチーム数に一致しない場合はエラーになる', async () => {

afterEach(() => {
mainMatchRepository.clear();
});

it.todo("n=2のとき、試合が生成できる");
it('制約が存在しない場合はエラーになる', async () => {
const res = await service.handle('open', ['1'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(true);
});

it.todo("n=4のとき、試合が生成できる");
it('必要なチーム数に一致しない場合はエラーになる', async () => {
const res = await service.handle('elementary', ['1', '2'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(true);
});

it.todo("n=8のとき、試合が生成できる");
it('n=2のとき、試合が生成できる', async () => {
const res = await new GenerateMainMatchService(mainMatchRepository, idGenerator, {
elementary: 2,
}).handle('elementary', ['1', '2'] as TeamID[]);


expect(Result.isErr(res)).toStrictEqual(false);
const actual = Result.unwrap(res);
expect(actual.length).toStrictEqual(2 - 1);
});

it('n=4のとき、試合が生成できる', async () => {
const res = await new GenerateMainMatchService(mainMatchRepository, idGenerator, {
elementary: 4,
}).handle('elementary', ['1', '2', '3', '4'] as TeamID[]);

expect(Result.isErr(res)).toStrictEqual(false);
const actual = Result.unwrap(res);
expect(actual.length).toStrictEqual(4 - 1);
});

it('n=8のとき、試合が生成できる', async () => {
const res = await new GenerateMainMatchService(mainMatchRepository, idGenerator, {
elementary: 8,
}).handle('elementary', ['1', '2', '3', '4', '5', '6', '7', '8'] as TeamID[]);

expect(Result.isErr(res)).toStrictEqual(false);
const actual = Result.unwrap(res);
expect(actual.length).toStrictEqual(8 - 1);
});

it('親が存在しない試合は1つだけ生成される', async () => {
const res = await service.handle('elementary', ['1', '2', '3', '4'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(false);

const actual = Result.unwrap(res);
const notHasParentMatches = actual.filter((m) => m.getParentID() === undefined);
expect(notHasParentMatches.length).toStrictEqual(1);
});

it('子が存在しない試合はn/2個生成される', async () => {
const res = await service.handle('elementary', ['1', '2', '3', '4'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(false);

const actual = Result.unwrap(res);
const notHasChildMatches = actual.filter((m) => m.getChildMatches() === undefined);
expect(notHasChildMatches.length).toStrictEqual(4 / 2);
});

it('孤児試合は生成されない', async () => {
const res = await service.handle('elementary', ['1', '2', '3', '4'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(false);

const actual = Result.unwrap(res);
const orphanMatches = actual.filter(
(m) => m.getChildMatches() === undefined && m.getParentID() === undefined
);
expect(orphanMatches.length).toStrictEqual(0);
});

it('親/子が設定されている場合、それらの試合は必ず存在する', async () => {
const res = await service.handle('elementary', ['1', '2', '3', '4'] as TeamID[]);
expect(Result.isErr(res)).toStrictEqual(false);

const actual = Result.unwrap(res);
const hasParent = actual.filter((m) => m.getParentID() !== undefined);
const hasChild = actual.filter((m) => m.getChildMatches() !== undefined);

expect(hasParent.length).toStrictEqual(actual.length - 1);
expect(hasChild.length).toStrictEqual(actual.length - 4 / 2);

const matchIDs = actual.map((m) => m.getID());

for (const match of hasParent) {
expect(matchIDs).toContain(match.getParentID());
}

for (const match of hasChild) {
expect(matchIDs).toContain(match.getChildMatches()?.match1.getID());
expect(matchIDs).toContain(match.getChildMatches()?.match2.getID());
}
});
});
Loading

0 comments on commit ed4bafe

Please sign in to comment.