# Padel Scoring App Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Turn the Connect IQ scaffold into a padel score tracker: a top-down court with a yellow serve ball, sets/games on the left, points on the right, heart rate on top, time on bottom, driven by swipe/tap gestures. **Architecture:** A UI-free scoring engine (`PadelMatch`) holds all match state and rules and is fully unit-tested. UI classes (`CourtRenderer`, `HeartRateProvider`, the `View`, the `Delegate`) read from the engine and are verified in the simulator. Rules live in a `RulesConfig` object so a future settings screen can drive them. **Tech Stack:** Monkey C, Connect IQ SDK 9.2.0, product `venu445mm` (Venu 4, 45mm), Toybox `WatchUi`/`Graphics`/`Sensor`/`UserProfile`/`Test`. --- ## Reference: full spec Design spec lives at `docs/superpowers/specs/2026-07-02-padel-scoring-design.md`. Read it before starting. ## File structure - `source/RulesConfig.mc` — rules struct + v1 defaults (golden point, best-of-3, tiebreak at 6-6). - `source/PadelMatch.mc` — the scoring engine (no UI imports). Module-level `US`/`THEM` and quadrant enum live here. - `source/PadelMatchTest.mc` — unit tests + plain helper functions for the engine. - `source/CourtRenderer.mc` — draws the court + yellow serve ball against a `Dc`. - `source/HeartRateProvider.mc` — wraps the HR sensor + zone lookup. - `source/garmin-padelView.mc` — draws the whole screen in `onUpdate`; 1s timer for the clock. - `source/garmin-padelDelegate.mc` — maps swipes/taps to the engine. - `source/garmin-padelApp.mc` — owns the engine/rules/HR, wires view+delegate, enables sensors. - `source/garmin-padelMenuDelegate.mc` — **deleted** (scaffold menu not used). - `resources/strings/strings.xml` — team labels + match-end text. - `manifest.xml` — regenerated to add `Sensor` + `UserProfile` permissions. ## Conventions used throughout - Teams: module constants `US = 0`, `THEM = 1`. - Quadrants: module enum `QUAD_BOTTOM_RIGHT, QUAD_BOTTOM_LEFT, QUAD_UPPER_LEFT, QUAD_UPPER_RIGHT` (values 0..3). - Your team is the bottom half of the court; opponents the top. --- ## Task 0: Prerequisites — env vars, git, gitignore **Files:** - Create: `.gitignore` - [ ] **Step 1: Set SDK env vars (run in every shell you use for this plan)** ```bash export SDK="$HOME/Library/Application Support/Garmin/ConnectIQ/Sdks/connectiq-sdk-mac-9.2.0-2026-06-09-92a1605b2" export KEY="$HOME/Downloads/developer_key" export DEVICE=venu445mm ``` Verify: `"$SDK/bin/monkeyc" --version` prints a version string. - [ ] **Step 2: Create `.gitignore`** ``` bin/ *.prg ``` - [ ] **Step 3: Initialize git and commit the current scaffold** ```bash cd /Users/kriss/Dev/scm/scm.vilanet.fr/kriss/garmin-padel git init git add -A git commit -m "chore: initial padel scaffold + design docs" ``` Expected: a commit is created (the `bin/` output is excluded). --- ## Task 1: RulesConfig **Files:** - Create: `source/RulesConfig.mc` - [ ] **Step 1: Write `RulesConfig.mc`** ```monkeyc import Toybox.Lang; // Configurable padel rules. v1 uses the defaults below; a future settings // screen will mutate an instance of this class — the engine reads it as-is. class RulesConfig { public var goldenPoint as Boolean; // true = sudden death at 40-40 public var gamesPerSet as Number; // games needed to take a set (before win-by-2) public var setsToWin as Number; // sets needed to win the match public var tiebreakEnabled as Boolean; // play a tiebreak at gamesPerSet-all public var tiebreakTarget as Number; // points needed to win a tiebreak (before win-by-2) public function initialize() { goldenPoint = true; gamesPerSet = 6; setsToWin = 2; tiebreakEnabled = true; tiebreakTarget = 7; } } ``` - [ ] **Step 2: Commit** ```bash git add source/RulesConfig.mc git commit -m "feat: add RulesConfig with v1 defaults" ``` --- ## Task 2: Engine test suite (test-first) Write the complete unit-test suite for `PadelMatch` before the engine exists. It will fail to compile (no `PadelMatch`) — that is the expected failing state. **Files:** - Create: `source/PadelMatchTest.mc` - [ ] **Step 1: Write `PadelMatchTest.mc`** ```monkeyc import Toybox.Lang; import Toybox.Test; // ---- plain helpers (not annotated :test, so not run as cases) ---- function newMatch() as PadelMatch { return new PadelMatch(new RulesConfig()); } function newAdvantageMatch() as PadelMatch { var r = new RulesConfig(); r.goldenPoint = false; return new PadelMatch(r); } // Win one game for `team` under golden-point rules (4 straight points). function winGameFor(m as PadelMatch, team as Number) as Void { for (var i = 0; i < 4; i += 1) { m.pointTo(team); } } // ---- test cases ---- (:test) function testInitialState(logger as Logger) as Boolean { var m = newMatch(); Test.assertEqualMessage(m.sets(US), 0, "sets US"); Test.assertEqualMessage(m.games(US), 0, "games US"); Test.assertEqualMessage(m.pointLabel(US), "0", "points US"); Test.assertEqualMessage(m.isMatchOver(), false, "not over"); Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "US serves deuce"); return true; } (:test) function testPointLabelsProgress(logger as Logger) as Boolean { var m = newMatch(); m.pointTo(US); Test.assertEqualMessage(m.pointLabel(US), "15", "15"); m.pointTo(US); Test.assertEqualMessage(m.pointLabel(US), "30", "30"); m.pointTo(US); Test.assertEqualMessage(m.pointLabel(US), "40", "40"); return true; } (:test) function testGoldenGameWin(logger as Logger) as Boolean { var m = newMatch(); winGameFor(m, US); Test.assertEqualMessage(m.games(US), 1, "US won a game"); Test.assertEqualMessage(m.pointLabel(US), "0", "points reset"); return true; } (:test) function testAdvantageDeuceCycle(logger as Logger) as Boolean { var m = newAdvantageMatch(); // 3-3 deuce for (var i = 0; i < 3; i += 1) { m.pointTo(US); m.pointTo(THEM); } Test.assertEqualMessage(m.pointLabel(US), "40", "deuce US 40"); m.pointTo(US); Test.assertEqualMessage(m.pointLabel(US), "AD", "US advantage"); m.pointTo(THEM); Test.assertEqualMessage(m.pointLabel(US), "40", "back to deuce"); m.pointTo(US); m.pointTo(US); Test.assertEqualMessage(m.games(US), 1, "US won from advantage"); return true; } (:test) function testWinSetSixLove(logger as Logger) as Boolean { var m = newMatch(); for (var g = 0; g < 6; g += 1) { winGameFor(m, US); } Test.assertEqualMessage(m.sets(US), 1, "US won the set"); Test.assertEqualMessage(m.games(US), 0, "games reset"); return true; } (:test) function testTiebreakEntryAndWin(logger as Logger) as Boolean { var m = newMatch(); for (var i = 0; i < 6; i += 1) { winGameFor(m, US); winGameFor(m, THEM); } Test.assertEqualMessage(m.isInTiebreak(), true, "6-6 tiebreak"); Test.assertEqualMessage(m.games(US), 6, "games 6-6"); for (var p = 0; p < 7; p += 1) { m.pointTo(US); } Test.assertEqualMessage(m.isInTiebreak(), false, "tiebreak done"); Test.assertEqualMessage(m.sets(US), 1, "US won the set via tiebreak"); return true; } (:test) function testServeSideParity(logger as Logger) as Boolean { var m = newMatch(); // US serving Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "0 pts deuce"); m.pointTo(THEM); // 1 point played Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_LEFT, "1 pt ad"); m.pointTo(US); // 2 points played Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "2 pts deuce"); return true; } (:test) function testServerAlternation(logger as Logger) as Boolean { var m = newMatch(); winGameFor(m, US); // serve passes to THEM Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "THEM serves deuce"); return true; } (:test) function testTiebreakServing(logger as Logger) as Boolean { var m = newMatch(); for (var i = 0; i < 6; i += 1) { winGameFor(m, US); winGameFor(m, THEM); } // 12 games played, start server US -> US serves first in tiebreak Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "P0 US deuce"); m.pointTo(US); Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_RIGHT, "P1 THEM ad"); m.pointTo(US); Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "P2 THEM deuce"); m.pointTo(US); Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_LEFT, "P3 US ad"); return true; } (:test) function testUndoAcrossGame(logger as Logger) as Boolean { var m = newMatch(); winGameFor(m, US); // US 1 game, points 0-0, THEM to serve m.pointTo(THEM); // THEM has 15 m.undo(); // back to just-after-game Test.assertEqualMessage(m.pointLabel(THEM), "0", "point undone"); m.undo(); // undo the game-winning point Test.assertEqualMessage(m.games(US), 0, "game undone"); Test.assertEqualMessage(m.pointLabel(US), "40", "back to 40"); return true; } (:test) function testUndoAtStartIsNoop(logger as Logger) as Boolean { var m = newMatch(); m.undo(); Test.assertEqualMessage(m.games(US), 0, "still zero"); return true; } (:test) function testStartingServerToggle(logger as Logger) as Boolean { var m = newMatch(); m.toggleStartingServer(); Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "THEM serves first"); m.pointTo(US); // match started; toggle now locked m.toggleStartingServer(); Test.assertEqualMessage(m.canSetServer(), false, "server locked after first point"); return true; } (:test) function testMatchOver(logger as Logger) as Boolean { var m = newMatch(); for (var s = 0; s < 2; s += 1) { for (var g = 0; g < 6; g += 1) { winGameFor(m, US); } } Test.assertEqualMessage(m.isMatchOver(), true, "match over"); Test.assertEqualMessage(m.winner(), US, "US wins"); var setsBefore = m.sets(US); m.pointTo(US); // ignored after match over Test.assertEqualMessage(m.sets(US), setsBefore, "no scoring after match"); return true; } ``` - [ ] **Step 2: Build the test binary and confirm it FAILS to compile** ```bash "$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test ``` Expected: FAIL — errors like `Undefined symbol :PadelMatch` / `US` / `QUAD_BOTTOM_RIGHT`. This confirms the tests reference the not-yet-written engine. - [ ] **Step 3: Commit the tests** ```bash git add source/PadelMatchTest.mc git commit -m "test: add PadelMatch engine test suite (failing)" ``` --- ## Task 3: PadelMatch engine Implement the engine so every test in Task 2 passes. **Files:** - Create: `source/PadelMatch.mc` - [ ] **Step 1: Write `PadelMatch.mc`** ```monkeyc 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 }); } } ``` - [ ] **Step 2: Build the test binary (should now compile)** ```bash "$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test ``` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 3: Run the unit tests in the simulator** ```bash "$SDK/bin/connectiq" & "$SDK/bin/monkeydo" bin/test.prg "$DEVICE" -t ``` Expected: the console lists each `test...` case as `PASSED` and ends with a summary reporting `PASSED` for all cases and `FAILED` count 0. If any case fails, fix the engine (not the tests) and re-run. - [ ] **Step 4: Commit** ```bash git add source/PadelMatch.mc git commit -m "feat: implement PadelMatch scoring engine (tests pass)" ``` --- ## Task 4: CourtRenderer Pure drawing. Verified in the simulator (Task 8), but written and compiled now. **Files:** - Create: `source/CourtRenderer.mc` - [ ] **Step 1: Write `CourtRenderer.mc`** ```monkeyc import Toybox.Lang; import Toybox.Graphics; // Draws a top-down padel court centered at (cx, cy) and a yellow serve ball // in the given quadrant. Half-width HW and half-height HH keep it inside the // central band of the round screen. class CourtRenderer { private const HW = 60; // half court width (px) private const HH = 78; // half court height (px) private const BALL_R = 6; public function draw(dc as Graphics.Dc, cx as Number, cy as Number, quadrant as Number, showBall as Boolean) as Void { var left = cx - HW; var top = cy - HH; dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.setPenWidth(2); dc.drawRectangle(left, top, 2 * HW, 2 * HH); // outer court dc.drawLine(cx, top, cx, cy + HH); // center service line dc.setPenWidth(1); dc.drawLine(left, cy - HH / 2, left + 2 * HW, cy - HH / 2); // upper service line dc.drawLine(left, cy + HH / 2, left + 2 * HW, cy + HH / 2); // lower service line dc.setPenWidth(3); dc.drawLine(left, cy, left + 2 * HW, cy); // net if (showBall) { var pos = _ballPos(quadrant, cx, cy); dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT); dc.fillCircle(pos[0], pos[1], BALL_R); } } private function _ballPos(quadrant as Number, cx as Number, cy as Number) as Array { var dx = HW / 2; var dy = HH / 2; if (quadrant == QUAD_BOTTOM_RIGHT) { return [cx + dx, cy + dy]; } if (quadrant == QUAD_BOTTOM_LEFT) { return [cx - dx, cy + dy]; } if (quadrant == QUAD_UPPER_LEFT) { return [cx - dx, cy - dy]; } return [cx + dx, cy - dy]; // QUAD_UPPER_RIGHT } } ``` - [ ] **Step 2: Commit** ```bash git add source/CourtRenderer.mc git commit -m "feat: add CourtRenderer (court + yellow serve ball)" ``` --- ## Task 5: HeartRateProvider **Files:** - Create: `source/HeartRateProvider.mc` - [ ] **Step 1: Write `HeartRateProvider.mc`** ```monkeyc import Toybox.Lang; import Toybox.Sensor; import Toybox.UserProfile; // Wraps the heart-rate sensor and maps the current bpm to a zone (1..5). // Returns null bpm when no reading is available (e.g. simulator without data // simulation), so the view can show a placeholder. class HeartRateProvider { private var _hr as Number?; private var _zones as Array?; public function initialize() { _hr = null; _zones = null; } public function onStart() as Void { Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]); Sensor.enableSensorEvents(method(:onSensor)); _zones = UserProfile.getHeartRateZones(UserProfile.HR_ZONE_SPORT_GENERIC); } public function onStop() as Void { Sensor.enableSensorEvents(null); } public function onSensor(info as Sensor.Info) as Void { _hr = info.heartRate; } public function getHeartRate() as Number? { return _hr; } // Returns 1..5, or 0 if HR/zones unavailable. public function getZone() as Number { if (_hr == null || _zones == null) { return 0; } var z = _zones as Array; var hr = _hr as Number; // _zones holds 6 boundaries: [z1lo, z2lo, z3lo, z4lo, z5lo, max] for (var i = z.size() - 2; i >= 0; i -= 1) { if (hr >= z[i]) { return i + 1 > 5 ? 5 : i + 1; } } return 1; } } ``` - [ ] **Step 2: Build a normal (non-test) binary to typecheck the new file** ```bash "$SDK/bin/monkeyc" -f monkey.jungle -o bin/garmin-padel.prg -y "$KEY" -d "$DEVICE" ``` Expected: `BUILD SUCCESSFUL` (the app still uses the old scaffold view/delegate at this point; those are replaced next). If the build complains about `HR_ZONE_SPORT_GENERIC`, confirm the constant name against the API docs for SDK 9.2.0 and adjust. - [ ] **Step 3: Commit** ```bash git add source/HeartRateProvider.mc git commit -m "feat: add HeartRateProvider (bpm + HR zone)" ``` --- ## Task 6: App wiring — own the engine, rules, HR Replace the scaffold app so it owns shared state and exposes it to the view/delegate. **Files:** - Modify: `source/garmin-padelApp.mc` - [ ] **Step 1: Rewrite `garmin-padelApp.mc`** ```monkeyc import Toybox.Application; import Toybox.Lang; import Toybox.WatchUi; class garmin_padelApp extends Application.AppBase { private var _rules as RulesConfig; private var _match as PadelMatch; private var _hr as HeartRateProvider; public function initialize() { AppBase.initialize(); _rules = new RulesConfig(); _match = new PadelMatch(_rules); _hr = new HeartRateProvider(); } public function onStart(state as Dictionary?) as Void { _hr.onStart(); } public function onStop(state as Dictionary?) as Void { _hr.onStop(); } public function getMatch() as PadelMatch { return _match; } public function getHeartRate() as HeartRateProvider { return _hr; } public function resetMatch() as Void { _match = new PadelMatch(_rules); } public function getInitialView() as [Views] or [Views, InputDelegates] { return [ new garmin_padelView(), new garmin_padelDelegate() ]; } } function getApp() as garmin_padelApp { return Application.getApp() as garmin_padelApp; } ``` - [ ] **Step 2: Commit** ```bash git add source/garmin-padelApp.mc git commit -m "feat: app owns match/rules/HR and exposes accessors" ``` --- ## Task 7: The View — draw the whole screen **Files:** - Modify: `source/garmin-padelView.mc` - [ ] **Step 1: Rewrite `garmin-padelView.mc`** ```monkeyc import Toybox.Lang; import Toybox.Graphics; import Toybox.WatchUi; import Toybox.System; import Toybox.Timer; class garmin_padelView extends WatchUi.View { private const ACCENT = 0x00E0A0; // padel green (sets/games) private const LABEL_COLOR = 0x808080; // muted grey (S/G/pts labels) private var _court as CourtRenderer; private var _timer as Timer.Timer; public function initialize() { View.initialize(); _court = new CourtRenderer(); _timer = new Timer.Timer(); } public function onShow() as Void { _timer.start(method(:onTick), 1000, true); // refresh clock + HR every second } public function onHide() as Void { _timer.stop(); } public function onTick() as Void { WatchUi.requestUpdate(); } public function onUpdate(dc as Graphics.Dc) as Void { var m = getApp().getMatch(); var w = dc.getWidth(); var h = dc.getHeight(); var cx = w / 2; var cy = h / 2; dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); dc.clear(); _court.draw(dc, cx, cy, m.servingQuadrant(), !m.isMatchOver()); _drawLeftColumn(dc, m); _drawRightColumn(dc, m, w); _drawHeartRate(dc, cx, 40); _drawTime(dc, cx, h - 44); if (m.isMatchOver()) { _drawWinner(dc, cx, cy, m); } } // Left: S and G mini-columns, your team on top, opponents below. private function _drawLeftColumn(dc as Graphics.Dc, m as PadelMatch) as Void { var sX = 24; var gX = 60; var labelY = 150; var usY = 178; var themY = 206; dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); dc.drawText(sX, labelY, Graphics.FONT_XTINY, "S", Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(gX, labelY, Graphics.FONT_XTINY, "G", Graphics.TEXT_JUSTIFY_CENTER); dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT); dc.drawText(sX, usY, Graphics.FONT_TINY, m.sets(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(sX, themY, Graphics.FONT_TINY, m.sets(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(gX, usY, Graphics.FONT_TINY, m.games(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(gX, themY, Graphics.FONT_TINY, m.games(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); } // Right: pts column, your team on top, opponents below. private function _drawRightColumn(dc as Graphics.Dc, m as PadelMatch, w as Number) as Void { var pX = w - 42; var labelY = 150; var usY = 178; var themY = 206; dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); dc.drawText(pX, labelY, Graphics.FONT_XTINY, "pts", Graphics.TEXT_JUSTIFY_CENTER); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.drawText(pX, usY, Graphics.FONT_TINY, m.pointLabel(US), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(pX, themY, Graphics.FONT_TINY, m.pointLabel(THEM), Graphics.TEXT_JUSTIFY_CENTER); } private function _drawHeartRate(dc as Graphics.Dc, cx as Number, y as Number) as Void { var hr = getApp().getHeartRate(); var bpm = hr.getHeartRate(); var text = (bpm == null) ? "-- bpm" : bpm.toString() + " bpm"; dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT); dc.drawText(cx, y, Graphics.FONT_TINY, text, Graphics.TEXT_JUSTIFY_CENTER); _drawZoneBar(dc, cx, y + 26, hr.getZone()); } // 5-segment bar; segments up to the current zone are filled. private function _drawZoneBar(dc as Graphics.Dc, cx as Number, y as Number, zone as Number) as Void { var segW = 12; var segH = 6; var gap = 3; var totalW = 5 * segW + 4 * gap; var x = cx - totalW / 2; for (var i = 1; i <= 5; i += 1) { if (i <= zone) { dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT); dc.fillRectangle(x, y, segW, segH); } else { dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); dc.drawRectangle(x, y, segW, segH); } x += segW + gap; } } private function _drawTime(dc as Graphics.Dc, cx as Number, y as Number) as Void { var now = System.getClockTime(); var text = now.hour.format("%02d") + ":" + now.min.format("%02d"); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.drawText(cx, y, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER); } private function _drawWinner(dc as Graphics.Dc, cx as Number, cy as Number, m as PadelMatch) as Void { var text = (m.winner() == US) ? "You win!" : "They win"; dc.setColor(ACCENT, Graphics.COLOR_BLACK); dc.fillRectangle(cx - 70, cy - 18, 140, 36); dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT); dc.drawText(cx, cy - 14, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER); } } ``` - [ ] **Step 2: Commit** ```bash git add source/garmin-padelView.mc git commit -m "feat: draw full padel screen (score, court, HR, time)" ``` --- ## Task 8: The Delegate — gestures **Files:** - Modify: `source/garmin-padelDelegate.mc` - Delete: `source/garmin-padelMenuDelegate.mc` - [ ] **Step 1: Rewrite `garmin-padelDelegate.mc`** ```monkeyc import Toybox.Lang; import Toybox.WatchUi; class garmin_padelDelegate extends WatchUi.BehaviorDelegate { public function initialize() { BehaviorDelegate.initialize(); } public function onSwipe(swipeEvent as WatchUi.SwipeEvent) as Boolean { var m = getApp().getMatch(); var dir = swipeEvent.getDirection(); if (dir == WatchUi.SWIPE_UP) { m.pointTo(US); } else if (dir == WatchUi.SWIPE_DOWN) { m.pointTo(THEM); } else if (dir == WatchUi.SWIPE_LEFT) { m.undo(); } else { return false; } WatchUi.requestUpdate(); return true; } public function onTap(clickEvent as WatchUi.ClickEvent) as Boolean { var m = getApp().getMatch(); if (m.isMatchOver()) { getApp().resetMatch(); } else if (m.canSetServer()) { m.toggleStartingServer(); } else { return false; } WatchUi.requestUpdate(); return true; } } ``` - [ ] **Step 2: Delete the unused menu delegate** ```bash git rm source/garmin-padelMenuDelegate.mc ``` - [ ] **Step 3: Commit** ```bash git add source/garmin-padelDelegate.mc git commit -m "feat: swipe to score/undo, tap to set server / new match" ``` --- ## Task 9: Permissions, strings, full build & simulator verification **Files:** - Modify: `manifest.xml` (via palette command — do not hand-edit) - Modify: `resources/strings/strings.xml` - [ ] **Step 1: Add Sensor + UserProfile permissions** In VS Code run `Monkey C: Edit Permissions` and enable **Sensor** and **UserProfile**. This regenerates `manifest.xml`. Verify from the CLI: ```bash grep -E "Sensor|UserProfile" manifest.xml ``` Expected: two `` / `` lines. - [ ] **Step 2: Update strings (remove scaffold menu strings, keep app name)** ```xml Padel ``` (The old `prompt`/`menu_label_*` strings are removed; nothing references them now that the menu is gone.) - [ ] **Step 3: Build the release binary** ```bash "$SDK/bin/monkeyc" -f monkey.jungle -o bin/garmin-padel.prg -y "$KEY" -d "$DEVICE" ``` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 4: Run in the simulator and verify behavior** ```bash "$SDK/bin/connectiq" & "$SDK/bin/monkeydo" bin/garmin-padel.prg "$DEVICE" ``` Then in the simulator, verify: - Court is centered with a yellow ball at **bottom-right** (US serves, deuce). - **Tap** → ball jumps to **upper-left**; tap again → back to bottom-right. - **Swipe up** → your `pts` go `0→15→30→40`; after the 4th point (golden) your `G` increments and points reset; the ball moves to the opponents' half. - **Swipe down** scores for opponents; **swipe left** undoes the last point. - Top shows `-- bpm` (or a value if HR data simulation is enabled via the simulator's **Settings → data-simulation / sensors**) with the zone bar; bottom shows the current time. - Win a full match → the winner banner appears; **tap** starts a fresh match at server-positioning. - [ ] **Step 5: Re-run unit tests to confirm no regressions** ```bash "$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test "$SDK/bin/monkeydo" bin/test.prg "$DEVICE" -t ``` Expected: all cases `PASSED`, 0 failures. - [ ] **Step 6: Commit** ```bash git add manifest.xml resources/strings/strings.xml git commit -m "feat: add Sensor/UserProfile permissions, tidy strings" ``` --- ## Done The scaffold is now a working padel score tracker. Deferred to future iterations (see spec §8): the settings screen (driving `RulesConfig` and the top/bottom info slots), match history, resume-after-close persistence, and recorded Garmin activity.