scoring

package
v0.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package scoring settles a confirmed match.

It sits between the pure calculation in internal/ttr and the repositories: it decides nothing about ratings itself and stores nothing by hand, it coordinates the two. That is also why it is not part of internal/ttr, which must stay free of storage so the rating rules remain testable on plain values.

Everything here runs inside one transaction. Rating, history and the match status have to move together or not at all — a rating raised without the history entry that explains it cannot be traced afterwards, and a match marked confirmed without the rating is silently lost.

Index

Constants

View Source
const UndoWindow = 10 * time.Minute

UndoWindow is how long a settled result can be taken back.

Ten minutes: long enough for "wrong player, wrong set" to be noticed by the person still standing at the table, short enough that nobody edits the evening afterwards. The other guard is the one that keeps the arithmetic honest, and it has no clock in it — see ErrNotLast.

Variables

View Source
var (
	// ErrNotPending reports a match that has already been settled or
	// contested. Confirming twice would double the rating change, which the
	// unique index on the history would refuse anyway — this catches it
	// earlier and with a usable reason.
	ErrNotPending = errors.New("match is not waiting for confirmation")

	// ErrNotYours reports somebody ruling on a match that is not theirs to
	// rule on: a bystander, or — when confirming — the player who reported
	// it. A result confirmed by whoever entered it is not confirmed at all.
	ErrNotYours = errors.New("this match is not yours to rule on")

	// ErrSamePlayer reports a match somebody plays against themselves. The
	// schema refuses it too (matches_players_differ); catching it here makes
	// the answer a sentence rather than a constraint violation.
	ErrSamePlayer = errors.New("a player cannot play themselves")

	// ErrNotUndoable reports a match that cannot be taken back: not
	// confirmed, or already gone.
	ErrNotUndoable = errors.New("match cannot be taken back")

	// ErrTooLate reports a match confirmed longer ago than UndoWindow. The
	// undo exists for a typo somebody notices at once, not for editing an
	// evening afterwards.
	ErrTooLate = errors.New("match was confirmed too long ago to take back")

	// ErrNotLast reports a match that is no longer the newest one for both
	// of its players. Taking it back writes the ratings from before it
	// straight back, and that is only correct while nothing has happened
	// since — otherwise the later match would be silently undone as well.
	ErrNotLast = errors.New("a newer match has counted since")

	// ErrNotDisputed reports a match that is not contested. Only a contested
	// result can be corrected: a pending one is still waiting for a plain yes
	// or no, and a confirmed one has already moved two ratings.
	ErrNotDisputed = errors.New("match is not contested")
)

Functions

func Dispute

func Dispute(ctx context.Context, store repository.Store, matchID, by uuid.UUID) error

Dispute marks a match as contested. Nothing is scored, and nothing is scored until somebody corrects it — which either participant can do, see Correct.

Disputing and correcting are two steps rather than one on purpose: somebody who taps "that is wrong" and walks away has still stopped a wrong number from being scored, which is the whole point of the step.

Types

type Correction

type Correction struct {
	Match domain.Match
	// Opponent is who has to confirm now: the participant who did not
	// correct it.
	Opponent domain.Player
	// HomeWon and the set tally come from validating the corrected result,
	// for the same reason Settlement carries them.
	HomeWon            bool
	HomeSets, AwaySets int
}

Correction is what a corrected result amounts to.

func Correct

func Correct(
	ctx context.Context,
	store repository.Store,
	matchID, by uuid.UUID,
	result match.Result,
) (Correction, error)

Correct replaces the result of a contested match and hands it back for confirmation.

by may be either participant. The player who disputed usually knows the real score, but so does the one who mistyped it, and insisting on one of them would only send people back to the table to swap phones. Whoever corrects becomes the reporter, which makes the other one the confirmer through the same rule Confirm already applies.

result is in home/away orientation rather than the corrector's: the caller owns the viewpoint, the domain owns the sides. A rejection from match.Validate is returned as-is so the caller can say what was wrong with it — a corrected result is held to exactly the same rules as a fresh one.

type Settlement

