Files
ProLibertateGames/docs/UI-catan.md
T
Claude d7d3c8a3ed
CI / Build and check (pull_request) Successful in 7m28s
CI / Deploy to Cloudflare Pages (pull_request) Skipped
Katan: open trade offers and counter-offers
- Offers can go to the whole table (asked in seat order, empty hands skipped) or one seat.
- The asked seat may accept (trade happens at once), decline, or counter with new terms.
- If nobody accepts and there are counters, the offerer takes one or turns them all down (new TRADE_COUNTERS phase).
- AI: fixes an inverted accept test (it weighed what it gives against what it gets backwards), picks the best counter.
- UI: Everyone option in the offer dialog, Counter… in the reply dialog, a counter-choice dialog.
- Tests and docs updated.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018njn2eMPAYqAoE49hGRg2X
2026-10-15 20:24:45 +00:00

144 lines
8.3 KiB
Markdown

# Katan UI — how it is built, and how to change it
The Katan screen (`app/src/main/java/org/prolibertate/games/ui/game/catan/`) is
built in layers so that **look, wording, layout and interaction can each be
changed on their own**. If you are an agent asked to restyle, retheme,
re-copy or re-lay-out Katan, start here and touch only the layer you were asked
about.
The rules live elsewhere and are not part of the UI: `game/catan/` is a pure
state machine (see [RULES-catan.md](RULES-catan.md)). **Nothing in `ui/game/catan/`
decides what is legal.** The engine's legal-move list is the single source of
truth, so the UI can only ever offer a move the rules would accept.
## The layers
| Layer | File | Owns | Change it to… |
| --- | --- | --- | --- |
| **Style** | `CatanStyle.kt` | Every colour, proportion and the light/dark palettes | Re-skin: new terrain colours, seat colours, piece sizes, token look |
| **Copy** | `CatanText.kt` | Every sentence, button label and name shown | Reword, localise, rename resources |
| **Geometry** | `CatanGeometry.kt` | Engine lattice → pixels, and pixel → nearest vertex/edge/hex | Change board scale, margin, harbour placement, tap reach (rarely) |
| **Interaction** | `CatanInteraction.kt` | What is tappable, what a tap means, which buttons are live | Change *how* something is selected (never *whether* it is legal) |
| **Board drawing** | `CatanBoardView.kt` | The `Canvas`: tiles, tokens, ports, robber, roads, pieces, highlights | Change how a piece is drawn (art, shapes, animation) |
| **Panels** | `CatanPanels.kt` | Resource tiles, hand, player cards, dice, action bar, log | Restyle or rearrange widgets |
| **Dialogs** | `CatanDialogs.kt` | Discard, robber victim, play card, trade, trade reply, game over | Change a dialog's flow or look |
| **Screen** | `CatanScreen.kt` | Wiring to `MatchController`, portrait/landscape layout, which dialog shows | Change the layout or add a new element |
| **Previews** | `CatanPreviews.kt` | Android Studio previews of real positions | Iterate on the look without a match |
Everything outside this folder that mentions Katan is one-line wiring:
`PlayScreen.kt` (builds the `MatchController`), `GameSetupScreen.kt` (players and
points-to-win options), `Catalog.kt` (the catalogue entry).
## Recipes
**Change colours or sizes.** Edit `CatanStyle.kt`. `LightStyle` is the reference
palette; `DarkStyle` is derived from it. Metrics (`roadWidth`, `settlementSize`,
`tokenRadius`, …) are fractions of the hex radius, so they scale with the screen.
To try a whole new theme, provide it at the root:
`CompositionLocalProvider(LocalCatanStyle provides myStyle) { … }`.
**Change wording or translate.** Edit `CatanText.kt`. Composables never build a
sentence themselves. `prompt(...)` is the one-line "what is happening / what do I
do" text; it is a pure function of the state, with tests in `CatanTextTest`.
**Redraw a piece** (settlement, road, robber, port, token). Edit the matching
`draw…` function in `CatanBoardView.kt`. Positions come from `BoardLayout`, so you
only decide the shape. Keep the hit target generous (see Accessibility).
**Rearrange the screen.** Edit `CatanScreen.kt`. It builds four independent
pieces (`board`, `players`, `promptRow`, `handAndActions`) and arranges them
differently in portrait and landscape. Moving or dropping a piece does not affect
the others.
**Add a widget.** Make it a stateless composable in `CatanPanels.kt` (values in,
lambdas out). Do not give it the `MatchController`.
**Change how a selection works** (for example a one-tap build instead of a
build-mode toggle). Edit `CatanInteraction.kt` and its tests, then adjust the
screen's UI state. The legal-move list stays the gate.
## Rules of the road
1. **Never put a rule in a composable.** If a button's enabled state or a tap's
result depends on game logic, it goes in `CatanInteraction` (pure, tested) and
is derived from the engine's `legal` list or `CatanRules`.
2. **Never hard-code a colour or a string** outside `CatanStyle` / `CatanText`.
3. **Stateless by default.** Panels and dialogs take values and report events. The
only state they may own is the transient selection they are helping the player
make (cards picked in a trade, for example).
4. **Hidden information.** The local view comes from `CatanRules.viewFor`: other
players' development cards and the deck are empty on a client. Do not show the
size of the deck; it is not known to a client. Card *counts* (`resourceCount`,
`devHandCounts`) are public.
5. **The state is immutable.** Never mutate it; submit a move.
6. **The layout must not move.** Everything outside the board has a *fixed
height*, and the board takes whatever is left, so the island's scale is the
same all game. Concretely: a control is *enabled or disabled*, never
*present or absent* (the action bar always shows every button); text that can
vary in length is pinned to a fixed number of lines (`minLines = maxLines`);
a line that is sometimes empty is still drawn (a non-breaking space), as the
award line on a player card is. A change that makes any non-board element
taller or shorter at runtime will rescale the board under the player's thumb.
## Interaction model (what the player actually does)
| Phase | The player… |
| --- | --- |
| Setup | taps a highlighted crossing (settlement), then a highlighted edge beside it (road). *Cancel* re-picks the settlement |
| Roll | presses **Roll dice** |
| Seven | **Discard** dialog (forced); then taps a hex to move the robber; a victim dialog appears only if more than one player can be robbed |
| Main turn | arms **Road / Settlement / City** and taps a highlighted spot; **Buy card**; **Play card**; **Trade**; **End turn** |
| Road Building | taps highlighted edges, or **Done with roads** |
| Offered a trade | **Accept / Decline / Counter…** dialog (forced); *Counter…* opens a picker pre-filled with the mirror of the offer |
| Counter-offers received | **Counter-offers** dialog: *Accept* beside the one to take, or *No deal* |
| Game over | result dialog |
Trade offers are free-form (two card multisets), so they are validated with
`CatanRules.isLegalMove`, not looked up in the legal list.
## Testing
Gradle does not run in the agent sandbox; **CI is the compile and test check**
(dispatch `ci.yml` on your branch). What is covered:
- `CatanGeometryTest` — the island fits any box, edges and corners are one radius,
hit-testing finds the right vertex/edge/hex.
- `CatanInteractionTest` — targets and tap outcomes for setup, building, free
roads, the robber (including the victim choice), discards, bank trades, offers,
development cards and trade replies, all against real engine states.
- `CatanTextTest` — the prompts and tallies.
Drawing and dialogs are not unit-tested; use the previews in `CatanPreviews.kt`
and play a game on a dev build.
## Accessibility notes
- Tap targets: vertices and edges accept a tap within `tapReach` (0.7) hex radii,
about 23 dp on a phone. If you shrink the board, keep that physical size.
- Seat colours are blue, orange, white and purple, each outlined in near-black so
they read on any terrain. Colour is the only piece identity today; adding a
shape or letter per seat would help colour-blind players (a good first task).
- The board has a content description but no per-hex semantics; a screen-reader
mode would need one.
## Status and known gaps
The screen is **playable and feature-complete for the engine's rules**, but is
**unverified on two real devices**, so the catalogue entry is `devPreview`
(openable on a dev build, hidden from production). To release it: verify
two-device play, then in `Catalog.kt` set `available = true` and remove
`devPreview = true`, and update `CatalogTest`.
Not done yet, in rough priority order:
1. Two-device verification (host + guest over the LAN) and a full game on a phone
and a tablet.
2. Feedback on events: dice-roll animation, a highlight on hexes that produced,
resource-gain flourishes, haptics. The state carries the dice and a text log;
it does not say which hexes paid out, so that needs either a diff of two states
or an engine addition.
3. Art: terrain textures, resource and harbour icons, a real robber pawn.
4. A timer or cancel button on offers (the engine has neither).
5. A board that shows the *last* move (a faint highlight of what just changed).
6. Landscape polish on large tablets; the right-hand panel scrolls but is plain.