From a30ef913edfd74c5d70eca42b414256ac0a0ef02 Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Thu, 28 Dec 2023 01:44:30 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E9=A0=86=E4=BD=8D=E8=A8=88?= =?UTF-8?q?=E7=AE=97=E3=81=AE=E5=AE=9F=E8=A3=85=E3=81=A8=E3=83=88=E3=83=BC?= =?UTF-8?q?=E3=83=8A=E3=83=A1=E3=83=B3=E3=83=88=E7=94=9F=E6=88=90=E3=81=AE?= =?UTF-8?q?=E4=BB=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/match/adaptor/dummyRepository.ts | 4 + src/match/adaptor/json.ts | 5 ++ src/match/match.ts | 3 + src/match/service/generate.test.ts | 53 +++++++++++ src/match/service/generate.ts | 128 ++++++++++++++++++++++++++- src/match/service/repository.ts | 1 + src/match/tournament.ts | 36 ++++++++ 7 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 src/match/tournament.ts diff --git a/src/match/adaptor/dummyRepository.ts b/src/match/adaptor/dummyRepository.ts index 95e1301..ee70861 100644 --- a/src/match/adaptor/dummyRepository.ts +++ b/src/match/adaptor/dummyRepository.ts @@ -35,4 +35,8 @@ export class DummyMatchRepository implements MatchRepository { this.data[i] = match; return Result.ok(match); } + + public async findAll(): Promise> { + return Result.ok(this.data); + } } diff --git a/src/match/adaptor/json.ts b/src/match/adaptor/json.ts index a9ab43a..7dfcaea 100644 --- a/src/match/adaptor/json.ts +++ b/src/match/adaptor/json.ts @@ -72,6 +72,11 @@ export class JSONMatchRepository implements MatchRepository { return Option.some(match); } + public async findAll(): Promise> { + return Result.ok(this.data); + } + + public async update(match: Match): Promise> { const i = this.data.findIndex((m) => m.id === match.id); this.data[i] = match; diff --git a/src/match/match.ts b/src/match/match.ts index 53a6cfc..6992d46 100644 --- a/src/match/match.ts +++ b/src/match/match.ts @@ -57,6 +57,9 @@ export type MatchResultFinalPair = { // じゃんけんで決定したとき用 winnerID: string; }; +export const isMatchResultPair = (arg: MatchResultPair | MatchResultFinalPair | undefined): arg is MatchResultPair => { + return (arg as MatchResultPair).Left !== undefined; +} export interface CreateMatchArgs { id: string; diff --git a/src/match/service/generate.test.ts b/src/match/service/generate.test.ts index 2800a4b..bf10460 100644 --- a/src/match/service/generate.test.ts +++ b/src/match/service/generate.test.ts @@ -37,3 +37,56 @@ describe("予選の対戦表を正しく生成できる", () => { } }); }); + +describe("本選の対戦表を正しく生成できる", async () => { + const repository = new DummyRepository(); + const matchRepository = new DummyMatchRepository(); + const service = new GenerateMatchService(repository, matchRepository); + const dummyData = generateDummyData(16); + dummyData.map((v) => repository.create(v)); + + const match = await service.generatePrimaryMatch(); + if (Result.isErr(match)) { + return; + } + match[1].map((v) => { + v.map(j => { + j.results = { + Left: { + teamID: j.teams.Left?.id ?? "", + points: Number(j.teams.Left?.id ?? 0), + time: Number(j.teams.Left?.id ?? 0) + }, + Right: { + teamID: j.teams.Right?.id ?? "" , + points: Number(j.teams.Right?.id ?? 0), + time: Number(j.teams.Right?.id ?? 0) + } + } + // fixme: 消す(最下位とその1つ上の点数を同じにしている) + if (j.teams.Left) { + if (j.teams.Left.id === "1") { + j.results.Left.points = 0; + j.results.Left.time = 0; + } + } + if (j.teams.Right) { + if (j.teams.Right.id === "1") { + j.results.Right.points = 0; + j.results.Right.time = 1; + } + } + matchRepository.update(j) + }) + }); + + it("ランキングを正しく生成できる", async () => { + const res = await service.generateRanking(); + console.log(res); + }); + + it("本選の対戦表を正しく生成できる", async () => { + const res = await service.generateFinalMatch(); + console.table(res[1]); + }); +}); diff --git a/src/match/service/generate.ts b/src/match/service/generate.ts index 89c8673..a3f39da 100644 --- a/src/match/service/generate.ts +++ b/src/match/service/generate.ts @@ -1,10 +1,18 @@ import { EntryRepository } from "../../entry/repository.js"; import { Result } from "@mikuroxina/mini-fn"; import { Entry } from "../../entry/entry.js"; -import { Match } from "../match.js"; +import { isMatchResultPair, Match } from "../match.js"; import { MatchRepository } from "./repository.js"; import * as crypto from "crypto"; +export type TournamentRank = { + rank: number; + points: number; + time: number; + entry: Entry; +}; +export type Tournament = [TournamentRank, TournamentRank] | [Tournament, Tournament]; + export class GenerateMatchService { private readonly COURSE_COUNT = 3; private readonly entryRepository: EntryRepository; @@ -12,7 +20,7 @@ export class GenerateMatchService { constructor( entryRepository: EntryRepository, - matchRepository: MatchRepository, + matchRepository: MatchRepository ) { this.entryRepository = entryRepository; this.matchRepository = matchRepository; @@ -54,7 +62,7 @@ export class GenerateMatchService { id: crypto.randomUUID(), matchType: "primary", teams: { Left: courses[i][k], Right: courses[i][opponentIndex] }, - courseIndex: i, + courseIndex: i }); courseMatches.push(match); } @@ -71,4 +79,118 @@ export class GenerateMatchService { } // ToDo: 本選トーナメント対戦表の生成 + async generateFinalMatch(): Promise> { + /* + 初期対戦表を生成 + 1 vs 8, 4 vs 5, 2 vs 7, 3 vs 6 (数字は順位) + */ + + const rank = await this.generateRanking(); + const tournament = [ + this.tournament(rank[0]), + this.tournament(rank[1]) + ] + // 初期トーナメントから試合を生成する + console.log(JSON.stringify(tournament)); + // ToDo: 部門ごとにトーナメントを生成 + return Result.ok([]); + } + + // ToDo: 本戦の順位を計算できるようにする + // ToDo: (予選)タイムと得点が同じ場合だったときの順位決定処理 + // ToDo: 部門ごとにランキングを生成できるように + async generateRanking(): Promise { + const res = await this.matchRepository.findAll(); + if (Result.isErr(res)) { + throw res[1]; + } + // チームごとの得点/時間 + const rankBase: TournamentRank[] = []; + // チームごとの得点を計算したい + // -> まず全ての対戦を取得 + for (const v of res[1]) { + // 本選は関係ないので飛ばす + if (v.matchType !== "primary") continue; + // 終わってない場合は飛ばす + if (!v.results) continue; + if (!isMatchResultPair(v.results)) continue; + + // 対戦の結果を取って、tournamentRankを作る + const left = v.results.Left; + const right = v.results.Right; + + // 左チームの結果を追加 + const leftRank = rankBase.find(v => v.entry.id === left.teamID); + if (!leftRank) { + // なければ作る + rankBase.push({ + rank: 0, + points: left.points, + time: left.time, + entry: v.teams.Left + }); + } else { + // あれば足す + leftRank.points += left.points; + leftRank.time += left.time; + } + + // 右チームの結果を追加 + const rightRank = rankBase.find(v => v.entry.id === right.teamID); + if (!rightRank) { + // なければ作る + rankBase.push({ + rank: 0, + points: right.points, + time: right.time, + entry: v.teams.Right + }); + } else { + // あれば足す + rightRank.points += right.points; + rightRank.time += right.time; + } + + } + + // 部門ごとに分ける [0]: Elementary, [1]: Open + const categoryRank: TournamentRank[][] = [[],[]]; + for (const v of rankBase) { + if (v.entry.category === "Elementary") { + categoryRank[0].push(v); + } + if (v.entry.category === "Open") { + categoryRank[1].push(v); + } + } + + const sortAndRanking = (t: TournamentRank[]) => { + // ソートする + t.sort((a, b) => { + if (a.points === b.points) { + // 得点が同じならゴールタイムが*早い順に*ソート (得点とは逆) + return a.time - b.time; + } + return b.points - a.points; + }); + // ソートが終わったら順位をつける + return t.map((v, i) => { + v.rank = i + 1; + return v; + }); + } + + return [ + sortAndRanking(categoryRank[0]), + sortAndRanking(categoryRank[1]) + ] + } + + private tournament(ids: TournamentRank[] | Tournament[] | Tournament): Tournament { + if (ids.length == 2) return ids as Tournament; // この場合必ずTournament + + const pairs = new Array(ids.length / 2).fill(null).map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); + return this.tournament(pairs); + } + } diff --git a/src/match/service/repository.ts b/src/match/service/repository.ts index 53a948d..aa9c989 100644 --- a/src/match/service/repository.ts +++ b/src/match/service/repository.ts @@ -7,4 +7,5 @@ export interface MatchRepository { findByID(id: string): Promise>; update(match: Match): Promise>; findByType(type: string): Promise>; + findAll(): Promise>; } diff --git a/src/match/tournament.ts b/src/match/tournament.ts new file mode 100644 index 0000000..ef7188b --- /dev/null +++ b/src/match/tournament.ts @@ -0,0 +1,36 @@ +import { Entry } from "../entry/entry.js"; + +type Tournament = [TournamentRank, TournamentRank] | [Tournament, Tournament]; + +const tournament = (ids: TournamentRank[] | Tournament[] | Tournament): Tournament => { + if (ids.length == 2) return ids as Tournament; // この場合必ずTournament + + const pairs = new Array(ids.length / 2).fill(null).map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); + return tournament(pairs); +}; + + +const generateDummyData = (n: number): TournamentRank[] => { + const res: TournamentRank[] = Array(); + + for (let i = 1; i < n+1; i++) { + res.push({ + rank: i, + entry: Entry.new({ + id: `${i}`, + teamName: `チーム ${i}`, + members: [`チーム${i}のメンバー`], + isMultiWalk: true, + category: i % 2 === 0 ? "Open" : "Elementary" + }) + } + ); + } + return res; +}; +const ids = generateDummyData(8); +export type TournamentRank = { + rank: number; + entry: Entry; +}; +console.log({ tournament: tournament(ids) }); // => [[[1, 8],[4, 5]],[[2, 7],[3, 6]]] From f3058ebefdd8209789f62ac5d1eef76209ed0aa3 Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Thu, 28 Dec 2023 17:28:41 +0900 Subject: [PATCH 2/5] wip --- src/match/service/generate.test.ts | 9 +++++---- src/match/service/generate.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/match/service/generate.test.ts b/src/match/service/generate.test.ts index bf10460..71b6107 100644 --- a/src/match/service/generate.test.ts +++ b/src/match/service/generate.test.ts @@ -11,11 +11,12 @@ const generateDummyData = (n: number): Entry[] => { for (let i = 0; i < n; i++) { res.push( Entry.new({ - id: `${i}`, - teamName: `チーム ${i}`, - members: [`チーム${i}のメンバー1`], + id: `${i+1}`, + teamName: `チーム ${i+1}`, + members: [`チーム${i+1}のメンバー1`], isMultiWalk: true, - category: i % 2 === 0 ? "Open" : "Elementary", + // 1~8がOpen, 9~16がElementary + category: i < 8 ? "Open" : "Elementary", }), ); } diff --git a/src/match/service/generate.ts b/src/match/service/generate.ts index a3f39da..99a6e13 100644 --- a/src/match/service/generate.ts +++ b/src/match/service/generate.ts @@ -86,19 +86,21 @@ export class GenerateMatchService { */ const rank = await this.generateRanking(); - const tournament = [ + const [elementaryTournament] = [ this.tournament(rank[0]), this.tournament(rank[1]) ] + console.log(JSON.stringify(this.tournament(rank[1]))) // 初期トーナメントから試合を生成する - console.log(JSON.stringify(tournament)); + const a = this.flattenTournament(elementaryTournament); + console.log(a); // ToDo: 部門ごとにトーナメントを生成 return Result.ok([]); } // ToDo: 本戦の順位を計算できるようにする - // ToDo: (予選)タイムと得点が同じ場合だったときの順位決定処理 - // ToDo: 部門ごとにランキングを生成できるように + // - ToDo: (予選)タイムと得点が同じ場合だったときの順位決定処理 + // - ToDo: 部門ごとにランキングを生成できるように -> OK async generateRanking(): Promise { const res = await this.matchRepository.findAll(); if (Result.isErr(res)) { @@ -193,4 +195,23 @@ export class GenerateMatchService { return this.tournament(pairs); } + private flattenTournament(t: Tournament): [TournamentRank, TournamentRank] { + const isTournamentRank = (t: TournamentRank | Tournament): t is TournamentRank => { + return (t as TournamentRank).rank !== undefined; + } + const isTournament = (t: TournamentRank | Tournament): t is Tournament => { + return Array.isArray(t) && t.length === 2; + } + + if (Array.isArray(t)) { + const [rank1, rank2] = t; + if (isTournamentRank(rank1) && isTournamentRank(rank2)) { + return [rank1, rank2]; + } else if (isTournament(rank1) && isTournament(rank2)) { + return this.flattenTournament(rank1); + } + } + throw new Error('Invalid tournament structure'); + } + } From d9c6d0779a9fb1246fa7034db175cf78172e7bad Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 29 Dec 2023 18:33:46 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=9C=AC=E9=81=B8=E5=AF=BE?= =?UTF-8?q?=E6=88=A6=E8=A1=A8=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 ++++---- src/match/adaptor/dummyRepository.ts | 2 +- src/match/adaptor/json.ts | 1 - src/match/controller.ts | 10 +-- src/match/main.ts | 10 ++- src/match/match.ts | 6 +- src/match/service/generate.test.ts | 33 ++++--- src/match/service/generate.ts | 123 ++++++++++++++++----------- src/match/tournament.ts | 30 ++++--- 9 files changed, 139 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index c84bdf3..a32e8c6 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ Matz葉がにロボコン 大会運営支援ツール ### サーバーを動作させる -上記必要なものをインストールしてください. +上記必要なものをインストールしてください. データ保存用の`data.json`を用意してください. + ```json { "entry": [], @@ -43,7 +44,7 @@ bun dev ### Authors/License | | | | -|:--------------------------------------------------------:|:-----------------------------------------------------------:|:-------------------------------------------------------:| +| :------------------------------------------------------: | :---------------------------------------------------------: | :-----------------------------------------------------: | | **laminne (T. YAMAMOTO)**
🔧 🦀 | **kiharu3112**
🔧 🦀 | **tufusa**
🔧 🦀 | 🔧: KCMS/KCMSFの開発 @@ -70,12 +71,12 @@ MIT License body: `application/json` -| 項目名 | 型(TS表記) | 説明 | 備考 | -|-------------|----------------------------------|-------------|-----------------------------| -| teamName | `string` | チーム名 | 重複するとエラー | -| members | `[string, string]` | メンバーの名前 | 小学生部門: 1 or 2人 / オープン部門: 1人 | -| isMultiWalk | `boolean` | ロボットが多足歩行型か | | -| category | `"Elementary" or "Open"` (union) | 出場する部門 | | +| 項目名 | 型(TS表記) | 説明 | 備考 | +| ----------- | -------------------------------- | ---------------------- | ---------------------------------------- | +| teamName | `string` | チーム名 | 重複するとエラー | +| members | `[string, string]` | メンバーの名前 | 小学生部門: 1 or 2人 / オープン部門: 1人 | +| isMultiWalk | `boolean` | ロボットが多足歩行型か | | +| category | `"Elementary" or "Open"` (union) | 出場する部門 | | #### 出力 @@ -85,10 +86,7 @@ body: `application/json` { "id": "39440930485098", "teamName": "ニカ.reverse()", - "members": [ - "木下竹千代", - "織田幸村" - ], + "members": ["木下竹千代", "織田幸村"], "isMultiWalk": false, "category": "Elementary" } @@ -113,7 +111,7 @@ body: `application/json` パスパラメータ - `id`: `string` - - 取り消すエントリーのID + - 取り消すエントリーのID body: `application/json` @@ -141,10 +139,7 @@ body: `application/json` { "id": "39440930485098", "teamName": "ニカ.reverse()", - "members": [ - "木下竹千代", - "織田幸村" - ], + "members": ["木下竹千代", "織田幸村"], "isMultiWalk": false, "category": "Elementary" } @@ -160,7 +155,7 @@ body: `application/json` パスパラメータ - `matchType`: `"primary"|"final"` - - 部門名 + - 部門名 #### 出力 @@ -213,7 +208,7 @@ body: `application/json` パスパラメータ - `matchType`: `"final"|"primary"` - - 部門名 + - 部門名 ```json {} @@ -284,7 +279,7 @@ body: `application/json` パスパラメータ - id: `string` - - 試合ID + - 試合ID body: `application/json` diff --git a/src/match/adaptor/dummyRepository.ts b/src/match/adaptor/dummyRepository.ts index ee70861..7829683 100644 --- a/src/match/adaptor/dummyRepository.ts +++ b/src/match/adaptor/dummyRepository.ts @@ -3,7 +3,7 @@ import { MatchRepository } from "../service/repository.js"; import { Option, Result } from "@mikuroxina/mini-fn"; export class DummyMatchRepository implements MatchRepository { - private data: Match[]; + private readonly data: Match[]; constructor() { this.data = []; diff --git a/src/match/adaptor/json.ts b/src/match/adaptor/json.ts index 7dfcaea..8bc5e87 100644 --- a/src/match/adaptor/json.ts +++ b/src/match/adaptor/json.ts @@ -76,7 +76,6 @@ export class JSONMatchRepository implements MatchRepository { return Result.ok(this.data); } - public async update(match: Match): Promise> { const i = this.data.findIndex((m) => m.id === match.id); this.data[i] = match; diff --git a/src/match/controller.ts b/src/match/controller.ts index 5be7836..ac863a3 100644 --- a/src/match/controller.ts +++ b/src/match/controller.ts @@ -13,7 +13,7 @@ export class MatchController { constructor( matchService: GenerateMatchService, editService: EditMatchService, - getService: GetMatchService + getService: GetMatchService, ) { this.matchService = matchService; this.editService = editService; @@ -54,7 +54,7 @@ export class MatchController { if (Result.isErr(res)) { return Result.err(res[1]); } - return Result.ok(res[1].map(i => this.toJSON(i.toDomain()))); + return Result.ok(res[1].map((i) => this.toJSON(i.toDomain()))); } private toJSON(i: Match) { @@ -67,7 +67,7 @@ export class MatchController { id: i.id, teamName: i.teamName, isMultiWalk: i.isMultiWalk, - category: i.category + category: i.category, }; }; @@ -75,11 +75,11 @@ export class MatchController { id: i.id, teams: { left: toTeamJSON(i.teams.Left), - right: toTeamJSON(i.teams.Right) + right: toTeamJSON(i.teams.Right), }, matchType: i.matchType, courseIndex: i.courseIndex, - results: i.results + results: i.results, }; } } diff --git a/src/match/main.ts b/src/match/main.ts index 002869a..82ac5a1 100644 --- a/src/match/main.ts +++ b/src/match/main.ts @@ -14,7 +14,11 @@ const entryRepository = await JSONEntryRepository.new(); const generateService = new GenerateMatchService(entryRepository, repository); const editService = new EditMatchService(repository); const getService = new GetMatchService(repository); -const controller = new MatchController(generateService, editService, getService); +const controller = new MatchController( + generateService, + editService, + getService, +); matchHandler.post("/:match", async (c) => { const { match } = c.req.param(); @@ -27,13 +31,13 @@ matchHandler.post("/:match", async (c) => { }); matchHandler.get("/:type", async (c) => { - const {type} = c.req.param(); + const { type } = c.req.param(); const res = await controller.getMatchByType(type); if (Result.isErr(res)) { return c.json([{ error: res[1].message }], 400); } return c.json(res[1]); -}) +}); matchHandler.put("/:match", async (c) => { const { match } = c.req.param(); diff --git a/src/match/match.ts b/src/match/match.ts index 6992d46..54b1af3 100644 --- a/src/match/match.ts +++ b/src/match/match.ts @@ -57,9 +57,11 @@ export type MatchResultFinalPair = { // じゃんけんで決定したとき用 winnerID: string; }; -export const isMatchResultPair = (arg: MatchResultPair | MatchResultFinalPair | undefined): arg is MatchResultPair => { +export const isMatchResultPair = ( + arg: MatchResultPair | MatchResultFinalPair | undefined, +): arg is MatchResultPair => { return (arg as MatchResultPair).Left !== undefined; -} +}; export interface CreateMatchArgs { id: string; diff --git a/src/match/service/generate.test.ts b/src/match/service/generate.test.ts index 71b6107..aa8e2dd 100644 --- a/src/match/service/generate.test.ts +++ b/src/match/service/generate.test.ts @@ -11,9 +11,9 @@ const generateDummyData = (n: number): Entry[] => { for (let i = 0; i < n; i++) { res.push( Entry.new({ - id: `${i+1}`, - teamName: `チーム ${i+1}`, - members: [`チーム${i+1}のメンバー1`], + id: `${i + 1}`, + teamName: `チーム ${i + 1}`, + members: [`チーム${i + 1}のメンバー1`], isMultiWalk: true, // 1~8がOpen, 9~16がElementary category: i < 8 ? "Open" : "Elementary", @@ -27,7 +27,8 @@ describe("予選の対戦表を正しく生成できる", () => { const repository = new DummyRepository(); const matchRepository = new DummyMatchRepository(); const service = new GenerateMatchService(repository, matchRepository); - const dummyData = generateDummyData(10); + const dummyData = generateDummyData(8); + console.log(dummyData.length); dummyData.map((v) => repository.create(v)); it("初期状態の対戦表を生成できる", async () => { @@ -46,24 +47,24 @@ describe("本選の対戦表を正しく生成できる", async () => { const dummyData = generateDummyData(16); dummyData.map((v) => repository.create(v)); - const match = await service.generatePrimaryMatch(); + const match = await service.generatePrimaryMatch(); if (Result.isErr(match)) { return; } match[1].map((v) => { - v.map(j => { + v.map((j) => { j.results = { Left: { teamID: j.teams.Left?.id ?? "", points: Number(j.teams.Left?.id ?? 0), - time: Number(j.teams.Left?.id ?? 0) + time: Number(j.teams.Left?.id ?? 0), }, Right: { - teamID: j.teams.Right?.id ?? "" , + teamID: j.teams.Right?.id ?? "", points: Number(j.teams.Right?.id ?? 0), - time: Number(j.teams.Right?.id ?? 0) - } - } + time: Number(j.teams.Right?.id ?? 0), + }, + }; // fixme: 消す(最下位とその1つ上の点数を同じにしている) if (j.teams.Left) { if (j.teams.Left.id === "1") { @@ -77,17 +78,15 @@ describe("本選の対戦表を正しく生成できる", async () => { j.results.Right.time = 1; } } - matchRepository.update(j) - }) + matchRepository.update(j); + }); }); it("ランキングを正しく生成できる", async () => { - const res = await service.generateRanking(); - console.log(res); + await service.generateRanking(); }); it("本選の対戦表を正しく生成できる", async () => { - const res = await service.generateFinalMatch(); - console.table(res[1]); + await service.generateFinalMatch("open"); }); }); diff --git a/src/match/service/generate.ts b/src/match/service/generate.ts index 99a6e13..ac2fa4b 100644 --- a/src/match/service/generate.ts +++ b/src/match/service/generate.ts @@ -11,7 +11,16 @@ export type TournamentRank = { time: number; entry: Entry; }; -export type Tournament = [TournamentRank, TournamentRank] | [Tournament, Tournament]; +export type Tournament = + | [TournamentRank, TournamentRank] + | [Tournament, Tournament]; +type TournamentPermutation = TournamentRank[]; +type BaseTuple< + T, + L extends number, + Tup extends T[] = [], +> = Tup["length"] extends L ? Tup : BaseTuple; +type Tuple = BaseTuple; export class GenerateMatchService { private readonly COURSE_COUNT = 3; @@ -20,7 +29,7 @@ export class GenerateMatchService { constructor( entryRepository: EntryRepository, - matchRepository: MatchRepository + matchRepository: MatchRepository, ) { this.entryRepository = entryRepository; this.matchRepository = matchRepository; @@ -62,7 +71,7 @@ export class GenerateMatchService { id: crypto.randomUUID(), matchType: "primary", teams: { Left: courses[i][k], Right: courses[i][opponentIndex] }, - courseIndex: i + courseIndex: i, }); courseMatches.push(match); } @@ -79,23 +88,48 @@ export class GenerateMatchService { } // ToDo: 本選トーナメント対戦表の生成 - async generateFinalMatch(): Promise> { + async generateFinalMatch( + matchType: "elementary" | "open", + ): Promise> { /* 初期対戦表を生成 1 vs 8, 4 vs 5, 2 vs 7, 3 vs 6 (数字は順位) */ - const rank = await this.generateRanking(); - const [elementaryTournament] = [ - this.tournament(rank[0]), - this.tournament(rank[1]) - ] - console.log(JSON.stringify(this.tournament(rank[1]))) - // 初期トーナメントから試合を生成する - const a = this.flattenTournament(elementaryTournament); - console.log(a); - // ToDo: 部門ごとにトーナメントを生成 - return Result.ok([]); + const [elementaryRank, openRank] = await this.generateRanking(); + const [elementaryTournament, openTournament] = [ + this.generateTournamentPair(this.generateTournament(elementaryRank)), + this.generateTournamentPair(this.generateTournament(openRank)), + ]; + + const matches: Match[] = []; + if (matchType === "elementary") { + for (const v of elementaryTournament) { + matches.push( + Match.new({ + id: crypto.randomUUID(), + matchType: "final", + teams: { Left: v[0].entry, Right: v[1].entry }, + courseIndex: 0, + }), + ); + } + } else { + for (const v of openTournament) { + Match.new({ + id: crypto.randomUUID(), + matchType: "final", + teams: { Left: v[0].entry, Right: v[1].entry }, + courseIndex: 0, + }); + } + } + + for (const v of matches) { + await this.matchRepository.create(v); + } + + return Result.ok(matches); } // ToDo: 本戦の順位を計算できるようにする @@ -122,14 +156,14 @@ export class GenerateMatchService { const right = v.results.Right; // 左チームの結果を追加 - const leftRank = rankBase.find(v => v.entry.id === left.teamID); + const leftRank = rankBase.find((v) => v.entry.id === left.teamID); if (!leftRank) { // なければ作る rankBase.push({ rank: 0, points: left.points, time: left.time, - entry: v.teams.Left + entry: v.teams.Left, }); } else { // あれば足す @@ -138,25 +172,24 @@ export class GenerateMatchService { } // 右チームの結果を追加 - const rightRank = rankBase.find(v => v.entry.id === right.teamID); + const rightRank = rankBase.find((v) => v.entry.id === right.teamID); if (!rightRank) { // なければ作る rankBase.push({ rank: 0, points: right.points, time: right.time, - entry: v.teams.Right + entry: v.teams.Right, }); } else { // あれば足す rightRank.points += right.points; rightRank.time += right.time; } - } // 部門ごとに分ける [0]: Elementary, [1]: Open - const categoryRank: TournamentRank[][] = [[],[]]; + const categoryRank: TournamentRank[][] = [[], []]; for (const v of rankBase) { if (v.entry.category === "Elementary") { categoryRank[0].push(v); @@ -180,38 +213,32 @@ export class GenerateMatchService { v.rank = i + 1; return v; }); - } + }; - return [ - sortAndRanking(categoryRank[0]), - sortAndRanking(categoryRank[1]) - ] + return [sortAndRanking(categoryRank[0]), sortAndRanking(categoryRank[1])]; } - private tournament(ids: TournamentRank[] | Tournament[] | Tournament): Tournament { - if (ids.length == 2) return ids as Tournament; // この場合必ずTournament - - const pairs = new Array(ids.length / 2).fill(null).map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); - return this.tournament(pairs); - } + private generateTournament(ranks: TournamentRank[]): TournamentPermutation { + const genTournament = ( + ids: TournamentRank[] | Tournament[] | Tournament, + ): TournamentPermutation => { + if (ids.length == 2) return ids as TournamentPermutation; - private flattenTournament(t: Tournament): [TournamentRank, TournamentRank] { - const isTournamentRank = (t: TournamentRank | Tournament): t is TournamentRank => { - return (t as TournamentRank).rank !== undefined; - } - const isTournament = (t: TournamentRank | Tournament): t is Tournament => { - return Array.isArray(t) && t.length === 2; - } + const pairs = new Array(ids.length / 2) + .fill(null) + .map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); + return genTournament(pairs).flat(); + }; - if (Array.isArray(t)) { - const [rank1, rank2] = t; - if (isTournamentRank(rank1) && isTournamentRank(rank2)) { - return [rank1, rank2]; - } else if (isTournament(rank1) && isTournament(rank2)) { - return this.flattenTournament(rank1); - } - } - throw new Error('Invalid tournament structure'); + return genTournament(ranks); } + private eachSlice = (array: T[], size: L) => + new Array(array.length / size) + .fill(0) + .map((_, i) => array.slice(i * size, (i + 1) * size) as Tuple); + + private generateTournamentPair = ( + tournament: TournamentRank[], + ): [TournamentRank, TournamentRank][] => this.eachSlice(tournament, 2); } diff --git a/src/match/tournament.ts b/src/match/tournament.ts index ef7188b..d6688e0 100644 --- a/src/match/tournament.ts +++ b/src/match/tournament.ts @@ -2,29 +2,31 @@ import { Entry } from "../entry/entry.js"; type Tournament = [TournamentRank, TournamentRank] | [Tournament, Tournament]; -const tournament = (ids: TournamentRank[] | Tournament[] | Tournament): Tournament => { +const tournament = ( + ids: TournamentRank[] | Tournament[] | Tournament, +): Tournament => { if (ids.length == 2) return ids as Tournament; // この場合必ずTournament - const pairs = new Array(ids.length / 2).fill(null).map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); + const pairs = new Array(ids.length / 2) + .fill(null) + .map((_, i) => [ids[i], ids[ids.length - 1 - i]] as Tournament); return tournament(pairs); }; - const generateDummyData = (n: number): TournamentRank[] => { const res: TournamentRank[] = Array(); - for (let i = 1; i < n+1; i++) { + for (let i = 1; i < n + 1; i++) { res.push({ - rank: i, - entry: Entry.new({ - id: `${i}`, - teamName: `チーム ${i}`, - members: [`チーム${i}のメンバー`], - isMultiWalk: true, - category: i % 2 === 0 ? "Open" : "Elementary" - }) - } - ); + rank: i, + entry: Entry.new({ + id: `${i}`, + teamName: `チーム ${i}`, + members: [`チーム${i}のメンバー`], + isMultiWalk: true, + category: i % 2 === 0 ? "Open" : "Elementary", + }), + }); } return res; }; From 1167405a29026077537f1b5732e2f8fbfc577f26 Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 29 Dec 2023 18:45:13 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E3=82=B3=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=A9=E3=83=BC=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/match/controller.ts | 13 +++++++++++++ src/match/service/generate.ts | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/match/controller.ts b/src/match/controller.ts index ac863a3..ca28877 100644 --- a/src/match/controller.ts +++ b/src/match/controller.ts @@ -57,6 +57,19 @@ export class MatchController { return Result.ok(res[1].map((i) => this.toJSON(i.toDomain()))); } + async generateFinal(category: string) { + if (!(category === "elementary" || category === "open")) { + return Result.err(new Error("invalid match type")); + } + const res = await this.matchService.generateFinalMatch(category); + if (Result.isErr(res)) { + return Result.err(res[1]); + } + return Result.ok( + res[1].map(v => this.toJSON(v)) + ) + } + private toJSON(i: Match) { const toTeamJSON = (i?: Entry) => { if (!i) { diff --git a/src/match/service/generate.ts b/src/match/service/generate.ts index ac2fa4b..1277c2b 100644 --- a/src/match/service/generate.ts +++ b/src/match/service/generate.ts @@ -89,7 +89,7 @@ export class GenerateMatchService { // ToDo: 本選トーナメント対戦表の生成 async generateFinalMatch( - matchType: "elementary" | "open", + category: "elementary" | "open", ): Promise> { /* 初期対戦表を生成 @@ -103,7 +103,7 @@ export class GenerateMatchService { ]; const matches: Match[] = []; - if (matchType === "elementary") { + if (category === "elementary") { for (const v of elementaryTournament) { matches.push( Match.new({ From d944e30ab46506eac2f59bd0be8604d037a9a9cf Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 29 Dec 2023 21:50:33 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat(API):=20=E5=AF=BE=E6=88=A6=E8=A1=A8?= =?UTF-8?q?=E3=81=AE=E7=94=9F=E6=88=90=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++- src/match/controller.ts | 65 ++++++++++++++++++++++++++++++++--------- src/match/main.ts | 29 +++++++++++------- 3 files changed, 74 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a32e8c6..fd310ac 100644 --- a/README.md +++ b/README.md @@ -199,9 +199,11 @@ body: `application/json` - `UNKNOWN_CATEGORY`: 存在しないカテゴリ - `UNKNOWN_MATCH_TYPE`: 存在しない対戦種類 -### `POST /match/{matchType}` +### `POST /match/{matchType}/{category}` 各部門の本選、予選対戦表を生成します +※ 既に生成済みの場合は上書きされます +※ オープン部門の予選対戦表は生成できません(エラーになります) #### 入力 @@ -209,6 +211,8 @@ body: `application/json` - `matchType`: `"final"|"primary"` - 部門名 +- `category`: `"elementary"|"open"` + - カテゴリ ```json {} diff --git a/src/match/controller.ts b/src/match/controller.ts index ca28877..44dc79c 100644 --- a/src/match/controller.ts +++ b/src/match/controller.ts @@ -20,16 +20,32 @@ export class MatchController { this.getService = getService; } - async generateMatch(type: string) { - switch (type) { - case "primary": - return await this.generatePrimary(); - default: - return Result.err(new Error("unknown match type")); + async generateMatch( + type: string, + category: string, + ): Promise> { + if (type === "primary") { + if (category === "open") + return Result.err(new Error("cant generate open primary matches")); + + const res = await this.generatePrimary(); + if (Result.isErr(res)) { + return Result.err(new Error("failed to generate primary matches")); + } + return Result.ok(res[1]); + } else if (type === "final") { + const res = await this.generateFinal(category); + if (Result.isErr(res)) { + return Result.err(new Error("failed to generate final matches")); + } + return Result.ok([res[1]]); } + return Result.err(new Error("invalid match type")); } - private async generatePrimary() { + private async generatePrimary(): Promise< + Result.Result + > { const res = await this.matchService.generatePrimaryMatch(); if (Result.isErr(res)) { return Result.err(res[1]); @@ -37,7 +53,10 @@ export class MatchController { return Result.ok(res[1].map((i) => i.map(this.toJSON))); } - async editMatch(id: string, args: matchUpdateJSON) { + async editMatch( + id: string, + args: matchUpdateJSON, + ): Promise> { const res = await this.editService.handle(id, args); if (Result.isErr(res)) { return Result.err(res[1]); @@ -57,7 +76,9 @@ export class MatchController { return Result.ok(res[1].map((i) => this.toJSON(i.toDomain()))); } - async generateFinal(category: string) { + async generateFinal( + category: string, + ): Promise> { if (!(category === "elementary" || category === "open")) { return Result.err(new Error("invalid match type")); } @@ -65,13 +86,11 @@ export class MatchController { if (Result.isErr(res)) { return Result.err(res[1]); } - return Result.ok( - res[1].map(v => this.toJSON(v)) - ) + return Result.ok(res[1].map((v) => this.toJSON(v))); } - private toJSON(i: Match) { - const toTeamJSON = (i?: Entry) => { + private toJSON(i: Match): matchJSON { + const toTeamJSON = (i?: Entry): matchTeamJSON | undefined => { if (!i) { return i; } @@ -116,3 +135,21 @@ interface matchResultFinalPairJSON { interface matchUpdateJSON { results: matchResultPairJSON | matchResultFinalPairJSON; } + +interface matchTeamJSON { + id: string; + teamName: string; + isMultiWalk: boolean; + category: string; +} + +interface matchJSON { + id: string; + teams: { + left: undefined | matchTeamJSON; + right: undefined | matchTeamJSON; + }; + matchType: "primary" | "final"; + courseIndex: number; + results: matchResultPairJSON | matchResultFinalPairJSON | undefined; +} diff --git a/src/match/main.ts b/src/match/main.ts index 82ac5a1..4fedc53 100644 --- a/src/match/main.ts +++ b/src/match/main.ts @@ -19,17 +19,6 @@ const controller = new MatchController( editService, getService, ); - -matchHandler.post("/:match", async (c) => { - const { match } = c.req.param(); - const res = await controller.generateMatch(match); - if (Result.isErr(res)) { - return c.json([{ error: res[1].message }]); - } - - return c.json(res[1]); -}); - matchHandler.get("/:type", async (c) => { const { type } = c.req.param(); const res = await controller.getMatchByType(type); @@ -55,3 +44,21 @@ matchHandler.put("/:match", async (c) => { return c.json(res[1]); }); + +matchHandler.post("/:type/:category", async (c) => { + /* + 例: + (elementary, primary) -> 小学生部門 予選対戦表を生成 + (elementary, final) -> 小学生部門 決勝トーナメントを生成 + (open, primary) -> エラー + (open, final) -> オープン部門 決勝トーナメントを生成 + + */ + const { type, category } = c.req.param(); + const res = await controller.generateMatch(type, category); + if (Result.isErr(res)) { + return c.json([{ error: res[1].message }], 400); + } + + return c.json(res[1]); +});