diff --git a/source/PadelMatch.mc b/source/PadelMatch.mc new file mode 100644 index 0000000..1107d4e --- /dev/null +++ b/source/PadelMatch.mc @@ -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; // raw point count per team in the current game + private var _games as Array; // games won in the current set + private var _sets as Array; // 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; + 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; + + 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; + _games = s["games"] as Array; + _sets = s["sets"] as Array; + _server = s["server"] as Number; + _inTiebreak = s["inTiebreak"] as Boolean; + _tbPoints = s["tbPoints"] as Array; + _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 + }); + } +}