Skip to content

Commit

Permalink
Refactor engines into classes
Browse files Browse the repository at this point in the history
  • Loading branch information
laingsimon committed Jun 21, 2024
1 parent f570f16 commit cef2a2d
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 321 deletions.
43 changes: 8 additions & 35 deletions CourageScores/ClientApp/src/components/tournaments/competition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {TournamentSideDto} from "../../interfaces/models/dtos/Game/TournamentSid
import {any} from "../../helpers/collections";
import {ILayoutDataForRound} from "./layout";
import {IMnemonicAccumulator} from "./layout/shared";
import {getPlayedLayoutData} from "./layout/new-played";
import {getUnplayedLayoutData} from "./layout/new-unplayed";
import {PlayedEngine} from "./layout/PlayedEngine";
import {UnplayedEngine} from "./layout/UnplayedEngine";
import {ILayoutEngine} from "./layout/ILayoutEngine";

export interface ITournamentLayoutGenerationContext {
matchOptionDefaults: GameMatchOptionDto;
Expand All @@ -14,38 +15,10 @@ export interface ITournamentLayoutGenerationContext {
}

export function getLayoutData(round: TournamentRoundDto, sides: TournamentSideDto[], context: ITournamentLayoutGenerationContext): ILayoutDataForRound[] {
return setRoundNames(round && any(round.matches)
? getPlayedLayoutData(sides.filter((s: TournamentSideDto) => !s.noShow), round, context)
: getUnplayedLayoutData(sides.filter((s: TournamentSideDto) => !s.noShow)));
}

function setRoundNames(layoutData: ILayoutDataForRound[]): ILayoutDataForRound[] {
const layoutDataCopy: ILayoutDataForRound[] = layoutData.filter(_ => true);
const newLayoutData: ILayoutDataForRound[] = [];
let unnamedRoundNumber: number = layoutDataCopy.length - 3;

while (any(layoutDataCopy)) {
const lastRound: ILayoutDataForRound = layoutDataCopy.pop();
let roundName = null;
switch (newLayoutData.length) {
case 0:
roundName = 'Final';
break;
case 1:
roundName = 'Semi-Final';
break;
case 2:
roundName = 'Quarter-Final';
break;
default:
roundName = `Round ${unnamedRoundNumber--}`;
break;
}
const unplayedEngine: ILayoutEngine = new UnplayedEngine();
const engine: ILayoutEngine = round && any(round.matches)
? new PlayedEngine(context, round, unplayedEngine)
: unplayedEngine;

lastRound.name = lastRound.name || roundName;
newLayoutData.unshift(lastRound);
}

return newLayoutData;
return engine.calculate(sides.filter((s: TournamentSideDto) => !s.noShow));
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {TournamentSideDto} from "../../../interfaces/models/dtos/Game/TournamentSideDto";
import {ILayoutDataForRound} from "../layout";

export interface ILayoutEngine {
calculate(sides: TournamentSideDto[]): ILayoutDataForRound[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {ILayoutEngine} from "./ILayoutEngine";
import {TournamentSideDto} from "../../../interfaces/models/dtos/Game/TournamentSideDto";
import {TournamentRoundDto} from "../../../interfaces/models/dtos/Game/TournamentRoundDto";
import {ILayoutDataForMatch, ILayoutDataForRound, ILayoutDataForSide} from "../layout";
import {ITournamentLayoutGenerationContext} from "../competition";
import {any} from "../../../helpers/collections";
import {TournamentMatchDto} from "../../../interfaces/models/dtos/Game/TournamentMatchDto";
import {GameMatchOptionDto} from "../../../interfaces/models/dtos/Game/GameMatchOptionDto";

export class PlayedEngine implements ILayoutEngine {
private readonly _context: ITournamentLayoutGenerationContext;
private readonly _round: TournamentRoundDto;
private readonly _unplayedEngine: ILayoutEngine;

constructor(context: ITournamentLayoutGenerationContext, round: TournamentRoundDto, unplayedEngine: ILayoutEngine) {
this._round = round;
this._context = context;
this._unplayedEngine = unplayedEngine;
}

calculate(sides: TournamentSideDto[]): ILayoutDataForRound[] {
const unplayedRounds: ILayoutDataForRound[] = this._unplayedEngine.calculate(sides);
const rounds: TournamentRoundDto[] = this.flattenAllRounds();
let remainingSides: TournamentSideDto[] = sides.filter(s => !!s);

return unplayedRounds.map((unplayedRound: ILayoutDataForRound, index: number): ILayoutDataForRound => {
const playedRound: TournamentRoundDto = rounds[index];
if (!playedRound) {
const adaptedRound: ILayoutDataForRound = Object.assign({}, unplayedRound);
adaptedRound.possibleSides = remainingSides.filter(s => !!s); // copy the array so it cannot be modified
return adaptedRound;
}

const winners: TournamentSideDto[] = [];
const round: ILayoutDataForRound = this.createRound(playedRound, unplayedRound, winners, remainingSides, unplayedRounds[index + 1]);
const unselectedSides: TournamentSideDto[] = remainingSides.filter((remainingSide: TournamentSideDto) => {
return !any(round.alreadySelectedSides, (s: TournamentSideDto) => s.id === remainingSide.id); // exclude any already selected side
});
remainingSides = winners.concat(unselectedSides);
return round;
});
}

private flattenAllRounds(): TournamentRoundDto[] {
let currentRound: TournamentRoundDto = this._round;
const rounds: TournamentRoundDto[] = [];

while (currentRound) {
rounds.push(currentRound);
currentRound = currentRound.nextRound;
}

return rounds;
}

private createRound(playedRound: TournamentRoundDto, unplayedRound: ILayoutDataForRound,
winners: TournamentSideDto[], remainingSides: TournamentSideDto[], nextRound?: ILayoutDataForRound): ILayoutDataForRound {
const alreadySelectedSides: TournamentSideDto[] = unplayedRound.alreadySelectedSides || []; // sides will be added in as matches are created

return {
name: unplayedRound.name,
round: playedRound,
preRound: unplayedRound.preRound,
possibleSides: remainingSides,
alreadySelectedSides: alreadySelectedSides,
matches: unplayedRound.matches.map((unplayedMatch: ILayoutDataForMatch, index: number): ILayoutDataForMatch => {
const playedMatch: TournamentMatchDto = playedRound ? playedRound.matches[index] : null;
if (!playedMatch) {
// add to unplayed sides
return unplayedMatch;
}

return this.createMatch(playedRound, unplayedMatch, playedMatch, index, alreadySelectedSides, winners, nextRound);
}),
};
}

private setSidePlayingInNextRound(side: TournamentSideDto, nextRound: ILayoutDataForRound, unplayedMatch: ILayoutDataForMatch) {
if (!nextRound) {
return;
}

for (const match of nextRound.matches) {
if (match.sideA.mnemonic === `winner(${unplayedMatch.mnemonic})`) {
match.sideA.mnemonic = side.name;
return;
}
if (match.sideB.mnemonic === `winner(${unplayedMatch.mnemonic})`) {
match.sideB.mnemonic = side.name;
return;
}
}
}

private createMatch(playedRound: TournamentRoundDto, unplayedMatch: ILayoutDataForMatch,
playedMatch: TournamentMatchDto, index: number, alreadySelectedSides: TournamentSideDto[], winners: TournamentSideDto[],
nextRound?: ILayoutDataForRound): ILayoutDataForMatch {
let winner: string = null;
const matchOptions: GameMatchOptionDto = playedRound.matchOptions[index] || this._context.matchOptionDefaults;
const numberOfLegs: number = matchOptions.numberOfLegs;
if (playedMatch.scoreA > (numberOfLegs / 2.0)) {
winners.push(playedMatch.sideA);
winner = 'sideA';
this.setSidePlayingInNextRound(playedMatch.sideA, nextRound, unplayedMatch);
} else if (playedMatch.scoreB > (numberOfLegs / 2.0)) {
winners.push(playedMatch.sideB);
winner = 'sideB';
this.setSidePlayingInNextRound(playedMatch.sideB, nextRound, unplayedMatch);
}
alreadySelectedSides.push(playedMatch.sideA);
alreadySelectedSides.push(playedMatch.sideB);

return {
sideA: this.getSide(playedMatch.sideA, unplayedMatch.sideA.mnemonic),
sideB: this.getSide(playedMatch.sideB, unplayedMatch.sideB.mnemonic),
scoreA: (playedMatch.scoreA ? playedMatch.scoreA.toString() : null) || '0',
scoreB: (playedMatch.scoreB ? playedMatch.scoreB.toString() : null) || '0',
match: playedMatch,
bye: unplayedMatch.bye,
winner: winner,
mnemonic: unplayedMatch.mnemonic,
hideMnemonic: unplayedMatch.hideMnemonic,
numberOfSidesOnTheNight: unplayedMatch.numberOfSidesOnTheNight,
matchOptions: unplayedMatch.matchOptions,
saygId: playedMatch.saygId,
};
}

private getSide(side?: TournamentSideDto, mnemonic?: string): ILayoutDataForSide {
return {
id: side ? side.id : null,
name: side ? side.name: null,
link: side ? this._context.getLinkToSide(side) : null,
mnemonic: side && side.id
? null
: mnemonic
};
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {getUnplayedLayoutData} from "./new-unplayed";
import {ILayoutDataForMatch, ILayoutDataForRound, ILayoutDataForSide} from "../layout";
import {TournamentSideDto} from "../../../interfaces/models/dtos/Game/TournamentSideDto";
import {repeat} from "../../../helpers/projection";
import {UnplayedEngine} from "./UnplayedEngine";
import {ILayoutEngine} from "./ILayoutEngine";

describe('new-unplayed', () => {
describe('UnplayedEngine', () => {
let possibleSides: TournamentSideDto[];
let engine: ILayoutEngine;

function getSides(count: number): TournamentSideDto[] {
return repeat(count, (index: number): TournamentSideDto => {
Expand Down Expand Up @@ -69,28 +71,29 @@ describe('new-unplayed', () => {

beforeEach(() => {
possibleSides = null;
engine = new UnplayedEngine();
});

it('returns no rounds or matches for no sides', () => {
possibleSides = getSides(0);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([]);
});

it('returns no rounds or matches for one side', () => {
possibleSides = getSides(1);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([]);
});

it('returns 1 round with 1 match for 2 sides', () => {
possibleSides = getSides(2);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([
round('Final', match('A', 'B', 'M1', undefined, '!vs')),
Expand All @@ -100,7 +103,7 @@ describe('new-unplayed', () => {
it('returns 2 (full) rounds for 7 sides', () => {
possibleSides = getSides(7);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([
preRound(
Expand All @@ -123,7 +126,7 @@ describe('new-unplayed', () => {
it('returns 3 (full) rounds for 8 sides', () => {
possibleSides = getSides(8);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([
round(
Expand All @@ -148,7 +151,7 @@ describe('new-unplayed', () => {
it('returns 1 preliminary match, then 3 (full) rounds for 9 sides', () => {
possibleSides = getSides(9);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([
preRound(
Expand Down Expand Up @@ -176,7 +179,7 @@ describe('new-unplayed', () => {
it('returns 2 preliminary matches, then 3 (full) rounds for 10 sides', () => {
possibleSides = getSides(10);

const result: ILayoutDataForRound[] = getUnplayedLayoutData(possibleSides);
const result: ILayoutDataForRound[] = engine.calculate(possibleSides);

expect(result).toEqual([
preRound(
Expand Down
Loading

0 comments on commit cef2a2d

Please sign in to comment.