Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 予選を同時に生成 #826

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/kcms/src/match/adaptor/controller/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FetchTeamService } from '../../../team/service/fetchTeam';
import { MainMatch, MainMatchID } from '../../model/main';
import { PreMatch, PreMatchID } from '../../model/pre';
import { FetchRunResultService } from '../../service/fetchRunResult';
import { GenerateAllPreMatchService } from '../../service/generateAllPre';
import { GenerateMainMatchService } from '../../service/generateMain';
import { GeneratePreMatchService } from '../../service/generatePre';
import { GenerateRankingService } from '../../service/generateRanking';
Expand All @@ -19,6 +20,7 @@ import {
MainSchema,
PostMatchGenerateManualResponseSchema,
PostMatchGenerateResponseSchema,
PostPreMatchGenerateResponseSchema,
PreSchema,
RunResultSchema,
ShortMainSchema,
Expand All @@ -30,6 +32,7 @@ export class MatchController {
private readonly getMatchService: GetMatchService,
private readonly fetchTeamService: FetchTeamService,
private readonly generatePreMatchService: GeneratePreMatchService,
private readonly generateAllPreMatchService: GenerateAllPreMatchService,
private readonly generateRankingService: GenerateRankingService,
private readonly fetchRunResultService: FetchRunResultService,
private readonly generateMainMatchService: GenerateMainMatchService
Expand Down Expand Up @@ -123,6 +126,36 @@ export class MatchController {
);
}

async generateAllPreMatch(): Promise<
Result.Result<Error, z.infer<typeof PostPreMatchGenerateResponseSchema>>
> {
const data = [];
const res = await this.generateAllPreMatchService.handle();
for (const e of res.keys()) {
const matches = res.get(e);
if (matches) {
if (Result.isErr(matches)) return matches;
const createMatches = Result.unwrap(matches).map((v): z.infer<typeof ShortPreSchema> => {
return {
id: v.getID(),
matchCode: `${v.getCourseIndex()}-${v.getMatchIndex()}`,
matchType: 'pre',
departmentType: v.getDepartmentType(),
leftTeamID: v.getTeamID1(),
rightTeamID: v.getTeamID2(),
runResults: [],
};
});
data.push({
departmentType: e,
matches: createMatches,
});
}
}

return Result.ok(data);
}

async generateMatchManual(
departmentType: DepartmentType,
team1ID: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/kcms/src/match/adaptor/validator/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ export const PostMatchGenerateResponseSchema = z.union([
ShortMainSchema.array(),
]);

export const PostPreMatchGenerateResponseSchema = z.array(
z.object({
departmentType: DepartmentTypeSchema,
matches: ShortPreSchema.array(),
})
);

export const PostMatchGenerateManualParamsSchema = z.object({
departmentType: DepartmentTypeSchema,
});
Expand Down
19 changes: 19 additions & 0 deletions packages/kcms/src/match/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import {
PostMatchGenerateManualRoute,
PostMatchGenerateRoute,
PostMatchRunResultRoute,
PostPreMatchGenerateRoute,
} from './routing';
import { CreateRunResultService } from './service/createRunResult';
import { FetchRunResultService } from './service/fetchRunResult';
import { GenerateAllPreMatchService } from './service/generateAllPre';
import { GenerateMainMatchService } from './service/generateMain';
import { GeneratePreMatchService } from './service/generatePre';
import { GenerateRankingService } from './service/generateRanking';
Expand Down Expand Up @@ -54,13 +56,21 @@ const generatePreMatchService = new GeneratePreMatchService(
idGenerator,
preMatchRepository
);

const generateAllPreMatchService = new GenerateAllPreMatchService(
fetchTeamService,
idGenerator,
preMatchRepository
);

const generateRankingService = new GenerateRankingService(preMatchRepository, mainMatchRepository);
const fetchRunResultService = new FetchRunResultService(mainMatchRepository, preMatchRepository);
const generateMainMatchService = new GenerateMainMatchService(mainMatchRepository, idGenerator);
const matchController = new MatchController(
getMatchService,
fetchTeamService,
generatePreMatchService,
generateAllPreMatchService,
generateRankingService,
fetchRunResultService,
generateMainMatchService
Expand Down Expand Up @@ -108,6 +118,15 @@ matchHandler.openapi(PostMatchGenerateRoute, async (c) => {
return c.json(res[1], 200);
});

matchHandler.openapi(PostPreMatchGenerateRoute, async (c) => {
const res = await matchController.generateAllPreMatch();
if (Result.isErr(res)) {
return c.json({ description: res[1].message }, 400);
}

return c.json(res[1], 200);
});

/**
* 本戦試合を手動で生成
*/
Expand Down
24 changes: 24 additions & 0 deletions packages/kcms/src/match/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PostMatchGenerateResponseSchema,
PostMatchRunResultParamsSchema,
PostMatchRunResultRequestSchema,
PostPreMatchGenerateResponseSchema,
} from '../match/adaptor/validator/match';

export const GetMatchRoute = createRoute({
Expand Down Expand Up @@ -138,6 +139,29 @@ export const PostMatchGenerateRoute = createRoute({
},
});

export const PostPreMatchGenerateRoute = createRoute({
method: 'post',
path: '/match/pre',
responses: {
200: {
content: {
'application/json': {
schema: PostPreMatchGenerateResponseSchema,
},
},
description: 'Generate match table',
},
400: {
content: {
'application/json': {
schema: CommonErrorSchema,
},
},
description: 'Common error',
},
},
});

export const PostMatchGenerateManualRoute = createRoute({
method: 'post',
path: '/match/main/{departmentType}/generate/manual',
Expand Down
63 changes: 63 additions & 0 deletions packages/kcms/src/match/service/generateAllPre.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Result } from '@mikuroxina/mini-fn';
import { config } from 'config';
import { describe, expect, it } from 'vitest';
import { SnowflakeIDGenerator } from '../../id/main';
import { DummyRepository } from '../../team/adaptor/repository/dummyRepository';
import { TeamID } from '../../team/models/team';
import { FetchTeamService } from '../../team/service/fetchTeam';
import { testTeamData } from '../../testData/entry';
import { DummyPreMatchRepository } from '../adaptor/dummy/preMatchRepository';
import { GenerateAllPreMatchService } from './generateAllPre';

describe('GeneratePreMatchService', () => {
const teamRepository = new DummyRepository([...testTeamData.values()]);
const fetchService = new FetchTeamService(teamRepository);
const generator = new SnowflakeIDGenerator(1, () =>
BigInt(new Date('2024/01/01 00:00:00 UTC').getTime())
);
const preMatchRepository = new DummyPreMatchRepository();
const generateService = new GenerateAllPreMatchService(
fetchService,
generator,
preMatchRepository
);

const expectedTeamPair = [
[
['A1', 'B3'],
['A4', 'N1'],
['B3', 'A1'],
['N1', 'A4'],
],
[
['A2', 'C1'],
['B1', 'N2'],
['C1', 'A2'],
['N2', 'B1'],
],
[
['A3', 'C2'],
['B2', undefined],
[undefined, 'A3'],
['C2', 'B2'],
],
];

it('正しく予選対戦表を生成できる', async () => {
const res = await generateService.handle();
for (const e of res.keys()) {
const matches = res.get(e);
if (matches) {
expect(Result.isOk(matches)).toBe(true);
for (let i = 0; i < config.match.pre.course[e].length; i++) {
const course = Result.unwrap(matches).filter((v) => v.getCourseIndex() === i + 1);
const pair = course.map((v) => [
testTeamData.get(v.getTeamID1() ?? ('' as TeamID))?.getTeamName(),
testTeamData.get(v.getTeamID2() ?? ('' as TeamID))?.getTeamName(),
]);
expect(pair).toStrictEqual(expectedTeamPair[i]);
}
}
}
});
});
138 changes: 138 additions & 0 deletions packages/kcms/src/match/service/generateAllPre.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Result } from '@mikuroxina/mini-fn';
import { config, DepartmentType } from 'config';
import { SnowflakeIDGenerator } from '../../id/main';
import { Team } from '../../team/models/team';
import { FetchTeamService } from '../../team/service/fetchTeam';
import { PreMatch } from '../model/pre';
import { PreMatchRepository } from '../model/repository';

export class GenerateAllPreMatchService {
constructor(
private readonly fetchTeam: FetchTeamService,
private readonly idGenerator: SnowflakeIDGenerator,
private readonly preMatchRepository: PreMatchRepository
) {}

async handle(): Promise<Map<DepartmentType, Result.Result<Error, PreMatch[]>>> {
const matches = new Map<DepartmentType, Result.Result<Error, PreMatch[]>>();
for (const e of config.departmentTypes) {
if (config.match.pre.course[e].length) {
const pair = await this.makePairs(e);
const match = await this.makeMatches(pair);
matches.set(e, match);
}
}
return matches;
}

private async makeMatches(
data: (Team | undefined)[][][]
): Promise<Result.Result<Error, PreMatch[]>> {
// 与えられたペアをもとに試合を生成する

// コースごとに生成
const generated = data.map((course, courseIndex) => {
// ペアをもとに試合を生成
return course.map((pair, matchIndex): Result.Result<Error, PreMatch> => {
const id = this.idGenerator.generate<PreMatch>();
if (Result.isErr(id)) {
return id;
}
const match = PreMatch.new({
id: Result.unwrap(id),
// ToDo: 他部門のコースがすでに使用されているときにコース番号をどうするかを考える
courseIndex: courseIndex + 1,
matchIndex: matchIndex + 1,
departmentType: (pair[0] || pair[1]!).getDepartmentType(),
teamID1: pair[0]?.getID(),
teamID2: pair[1]?.getID(),
runResults: [],
});
return Result.ok(match);
});
});
const flatten = generated.flat();
const match = flatten.filter((v) => Result.isOk(v)).map((v) => Result.unwrap(v));

const res = await this.preMatchRepository.createBulk(match);
if (Result.isErr(res)) {
return res;
}

return Result.ok(match);
}

/**
* チームのペアだけを作る関数
*/
async makePairs(departmentType: DepartmentType): Promise<(Team | undefined)[][][]> {
// 多言語環境でソート可能にするためにcollatorを使う
const collator = new Intl.Collator('ja');
const courseCount = config.match.pre.course[departmentType].length;

// エントリー済みのチームを取得
const teamRes = await this.fetchTeam.findAll();
if (Result.isErr(teamRes)) {
return [];
}
const team = Result.unwrap(teamRes).filter(
(v) => v.getIsEntered() && v.getDepartmentType() === departmentType
);

// チームをクラブ名でソートする (ToDo: クラブ名がない場合にどこの位置に動かすかを決める必要がありそう
const teams = team.sort((a, b) =>
collator.compare(a.getClubName() ?? 'N', b.getClubName() ?? 'N')
);

// コースの数でスライスする
// 初期化時に必要な個数作っておく
const slicedTeams: Team[][] = new Array(
Math.ceil(teams.length / config.match.pre.course[departmentType].length)
).fill([]);
for (let i = 0; i < Math.ceil(teams.length / courseCount); i++) {
// コース数
slicedTeams[i] = teams.slice(i * courseCount, (i + 1) * courseCount);
}

/* スライスされた配列を転置する
* 列数 != 行数の場合、undefinedが入るので、filterで除外している
*/
const transpose = slicedTeams[0]
.map((_, i) => slicedTeams.map((r) => r[i]))
.map((r) => r.filter((v) => v !== undefined));

// 配列の要素ごとに、右側のコースを走る相手を決める
return transpose.map((v) => {
if (v.length === 1) {
console.warn('WANING: 1チームだけのコースがあります');
return [
[v[0], undefined],
[undefined, undefined],
[undefined, v[0]],
];
} else if (v.length === 2) {
console.warn('WANING: 2チームだけのコースがあります');
return [
[v[0], v[1]],
[undefined, undefined],
[v[1], v[0]],
];
} else if (v.length === 3) {
console.warn('WANING: 3チームだけのコースがあります');
return [
[v[0], v[2]],
[v[1], undefined],
[undefined, v[0]],
[v[2], v[1]],
];
} else {
// ずらす数(floor(配列の長さ/2))
const shift = Math.floor(v.length / 2);
// ずらした配列を作成
const shifted = v.slice(shift).concat(v.slice(0, shift));
// ペアを作る
return v.map((_, i) => [v[i], shifted[i]]);
}
});
}
}
Loading