Scoring System
Comprehensive scoring for stroke play and stableford formats, including countback tie resolution.
Scorecard workflow
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌────────┐
│ PENDING │───▶│ IN_PROGRESS │───▶│ ATTESTED │───▶│ LOCKED │
└──────────┘ └─────────────┘ └──────────┘ └────────┘
│ │ │ │
Entry Scoring Player Admin
created started verified finalized
States
| State | Description |
|---|---|
PENDING | Entry exists, no scores entered |
IN_PROGRESS | Scoring started, not complete |
ATTESTED | Player has verified/signed scorecard |
LOCKED | Admin finalized, no further changes |
Stableford scoring
Points table
Points awarded based on net score relative to par:
| Net Score | Points | Name |
|---|---|---|
| ≥ Double Bogey | 0 | - |
| Bogey | 1 | Net Bogey |
| Par | 2 | Net Par |
| Birdie | 3 | Net Birdie |
| Eagle | 4 | Net Eagle |
| ≤ Albatross | 5 | Net Albatross+ |
Strokes received
Based on stroke index (SI) and course handicap (CH):
function calculateStrokesReceived(
strokeIndex: number,
courseHandicap: number,
holesInPlay = 18
): number {
if (courseHandicap >= 0) {
// Receiving strokes
if (courseHandicap <= holesInPlay) {
return strokeIndex <= courseHandicap ? 1 : 0;
} else {
// High handicap - some holes get 2 strokes
const extraStrokes = courseHandicap - holesInPlay;
if (strokeIndex <= extraStrokes) return 2;
if (strokeIndex <= courseHandicap) return 1;
return 0;
}
} else {
// Plus handicap - giving strokes
const strokesToGive = Math.abs(courseHandicap);
return strokeIndex <= strokesToGive ? -1 : 0;
}
}
Examples:
| Course Handicap | SI 1 | SI 10 | SI 18 |
|---|---|---|---|
| 10 | 1 stroke | 1 stroke | 0 strokes |
| 18 | 1 stroke | 1 stroke | 1 stroke |
| 24 | 2 strokes | 1 stroke | 1 stroke |
| +2 (plus) | -1 stroke | 0 strokes | 0 strokes |
Calculation example
Hole 5: Par 4, SI 3, Gross 6
Course Handicap: 15
Strokes received: 1 (SI 3 ≤ CH 15)
Net score: 6 - 1 = 5
Net to par: 5 - 4 = +1 (Net Bogey)
Stableford points: 1
Stroke play scoring
Net score calculation
Net Score = Gross Score - Course Handicap
For hole-by-hole:
Net Hole Score = Gross Strokes - Strokes Received
To-par calculation
To Par = Gross Total - Course Par
Net To Par = Net Total - Course Par
Net Double Bogey (NDB)
Maximum score for handicapping purposes. Scores above NDB are adjusted down.
Formula
NDB = Par + 2 + Strokes Received
Examples
| Hole | Par | SI | CH 12 | CH 24 | NDB (CH 12) | NDB (CH 24) |
|---|---|---|---|---|---|---|
| 1 | 4 | 5 | 1 | 2 | 7 | 8 |
| 2 | 3 | 15 | 0 | 1 | 5 | 6 |
| 3 | 5 | 1 | 1 | 2 | 8 | 9 |
Application
function adjustScoreForHandicapping(
grossStrokes: number,
par: number,
strokeIndex: number,
courseHandicap: number,
holesInPlay = 18
): number {
const maxScore = par + 2 + calculateStrokesReceived(strokeIndex, courseHandicap, holesInPlay);
return Math.min(grossStrokes, maxScore);
}
Countback tie resolution
Standard WHS/R&A procedure for breaking ties.
Sequence
- Back 9: Sum of last 9 holes (holes 10-18)
- Back 6: Sum of last 6 holes (holes 13-18)
- Back 3: Sum of last 3 holes (holes 16-18)
- Last hole: Score on hole 18
Format-specific comparison
| Format | Better Score |
|---|---|
| Stableford | Higher points |
| Stroke | Lower strokes |
Data structure
interface CountbackScores {
back9: number; // Holes 10-18
back6: number; // Holes 13-18
back3: number; // Holes 16-18
last: number; // Hole 18
}
Example (Stableford)
| Player | Total | Back 9 | Back 6 | Back 3 | Last | Position |
|---|---|---|---|---|---|---|
| Alice | 36 | 20 | 14 | 8 | 3 | 1st |
| Bob | 36 | 20 | 14 | 7 | 2 | 2nd |
| Carol | 36 | 19 | 13 | 7 | 3 | 3rd |
Alice and Bob tied on total (36) and back 9 (20) and back 6 (14), but Alice wins on back 3 (8 vs 7).
Position assignment
const positions = assignPositionsWithTies(results, 'STABLEFORD');
// Returns: Map<entryId, { position: number, isTied: boolean }>
Tied entries after full countback share position:
- T1, T1, 3rd (two tied for 1st)
- 1st, T2, T2, 4th (two tied for 2nd)
Round scoring
Calculate round totals
function calculateRoundStableford(
holes: Array<{ grossStrokes: number | null; par: number; strokeIndex: number }>,
courseHandicap: number
): { totalPoints: number; holesScored: number } {
let totalPoints = 0;
let holesScored = 0;
for (const hole of holes) {
if (hole.grossStrokes === null) continue;
const strokesReceived = calculateStrokesReceived(
hole.strokeIndex,
courseHandicap,
holes.length
);
const result = calculateStablefordPoints({
grossStrokes: hole.grossStrokes,
par: hole.par,
strokesReceived,
});
totalPoints += result.stablefordPoints;
holesScored++;
}
return { totalPoints, holesScored };
}
Scorecard data
interface Scorecard {
id: string;
entryId: string;
roundId: string;
status: 'PENDING' | 'IN_PROGRESS' | 'ATTESTED' | 'LOCKED';
grossTotal: number | null;
netTotal: number | null;
stablefordPoints: number | null;
courseHandicap: number | null;
playingHandicap: number | null;
countbackScores: CountbackScores | null;
scores: HoleScore[];
}
interface HoleScore {
holeNumber: number;
par: number;
strokeIndex: number;
grossStrokes: number | null;
netStrokes: number | null;
stablefordPoints: number | null;
isNoReturn: boolean;
}
9-hole rounds
TeeTime supports 9-hole competitions with adjusted calculations:
- Strokes received calculated for 9 holes
- Course handicap = 18-hole CH ÷ 2 (rounded)
- Countback uses holes 5-9, 7-9, 8-9, 9
{
roundNumber: 1,
date: new Date('2025-12-15'),
courseId: 'course-123',
isNineHole: true,
holesInPlay: 9,
}
No Return (NR)
When a player picks up without completing a hole:
{
holeNumber: 7,
grossStrokes: null,
isNoReturn: true,
stablefordPoints: 0, // Zero points for Stableford
netStrokes: null, // NR for stroke play
}
- Stableford: 0 points for NR holes
- Stroke play: Typically disqualified or NR recorded