diff --git a/README.md b/README.md index e45a4ec..33f7750 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,9 @@ game: the game is out of print and the rulebook could not be obtained, so the document says which parts are attested and which were derived - [Katan](docs/RULES-catan.md) — the island game (catalogue id `catan`); the - rules engine and AI are in place, the screen lands with its own task + rules engine, AI and screen are in place +- [Katan: Cities & Knights](docs/RULES-catan-ck.md) — the expansion rule set, chosen on + the setup screen: commodities, knights, barbarians, city improvements and progress cards ## Layout @@ -396,7 +398,7 @@ game/ rules engines and AI — pure Kotlin, no Android, unit-tested mastermind/ Mastermind codes, scoring, and a code breaker pirates/ Pirates and Bulgars — the cross board, the hunt, search tayu/ Ta Yü tile geometry, river rules, scoring, AI - catan/ Katan island geometry, production, trade, dev cards, heuristic AI + catan/ Katan island geometry, production, trade, dev cards, Cities & Knights, heuristic AI net/ wire protocol, LAN transport, lobby, match driver score/ scorekeeper sheet — pure Kotlin, unit-tested — and its store settings/ DataStore-backed preferences diff --git a/app/src/main/java/org/prolibertate/games/game/Catalog.kt b/app/src/main/java/org/prolibertate/games/game/Catalog.kt index abb732a..fa63168 100644 --- a/app/src/main/java/org/prolibertate/games/game/Catalog.kt +++ b/app/src/main/java/org/prolibertate/games/game/Catalog.kt @@ -360,7 +360,7 @@ object GameCatalog { minPlayers = 3, maxPlayers = 4, teamBased = false, - blurb = "Trade, build, and settle the island.", + blurb = "Trade, build, and settle the island, in the base game or Cities & Knights.", // Engine and screen exist but have not been verified on two real // devices yet. Dev builds can open it as a *Preview*; a production // build does not list it. Flip `available` on, and `devPreview` diff --git a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanInteraction.kt b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanInteraction.kt index f347f92..c62f975 100644 --- a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanInteraction.kt +++ b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanInteraction.kt @@ -71,11 +71,14 @@ data class BoardTargets( val edges: Set = emptySet(), val hexes: Set = emptySet(), val knights: Set = emptySet(), + /** The player's other knights: tappable for their menu, but with nothing to do, so not ringed. */ + val idleKnights: Set = emptySet(), ) { - val isEmpty: Boolean get() = vertices.isEmpty() && edges.isEmpty() && hexes.isEmpty() && knights.isEmpty() + val isEmpty: Boolean + get() = vertices.isEmpty() && edges.isEmpty() && hexes.isEmpty() && knights.isEmpty() && idleKnights.isEmpty() /** Every crossing a tap can land on. */ - val tappableVertices: Set get() = vertices + knights + val tappableVertices: Set get() = vertices + knights + idleKnights } /** @@ -143,6 +146,8 @@ object CatanInteraction { setupVertex: Int?, movingKnight: Int? = null, armed: ArmedCard? = null, + /** The local seat, so its sleeping knights can be tapped for their menu too. */ + localSeat: Int? = null, ): BoardTargets { val setup = legal.filterIsInstance() if (setup.isNotEmpty()) { @@ -187,7 +192,15 @@ object CatanInteraction { BuildKind.KNIGHT -> BoardTargets(vertices = legal.filterIsInstance().map { it.vertex }.toSet()) - null -> BoardTargets(knights = knightsWithActions(legal)) + null -> { + val ready = knightsWithActions(legal) + val mine = if (localSeat != null && legal.isNotEmpty()) { + state.ck?.knights?.filter { it.owner == localSeat }?.map { it.vertex }?.toSet().orEmpty() + } else { + emptySet() + } + BoardTargets(knights = ready, idleKnights = mine - ready) + } } } @@ -323,6 +336,8 @@ object CatanInteraction { setupVertex: Int?, movingKnight: Int? = null, armed: ArmedCard? = null, + /** The local player's knights, which can be tapped for their menu even with nothing to do. */ + ownKnights: Set = emptySet(), ): TapOutcome { val setup = legal.filterIsInstance() if (setup.isNotEmpty()) { @@ -350,7 +365,7 @@ object CatanInteraction { BuildKind.CITY -> BuildCity(vertex) BuildKind.WALL -> BuildWall(vertex) BuildKind.KNIGHT -> HireKnight(vertex) - else -> return if (building == null && vertex in knightsWithActions(legal)) { + else -> return if (building == null && (vertex in knightsWithActions(legal) || vertex in ownKnights)) { TapOutcome.PickKnight(vertex) } else { TapOutcome.Ignored diff --git a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanPanels.kt b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanPanels.kt index 59aaa10..e23a986 100644 --- a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanPanels.kt +++ b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanPanels.kt @@ -239,7 +239,7 @@ fun CatanPlayerCard( } else { CardLine("$cards cards · ${ck.progressCounts[seat]} progress", MaterialTheme.typography.bodySmall) CardLine( - "Knights ${CatanCk.knightsOf(state, seat).size} (strength $strength) · " + + "⚔ ${CatanCk.knightsOf(state, seat).size} · str $strength · " + "Road ${CatanRules.longestRoadFor(state, seat)}", MaterialTheme.typography.bodySmall, ) diff --git a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanScreen.kt b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanScreen.kt index cff6723..32b98d9 100644 --- a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanScreen.kt +++ b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanScreen.kt @@ -117,7 +117,6 @@ fun CatanScreen( setupVertex = null } if (movingKnight != null && legal.none { it is MoveKnight && it.from == movingKnight }) movingKnight = null - if (knightMenu != null && CatanInteraction.knightActions(legal, knightMenu!!).isEmpty()) knightMenu = null armed?.let { card -> if (card.plays.none { it in legal }) armed = null } if (legal.isEmpty()) { showCards = false @@ -159,7 +158,8 @@ fun CatanScreen( return } - val targets = CatanInteraction.targets(current, legal, building, setupVertex, movingKnight, armed) + val targets = CatanInteraction.targets(current, legal, building, setupVertex, movingKnight, armed, localSeat) + val ownKnights = current.ck?.knights?.filter { it.owner == localSeat }?.map { it.vertex }?.toSet().orEmpty() val devHand = CatanInteraction.devHand(current, localSeat, legal) val progressHand = CatanInteraction.progressHand(current, localSeat, legal) val playing = current.phase == CatanPhase.PLAYING && legal.isNotEmpty() @@ -219,7 +219,12 @@ fun CatanScreen( targets = targets, selectedVertex = setupVertex, onVertexTap = { - handle(CatanInteraction.onVertexTap(it, legal, building, setupVertex, movingKnight, armed)) + handle( + CatanInteraction.onVertexTap( + it, legal, building, setupVertex, movingKnight, armed, + ownKnights = if (legal.isNotEmpty()) ownKnights else emptySet(), + ), + ) }, onEdgeTap = { handle(CatanInteraction.onEdgeTap(it, legal, building, setupVertex, armed)) }, onHexTap = { handle(CatanInteraction.onHexTap(it, legal, armed)) }, diff --git a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanStyle.kt b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanStyle.kt index 90db565..7cc53c6 100644 --- a/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanStyle.kt +++ b/app/src/main/java/org/prolibertate/games/ui/game/catan/CatanStyle.kt @@ -58,8 +58,8 @@ data class CatanStyle( /** Wall round a city, and the barbarian strip. */ val wall: Color = Color(0xFF5D4037), val barbarian: Color = Color(0xFFB71C1C), - /** A knight that is awake and ready to act. */ - val knightReady: Color = Color(0xFFFFEB3B), + /** The halo round a knight that is awake. White, so it is not mistaken for the yellow tap ring. */ + val knightReady: Color = Color(0xFFFFFFFF), // --- Players ------------------------------------------------------------ /** One colour per seat, in seat order. Roads, settlements, cities, player strip. */ diff --git a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkInteractionTest.kt b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkInteractionTest.kt index 4b9fd6f..bc0807c 100644 --- a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkInteractionTest.kt +++ b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkInteractionTest.kt @@ -2,6 +2,7 @@ package org.prolibertate.games.ui.game.catan import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.prolibertate.games.game.GameCatalog @@ -261,4 +262,26 @@ class CatanCkInteractionTest { listOf("setup", "road", "build SETTLEMENT", "build CITY", "knightMenu", "improve", "bankTrade", "roll", "endTurn") .forEach { assertTrue("never exercised: $it in $total", (total[it] ?: 0) > 0) } } + + @Test + fun `a sleeping knight with nothing to do can still be tapped for its menu`() { + val base = CatanRules.initialState(config(3, 5L)) + .copy(phase = CatanPhase.PLAYING, turn = 0, turnNumber = 1) + val state = base.copy( + ck = base.ck!!.copy(knights = listOf(org.prolibertate.games.game.catan.Knight(10, 0, 1))), + ) + val legal = CatanRules.legalMoves(state, 0) + assertTrue("no grain, so nothing to do", CatanInteraction.knightActions(legal, 10).isEmpty()) + + val targets = CatanInteraction.targets(state, legal, null, null, localSeat = 0) + assertTrue(10 in targets.idleKnights) + assertTrue("no tap ring for a knight with nothing to do", 10 !in targets.knights) + assertTrue(10 in targets.tappableVertices) + assertEquals( + TapOutcome.PickKnight(10), + CatanInteraction.onVertexTap(10, legal, null, null, ownKnights = setOf(10)), + ) + // On somebody else's turn the legal list is empty and nothing is offered. + assertTrue(CatanInteraction.targets(state, emptyList(), null, null, localSeat = 0).idleKnights.isEmpty()) + } } diff --git a/docs/README.md b/docs/README.md index d6ef5a9..3a93b26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ Naming: the catalogue id, lowercase, e.g. `crazy8s` → `RULES-crazy8s.md`. | Golf | [RULES-golf.md](RULES-golf.md) | | Hearts | — (TODO) | | Kaiser | [RULES-kaiser.md](RULES-kaiser.md) | -| Katan (catalogue id `catan`) | [RULES-catan.md](RULES-catan.md) | +| Katan (catalogue id `catan`) | [RULES-catan.md](RULES-catan.md) — and its expansion rule set, [RULES-catan-ck.md](RULES-catan-ck.md) | | Klondike | — (TODO) | | Mastermind | [RULES-mastermind.md](RULES-mastermind.md) | | Morris | [RULES-morris.md](RULES-morris.md) | diff --git a/docs/RULES-catan-ck.md b/docs/RULES-catan-ck.md new file mode 100644 index 0000000..70029bb --- /dev/null +++ b/docs/RULES-catan-ck.md @@ -0,0 +1,220 @@ +# Katan: Cities & Knights — implemented rules + +This is the second rule set of Katan (see [RULES-catan.md](RULES-catan.md) for the +base game). It is chosen on the setup screen — **Rules: Base game / Cities & +Knights** — and stored as `CatanOptions.ruleSet` (`CatanRuleSet.CITIES_AND_KNIGHTS`). +A table plays one or the other; the choice reaches guests with the rest of the +options. + +The expansion is a whole rule set rather than a toggle, because it replaces rules +(no development cards, no Largest Army, a different seven) as well as adding them. +Everything it needs lives behind `CatanState.ck`, which is `null` in the base game, +so a base game's state and rules are untouched. The code is: + +| File | Holds | +| --- | --- | +| `CatanCkModel.kt` | the data: commodities, improvement tracks, progress cards, knights, the chore queue, every new move | +| `CatanCk.kt` | the rules: legal moves, applying them, the roll, the barbarians, scoring | +| `CatanCkAi.kt` | the computer opponent for this rule set | +| `CatanRules.kt` | the single entry point; calls into `CatanCk` whenever `state.ck != null` | + +Behaviour is pinned by `CatanCkRulesTest` (each rule on a crafted position) and +`CatanCkSelfPlayTest` (whole games, engine against engine). Three or four players, +as in the base game. + +## Verification + +The rules were checked against the in-game almanac of Catan Universe (rules text +only: costs, card wordings, the barbarian and improvement rules). Where the almanac +is silent or ambiguous the reading chosen is listed under *Choices made here* below. + +## What changes from the base game + +| | Base game | Cities & Knights | +| --- | --- | --- | +| Points to win | 10 | **13** (setup offers 8 / 10 / 13 / 15) | +| Development cards | 25-card deck | **none** — knights and progress cards replace them | +| Largest Army | 5 *(see RULES-catan.md)* | **none** | +| Longest Road | 5 *(see RULES-catan.md)* | **2** | +| Second setup piece | settlement | **city** (resources only, no commodities) | +| Dice | two | **two number dice + the event die** | +| A seven | discard half over 7; robber moves | discard half over **7 + 2 per wall**; the robber **only moves after the first barbarian attack** | +| Cities | 2 of a resource | forest: 1 lumber + **1 paper**; pasture: 1 wool + **1 cloth**; mountains: 1 ore + **1 coin**; hills / fields: 2 of the resource | + +## The dice + +The yellow die and the red die are added for production, as in the base game. The +**event die** has three ships and one gate each of yellow (trade), blue (politics) +and green (science). It is resolved **before** production: + +- **Ship** — the barbarians come one step nearer (they start 7 steps out). +- **Gate** — every player with at least one level of improvement in that colour, + **in turn order from the roller**, whose **red die is no higher than their level** + in that track, draws a progress card of that colour. + +`RollDice(forced, event)` stages both for tests, like the base game's forced roll. +An **Alchemist** played before the roll sets the two number dice (the event die is +still rolled). + +## Commodities + +Three commodities — **cloth, coin, paper** — join the five resources. The bank holds +12 of each (19 of each resource). Hands, discards, thefts and trades all treat them as +cards; progress cards are not cards for any of these purposes. + +When the bank cannot meet the demand for a card type: with **one** seat owed it, +that seat takes what is left; with **two or more** owed, nobody receives it. + +## The barbarians + +The strip shows how many steps away the ship is. On the seventh ship it **lands**: + +- Barbarian strength is the **number of cities on the board** (a metropolis is a city). +- Catan's strength is the sum of the strengths of every **active** knight (basic 1, + strong 2, mighty 3). +- If Catan's strength is **at least** the barbarians', Catan holds: the player with + the strictly highest contribution takes a **Defender of Catan** card (one point); + if several tie for highest, **each draws a progress card from a deck of their + choice** instead. +- Otherwise Catan falls: among the players who own a city that is not a metropolis, + those who contributed **least** each lose one city, **to their own choice**. A lost + city becomes a settlement (or, if the seat has no settlement piece left, is removed), + and its wall goes. **A metropolis can never be pillaged.** +- Either way every knight stands down, the ship returns to seven steps, and the + **robber wakes** — until now it has not been on the board. + +## Knights + +Each seat has **two knights of each strength** (six in all). + +| Action | Cost | Rule | +| --- | --- | --- | +| Hire | 1 ore + 1 wool | a **basic** knight on an empty crossing at the end of one of your roads; only if one of your two basic knights is free | +| Activate | 1 grain | a sleeping knight wakes; only active knights count against the barbarians | +| Promote | 1 ore + 1 wool | one level, once per knight per turn; a **mighty** knight needs **level three of politics** (the fortress); the next level's pieces must be free | +| Move | — | an active knight moves along **its own roads** to an empty crossing, or onto a **strictly weaker** foreign knight, driving it off; it then rests | +| Chase the robber | — | an active knight on a crossing of the robber's hex sends the robber off (only once the robber is awake); it then rests, and the robber move follows | + +- **A knight cannot act on the turn it woke.** It can be woken again after it acts. +- A driven-off knight is moved by **its owner** along its own roads to any empty + crossing it can reach; if there is none, it is lost. +- A knight is a wall, like an opposing building: roads cannot be laid past an enemy + knight, a settlement cannot be built on a knight, and the knight cuts **Longest Road**. +- Knights can act (wake, move, chase) **before the roll** as well as after. + +## City walls + +2 brick, on a city without a wall, **three per player**. Each wall raises the hand +limit on a seven by two (so a seven discards when you hold more than `7 + 2 × walls`). + +## City improvements + +Three tracks, five levels each, bought with the track's commodity: **trade (cloth, +yellow), politics (coin, blue), science (paper, green)**. Level *n* costs *n* of the +commodity. You must own a city to improve. + +| Track | Level 3 gives | +| --- | --- | +| Trade | commodities trade with the bank **2:1** | +| Politics | knights may be promoted to **mighty** | +| Science | the **aqueduct**: when the dice pay you nothing, take any resource | + +The level names (Trading Post … Grand Bazaar, Town Hall … Palace, Abbey … University) +are this app's own. + +### Metropolises + +The first player to reach **level four** in a track takes that track's **metropolis** +(a marker on one of their cities: +2 points; a city carries at most one). Reaching +**level five** takes it from a holder who is still on four. A player with several +cities chooses which; with one it is placed at once; a player with no free city +**waits**, and claims it when a city is built. + +## Progress cards + +54 cards: 18 in each of three decks. A card played returns to the **bottom** of its +deck. A hand holds **four**; a fifth must be given back at once. They can be played at +any point of your own turn (before or after the roll), except where noted. + +**Science (green)** — Alchemist ×2 *(before the roll only)*, Crane ×2, Engineer, Inventor ×2, +Irrigation ×2, Medicine ×2, Mining ×2, Printer, Road Building ×2, Smith ×2. + +**Politics (blue)** — Bishop ×2 *(after the first attack)*, Constitution, Deserter ×2, +Diplomat ×2, Intrigue ×2, Saboteur ×2, Spy ×3, Warlord ×2, Wedding ×2. + +**Trade (yellow)** — Commercial Harbor ×2, Master Merchant ×2, Merchant ×6, +Merchant Fleet ×2, Resource Monopoly ×4, Trade Monopoly ×2. + +**Printer** and **Constitution** are one point each and are **laid face up the moment +they are drawn**; they never sit in a hand. + +How each plays is shown on the card in the app (`CatanText.progressHelp`); the engine's +behaviour is in `CatanCk.applyProgress`. Points worth knowing: + +- **Saboteur** hits every other player with **as many points as you or more**, who each + discard half their cards (rounded down). **Wedding** takes two cards (or what they + have) from each player with **more** points. **Master Merchant** needs a victim with + more points, and takes two cards you choose. +- **Spy**: you see the target's progress cards and take one; nobody else sees them. +- **Merchant**: placed beside one of your buildings; you trade that hex's resource 2:1 + and hold a victory point until someone else plays a Merchant. +- **Deserter**: the victim removes a knight of *their* choice; you place a free one of the + same strength (or a basic if yours is not free), awake or asleep as theirs was. +- **Diplomat**: removes a road with a loose end (an end touching no other road, building + or knight); if it was yours you may rebuild it anywhere for free. +- **Inventor** swaps two tokens, never a 2, 6, 8 or 12. + +## The chore queue + +Anything that needs an answer from somebody — a discard, a city to lose, a progress card +to hand over, a metropolis site, a knight to place — is queued as a **chore** +(`CatanPhase.CHORE`, `CkState.chores`). The game waits on the seat the first chore names +(not always the player whose turn it is), then resumes where `CkState.back` says. This is +how a seven, a barbarian landing, a wedding or a spy are all one mechanism, and why +the AI and the network driver needed no new concepts. + +The two free-form chores — **discarding** and **handing over** cards — are structural +like the base game's trade offers: the engine checks them in `isLegalMove` and the legal +list carries one example, so a seat is never without a move. + +## Scoring and winning + +Settlement 1, city 2, **metropolis +2**, **Longest Road 2**, **Defender of Catan 1** each, +**Printer / Constitution 1** each, **the Merchant 1**. Nothing is hidden. Win at 13 (default), +checked after every move and when a turn arrives. + +## Choices made here, and why + +- **The red die must be no higher than the improvement level** to draw a progress card + (level 1 draws on a 1, level 5 on 1–5). The almanac says the red die must match a + number on one of your improvements and that the number rises with the level; this is + the reading that makes both true. +- **One resource is offered to every player for a Commercial Harbor.** The printed card + lets you offer a different one to each. +- **The merchant cannot go on the desert**, which produces nothing to trade. +- **Improvements need a city** to be built. +- **A 3:1 harbour trades commodities too**; a 2:1 harbour trades only its own resource. +- **A knight that cannot find a refuge is lost**, as the almanac says of an intrigue. +- **A claim on a metropolis with no city to carry it waits** and is made when a city is + built. +- **Domestic trade** may include commodities; the one-offer-at-a-time rules of the base + game apply. +- **No Seafarers, no 5–6 player extension**, no scenario or variant of any kind. + +## The computer opponent + +`CatanCkAi` plays a safe, unambitious game: it settles and raises cities as the base AI +does, spends commodities on improvements as soon as it can (leaning to politics), hires +and wakes knights as the barbarians draw near, chases the robber off hexes it needs, plays +the progress cards whose value is obvious, and answers every chore with the card it values +least. It never plans a knight's route, never plays an Alchemist, Inventor, Diplomat, +Deserter or Intrigue, and makes no domestic offers (though it answers them). Every engine +game in `CatanCkSelfPlayTest` ends in a win at thirteen points. + +## Known gaps + +- The screen is built and its logic is tested, but **not yet verified on two real + devices**; the catalogue entry stays a dev-channel preview. +- No animation for the event die, the barbarian landing or a knight's move. +- The Commercial Harbor offers a single resource type (see above). +- The AI is not a strong opponent and makes no claim to be. diff --git a/docs/RULES-catan.md b/docs/RULES-catan.md index c90c0e5..b4c07a7 100644 --- a/docs/RULES-catan.md +++ b/docs/RULES-catan.md @@ -12,14 +12,27 @@ players, as published. The engine is a pure state machine (`CatanRules`) with no Android types, and its behaviour is pinned by `CatanBoardTest`, `CatanRulesTest` and `CatanAiTest`. +## Rule sets + +A table plays one of two rule sets, chosen on the setup screen +(`CatanOptions.ruleSet`): + +- **Base game** — everything in this document. +- **Cities & Knights** — the expansion, documented in + [RULES-catan-ck.md](RULES-catan-ck.md). It reuses this document's board, setup, + building, robber and trade rules and changes or replaces the rest; its state lives + behind `CatanState.ck`, which is `null` in the base game. + ## Table +- **Rules — *toggle*.** Base game or Cities & Knights (`CatanOptions.ruleSet`). - **Players — *toggle*.** Three or four. `CatanOptions.playerCount`; the engine refuses anything else. - **Points to win — *toggle*.** Defaults to 10. `CatanOptions.pointsToWin`, at least 2. A lower target makes a test game short. -The setup screen exposes both (players: 3 or 4; game to 5, 8, 10 or 12 points). +The setup screen exposes all three (players: 3 or 4; game to 5, 8, 10 or 12 points in the +base game, 8, 10, 13 or 15 in Cities & Knights, whose published target is 13). ## Board @@ -296,6 +309,13 @@ timing rules; Longest Road and Largest Army worth five; victory at ten. ## Known gaps +- **Longest Road and Largest Army are worth 5 each here.** The published base game awards + **2** each (Catan Universe's almanac says so for Largest Army, and the printed rules + for both). Cities & Knights scores Longest Road at 2, as printed. Left unchanged in + the base game so far; it is a one-constant change in `publicVictoryPoints`. +- **A bank short of a resource pays nobody that roll**, even when only one player is owed + it. The published ruling lets a lone claimant take what is left; Cities & Knights does. + - **No domestic auction or counter-offer.** An offer is addressed to a single seat. Open trading to the whole table, and counter-offers, are the obvious next step and are not implemented. diff --git a/docs/UI-catan.md b/docs/UI-catan.md index 6582b20..99be891 100644 --- a/docs/UI-catan.md +++ b/docs/UI-catan.md @@ -120,6 +120,34 @@ and play a game on a dev build. - 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