From cad84029fd9a76ff0ae715499f7ae5b5e660725c Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 4 Jul 2026 09:58:02 +0200 Subject: [PATCH] chore: initial padel scaffold + design docs Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 5 + CLAUDE.md | 36 + .../plans/2026-07-02-padel-scoring.md | 1016 +++++++++++++++++ .../specs/2026-07-02-padel-scoring-design.md | 166 +++ manifest.xml | 35 + monkey.jungle | 1 + resources/drawables/drawables.xml | 3 + resources/drawables/launcher_icon.svg | 7 + resources/drawables/monkey.png | Bin 0 -> 16065 bytes resources/layouts/layout.xml | 4 + resources/menus/menu.xml | 4 + resources/strings/strings.xml | 8 + source/garmin-padelApp.mc | 28 + source/garmin-padelDelegate.mc | 15 + source/garmin-padelMenuDelegate.mc | 19 + source/garmin-padelView.mc | 33 + 16 files changed, 1380 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 docs/superpowers/plans/2026-07-02-padel-scoring.md create mode 100644 docs/superpowers/specs/2026-07-02-padel-scoring-design.md create mode 100644 manifest.xml create mode 100644 monkey.jungle create mode 100644 resources/drawables/drawables.xml create mode 100644 resources/drawables/launcher_icon.svg create mode 100644 resources/drawables/monkey.png create mode 100644 resources/layouts/layout.xml create mode 100644 resources/menus/menu.xml create mode 100644 resources/strings/strings.xml create mode 100644 source/garmin-padelApp.mc create mode 100644 source/garmin-padelDelegate.mc create mode 100644 source/garmin-padelMenuDelegate.mc create mode 100644 source/garmin-padelView.mc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722a728 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +*.prg +*.prg.debug.xml +developer_key +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33e5b46 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A Garmin Connect IQ watch-app for padel, written in **Monkey C**. It is currently the default scaffold generated by the VS Code "Monkey C: New Project" command — a single view with a menu — and has not yet been customized beyond the template. + +- App entry: `garmin_padelApp` (set in `manifest.xml` via `entry="garmin_padelApp"`). +- Build target: a single product, `venu445mm` (Venu 4, 45mm), with `minApiLevel="6.0.2"`. + +## Build, run, and test + +There is no shell-script build harness; this project is built and run through the **Monkey C VS Code extension** (Connect IQ SDK), driven from the command palette: + +- **Build / run in simulator**: `Monkey C: Build for Device` then `Monkey C: Run App`, or just press `F5` (the simulator launches `connectiq` + `monkeydo`). +- **Edit build targets / products**: `Monkey C: Edit Products` or `Monkey C: Set Products by Product Category`. +- **Edit app attributes, permissions, languages**: the corresponding `Monkey C: Edit ...` palette commands. These regenerate `manifest.xml` — do not hand-edit it (it is marked generated). +- **Package for the store**: `Monkey C: Export Project` (produces a `.iq` file). + +CLI equivalents (if the SDK `bin` is on PATH) use `monkeyc` to compile against a device + SDK, `connectiq`/`monkeydo` to run in the simulator, and `monkeyc --unit-test` (with a test runner view) for unit tests. There are currently no tests in this repo. + +## Code architecture + +Connect IQ apps follow a fixed MVC-ish lifecycle. The four `source/*.mc` files map to the standard roles: + +- **`garmin-padelApp.mc`** — `AppBase` subclass. `getInitialView()` returns the initial `[View, InputDelegate]` pair. App-wide start/stop hooks live here. `getApp()` is the global accessor. +- **`garmin-padelView.mc`** — `WatchUi.View`. `onLayout` binds `Rez.Layouts.MainLayout`; `onShow`/`onUpdate`/`onHide` are the render/visibility lifecycle. +- **`garmin-padelDelegate.mc`** — `BehaviorDelegate` for the main view. `onMenu()` pushes the menu (`Rez.Menus.MainMenu`) with its delegate. +- **`garmin-padelMenuDelegate.mc`** — `MenuInputDelegate`. `onMenuItem(item as Symbol)` dispatches on menu-item symbols (`:item_1`, `:item_2`). + +Key conventions: +- **Resources are referenced via the generated `Rez` namespace** (`Rez.Layouts.*`, `Rez.Menus.*`, `Rez.Strings.*`, `Rez.Drawables.*`). These symbols are generated at build time from the XML in `resources/` — you do not write them by hand. +- **`resources/`** holds the declarative UI and assets: `layouts/layout.xml`, `menus/menu.xml`, `strings/strings.xml`, `drawables/`. Menu item ids in `menu.xml` must match the `:symbol` names handled in the menu delegate, and string ids must match `@Strings.*` references. +- **`monkey.jungle`** is the build config; it currently only points at `manifest.xml`. Per-device resource overrides and source paths would be added here. +- **`bin/`** is generated build output (`.mir`, `.mbc`, `Rez.mcgen`) — never edit; safe to delete and regenerate. diff --git a/docs/superpowers/plans/2026-07-02-padel-scoring.md b/docs/superpowers/plans/2026-07-02-padel-scoring.md new file mode 100644 index 0000000..2b171f5 --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-padel-scoring.md @@ -0,0 +1,1016 @@ +# 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. diff --git a/docs/superpowers/specs/2026-07-02-padel-scoring-design.md b/docs/superpowers/specs/2026-07-02-padel-scoring-design.md new file mode 100644 index 0000000..7a8f06c --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-padel-scoring-design.md @@ -0,0 +1,166 @@ +# Padel Scoring App — Design Spec + +**Date:** 2026-07-02 +**Target:** Garmin Connect IQ watch-app (Monkey C), product `venu445mm` (Venu 4, 45mm, round AMOLED), minApiLevel 6.0.2. +**Status:** Approved design, pre-implementation. + +## 1. Purpose + +A padel score tracker for the watch. It shows the live score (sets / games / points) and a +top-down court diagram whose highlighted quadrant tells you who serves the next point. Points +are recorded by swiping. No GPS, no recorded Garmin activity, no match history in v1. + +## 2. Screen layout + +Round screen. A top band (heart rate) and a bottom band (time) frame a central row split into +three columns: **left** = sets + games, **center** = court, **right** = current-game points. + +``` + ♥ 142 ▮▮▮▯▯ ← heart rate + zone (top band) + + S G pts + 2 4 +------+------+ 15 ← your team (upper number in each column) + 1 3 | | | 40 ← opponents (lower number) + +------+------+ net + | | ● ← yellow ball = next server + +------+------+ + + 14:32 ← current time (bottom band) +``` + +- **Left column — sets & games.** Two mini-columns: **S** (sets) and **G** (games). A small + label (`S`, `G`) sits above each pair of numbers; under it, two stacked numbers — **your team + on top**, opponents below. Sets in **bold accent color**, games in **accent color**. +- **Right column — points.** A small `pts` label (same small font as `S`/`G`) above two stacked + numbers for the current game — your team on top, opponents below — in **white** + (`0 / 15 / 30 / 40`, and `AD` if advantage is enabled). +- **Center — court.** Simplified top-down padel court, your team the **bottom** half and + opponents the **top** half. A small **yellow ball** is drawn in the quadrant of the **next + server** (see §4); it moves as serve rotates. Nothing else is highlighted. +- **Top band — heart rate.** Current heart rate (bpm) with a **zone indicator** (the current HR + zone 1–5, shown as a short colored bar / zone number). Requires a heart-rate sensor reading + (see Permissions below). +- **Bottom band — time.** Current time (`HH:MM`), refreshed by a 1-second timer. + +The small `S` / `G` / `pts` labels share one small font and a muted (dim grey) color so the +numbers stay dominant. + +**Accent color.** A single constant (proposed: a vivid padel green/teal) used for the sets and +games numbers, so it can be changed in one place. The serve ball is **yellow** (its own constant). + +**Permissions.** Reading live heart rate requires the **Sensor** permission; deriving the zone +from the user's profile requires the **UserProfile** permission. Both are added via the +`Monkey C: Edit Permissions` palette command (which regenerates `manifest.xml`). + +## 3. Interaction model + +**During play (gestures):** +- **Swipe up** → point to **your team**. +- **Swipe down** → point to the **opponents**. +- **Swipe left** → **undo** the last recorded point (steps back through game/set boundaries too). + +**Before the first point (tap):** +- A **tap** toggles the starting server between **bottom-right** (your team serves first) and + **upper-left** (opponents serve first) — the yellow ball jumps between those two quadrants. + These are the only two options because the first point + of any game is served from the deuce (right) court, and the opponents' right-hand court maps to + the screen's upper-left. Tapping is only accepted while the match is at its very start (no points + recorded yet). + +**At match end:** +- The court shows a "won by *your team* / *opponents*" state and stops accepting points; **undo + still works**. A **tap** starts a fresh match, returning to server-positioning. + +## 4. Serving / court geometry + +Your team = bottom half, opponents = top half. The yellow ball is drawn in the quadrant +determined by (serving team) × (serve side): + +| Serving team | Serve side | Quadrant | +|--------------|------------|--------------| +| Your team | deuce (right) | bottom-right | +| Your team | ad (left) | bottom-left | +| Opponents | deuce (their right) | upper-left | +| Opponents | ad (their left) | upper-right | + +**Serve side by point parity.** Even number of points played so far in the current game → +deuce (right); odd → ad (left). Game start (0-0) is always deuce. + +**Serving team.** +- Normal game: one team serves the entire game; the serving team alternates after each completed game. +- The first server of the match is chosen by tap (see §3). + +**Tiebreak serving** (when tiebreak is enabled and reached, see §5): +- Serve side still follows point parity: even total tiebreak points → deuce, odd → ad. +- The first server serves 1 point, then service alternates every 2 points. (Server for the point + with `P` points already played = the team given by `floor((P+1)/2) mod 2` offset from the + team due to serve at 6-6.) + +## 5. Scoring rules + +Rules are held in a `RulesConfig` object that the engine reads as parameters, so the future +settings screen can drive them without an engine rewrite. **v1 defaults** (all overridable later): + +- **Points in a game:** `0 / 15 / 30 / 40`. +- **40-40 resolution:** **golden point** (default). Next point at 40-40 wins the game. When + `RulesConfig` is set to advantage instead, 40-40 = deuce, then advantage (`AD`), then game; + must win by two points. +- **Set:** first to **6 games, win by 2**. +- **6-6:** **7-point tiebreak** (win by 2). When tiebreak is disabled in `RulesConfig`, the set + continues until a team leads by 2 games. +- **Match:** **best of 3 sets** (first to 2 sets). + +## 6. Architecture (Monkey C) + +Keeps the scaffold's file-per-role split; adds two logic-only files. The scoring engine imports no +`WatchUi`/`Graphics`, so it is unit-testable in isolation. + +- **`source/PadelMatch.mc`** — the scoring engine. Holds points/games/sets for both teams, the + serving team, tiebreak state, and match-over/winner. Public surface: + - `pointTo(team)` — record a point, advancing game/set/match as needed. + - `undo()` — revert the last recorded point. + - `servingQuadrant()` — returns which of the four quadrants holds the serve ball. + - `setStartingServer(team)` / toggle — only valid at match start. + - accessors for the display: points (per team, formatted), games, sets, `isMatchOver()`, `winner()`. + - **Undo strategy:** push a full state snapshot onto a stack before each `pointTo`; `undo` pops + and restores. This makes swipe-left correct across point/game/set boundaries with no special-casing. +- **`source/RulesConfig.mc`** — the rules struct (golden-point flag, sets-to-win, games-per-set, + tiebreak flag/target) plus the v1 defaults. +- **`source/CourtRenderer.mc`** — draws the top-down court and the yellow serve ball in a given + quadrant, so the view stays focused on layout. Pure drawing against a `Dc`. +- **`source/HeartRateProvider.mc`** — wraps the heart-rate sensor and zone lookup. Enables + `Sensor.SENSOR_HEARTRATE`, exposes the latest bpm, and maps it to a zone 1–5 using + `UserProfile.getHeartRateZones`. Returns a "no reading" state when HR is unavailable + (simulator / no strap) so the view can render a placeholder. +- **`source/garmin-padelView.mc`** — `WatchUi.View`. `onUpdate` draws the top HR band, the left + sets/games column, the center court (via `CourtRenderer`), the right points column, and the + bottom time band. A 1-second `Timer` calls `requestUpdate` to keep the clock (and HR) live. +- **`source/garmin-padelDelegate.mc`** — `BehaviorDelegate`. Maps `onSwipe` (up/down/left) and + `onTap` (pre-match server toggle; new match after match end) to the engine, then `requestUpdate`. +- **`source/garmin-padelApp.mc`** — owns the `PadelMatch`, `RulesConfig`, and `HeartRateProvider` + instances; wires the view + delegate; enables sensors on start and disables them on stop. + +Resources (`resources/`): the whole screen is drawn programmatically in `onUpdate` rather than via +a static layout, since positions depend on state. `strings.xml` holds team labels and the +match-end text; the template menu can be removed or repurposed later. Sensor + UserProfile +permissions are declared in `manifest.xml` via the Edit Permissions command. + +## 7. Testing + +- **Engine unit tests** (`monkeyc --unit-test` with a test runner) cover `PadelMatch`: + - point → game → set → match progression (golden point and advantage modes), + - tiebreak entry at 6-6, tiebreak win-by-2, + - serving team alternation between games and serve-side parity within a game, + - tiebreak serving rotation, + - `servingQuadrant()` mapping for all four cases, + - `undo()` across point/game/set boundaries and at match start (no-op). +- Rendering (`CourtRenderer`, view, HR band) is verified in the simulator (HR via the + simulator's data-simulation, since there is no real strap). + +## 8. Deferred (not in v1) + +- Settings screen (drives `RulesConfig`; makes the top/bottom info slots configurable). +- Match history / saved matches. +- Resume-after-close persistence (engine state is in memory only for v1). +- Recorded Garmin activity, GPS. (Heart rate **is** in v1 as a read-only display, but the match is + still not recorded as a Garmin activity.) diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..b75ea23 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monkey.jungle b/monkey.jungle new file mode 100644 index 0000000..87796c7 --- /dev/null +++ b/monkey.jungle @@ -0,0 +1 @@ +project.manifest = manifest.xml diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml new file mode 100644 index 0000000..6302154 --- /dev/null +++ b/resources/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources/drawables/launcher_icon.svg b/resources/drawables/launcher_icon.svg new file mode 100644 index 0000000..4719135 --- /dev/null +++ b/resources/drawables/launcher_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/drawables/monkey.png b/resources/drawables/monkey.png new file mode 100644 index 0000000000000000000000000000000000000000..2f172d744b90c1db12fa92eca61659365d83f563 GIT binary patch literal 16065 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 Hu0mjfw!2@T literal 0 HcmV?d00001 diff --git a/resources/layouts/layout.xml b/resources/layouts/layout.xml new file mode 100644 index 0000000..95968a4 --- /dev/null +++ b/resources/layouts/layout.xml @@ -0,0 +1,4 @@ + + diff --git a/resources/menus/menu.xml b/resources/menus/menu.xml new file mode 100644 index 0000000..1a48c79 --- /dev/null +++ b/resources/menus/menu.xml @@ -0,0 +1,4 @@ + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml new file mode 100644 index 0000000..5e63980 --- /dev/null +++ b/resources/strings/strings.xml @@ -0,0 +1,8 @@ + + garmin-padel + + Click the menu button + + Item 1 + Item 2 + diff --git a/source/garmin-padelApp.mc b/source/garmin-padelApp.mc new file mode 100644 index 0000000..d5c690d --- /dev/null +++ b/source/garmin-padelApp.mc @@ -0,0 +1,28 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelApp extends Application.AppBase { + + function initialize() { + AppBase.initialize(); + } + + // onStart() is called on application start up + function onStart(state as Dictionary?) as Void { + } + + // onStop() is called when your application is exiting + function onStop(state as Dictionary?) as Void { + } + + // Return the initial view of your application here + 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; +} \ No newline at end of file diff --git a/source/garmin-padelDelegate.mc b/source/garmin-padelDelegate.mc new file mode 100644 index 0000000..68e488c --- /dev/null +++ b/source/garmin-padelDelegate.mc @@ -0,0 +1,15 @@ +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } + + function onMenu() as Boolean { + WatchUi.pushView(new Rez.Menus.MainMenu(), new garmin_padelMenuDelegate(), WatchUi.SLIDE_UP); + return true; + } + +} \ No newline at end of file diff --git a/source/garmin-padelMenuDelegate.mc b/source/garmin-padelMenuDelegate.mc new file mode 100644 index 0000000..931d197 --- /dev/null +++ b/source/garmin-padelMenuDelegate.mc @@ -0,0 +1,19 @@ +import Toybox.Lang; +import Toybox.System; +import Toybox.WatchUi; + +class garmin_padelMenuDelegate extends WatchUi.MenuInputDelegate { + + function initialize() { + MenuInputDelegate.initialize(); + } + + function onMenuItem(item as Symbol) as Void { + if (item == :item_1) { + System.println("item 1"); + } else if (item == :item_2) { + System.println("item 2"); + } + } + +} \ No newline at end of file diff --git a/source/garmin-padelView.mc b/source/garmin-padelView.mc new file mode 100644 index 0000000..492c2e9 --- /dev/null +++ b/source/garmin-padelView.mc @@ -0,0 +1,33 @@ +import Toybox.Graphics; +import Toybox.WatchUi; + +class garmin_padelView extends WatchUi.View { + + function initialize() { + View.initialize(); + } + + // Load your resources here + function onLayout(dc as Dc) as Void { + setLayout(Rez.Layouts.MainLayout(dc)); + } + + // Called when this View is brought to the foreground. Restore + // the state of this View and prepare it to be shown. This includes + // loading resources into memory. + function onShow() as Void { + } + + // Update the view + function onUpdate(dc as Dc) as Void { + // Call the parent onUpdate function to redraw the layout + View.onUpdate(dc); + } + + // Called when this View is removed from the screen. Save the + // state of this View here. This includes freeing resources from + // memory. + function onHide() as Void { + } + +}