Brings in open trade offers and counter-offers. Cities & Knights gets the same flow for goods: OfferGoods may be open to the table, CounterGoods answers an offer, and AcceptCounter / DismissCounters are shared with the base game. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_016eTor2tpxKb4dXbd8RqnvZ
172 lines
10 KiB
Markdown
172 lines
10 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.
|
|
|
|
## Cities & Knights on the screen
|
|
|
|
The same layers carry the expansion; the screen only adds pieces, and **none of them
|
|
decides what is legal** — each reads the engine's legal-move list.
|
|
|
|
| Layer | What it adds |
|
|
| --- | --- |
|
|
| `CatanStyle.kt` | commodity colours, a colour per improvement track, the wall and barbarian colours, the awake-knight halo |
|
|
| `CatanText.kt` | goods, track, knight and progress-card names and help text; the prompts for every chore |
|
|
| `CatanInteraction.kt` | `BuildKind.WALL` / `KNIGHT`; chore taps; the moving-knight and armed-card modes; `knightActions`, `progressHand`, `goodsBankTrades` |
|
|
| `CatanBoardView.kt` | shields for knights (strength dots; faded when asleep, white halo when awake), dashed walls, a pennant for each metropolis, the merchant coin, a faint robber until the first attack |
|
|
| `CatanPanels.kt` | commodity tiles in the hand, the barbarian strip, the yellow / red / event dice, the extra player-card lines, the Improve and Progress buttons |
|
|
| `CatanCkDialogs.kt` | goods picker (discard, wedding, harbour), improvements, progress cards, the knight menu, deck / aqueduct / spy / drop choices, goods trade |
|
|
| `CatanScreen.kt` | the visual state: knight menu, moving knight, armed card, and which chore dialog shows |
|
|
|
|
**Interaction model.** Tap one of your own knights (it gets a yellow ring when it can do
|
|
something) for its menu: wake, promote, move or chase the robber. *Move* arms the board
|
|
with the crossings the knight can reach. A progress card that needs a place on the board
|
|
(Engineer, Medicine, Merchant, Diplomat, Intrigue, Inventor) closes the dialog and
|
|
**arms** the board; the next tap plays it. Chores that are answered with a place — a city
|
|
to lose, a metropolis site, a driven knight's refuge, a free knight — light the crossings
|
|
on the board and the prompt line says what to tap; the others open a dialog that cannot
|
|
be dismissed, because the game is waiting on it.
|
|
|
|
**Testing.** `CatanCkInteractionTest` plays whole games and checks that for *every* legal
|
|
move at *every* position some control (a tap, a button or a dialog choice) produces it, so
|
|
a legal move with no way to make it — a stuck phone — fails the build.
|
|
|
|
## 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.
|