Lucky Draw
Overview
A Lucky Draw is an elimination-style draw that supports any participant count, not just powers of 2. Unlike standard elimination draws that use byes to pad to a power-of-2 draw size, lucky draws create rounds with non-power-of-2 matchUp counts and use a "lucky loser" mechanism to balance rounds when an odd number of winners is produced.
In the factory, this draw type is represented by the constant LUCKY_DRAW.
When a lucky draw is generated with a power-of-2 participant count, it falls back to a standard elimination tree since no lucky loser mechanism is needed.
Structure
A lucky draw for 11 participants produces rounds with the following matchUp counts:
Round 1: 6 matchUps (12 participants, but only 11 available -- 1 gets a bye equivalent)
Round 2: 3 matchUps
Round 3: 2 matchUps
Round 4: 1 matchUp (Final)
Key structural differences from standard elimination:
- No connecting lines between rounds. Unlike elimination draws where specific matchUp winners feed into specific next-round matchUps, lucky draws do not have fixed advancement paths between rounds.
- Non-power-of-2 matchUp counts. Rounds can have odd numbers of matchUps (e.g., 3, 5, 7).
- Pre-feed rounds. When a round has an odd number of matchUps, it produces an odd number of winners. This is one too many for a standard halving to work, so one loser from that round is selected to advance -- the "lucky loser."
Example: drawSize 25
The schematic below shows a 25-participant lucky draw structure. Pre-feed rounds (where a lucky loser is selected) are highlighted — these are rounds with an odd number of matchUps where the tournament director must select one loser to advance.
| Round | Participants | MatchUps | Pre-feed? | Lucky loser advances? |
|---|---|---|---|---|
| Round 1 | 26 (25 + 1 BYE) | 13 | Yes | Yes -- 1 of 13 losers |
| Round 2 | 14 | 7 | Yes | Yes -- 1 of 7 losers |
| Round 3 | 8 | 4 | No | No |
| Round 4 | 4 | 2 | No | No |
| Final | 2 | 1 | -- | -- |
Explicit Round Profile
By default, a lucky draw's per-round matchUp counts follow a deterministic ceil-halving cascade (e.g., drawSize 40 → [20, 10, 5, 3, 2, 1]). Pass an optional roundProfile to generateDrawDefinition to specify the cascade explicitly — useful when you want a tighter compression than ceil-halving, or when scheduling and broadcast constraints prefer fixed per-round sizes.
engine.generateDrawDefinition({
roundProfile: [20, 12, 8, 4, 2, 1],
drawType: 'LUCKY_DRAW',
drawSize: 40,
});
For the profile above (drawSize 40, 6 rounds):
| Transition | Winners | Next round slots | Lucky losers needed |
|---|---|---|---|
| R1 → R2 | 20 | 24 (12 × 2) | 4 |
| R2 → R3 | 12 | 16 (8 × 2) | 4 |
| R3 → R4 | 8 | 8 (4 × 2) | 0 |
| R4 → R5 | 4 | 4 (2 × 2) | 0 |
| R5 → R6 | 2 | 2 (1 × 2) | 0 |
getLuckyDrawRoundStatus surfaces the per-round count as requiredLuckyLoserCount, and luckyDrawAdvancement accepts participantIds: string[] for transitions where more than one lucky loser is required.
Constraints
The profile must satisfy:
roundProfile[0] * 2 === drawSize— the first round's slot count equals the participant pool.roundProfile[n+1] * 2 >= roundProfile[n]for every transition — no winner-dropping. The next round must have at least enough slots to accommodate all winners from the previous round.- Last entry === 1 — the final round is a single matchUp.
- drawSize must be even. Odd draw sizes still work through the default ceil-halving cascade (with a BYE), but explicit profiles require an even pool.
When a profile is provided, the factory persists it on the structure as a customRoundProfile extension so later queries (getLuckyDrawRoundStatus, luckyDrawAdvancement) can derive the required LL count per transition. Omit the parameter to fall back to the standard ceil-halving cascade.
Lucky-loser placement with multiple LL
When more than one LL advances into a single transition, the placement algorithm scores each candidate slot against three constraints:
- Hard preference for a different half from the LL's defeating winner — preserves the "don't replay your conqueror" property of the single-LL case.
- Soft preference for a different quarter — keeps the LL away from a tight rematch path.
- Distribution across halves — when multiple LL feed into the same round, the algorithm prefers spreading them out rather than clustering all picks into one half of the bracket.
This is best-effort: in tight brackets with high LL counts the algorithm may not satisfy every preference, but the half-avoidance constraint takes precedence over quarter and distribution.
Pre-Feed Rounds and Lucky Loser Selection
A pre-feed round is a round with an odd number of matchUps (and thus an odd number of winners). After a pre-feed round completes, the next round needs one additional participant to fill its draw positions. That participant is selected from the losers of the pre-feed round.
Selection Criteria
The system presents the tournament director with margin-of-defeat data at multiple levels of granularity. The tournament director uses whichever level provides sufficient differentiation:
- Set ratio -- often enough to distinguish losers (e.g., a 3-set loss vs a straight-set loss).
- Game ratio -- useful when set ratios are tied. Computed from
side1Score/side2Score. - Point ratio -- the most granular level, from
side1PointScore/side2PointScore. Only needed when higher-level ratios don't differentiate.
When no statistical differentiation exists at any level, the tournament director makes the selection at their discretion (e.g., by coin flip).
The system ranks eligible losers by margin but does not auto-select; the tournament director always makes the final decision. Participants who lost by walkover or default have no meaningful margin and are excluded from the ranked list.
Per-Round vs Cumulative Margin
By default, margin is calculated based only on the matchUp in which the participant lost. The cumulativeMargin option considers all prior rounds' matchUps for each participant, which can be useful in formats where consistency across rounds should factor into the selection.
The getLuckyDrawRoundStatus method accepts a cumulativeMargin boolean parameter to toggle between these modes.
API
Generating a Lucky Draw
const { drawDefinition } = engine.generateDrawDefinition({
drawType: 'LUCKY_DRAW',
drawSize: 11,
});
Detecting Lucky Rounds
Use hasLuckyRounds to determine whether a structure contains lucky rounds — rounds where the matchUp count transitions from odd to even, meaning one participant advances without playing. This is the definitive structural test for a lucky-style draw, independent of drawType.
const { hasLuckyRounds } = engine;
// Works on any structure, regardless of stage (MAIN, QUALIFYING, etc.)
const isLucky = hasLuckyRounds({ structure });
// Or pass matchUps directly
const isLucky = hasLuckyRounds({ matchUps });
A "lucky round" is identified when round N has an odd number of matchUps and round N+1 has an even number. This transition means one extra winner from round N must be accommodated by selecting a loser to advance (the lucky loser).
hasLuckyRounds correctly handles qualifying structures that happen to have non-power-of-2 draw sizes — these are not lucky draws because their round transitions don't follow the odd→even pattern.
Checking Round Status
Use getLuckyDrawRoundStatus to determine which rounds need a lucky loser selection and to get the ranked list of eligible losers:
const { rounds } = engine.getLuckyDrawRoundStatus({ drawId });
const preFeedRound = rounds.find((r) => r.needsLuckySelection);
// preFeedRound.eligibleLosers is sorted by margin (narrowest loss first)
The returned rounds array contains objects with the following properties:
| Property | Type | Description |
|---|---|---|
roundNumber | number | The round number |
matchUpsCount | number | Total matchUps in this round |
completedCount | number | Number of completed matchUps |
isComplete | boolean | Whether all matchUps in the round are complete |
isPreFeedRound | boolean | Whether this round has an odd number of matchUps (not final round) |
needsLuckySelection | boolean | Whether this round is complete, is a pre-feed round, and the next round has an open position |
nextRoundHasOpenPosition | boolean | Whether the subsequent round has an unfilled draw position |
requiredLuckyLoserCount | number | Number of lucky losers this round contributes to the next round (1 for default cascade; 0+ derived from the explicit roundProfile extension when present) |
eligibleLosers | array | Ranked list of losers (only present when needsLuckySelection is true) |
Each entry in eligibleLosers contains:
| Property | Type | Description |
|---|---|---|
participantId | string | The losing participant's ID |
participantName | string | The losing participant's name |
matchUpId | string | The matchUp in which they lost |
scoreString | string | The score of the matchUp |
margin | number | Margin of defeat (0-1, higher = closer match; excluded for walkovers/defaults) |
gameDifferential | number | Difference in games won between winner and loser |
setsWonByLoser | number | Number of sets won by the losing participant |
Advancing a Lucky Loser
After reviewing the eligible losers, advance the selected participant. For single-LL transitions (the default ceil-halving cascade), pass a scalar participantId:
engine.luckyDrawAdvancement({
participantId: preFeedRound.eligibleLosers[0].participantId,
roundNumber: preFeedRound.roundNumber,
drawId,
});
For multi-LL transitions (when an explicit roundProfile requires more than one LL into the next round), pass participantIds — the length must equal round.requiredLuckyLoserCount:
engine.luckyDrawAdvancement({
participantIds: preFeedRound.eligibleLosers
.slice(0, preFeedRound.requiredLuckyLoserCount)
.map((l) => l.participantId),
roundNumber: preFeedRound.roundNumber,
drawId,
});
The luckyDrawAdvancement method:
- Validates that the draw is a lucky draw.
- Verifies the specified round is a pre-feed round that needs selection.
- Confirms each provided participant is an eligible loser from that round, with no duplicates, and that the count matches
requiredLuckyLoserCount. - Places each participant into an open draw position in the next round using the half/quarter-avoidance placement algorithm described above.
An optional selectionBasis parameter ('MARGIN', 'RANDOM', or 'MANUAL') can be provided for telemetry purposes.
Calculating Match Margin
The calculateMatchUpMargin method returns detailed margin data for any matchUp:
const {
margin, // Combined 0-1 value (undefined for walkovers/defaults)
setRatio, // Loser sets / total sets decided
gameRatio, // Loser games / total games (undefined if no game data)
pointRatio, // Loser points / total points (undefined if no point data)
gameDifferential, // Winner games - loser games
setsWonByWinner,
setsWonByLoser,
} = engine.calculateMatchUpMargin({ matchUpId });
Complete Workflow Example
// 1. Generate a lucky draw for 11 participants
engine.generateDrawDefinition({
drawType: 'LUCKY_DRAW',
drawName: 'Main Draw',
drawSize: 11,
eventId,
});
// 2. After Round 1 completes (6 matchUps, odd count = pre-feed round),
// check which round needs a lucky selection
const { rounds } = engine.getLuckyDrawRoundStatus({ drawId });
const preFeedRound = rounds.find((r) => r.needsLuckySelection);
if (preFeedRound) {
console.log('Eligible losers (ranked by margin):');
for (const loser of preFeedRound.eligibleLosers) {
console.log(` ${loser.participantName}: margin=${loser.margin}, score=${loser.scoreString}`);
}
// 3. Tournament director selects a lucky loser (here, the one with narrowest margin)
const selected = preFeedRound.eligibleLosers[0];
engine.luckyDrawAdvancement({
participantId: selected.participantId,
roundNumber: preFeedRound.roundNumber,
drawId,
});
}
Related
- Adaptive Draw -- Multi-structure variant using lucky draw logic for any participant count
- Draw Types Overview -- List of all pre-defined draw types
- Single Elimination -- Standard power-of-2 knockout draw
- Draw Links -- How structures connect in multi-structure draws
- Generation Governor -- API reference for
generateDrawDefinition