Katan: Cities & Knights docs, knight menu for any own knight, tidier player card
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_016eTor2tpxKb4dXbd8RqnvZ
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -71,11 +71,14 @@ data class BoardTargets(
|
||||
val edges: Set<Int> = emptySet(),
|
||||
val hexes: Set<Int> = emptySet(),
|
||||
val knights: Set<Int> = emptySet(),
|
||||
/** The player's other knights: tappable for their menu, but with nothing to do, so not ringed. */
|
||||
val idleKnights: Set<Int> = 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<Int> get() = vertices + knights
|
||||
val tappableVertices: Set<Int> 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<SetupPlace>()
|
||||
if (setup.isNotEmpty()) {
|
||||
@@ -187,7 +192,15 @@ object CatanInteraction {
|
||||
BuildKind.KNIGHT ->
|
||||
BoardTargets(vertices = legal.filterIsInstance<HireKnight>().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<Int> = emptySet(),
|
||||
): TapOutcome {
|
||||
val setup = legal.filterIsInstance<SetupPlace>()
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
+21
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user