Outcome Generation
The mocksEngine provides powerful tools for generating match outcomes with realistic scores, various match statuses, and controlled randomization.
generateOutcome
Generate complete match outcomes with scores:
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET3-S:6/TB7',
winningSide: 1,
});
Returns an outcome object with:
- score: Complete score structure with sets
- winningSide: 1 or 2 (or undefined for DOUBLE_WALKOVER, etc.)
- matchUpStatus: 'COMPLETED', 'RETIRED', 'DEFAULTED', etc.
Basic Usage
Simple Outcome
const { outcome } = mocksEngine.generateOutcome();
// Returns:
{
score: {
sets: [
{ side1Score: 6, side2Score: 3, winningSide: 1 },
{ side1Score: 6, side2Score: 4, winningSide: 1 },
],
scoreStringSide1: '6-3 6-4',
scoreStringSide2: '3-6 4-6',
},
winningSide: 1,
matchUpStatus: 'COMPLETED',
}
Specify Winner
const { outcome } = mocksEngine.generateOutcome({
winningSide: 2, // Side 2 wins
});
With MatchUp Format
Generate scores that comply with a specific format:
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET3-S:6/TB7', // Best of 3 sets, games to 6, tiebreak at 7
winningSide: 1,
});
Common formats:
SET3-S:6/TB7- Best of 3 sets, tiebreak at 6-6SET5-S:6/TB7- Best of 5 setsSET3-S:4/TB7- Short sets to 4SET1-S:T20- Timed set (20 minutes)SET3-S:6/TB7-F:TB10- Final set to 10-point tiebreak
Match Status Profiles
Control the distribution of match statuses:
All Completed
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { COMPLETED: 100 },
});
Mixed Statuses
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: {
COMPLETED: 70, // 70% completed normally
RETIRED: 15, // 15% retirements
WALKOVER: 10, // 10% walkovers
DEFAULTED: 5, // 5% defaults
},
});
Specific Status
// Retirement
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { RETIRED: 100 },
winningSide: 1,
});
// Walkover
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { WALKOVER: 100 },
winningSide: 1,
});
// Default
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { DEFAULTED: 100 },
winningSide: 2,
});
Default with Score
By default, DEFAULTED outcomes have no score. To include scores:
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { DEFAULTED: 100 },
defaultWithScorePercent: 100, // Always include score with defaults
winningSide: 1,
});
Double Walkover
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { DOUBLE_WALKOVER: 100 },
});
// Note: winningSide will be undefined
console.log(outcome.winningSide); // undefined
console.log(outcome.matchUpStatus); // 'DOUBLE_WALKOVER'
Suspended/Incomplete
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { SUSPENDED: 100 },
});
// No winningSide for suspended matches
console.log(outcome.winningSide); // undefined
Score Control
Side Weight
Control how often "deciding sets" are generated:
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET3-S:6/TB7',
sideWeight: 4, // Default - controls score variation
});
Lower sideWeight = more three-set matches
Higher sideWeight = more straight-set wins
Timed Sets
Generate outcomes for timed sets:
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET1-S:T20', // 20-minute timed set
pointsPerMinute: 1.5, // Points scored per minute
});
Aggregate Timed Sets
For formats where the winner is determined by total points across all sets (not by winning individual sets), use the aggregate modifier A in the format code:
// 2 timed sets, aggregate scoring — winner has most total points
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET2XA-S:T10', // 2 sets × 10 minutes, aggregate
matchUpStatusProfile: {},
});
// All sets are always played (no early exit)
expect(outcome.score.sets.length).toBe(2);
// Winner determined by total points, not sets won
const side1Total = outcome.score.sets.reduce((s, set) => s + set.side1Score, 0);
const side2Total = outcome.score.sets.reduce((s, set) => s + set.side2Score, 0);
if (outcome.winningSide === 1) expect(side1Total).toBeGreaterThan(side2Total);
Common aggregate timed formats:
SET2XA-S:T10- 2 × 10-minute sets, aggregate scoring (e.g. INTENNSE singles)SET1A-S:T10- 1 × 10-minute set, aggregate scoring (e.g. INTENNSE doubles)
The X modifier means "exactly" — all sets are played regardless of the running score. The A modifier triggers aggregate scoring where the winner is the side with the highest total points across all sets.
Validated Inputs
A matchUpStatusProfile is whole-number percentages that sum to at most 100, each non-negative; the
remainder is COMPLETED. A profile over 100, a negative weight, or a winningSide other than 1 or 2 is
refused with INVALID_VALUES (before, an over-100 profile was accepted because its sum was never
computed).
Format Families
Which scoring families generateOutcome produces, and what it produces for each. Every generated
outcome is one analyzeMatchUp accepts as a validated winning score.
| family | example | generated |
|---|---|---|
| standard sets | SET3-S:6/TB7 | games to setTo, tiebreak at tiebreakAt; the losing score is drawn from 1 upward, so 6-0 and 7-6(0) are never generated (author them with a score string) |
| one-game margin, no tiebreak | SET1-S:5WB1 | ends when a side reaches setTo: 5-0 to 5-4 |
| tiebreak-only sets | SET3-S:TB10 | tiebreak points |
| timed sets | SET1-S:T20 | point totals from pointsPerMinute |
| aggregate timed | SET2XA-S:T10 | point totals, every set played, the higher total wins; a level total goes to the decider |
| outs (innings) | INN4XA-S:O3-M:T50 | inning totals from 0 to outs × 3, a level inning has no winner, the higher aggregate wins |
| combined point total | SET1-S:P32, P32DP, P32WB2 | always decisive: the loser takes fewer than half the total. A level total is never generated, because whether it is legal depends on the structure (a winner-advancing draw refuses it); author one where a test needs it |
An invalid matchUpFormat, or a status profile the generator cannot read, is refused with an error,
and completeAllMatchUps passes that error on as the generation's error rather than recording an empty
result.
generateOutcomeFromScoreString
Create outcomes from score strings:
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '6-1 6-2',
winningSide: 1,
});
Score String Format
Scores are always from the winner's perspective:
// Side 1 wins
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '6-4 3-6 7-5',
winningSide: 1,
});
// Side 2 wins the same match (flipped perspective)
const { outcome2 } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '4-6 6-3 5-7', // Same match, side 2 perspective
winningSide: 2,
});
With Tiebreaks
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '7-6(3) 6-4',
winningSide: 1,
});
// Result includes tiebreak details in sets
outcome.score.sets[0].side1TiebreakScore; // 7
outcome.score.sets[0].side2TiebreakScore; // 3
With Match Status
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '6-3 4-2',
matchUpStatus: 'RETIRED',
winningSide: 1,
});
No Winner Specified
When winningSide is omitted, it's inferred from the score:
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '6-4 6-3',
// winningSide inferred as 1
});
Applying Outcomes
Direct Application
import { tournamentEngine } from 'tods-competition-factory';
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: 'SET3-S:6/TB7',
winningSide: 1,
});
tournamentEngine.devContext(true).setMatchUpStatus({
matchUpId: 'match-123',
drawId: 'draw-456',
outcome,
});
During Tournament Generation
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [
{
drawSize: 16,
outcomes: [
{
roundNumber: 1,
roundPosition: 1,
scoreString: '6-4 6-2',
winningSide: 1,
},
],
},
],
});
Batch Completion
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 32 }],
completeAllMatchUps: true,
randomWinningSide: true,
matchUpStatusProfile: {
COMPLETED: 85,
RETIRED: 10,
DEFAULTED: 5,
},
});
Targeted Completion
Complete matchUps in a specific stage and/or round using completeDrawMatchUps:
const {
drawIds: [drawId],
tournamentRecord,
} = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 16, drawType: 'FIRST_MATCH_LOSER_CONSOLATION' }],
});
const event = tournamentRecord.events[0];
const drawDefinition = event.drawDefinitions.find((d) => d.drawId === drawId);
// Complete only round 1 of the MAIN stage
mocksEngine.completeDrawMatchUps({
tournamentRecord,
drawDefinition,
event,
stage: 'MAIN',
roundNumber: 1,
completeAllMatchUps: '6-3 6-4',
});
// Then complete CONSOLATION separately
mocksEngine.completeDrawMatchUps({
tournamentRecord,
drawDefinition,
event,
stage: 'CONSOLATION',
completeAllMatchUps: '6-2 6-2',
});
Note: The drawDefinition must be a direct reference from tournamentRecord.events[n].drawDefinitions[n], not a copy from engine query methods.
See completeDrawMatchUps for all options.
Removing Outcomes
Reset a matchUp to TO_BE_PLAYED using removeMatchUpOutcome:
// Remove a completed matchUp's outcome
const result = mocksEngine.removeMatchUpOutcome({
tournamentRecord,
drawId,
matchUpId,
});
expect(result.success).toBe(true);
This triggers the same cleanup paths as manual score removal — removeDirectedParticipants, consolation feed reversal, etc. — making it useful for testing score reversal scenarios.
See removeMatchUpOutcome for all options.
Common Patterns
Testing Score Parsing
test('parses various score formats', () => {
const testCases = ['6-4 6-3', '7-6(5) 3-6 6-4', '6-0 6-0', '4-6 6-4 7-6(3)'];
testCases.forEach((scoreString) => {
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString,
winningSide: 1,
});
expect(outcome.score).toBeDefined();
expect(outcome.winningSide).toBe(1);
});
});
Testing Match Completion
test('completes match with retirement', () => {
const { tournamentRecord, drawIds } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 8 }],
});
tournamentEngine.setState(tournamentRecord);
const { matchUps } = tournamentEngine.allTournamentMatchUps();
const matchUp = matchUps[0];
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { RETIRED: 100 },
winningSide: 1,
});
const result = tournamentEngine.setMatchUpStatus({
matchUpId: matchUp.matchUpId,
drawId: drawIds[0],
outcome,
});
expect(result.success).toBe(true);
const { matchUps: updated } = tournamentEngine.allTournamentMatchUps();
const completed = updated.find((m) => m.matchUpId === matchUp.matchUpId);
expect(completed.matchUpStatus).toBe('RETIRED');
});
Testing Score Progression
test('generates progressive match completion', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [
{
drawSize: 8,
outcomes: [
// Quarter-finals
{ roundNumber: 1, roundPosition: 1, scoreString: '6-4 6-2', winningSide: 1 },
{ roundNumber: 1, roundPosition: 2, scoreString: '7-6(3) 6-4', winningSide: 1 },
{ roundNumber: 1, roundPosition: 3, scoreString: '6-3 6-4', winningSide: 2 },
{ roundNumber: 1, roundPosition: 4, scoreString: '6-2 6-3', winningSide: 1 },
// Semi-finals
{ roundNumber: 2, roundPosition: 1, scoreString: '6-4 3-6 7-5', winningSide: 1 },
{ roundNumber: 2, roundPosition: 2, scoreString: '7-5 6-4', winningSide: 2 },
// Final
{ roundNumber: 3, roundPosition: 1, scoreString: '6-4 6-4', winningSide: 1 },
],
},
],
});
tournamentEngine.setState(tournamentRecord);
const { completedMatchUps } = tournamentEngine.tournamentMatchUps();
expect(completedMatchUps.length).toBe(7);
});
Testing Edge Cases
test('handles special match statuses', () => {
const statuses = ['WALKOVER', 'DEFAULTED', 'DOUBLE_WALKOVER', 'RETIRED', 'SUSPENDED'];
statuses.forEach((status) => {
const { outcome } = mocksEngine.generateOutcome({
matchUpStatusProfile: { [status]: 100 },
winningSide: status === 'DOUBLE_WALKOVER' ? undefined : 1,
});
expect(outcome.matchUpStatus).toBe(status);
if (['DOUBLE_WALKOVER', 'SUSPENDED', 'INCOMPLETE'].includes(status)) {
expect(outcome.winningSide).toBeUndefined();
} else {
expect(outcome.winningSide).toBeDefined();
}
});
});
Generating Match Scenarios
test('creates specific match scenario', () => {
// Close 3-setter with multiple tiebreaks
const { outcome } = mocksEngine.generateOutcomeFromScoreString({
scoreString: '7-6(5) 6-7(3) 7-6(10)',
winningSide: 1,
matchUpStatus: 'COMPLETED',
});
expect(outcome.score.sets.length).toBe(3);
expect(outcome.score.sets[0].side1TiebreakScore).toBe(7);
expect(outcome.score.sets[2].side1TiebreakScore).toBe(12);
});
Outcome Object Structure
Completed Match
{
matchUpStatus: 'COMPLETED',
winningSide: 1,
score: {
sets: [
{
setNumber: 1,
side1Score: 6,
side2Score: 4,
winningSide: 1,
},
{
setNumber: 2,
side1Score: 6,
side2Score: 3,
winningSide: 1,
},
],
scoreStringSide1: '6-4 6-3',
scoreStringSide2: '4-6 3-6',
},
}
With Tiebreak
{
matchUpStatus: 'COMPLETED',
winningSide: 1,
score: {
sets: [
{
setNumber: 1,
side1Score: 7,
side2Score: 6,
side1TiebreakScore: 7,
side2TiebreakScore: 3,
winningSide: 1,
},
// ... more sets
],
scoreStringSide1: '7-6(3) 6-4',
scoreStringSide2: '6-7(3) 4-6',
},
}
Retired
{
matchUpStatus: 'RETIRED',
winningSide: 1,
score: {
sets: [], // May be empty or partial
scoreStringSide1: '',
scoreStringSide2: '',
},
}
Validation
The generated outcomes are automatically validated against:
- MatchUp format constraints (set count, games per set, tiebreak rules)
- CODES schema compliance
- Score string parsability
- Logical consistency (winner matches final score)
Tips
- Use matchUpFormat when testing format-specific scoring
- Control status distribution with matchUpStatusProfile for realistic scenarios
- Use generateOutcomeFromScoreString when you need specific scores
- Test edge cases like DOUBLE_WALKOVER and SUSPENDED
- Leverage outcomes in drawProfiles for complete scenario setup
- Remember winningSide is required except for no-winner statuses
- Score strings are always from winner's perspective
Next Steps
- Tournament Generation - Apply outcomes during generation
- Advanced Patterns - Complex outcome scenarios
- Participant Generation - Types and demographics