Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
CourtHive committed Dec 13, 2024
2 parents f86c1de + 1966786 commit 63067de
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 0 deletions.
17 changes: 17 additions & 0 deletions documentation/docs/governors/draws-governor.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,23 @@ engine.qualifierDrawPositionAssignment({

---

## qualifiersProgression

Randomly replaces existing qualifier drawPosition assignments in the MAIN draw with qualified participants from the QUALIFYING draw.

Fills as many qualifier drawPositions as possible with qualified participants, e.g. if there are 4 qualifier drawPositions and 2 currently qualified participants, only 2 will be assigned.

```js
engine.qualifierProgression({
drawId,
eventId,
targetRoundNumber, // optional - defaults to 1
tournamentId,
});
```

---

## removeDrawDefinitionExtension

```js
Expand Down
1 change: 1 addition & 0 deletions src/assemblies/governors/drawsGovernor/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { removeDrawPositionAssignment } from '@Mutate/drawDefinitions/removeDraw
export { automatedPlayoffPositioning } from '@Mutate/drawDefinitions/automatedPlayoffPositioning';
export { modifySeedAssignment } from '@Mutate/drawDefinitions/entryGovernor/modifySeedAssignment';
export { qualifierDrawPositionAssignment } from '@Mutate/matchUps/drawPositions/positionQualifier';
export { qualifierProgression } from '@Mutate/drawDefinitions/positionGovernor/qualifierProgression';
export { setStructureOrder } from '@Mutate/drawDefinitions/structureGovernor/setStructureOrder';
export { attachQualifyingStructure } from '@Mutate/drawDefinitions/attachQualifyingStructure';
export { renameStructures } from '@Mutate/drawDefinitions/structureGovernor/renameStructures';
Expand Down
10 changes: 10 additions & 0 deletions src/constants/errorConditionConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ export const MISSING_STRUCTURE = {
message: 'Missing structure',
code: 'ERR_MISSING_STRUCTURE',
};
export const MISSING_MAIN_STRUCTURE = {
message: 'Missing MAIN structure',
code: 'ERR_MISSING_MAIN_STRUCTURE',
};
export const UNLINKED_STRUCTURES = {
message: 'drawDefinition contains unlinked structures',
code: 'ERR_MISSING_STRUCTURE_LINKS',
Expand Down Expand Up @@ -503,6 +507,10 @@ export const MISSING_PARTICIPANT_ID = {
message: 'Missing participantId',
code: 'ERR_MISSING_PARTICIPANT_ID',
};
export const MISSING_QUALIFIED_PARTICIPANTS = {
message: 'Missing qualified participants',
code: 'ERR_MISSING_QUALIFIED_PARTICIPANTS',
};
export const PARTICIPANT_NOT_FOUND = {
message: 'Participant Not Found',
code: 'ERR_NOT_FOUND_PARTICIPANT',
Expand Down Expand Up @@ -894,6 +902,7 @@ export const errorConditionConstants = {
MISSING_DRAW_SIZE,
MISSING_ENTRIES,
MISSING_EVENT,
MISSING_QUALIFIED_PARTICIPANTS,
MISSING_MATCHUP_FORMAT,
MISSING_MATCHUP_ID,
MISSING_MATCHUP_IDS,
Expand Down Expand Up @@ -924,6 +933,7 @@ export const errorConditionConstants = {
MISSING_STAGE,
MISSING_STRUCTURE_ID,
MISSING_STRUCTURE,
MISSING_MAIN_STRUCTURE,
MISSING_STRUCTURES,
MISSING_TARGET_LINK,
MISSING_TIE_FORMAT,
Expand Down
170 changes: 170 additions & 0 deletions src/mutate/drawDefinitions/positionGovernor/qualifierProgression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { getSourceStructureIdsAndRelevantLinks } from '@Query/structure/getSourceStructureIdsAndRelevantLinks';
import { getPositionAssignments, structureAssignedDrawPositions } from '@Query/drawDefinition/positionsGetter';
import { qualifierDrawPositionAssignment } from '@Mutate/matchUps/drawPositions/positionQualifier';
import { checkRequiredParameters } from '@Helpers/parameters/checkRequiredParameters';
import { getAllStructureMatchUps } from '@Query/matchUps/getAllStructureMatchUps';
import { isCompletedStructure } from '@Query/drawDefinition/structureActions';
import { getAppliedPolicies } from '@Query/extensions/getAppliedPolicies';
import { decorateResult } from '@Functions/global/decorateResult';
import { definedAttributes } from '@Tools/definedAttributes';
import { findExtension } from '@Acquire/findExtension';
import { ResultType } from '@Types/factoryTypes';
import { randomPop } from '@Tools/arrays';

// Constants and Types
import { DRAW_DEFINITION, EVENT, TOURNAMENT_RECORD } from '@Constants/attributeConstants';
import { MAIN, POSITION, QUALIFYING, WINNER } from '@Constants/drawDefinitionConstants';
import { DrawDefinition, Event, Tournament } from '@Types/tournamentTypes';
import { POLICY_TYPE_POSITION_ACTIONS } from '@Constants/policyConstants';
import { BYE } from '@Constants/matchUpStatusConstants';
import { TALLY } from '@Constants/extensionConstants';
import { SUCCESS } from '@Constants/resultConstants';
import {
MISSING_MAIN_STRUCTURE,
MISSING_QUALIFIED_PARTICIPANTS,
NO_DRAW_POSITIONS_AVAILABLE_FOR_QUALIFIERS,
} from '@Constants/errorConditionConstants';

interface QualifierProgressionArgs {
drawDefinition: DrawDefinition;
tournamentRecord: Tournament;
targetRoundNumber?: number;
event: Event;
}

export function qualifierProgression({
targetRoundNumber = 1,
tournamentRecord,
drawDefinition,
event,
}: QualifierProgressionArgs): ResultType {
const paramsCheck = checkRequiredParameters({ drawDefinition, event, tournamentRecord }, [
{ [DRAW_DEFINITION]: true, [EVENT]: true, [TOURNAMENT_RECORD]: true },
]);
if (paramsCheck.error) return paramsCheck;

const assignedParticipants: { participantId: string; drawPosition: number }[] = [];
const qualifyingParticipantIds: string[] = [];

const mainStructure = drawDefinition.structures?.find(
(structure) => structure.stage === MAIN && structure.stageSequence === 1,
);

if (!mainStructure) return decorateResult({ result: { error: MISSING_MAIN_STRUCTURE } });

const appliedPolicies =
getAppliedPolicies({ tournamentRecord, drawDefinition, structure: mainStructure, event }).appliedPolicies ?? {};

const policy = appliedPolicies[POLICY_TYPE_POSITION_ACTIONS];
const requireCompletedStructures = policy?.requireCompletedStructures;

const { qualifierPositions, positionAssignments } = structureAssignedDrawPositions({ structure: mainStructure });

if (!qualifierPositions.length)
return decorateResult({ result: { error: NO_DRAW_POSITIONS_AVAILABLE_FOR_QUALIFIERS } });

const assignedParticipantIds = positionAssignments.map((assignment) => assignment.participantId).filter(Boolean);

const { relevantLinks: eliminationSourceLinks } =
getSourceStructureIdsAndRelevantLinks({
structureId: mainStructure.structureId,
targetRoundNumber,
linkType: WINNER, // WINNER of qualifying structures will traverse link
drawDefinition,
}) || {};

const { relevantLinks: roundRobinSourceLinks } =
getSourceStructureIdsAndRelevantLinks({
structureId: mainStructure.structureId,
targetRoundNumber,
linkType: POSITION, // link will define how many finishingPositions traverse the link
drawDefinition,
}) || {};

for (const sourceLink of eliminationSourceLinks) {
const structure = drawDefinition.structures?.find(
(structure) => structure.structureId === sourceLink.source.structureId,
);
if (structure?.stage !== QUALIFYING) continue;

const structureCompleted = isCompletedStructure({ structureId: sourceLink.source.structureId, drawDefinition });

if (!requireCompletedStructures || structureCompleted) {
const qualifyingRoundNumber = structure.qualifyingRoundNumber;
const { matchUps } = getAllStructureMatchUps({
matchUpFilters: {
...(qualifyingRoundNumber && { roundNumbers: [qualifyingRoundNumber] }),
hasWinningSide: true,
},
afterRecoveryTimes: false,
inContext: true,
structure,
});

for (const matchUp of matchUps) {
const relevantSide = matchUp.matchUpStatus === BYE && matchUp.sides?.find(({ participantId }) => participantId);
const winningSide = matchUp.sides.find((side) => side?.sideNumber === matchUp.winningSide);

if (winningSide || relevantSide) {
const { participantId } = winningSide || relevantSide || {};
if (participantId && !assignedParticipantIds.includes(participantId)) {
qualifyingParticipantIds.push(participantId);
}
}
}
}
}

for (const sourceLink of roundRobinSourceLinks) {
const structure = drawDefinition?.structures?.find(
(structure) => structure.structureId === sourceLink.source.structureId,
);
if (structure?.stage !== QUALIFYING) continue;

const structureCompleted = isCompletedStructure({
structureId: sourceLink.source.structureId,
drawDefinition,
});

if (structureCompleted) {
const { positionAssignments } = getPositionAssignments({ structure });
const relevantParticipantIds: any =
positionAssignments
?.map((assignment) => {
const participantId = assignment.participantId;
const results = findExtension({ element: assignment, name: TALLY }).extension?.value;

return results ? { participantId, groupOrder: results?.groupOrder } : {};
})
.filter(
({ groupOrder, participantId }) => groupOrder === 1 && !assignedParticipantIds.includes(participantId),
)
.map(({ participantId }) => participantId) ?? [];

if (relevantParticipantIds) qualifyingParticipantIds.push(...relevantParticipantIds);
}
}

if (!qualifyingParticipantIds.length) return decorateResult({ result: { error: MISSING_QUALIFIED_PARTICIPANTS } });

qualifierPositions.forEach((position) => {
const randomParticipantId = randomPop(qualifyingParticipantIds);

if (randomParticipantId) {
const positionAssignmentResult: ResultType = qualifierDrawPositionAssignment({
qualifyingParticipantId: randomParticipantId,
structureId: mainStructure.structureId,
drawPosition: position.drawPosition,
tournamentRecord,
drawDefinition,
});

positionAssignmentResult?.success &&
assignedParticipants.push({ participantId: randomParticipantId, drawPosition: position.drawPosition });
}
});

return decorateResult({
result: definedAttributes({ ...SUCCESS, assignedParticipants }),
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import tournamentEngine from '@Engines/syncEngine';
import mocksEngine from '@Assemblies/engines/mock';
import { expect, it } from 'vitest';

import { MAIN, QUALIFYING } from '@Constants/drawDefinitionConstants';
import { INDIVIDUAL } from '@Constants/participantConstants';

import { COMPLETED } from '@Constants/matchUpStatusConstants';
import {
MISSING_QUALIFIED_PARTICIPANTS,
NO_DRAW_POSITIONS_AVAILABLE_FOR_QUALIFIERS,
} from '@Constants/errorConditionConstants';

it('can assign all available qualified participants to the main structure qualifying draw positions', () => {
const {
tournamentRecord,
eventIds: [eventId],
} = mocksEngine.generateTournamentRecord({
participantsProfile: { participantsCount: 100 },
eventProfiles: [{ eventName: 'test' }],
});
expect(tournamentRecord.participants.length).toEqual(100);

tournamentEngine.setState(tournamentRecord);

const { participants } = tournamentEngine.getParticipants({
participantFilters: { participantTypes: [INDIVIDUAL] },
});
const participantIds = participants.map((p) => p.participantId);
const mainParticipantIds = participantIds.slice(0, 12);
const qualifyingParticipantIds = participantIds.slice(12, 28);

let result = tournamentEngine.addEventEntries({
participantIds: mainParticipantIds,
eventId,
});
expect(result.success).toEqual(true);
result = tournamentEngine.addEventEntries({
participantIds: qualifyingParticipantIds,
entryStage: QUALIFYING,
eventId,
});
expect(result.success).toEqual(true);

const { drawDefinition: qualifyingDrawDefinition } = tournamentEngine.generateDrawDefinition({
qualifyingProfiles: [
{
structureProfiles: [
{
qualifyingPositions: 4,
drawSize: 16,
},
],
},
],
qualifyingOnly: true,
eventId,
});

// assert QUALIFYING structure is populated and MAIN structure is empty
const mainStructure = qualifyingDrawDefinition.structures.find(({ stage }) => stage === MAIN);
const qualifyingStructure = qualifyingDrawDefinition.structures.find(({ stage }) => stage === QUALIFYING);
expect(qualifyingStructure.matchUps.length).toEqual(12);
expect(mainStructure.matchUps.length).toEqual(0);

const addDrawDefinitionResult = tournamentEngine.addDrawDefinition({
activeTournamentId: tournamentRecord.tournamentId,
drawDefinition: qualifyingDrawDefinition,
allowReplacement: true,
eventId,
});

expect(addDrawDefinitionResult.success).toEqual(true);

// assert no MAIN draw qualifying positions are available
const noMainProgressionResult = tournamentEngine.qualifierProgression({
drawId: qualifyingDrawDefinition.drawId,
targetRoundNumber: 1,
tournamentId: tournamentRecord.tournamentId,
eventId: eventId,
});
expect(noMainProgressionResult.error).toEqual(NO_DRAW_POSITIONS_AVAILABLE_FOR_QUALIFIERS);

const { drawDefinition } = tournamentEngine.generateDrawDefinition({
qualifyingProfiles: [
{
structureProfiles: [{ seedsCount: 4, drawSize: 16, qualifyingPositions: 4 }],
},
],
eventId,
});

// assert MAIN and QUALIFYING structures are populated
const populatedMainStructure = drawDefinition.structures.find(({ stage }) => stage === MAIN);
const newQualifyingStructure = drawDefinition.structures.find(({ stage }) => stage === QUALIFYING);
expect(populatedMainStructure.matchUps.length).toEqual(15);
expect(newQualifyingStructure.matchUps.length).toEqual(12);

const addMainDrawDefinitionResult = tournamentEngine.addDrawDefinition({
activeTournamentId: tournamentRecord.tournamentId,
drawDefinition,
allowReplacement: true,
eventId,
});

expect(addMainDrawDefinitionResult.success).toEqual(true);

// assert no qualified participants are available
const noQualifiersProgressionResult = tournamentEngine.qualifierProgression({
drawId: drawDefinition.drawId,
targetRoundNumber: 1,
tournamentId: tournamentRecord.tournamentId,
eventId: eventId,
});

expect(noQualifiersProgressionResult.error).toEqual(MISSING_QUALIFIED_PARTICIPANTS);

newQualifyingStructure.matchUps.forEach(({ matchUpId }) =>
tournamentEngine.setMatchUpStatus({
tournamentId: tournamentRecord.tournamentId,
drawId: drawDefinition.drawId,
matchUpId,
matchUpStatus: COMPLETED,
outcome: { winningSide: 1 },
}),
);

const progressQualifiersResult = tournamentEngine.qualifierProgression({
drawId: drawDefinition.drawId,
targetRoundNumber: 1,
tournamentId: tournamentRecord.tournamentId,
eventId: eventId,
});

expect(progressQualifiersResult.assignedParticipants.length).toEqual(4);
expect(progressQualifiersResult.success).toEqual(true);

// assert qualified participants have been assigned to the main draw positions
const mainDrawPositionAssignments = populatedMainStructure.positionAssignments;
expect(mainDrawPositionAssignments.length).toEqual(16);
expect(mainDrawPositionAssignments.filter((p) => p.qualifier && p.participantId).length).toEqual(4);
expect(mainDrawPositionAssignments.filter((p) => p.qualifier && !p.participantId).length).toEqual(0);
});

0 comments on commit 63067de

Please sign in to comment.