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
| Size | Rounds | Matches |
|---|---|---|
| 4 | 2 | 3 |
| 8 | 3 | 7 |
| 16 | 4 | 15 |
| 32 | 5 | 31 |
| 64 | 6 | 63 |
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
| State | Meaning |
|---|---|
| AS | All Square |
| 1 UP | Player 1 leads by 1 |
| 2 DN | Player 1 trails by 2 |
| DORMIE | Lead 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
| Method | Endpoint | Description |
|---|---|---|
POST | /competitions/:id/matchplay/brackets | Create bracket |
GET | /competitions/:id/matchplay/brackets | Get brackets |
POST | /competitions/:id/matchplay/brackets/seed | Seed bracket |
GET | /competitions/:id/matchplay/brackets/:bracketId | Get bracket detail |
GET | /competitions/:id/matchplay/matches/:matchId | Get match detail |
POST | /competitions/:id/matchplay/matches/:matchId/holes | Record holes |
POST | /competitions/:id/matchplay/matches/:matchId/complete | Complete match |
Match result formats
| Result | Meaning |
|---|---|
| "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