feat: implement PadelMatch scoring engine (tests pass)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 10:39:21 +02:00
parent 4ae32743d7
commit 1d9d878c3d

200
source/PadelMatch.mc Normal file
View File

@@ -0,0 +1,200 @@
import Toybox.Lang;
// Teams
const US = 0;
const THEM = 1;
// Court quadrants (your team = bottom half)
enum {
QUAD_BOTTOM_RIGHT, // US, deuce (right)
QUAD_BOTTOM_LEFT, // US, ad (left)
QUAD_UPPER_LEFT, // THEM, deuce (their right -> screen left)
QUAD_UPPER_RIGHT // THEM, ad (their left -> screen right)
}
class PadelMatch {
private var _rules as RulesConfig;
private var _points as Array<Number>; // raw point count per team in the current game
private var _games as Array<Number>; // games won in the current set
private var _sets as Array<Number>; // sets won in the match
private var _server as Number; // team serving the current game
private var _inTiebreak as Boolean;
private var _tbPoints as Array<Number>;
private var _tbFirstServer as Number; // team that served the tiebreak's first point
private var _matchOver as Boolean;
private var _winner as Number; // US / THEM / -1
private var _history as Array<Dictionary>;
private const POINT_LABELS = ["0", "15", "30", "40"];
public function initialize(rules as RulesConfig) {
_rules = rules;
_points = [0, 0];
_games = [0, 0];
_sets = [0, 0];
_server = US;
_inTiebreak = false;
_tbPoints = [0, 0];
_tbFirstServer = US;
_matchOver = false;
_winner = -1;
_history = [];
}
// ---- public API ----
public function pointTo(team as Number) as Void {
if (_matchOver) { return; }
_pushHistory();
if (_inTiebreak) {
_tbPoints[team] += 1;
_resolveTiebreak(team);
} else {
_points[team] += 1;
_resolveGame(team);
}
}
public function undo() as Void {
var n = _history.size();
if (n == 0) { return; }
var s = _history[n - 1];
_history = _history.slice(0, n - 1);
_points = s["points"] as Array<Number>;
_games = s["games"] as Array<Number>;
_sets = s["sets"] as Array<Number>;
_server = s["server"] as Number;
_inTiebreak = s["inTiebreak"] as Boolean;
_tbPoints = s["tbPoints"] as Array<Number>;
_tbFirstServer = s["tbFirstServer"] as Number;
_matchOver = s["matchOver"] as Boolean;
_winner = s["winner"] as Number;
}
public function canSetServer() as Boolean {
return _history.size() == 0 && !_matchOver;
}
public function toggleStartingServer() as Void {
if (canSetServer()) { _server = 1 - _server; }
}
public function servingQuadrant() as Number {
var team;
var pointsPlayed;
if (_inTiebreak) {
pointsPlayed = _tbPoints[0] + _tbPoints[1];
team = _tiebreakServer(pointsPlayed);
} else {
pointsPlayed = _points[0] + _points[1];
team = _server;
}
var isDeuce = (pointsPlayed % 2) == 0;
return _quadrantFor(team, isDeuce);
}
public function pointLabel(team as Number) as String {
if (_inTiebreak) { return _tbPoints[team].toString(); }
var a = _points[team];
var b = _points[1 - team];
if (!_rules.goldenPoint && a == 4 && b == 3) { return "AD"; }
if (a >= 4) { return "40"; } // safety; game normally ends before display
return POINT_LABELS[a];
}
public function games(team as Number) as Number { return _games[team]; }
public function sets(team as Number) as Number { return _sets[team]; }
public function server() as Number { return _server; }
public function isInTiebreak() as Boolean { return _inTiebreak; }
public function isMatchOver() as Boolean { return _matchOver; }
public function winner() as Number { return _winner; }
public function tiebreakPoints(team as Number) as Number { return _tbPoints[team]; }
// ---- internals ----
private function _resolveGame(team as Number) as Void {
var other = 1 - team;
var a = _points[team];
var b = _points[other];
if (_rules.goldenPoint) {
if (a >= 4) { _winGame(team); }
} else {
if (a >= 4 && (a - b) >= 2) { _winGame(team); }
else if (a >= 4 && b >= 4) { _points[team] = 3; _points[other] = 3; } // both past 40 -> deuce
}
}
private function _winGame(team as Number) as Void {
_games[team] += 1;
_points = [0, 0];
_server = 1 - _server;
_resolveSet(team);
}
private function _resolveSet(team as Number) as Void {
var other = 1 - team;
var a = _games[team];
var b = _games[other];
if (a >= _rules.gamesPerSet && (a - b) >= 2) {
_winSet(team);
} else if (_rules.tiebreakEnabled && a == _rules.gamesPerSet && b == _rules.gamesPerSet) {
_inTiebreak = true;
_tbPoints = [0, 0];
_tbFirstServer = _server; // team due to serve after the 12th game
}
}
private function _resolveTiebreak(team as Number) as Void {
var other = 1 - team;
var a = _tbPoints[team];
var b = _tbPoints[other];
if (a >= _rules.tiebreakTarget && (a - b) >= 2) {
_games[team] += 1; // e.g. 7-6
_winSet(team);
}
}
private function _winSet(team as Number) as Void {
_sets[team] += 1;
_games = [0, 0];
if (_inTiebreak) {
_inTiebreak = false;
_server = 1 - _tbFirstServer; // tiebreak receiver serves first next set
}
_resolveMatch(team);
}
private function _resolveMatch(team as Number) as Void {
if (_sets[team] >= _rules.setsToWin) {
_matchOver = true;
_winner = team;
}
}
private function _tiebreakServer(pointsPlayed as Number) as Number {
var offset = ((pointsPlayed + 1) / 2) % 2; // integer division
return (offset == 0) ? _tbFirstServer : (1 - _tbFirstServer);
}
private function _quadrantFor(team as Number, isDeuce as Boolean) as Number {
if (team == US) {
return isDeuce ? QUAD_BOTTOM_RIGHT : QUAD_BOTTOM_LEFT;
}
return isDeuce ? QUAD_UPPER_LEFT : QUAD_UPPER_RIGHT;
}
private function _pushHistory() as Void {
_history.add({
"points" => [_points[0], _points[1]],
"games" => [_games[0], _games[1]],
"sets" => [_sets[0], _sets[1]],
"server" => _server,
"inTiebreak" => _inTiebreak,
"tbPoints" => [_tbPoints[0], _tbPoints[1]],
"tbFirstServer" => _tbFirstServer,
"matchOver" => _matchOver,
"winner" => _winner
});
}
}