-
Notifications
You must be signed in to change notification settings - Fork 6
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
feat: add function for qualifier progression #3480
Changes from 3 commits
08506dd
b9c78e6
1c29daf
946174e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { findExtension } from '@Acquire/findExtension'; | ||
import { findStructure } from '@Acquire/findStructure'; | ||
import { qualifierDrawPositionAssignment } from '@Assemblies/governors/drawsGovernor'; | ||
import { POSITION, QUALIFYING, WINNER } from '@Constants/drawDefinitionConstants'; | ||
import { | ||
MISSING_QUALIFIED_PARTICIPANTS, | ||
NO_DRAW_POSITIONS_AVAILABLE_FOR_QUALIFIERS, | ||
} from '@Constants/errorConditionConstants'; | ||
import { TALLY } from '@Constants/extensionConstants'; | ||
import { BYE } from '@Constants/matchUpStatusConstants'; | ||
import { POLICY_TYPE_POSITION_ACTIONS } from '@Constants/policyConstants'; | ||
import { SUCCESS } from '@Constants/resultConstants'; | ||
import { decorateResult } from '@Functions/global/decorateResult'; | ||
import { getPositionAssignments, structureAssignedDrawPositions } from '@Query/drawDefinition/positionsGetter'; | ||
import { isCompletedStructure } from '@Query/drawDefinition/structureActions'; | ||
import { getAppliedPolicies } from '@Query/extensions/getAppliedPolicies'; | ||
import { getAllStructureMatchUps } from '@Query/matchUps/getAllStructureMatchUps'; | ||
import { getSourceStructureIdsAndRelevantLinks } from '@Query/structure/getSourceStructureIdsAndRelevantLinks'; | ||
import { randomPop } from '@Tools/arrays'; | ||
import { definedAttributes } from '@Tools/definedAttributes'; | ||
import { ResultType } from '@Types/factoryTypes'; | ||
import { DrawDefinition, Event, Structure, Tournament } from '@Types/tournamentTypes'; | ||
|
||
interface QualifierProgressionArgs { | ||
drawDefinition: DrawDefinition; | ||
event: Event; | ||
mainStructureId: string; | ||
tournamentRecord: Tournament; | ||
} | ||
|
||
export function qualifierProgression({ | ||
drawDefinition, | ||
event, | ||
mainStructureId, | ||
tournamentRecord, | ||
}: QualifierProgressionArgs): ResultType { | ||
const qualifyingParticipantIds: string[] = []; | ||
|
||
const structure = findStructure({ drawDefinition, structureId: mainStructureId })?.structure ?? ({} as Structure); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd be more comfortable if this were explicitly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, can we trust that the value passed in as And, why even require the mainstructureId to be passed in? Since it is always the only structure we're interested in, why not just derive it from the drawDefinition since we want to check anyway? |
||
const appliedPolicies = | ||
getAppliedPolicies({ | ||
tournamentRecord, | ||
drawDefinition, | ||
structure, | ||
event, | ||
}).appliedPolicies ?? {}; | ||
|
||
const policy = appliedPolicies[POLICY_TYPE_POSITION_ACTIONS]; | ||
const requireCompletedStructures = policy?.requireCompletedStructures; | ||
|
||
const { qualifierPositions, positionAssignments } = structureAssignedDrawPositions({ structure }); | ||
|
||
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({ | ||
linkType: WINNER, // WINNER of qualifying structures will traverse link | ||
drawDefinition, | ||
structureId: structure.structureId, | ||
}) || {}; | ||
|
||
const { relevantLinks: roundRobinSourceLinks } = | ||
getSourceStructureIdsAndRelevantLinks({ | ||
linkType: POSITION, // link will define how many finishingPositions traverse the link | ||
drawDefinition, | ||
structureId: structure.structureId, | ||
}) || {}; | ||
|
||
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 winningSide = matchUp.sides.find((side) => side?.sideNumber === matchUp.winningSide); | ||
const relevantSide = matchUp.matchUpStatus === BYE && matchUp.sides?.find(({ participantId }) => participantId); | ||
|
||
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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Round Robins structures must always be completed because otherwise there will be no finishingPosition available, so checking whether completion is required is irrelevant |
||
structureId: sourceLink.source.structureId, | ||
drawDefinition, | ||
}); | ||
|
||
if (!requireCompletedStructures || 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); | ||
randomParticipantId && | ||
qualifierDrawPositionAssignment({ | ||
qualifyingParticipantId: randomParticipantId, | ||
tournamentRecord, | ||
drawDefinition, | ||
drawPosition: position.drawPosition, | ||
structureId: structure.structureId, | ||
}); | ||
}); | ||
|
||
return decorateResult({ | ||
result: definedAttributes({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to return an array of |
||
...SUCCESS, | ||
}), | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import { findStructure } from '@Acquire/findStructure'; | |
type GetSourceStructureDetailArgs = { | ||
drawDefinition: DrawDefinition; | ||
finishingPosition?: string; | ||
targetRoundNumber: number; | ||
targetRoundNumber?: number; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think that |
||
structureId: string; | ||
linkType: string; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
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, | ||
mainStructureId: mainStructure.structureId, | ||
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, | ||
mainStructureId: populatedMainStructure.structureId, | ||
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, | ||
mainStructureId: populatedMainStructure.structureId, | ||
tournamentId: tournamentRecord.tournamentId, | ||
eventId: eventId, | ||
}); | ||
|
||
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); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could usefully add:
const paramsCheck = checkRequiredParameters(params, [{ [DRAW_DEFINITION]: true, [EVENT]: true, [TOURNAMENT_RECORD]: true }]);
if (paramsCheck.error) return paramsCheck;