type Settlement struct {
	Match domain.Match
	// Home and Away are the players as they were before the match counted,
	// so a caller can show what changed.
	Home, Away domain.Player
	// HomeChange and AwayChange are the rating movements.
	HomeChange, AwayChange ttr.Change
	// HomeWon and the set tally come from re-validating the stored result.
	// They are carried rather than left to the caller because a rating
	// change cannot stand in for them: a strong favourite who wins can move
	// by zero points, and reading that as a loss is exactly the kind of
	// wrong that looks right.
	HomeWon            bool
	HomeSets, AwaySets int
	// Rated is false for a match in a tournament that does not count
	// (docs/adr/0012). The two changes are then both zero, and a caller that
	// prints them without asking would say "±0" where nothing was computed —
	// which reads as a rating that held rather than one that was never
	// touched.
	Rated bool
}

Settlement is what a confirmation amounted to.

func Confirm

func Confirm(
	ctx context.Context,
	store repository.Store,
	matchID, by uuid.UUID,
	at time.Time,
) (Settlement, error)

Confirm settles a pending match and records the result.

by must be the participant who did not report it. The match is re-validated from the stored sets rather than trusting a winner recorded earlier: the rating follows from the score, and deriving it twice from the same source is cheaper than keeping a second copy honest.

func Record

func Record(
	ctx context.Context,
	store repository.Store,
	homeID, awayID uuid.UUID,
	result match.Result,
	via domain.EnteredVia,
	tournamentID *uuid.UUID,
	tournamentRound *int,

	reportedBy uuid.UUID,
	at time.Time,
) (Settlement, error)

Record stores a result between two players and settles it at once.

It is what a scorekeeper does: somebody watched the match, both players were standing at the table, and the sheet is the authority. There is no third party left to ask, so the confirmation step Record skips would only be a question nobody is in a position to answer differently.

Everything else is the ordinary path — the same validation, the same rating, the same history — and it all happens in one transaction, so a failure leaves no half-recorded match behind.

via is a parameter rather than a constant in here, even though the kiosk is the only caller today. The column exists so that the measurement can tell a tournament evening from a normal Tuesday (issue #71), and a second caller that silently inherited "kiosk" would put back exactly the confusion it was added to remove.

tournamentID books the result to a tournament, or is nil for a match played outside one. It changes nothing about how the match is rated — a tournament match settles here like any other (docs/adr/0009) — and exists so a table can be built from the results afterwards.

func SettleRecorded added in v0.18.0

func SettleRecorded(ctx context.Context, tx repository.Store, m domain.Match, at time.Time) (Settlement, error)

SettleRecorded settles a match the caller has just created as pending in tx, with nobody confirming it. It is for the Zählwerk's results on a deployment that counts them at once (SP_SCOREBOARD_SETTLE=immediate, docs/adr/0027): the second half of Record, for a caller that writes its own row because the row carries what Record does not take — the check-in ids of docs/adr/0024.

at is when the result counted. The caller owns the transaction, as for settle.

type Undone

type Undone struct {
	// Home and Away are the players as they are again, with the ratings
	// from before the match restored.
	Home, Away domain.Player
	// HomeSets and AwaySets are what the deleted result said, so a caller
	// can name what disappeared.
	HomeSets, AwaySets int
}

Undone is what taking a result back amounted to.

func Remove added in v0.7.0

func Remove(ctx context.Context, store repository.Store, matchID uuid.UUID) (Undone, error)

Remove is the same act without the clock, for an admin acting deliberately rather than a kiosk fixing a typo it is still looking at.

Exactly one of the two conditions above is dropped, and it is the one that was a question of manners. Everything that keeps the arithmetic honest stays: a result that was never rated cannot be removed, and one that is no longer the newest for both players is refused with ErrNotLast rather than silently taking a later match down with it. Correcting a wrong result is therefore this plus entering the right one — the same shape as the kiosk undo in issue #49, because there is nothing in a settled match left to edit.

The price is written down in docs/adr/0008 and in issue #105: nobody confirms this. It moves two ratings on one person's say-so, which is why the flag belongs to very few people and why the caller logs who used it.

func Undo

func Undo(ctx context.Context, store repository.Store, matchID uuid.UUID, at time.Time) (Undone, error)

Undo removes a settled match and puts both ratings back where they were.

It exists because a result entered at the kiosk counts immediately: there is no pending state to dispute and nothing left to correct, so a typo would otherwise stand for good.

Two conditions, and the second is the one that matters. The window is a question of manners. The check that this is still the newest match for both players is a question of correctness: the ratings are restored by writing ttr_before back, which is right only while nothing has counted since. A match played in between would be undone along with it, silently.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL