Skip to main content

Matchplay

Bracket-based knockout competitions with hole-by-hole match scoring.

Overview

Matchplay competitions feature:

  • Head-to-head matches
  • Bracket/knockout progression
  • Hole-by-hole scoring (not total strokes)
  • Match results like "3&2" or "1 up"

Bracket creation

Create bracket

await matchplayService.createBracket({
competitionId: 'comp-123',
name: 'Club Championship Matchplay',
size: 32, // 32, 16, 8, or 4 players
});

Bracket sizes

SizeRoundsMatches
423
837
16415
32531
64663

Seeding

Auto-seed from qualifying

await matchplayService.seedBracket({
bracketId: 'bracket-123',
method: 'QUALIFYING_ORDER',
qualifyingCompetitionId: 'qual-456',
});

Manual seeding

await matchplayService.seedBracket({
bracketId: 'bracket-123',
method: 'MANUAL',
seeds: [
{ position: 1, entryId: 'entry-a' }, // #1 seed
{ position: 2, entryId: 'entry-b' }, // #2 seed
{ position: 3, entryId: 'entry-c' },
// ...
],
});

Handicap-based seeding

await matchplayService.seedBracket({
bracketId: 'bracket-123',
method: 'HANDICAP_ORDER',
// Lowest handicap = #1 seed
});

Standard bracket positioning

Traditional seeding ensures top seeds meet late:

Round 1:       QF:        SF:        Final:
#1 vs #16 ─┐
├─ Winner ─┐
#8 vs #9 ─┘ │
├─ Winner ─┐
#4 vs #13 ─┐ │ │
├─ Winner ─┘ │
#5 vs #12 ─┘ │
├─ Champion
#2 vs #15 ─┐ │
├─ Winner ─┐ │
#7 vs #10 ─┘ │ │
├─ Winner ─┘
#3 vs #14 ─┐ │
├─ Winner ─┘
#6 vs #11 ─┘

Match data

interface MatchplayMatch {
id: string;
bracketId: string;
round: number; // 1 = first round
position: number; // Position in round
entry1Id: string | null;
entry2Id: string | null;
winnerId: string | null;
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
result: string | null; // e.g., "3&2", "1 up", "19th"
holes: MatchplayHole[];
}

interface MatchplayHole {
holeNumber: number;
entry1Score: number | null;
entry2Score: number | null;
winner: 'ENTRY1' | 'ENTRY2' | 'HALVED' | null;
matchState: string; // e.g., "AS", "1 UP", "2 DN"
}

Match scoring

Record hole-by-hole

await matchplayService.recordHole({
matchId: 'match-123',
holeNumber: 1,
entry1Score: 4,
entry2Score: 5,
});
// Entry 1 wins hole, match state: "1 UP"

Batch record holes

await matchplayService.recordHoles({
matchId: 'match-123',
holes: [
{ holeNumber: 1, entry1Score: 4, entry2Score: 5 },
{ holeNumber: 2, entry1Score: 4, entry2Score: 4 },
{ holeNumber: 3, entry1Score: 3, entry2Score: 4 },
// ...
],
});

Match states

StateMeaning
ASAll Square
1 UPPlayer 1 leads by 1
2 DNPlayer 1 trails by 2
DORMIELead equals holes remaining

Match completion

Standard finish

Match ends when lead exceeds remaining holes:

// After 16 holes, Player 1 is 3 UP with 2 to play
// Match result: "3&2" (3 up with 2 to play)

All square after 18

// Match tied after 18 holes
// Play sudden death from hole 19
await matchplayService.recordHole({
matchId: 'match-123',
holeNumber: 19,
entry1Score: 4,
entry2Score: 5,
});
// Result: "19th" or "20th", etc.

Concession

await matchplayService.concede({
matchId: 'match-123',
concedingEntryId: 'entry-b',
afterHole: 12,
});
// Result: "6&5" (based on state at concession)

Handicap in matchplay

Stroke allowance

Calculate difference between players:

const strokesDiff = Math.round(
(player1CourseHandicap - player2CourseHandicap) * allowancePercent
);

// Player with higher handicap receives strokes
// on holes with stroke index <= strokesDiff

Example

Player A: Course Handicap 10
Player B: Course Handicap 18
Difference: 8 strokes

Player B receives 1 stroke on holes with SI 1-8

Net scoring

// Hole 5: Par 4, SI 3
// Player A gross: 5
// Player B gross: 6, receives 1 stroke
// Player A net: 5
// Player B net: 5
// Result: HALVED

Bracket progression

Automatic advancement

When match completes, winner advances:

// Round 1, Match 1 complete
// Winner auto-populates Round 2, Match 1 (position depends on bracket side)

Byes

For non-power-of-2 fields:

// 24 players in 32-bracket
// 8 first-round byes for top 8 seeds
await matchplayService.seedBracket({
bracketId: 'bracket-123',
method: 'HANDICAP_ORDER',
byes: 8, // Top 8 get first-round byes
});

Consolation brackets

Optional losers bracket:

await matchplayService.createConsolationBracket({
mainBracketId: 'bracket-123',
fromRound: 1, // Collect losers from round 1
});

API endpoints

MethodEndpointDescription
POST/competitions/:id/matchplay/bracketsCreate bracket
GET/competitions/:id/matchplay/bracketsGet brackets
POST/competitions/:id/matchplay/brackets/seedSeed bracket
GET/competitions/:id/matchplay/brackets/:bracketIdGet bracket detail
GET/competitions/:id/matchplay/matches/:matchIdGet match detail
POST/competitions/:id/matchplay/matches/:matchId/holesRecord holes
POST/competitions/:id/matchplay/matches/:matchId/completeComplete match

Match result formats

ResultMeaning
"5&4"Won 5 up with 4 to play
"3&2"Won 3 up with 2 to play
"2&1"Won 2 up with 1 to play
"1 up"Won 1 up on 18th
"19th"Won on 19th hole (first extra)
"20th"Won on 20th hole
"WO"Walkover (opponent withdrew)
"Conceded"Opponent conceded

Querying matches

Get player's matches

const matches = await matchplayService.getPlayerMatches({
competitionId: 'comp-123',
playerId: 'player-456',
});

Get round matches

const roundMatches = await matchplayService.getRoundMatches({
bracketId: 'bracket-123',
round: 2, // Quarter-finals
});

Get bracket standings

const bracket = await matchplayService.getBracket('bracket-123');
// Returns full bracket with all matches and current state