167 lines
9.1 KiB
Markdown
167 lines
9.1 KiB
Markdown
|
|
# 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.)
|