33 KiB
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-levelUS/THEMand 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 aDc.source/HeartRateProvider.mc— wraps the HR sensor + zone lookup.source/garmin-padelView.mc— draws the whole screen inonUpdate; 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 addSensor+UserProfilepermissions.
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)
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
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
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
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
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
"$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
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
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
});
}
}
- Step 2: Build the test binary (should now compile)
"$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
"$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
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
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<Number> {
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
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
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<Number>?;
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<Number>;
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
"$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
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
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
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
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
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
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
git rm source/garmin-padelMenuDelegate.mc
- Step 3: Commit
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:
grep -E "Sensor|UserProfile" manifest.xml
Expected: two <iq:uses-permission id="Sensor"/> / <iq:uses-permission id="UserProfile"/> lines.
- Step 2: Update strings (remove scaffold menu strings, keep app name)
<strings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://developer.garmin.com/downloads/connect-iq/resources.xsd">
<string id="AppName">Padel</string>
</strings>
(The old prompt/menu_label_* strings are removed; nothing references them now that the menu is gone.)
- Step 3: Build the release binary
"$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
"$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
ptsgo0→15→30→40; after the 4th point (golden) yourGincrements 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
"$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
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.