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
346 lines
16 KiB
Markdown
346 lines
16 KiB
Markdown
# Katan — implemented rules
|
|
|
|
This is the ruleset the engine enforces. Anything marked **toggle** is exposed
|
|
in the game setup screen.
|
|
|
|
Katan is this app's name for the island game the collection is built around.
|
|
The catalogue id is `catan` (so this file is `RULES-catan.md` and the code lives
|
|
in `game/catan/`), but the printed name is trademarked, so the game is listed
|
|
and titled **Katan**. The rules themselves are the base game for three to four
|
|
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 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
|
|
|
|
The island is the standard layout, derived once from axial hex coordinates in
|
|
`CatanBoard` and checked at class-init:
|
|
|
|
- **19 hexes**, the hexes within distance 2 of the centre.
|
|
- **54 vertices** and **72 edges**, shared between neighbouring hexes by exact
|
|
integer position, not floating point. `HEX_CORNERS`, `EDGE_VERTICES`,
|
|
`VERTEX_EDGES`, `VERTEX_HEXES` and `EDGE_HEXES` are the shared graph.
|
|
- **30 coastal edges**, one closed ring.
|
|
- **9 harbours**, on fixed positions around the coast.
|
|
|
|
Terrain is dealt from the physical mix and shuffled: **4 forest, 4 pasture,
|
|
4 fields, 3 hills, 3 mountains, 1 desert**.
|
|
|
|
Number tokens are the 18 the game ships, shuffled over the land hexes: four red
|
|
tokens (**6, 6, 8, 8**) and fourteen others
|
|
(**2, 3, 3, 4, 4, 5, 5, 9, 9, 10, 10, 11, 11, 12**). Seven never appears. The
|
|
desert bears no token and the robber starts on it.
|
|
|
|
### Keeping 6s and 8s apart
|
|
|
|
The published game asks that the two red numbers not sit on adjacent hexes, and
|
|
`generate` guarantees it rather than hoping for it:
|
|
|
|
1. Shuffle the land and take four hexes; accept the draw if none of the four
|
|
touch each other. Retry up to 60 times.
|
|
2. If no draw separates them, fall back to four of the six board-corner hexes,
|
|
which are pairwise non-adjacent by construction and at most one of which can
|
|
be the desert.
|
|
|
|
`no seed puts two red tokens on adjacent hexes` runs the whole thing over seeds
|
|
1..500.
|
|
|
|
Harbours: the nine **positions** are fixed (the physical frame is not shuffled),
|
|
but the nine **types** are shuffled onto them — **five 2:1 resource harbours**
|
|
(one per resource) and **four generic 3:1 harbours**. The frame positions are
|
|
chosen so no two harbours sit closer than three coastal edges apart.
|
|
|
|
## Setup
|
|
|
|
Placement runs in snake order: seats `0, 1, …, n-1` then `n-1, …, 1, 0`, each
|
|
placing one settlement and one road. During setup the distance rule still holds
|
|
(a settlement may not sit one edge from any other building) but the road need
|
|
not connect to anything already placed.
|
|
|
|
A player's **second** settlement earns one resource card for each adjacent
|
|
producing hex, supply permitting — duplicates counted, so a second settlement on
|
|
two forests earns two lumber. The first pass earns nothing.
|
|
|
|
After the last placement, seat 0 leads and the first real turn begins.
|
|
|
|
## Turn
|
|
|
|
A turn runs through the phases in `CatanPhase`:
|
|
|
|
1. **AWAIT_ROLL** — the player on the clock rolls.
|
|
2. A seven goes to **DISCARDING** and then **ROBBER**; anything else produces and
|
|
goes straight to **PLAYING**.
|
|
3. **PLAYING** — build, trade, buy and play development cards, then end the turn.
|
|
4. **FREE_ROADS** is a detour inside PLAYING while a Road Building card is being
|
|
spent.
|
|
|
|
### Rolling
|
|
|
|
The dice are derived from the game seed and a monotonic counter (`rngStep`), not
|
|
from a random generator stored in the state: the same seed and the same moves
|
|
replay identically. `RollDice(forced = …)` exists so tests can stage a number;
|
|
it is deliberately **not** a member of the enumerated move list, so a client
|
|
cannot send one over the network — only code holding the state directly can use
|
|
it.
|
|
|
|
### Production
|
|
|
|
For a non-seven total, each hex whose number matches pays its producing terrain:
|
|
a settlement takes **1**, a city takes **2**. The robber's hex pays nothing.
|
|
|
|
**When the bank cannot meet the demand for a resource, nobody receives that
|
|
resource this roll.** This is the official ruling, implemented per-resource:
|
|
demand is totalled first and, if it exceeds the bank's supply, every gain of
|
|
that resource is dropped.
|
|
|
|
## The robber
|
|
|
|
A **seven** sends the robber: first every player holding **more than seven**
|
|
resource cards discards half their hand (rounded down, so nine cards means four
|
|
gone), in seat order starting from the lowest overfull seat; then the roller
|
|
moves the robber and may steal.
|
|
|
|
The robber **must move to a different hex**. Moving onto a hex lets the roller
|
|
steal one card from one opponent who has a building on that hex and at least one
|
|
card in hand. The card stolen is chosen at random, derived like the dice. A
|
|
hex with no opponent holding cards offers only the move, no steal.
|
|
|
|
A played Knight enters the same ROBBER phase, so the robber move after a Knight
|
|
is the same code path.
|
|
|
|
## Trading
|
|
|
|
### Domestic
|
|
|
|
An offer is addressed to **one** seat or, with `to = null`, to **everyone**, and
|
|
names two card multisets: what the offerer gives, what they want.
|
|
|
|
Seats are asked **one at a time**, in seat order after the offerer (seats holding
|
|
no cards are skipped; an addressed offer asks only its addressee). The seat being
|
|
asked may:
|
|
|
|
- **accept** — the trade happens at once and the offer closes (first accept wins);
|
|
- **decline** — the offer moves on to the next seat;
|
|
- **counter** (`CounterTrade`) — propose different terms: what they would give and
|
|
what they want from the offerer. A counter must differ from the offer as made
|
|
(that is an accept), and both sides must hold the cards. The offer moves on.
|
|
|
|
When every seat has answered and nobody accepted: with no counters the offer
|
|
simply lapses; with counters the phase becomes `TRADE_COUNTERS` and the offerer
|
|
either **takes one** (`AcceptCounter(seat)`, still checked for affordability) or
|
|
**turns them all down** (`DismissCounters`).
|
|
|
|
An offer is legal only when the offerer's turn is in PLAYING, the addressee is
|
|
another seat, both legs are non-empty, the two legs are not the same multiset
|
|
(`countOf(give) != countOf(get)` — like-for-like is not a trade), and the
|
|
offerer actually holds the cards being offered.
|
|
|
|
A seat may **accept** only if it holds what was asked and the offerer still holds
|
|
what was promised; otherwise only decline (or counter) is offered. A refusal
|
|
leaves every card where it was.
|
|
|
|
Offers are free-form intents rather than enumerated moves (the space of card
|
|
multisets is combinatorial), so they are validated structurally by
|
|
`CatanRules.isLegalMove` and checked by the network driver's gate like any other
|
|
move.
|
|
|
|
### Maritime
|
|
|
|
Bank trade gives `ratio` of one resource for one of another, where the ratio is
|
|
the best the seat can get: **4:1** through the bank, **3:1** through a generic
|
|
harbour, **2:1** through a harbour matching the resource being given. A harbour
|
|
counts when the seat owns a settlement or city on **either** endpoint vertex of
|
|
its edge.
|
|
|
|
## Building and development cards
|
|
|
|
| Build | Cost |
|
|
| --- | --- |
|
|
| Road | 1 lumber + 1 brick |
|
|
| Settlement | 1 lumber + 1 brick + 1 wool + 1 grain |
|
|
| City | 2 grain + 3 ore |
|
|
| Development card | 1 wool + 1 grain + 1 ore |
|
|
|
|
A settlement goes on an **empty vertex** at least two edges from every standing
|
|
building and connected to the seat's own road network. A city upgrades one of
|
|
the seat's own settlements. A road goes on an empty edge that connects to the
|
|
seat's network: it must start from a crossing where the seat has a building, or
|
|
where one of its roads ends.
|
|
|
|
**An opponent's settlement or city on a crossing breaks the network there.** A
|
|
road cannot be laid past it, even from a crossing where one of your own roads
|
|
ends — only a crossing you own a building on is a way through. The same rule
|
|
applies to the free roads of a Road Building card, and it is the same wall that
|
|
cuts Longest Road (below).
|
|
|
|
**Pieces are limited**: each player has **15 roads, 5 settlements and 4 cities**.
|
|
A player who has laid them all cannot build another of that kind. Raising a
|
|
settlement into a city returns the settlement to the supply, so the five
|
|
settlements remain available to be laid again.
|
|
|
|
The development deck is the published 25 cards: **14 Knights, 2 Road Building,
|
|
2 Monopoly, 2 Year of Plenty, 5 Victory Point**. Cards are bought blind from the
|
|
top of the deck.
|
|
|
|
Timing rules the engine enforces:
|
|
|
|
- **One development card per turn.** `devCardPlayedThisTurn` gates every play.
|
|
- **A card cannot be played the turn it was bought.** Each card records
|
|
`boughtTurn` and is skipped while that equals the current turn.
|
|
- **Victory Point cards are never played.** They are counted straight from the
|
|
hand by `victoryPoints`, and are only revealed by winning.
|
|
- **Knight** → the robber phase, and counts toward Largest Army.
|
|
- **Road Building** → two free road placements (FREE_ROADS); the player may
|
|
forfeit any that remain.
|
|
- **Monopoly** → seizes every card of the named resource from the other seats.
|
|
- **Year of Plenty** → takes up to two cards from the bank; taking two of a kind
|
|
requires the bank to hold both.
|
|
|
|
## Scoring and winning
|
|
|
|
Victory points are:
|
|
|
|
- **Settlement 1, city 2.**
|
|
- **Longest Road 5** — the longest single road of **five or more** segments.
|
|
- **Largest Army 5** — first to **three** played knights.
|
|
- **Victory Point cards**, one each.
|
|
|
|
A seat wins on reaching **points to win** (default 10). The win is checked after
|
|
every build, when a turn ends, and when a seat's turn arrives — the last covers
|
|
points that changed on someone else's turn, such as a broken Longest Road being
|
|
re-awarded. A seat already at the target therefore wins at the start of its own
|
|
turn rather than having to act.
|
|
|
|
### Longest Road
|
|
|
|
The award goes to the longest **simple path** over a seat's own roads. The path
|
|
never passes *through* an opponent's building (it may run up to one) and never
|
|
revisits a vertex — so a figure of two loops sharing an intersection counts as
|
|
one road of five, not two roads, because the intersection is a branch.
|
|
|
|
Once held, the award is kept until another seat builds a **strictly longer**
|
|
road; an equal length leaves it where it is. If the holder's road falls below
|
|
five segments the award reopens to the longest road of five or more, lowest seat
|
|
first on a tie. A settlement placed across an opponent's road breaks it, and the
|
|
award is recomputed for everyone after any settlement.
|
|
|
|
### Largest Army
|
|
|
|
First to three knights takes it. It is stolen only by a **strictly larger** army:
|
|
a tie leaves the incumbent ahead, exactly as Longest Road does.
|
|
|
|
## The computer opponent
|
|
|
|
`CatanAi` plays a legal, mildly strategic game so a solo player can host against
|
|
computers, with the lobby filling empty seats.
|
|
|
|
It reads **only public information** — terrain, numbers, buildings, roads,
|
|
resource counts, development-card counts and knights played — plus its own hand.
|
|
It never looks into the deck, another seat's development cards, or the seed
|
|
beyond the roll the rules derive from it. That is a design constraint, not an
|
|
accident: it runs on the host, where the authoritative state is fully visible.
|
|
|
|
What it does:
|
|
|
|
- **Setup.** Rates a settlement by the production its three hexes give (weighted
|
|
by the token odds and by how much it wants each resource), plus a bonus for
|
|
resource variety and for sitting on a harbour, plus a small term for where the
|
|
accompanying road points.
|
|
- **Main phase**, in priority order: raise a city, build a settlement, play a
|
|
knight, play Year of Plenty or Monopoly when it closes a gap, play Road
|
|
Building when two segments are useful, buy a development card, build a road
|
|
that reaches a spot worth settling, trade at the bank only when the trade
|
|
completes a build this turn, make one domestic offer that closes a build, else
|
|
end the turn.
|
|
- **The robber.** Moves onto the hex where it blocks the most opposing
|
|
production (weighted by token odds), preferring to rob whoever is holding the
|
|
most cards and the most points.
|
|
- **Trades.** Accepts an offer when what arrives is worth more than what leaves
|
|
by its own resource valuations; a mild table, not a shark.
|
|
- **Discards.** Throws away what it values least (wool, then brick, then lumber,
|
|
before grain and ore).
|
|
|
|
It makes **at most one domestic offer a turn**. A declined offer changes nothing,
|
|
so re-offering the same thing would only bounce between two seats forever; the
|
|
engine allows more offers, the AI chooses not to.
|
|
|
|
`CatanAiTest` plays three- and four-seat tables of engines out to a result from
|
|
several seeds, refusing any illegal move the AI chooses, and checks the winner
|
|
reached the target.
|
|
|
|
## Provenance and rule choices
|
|
|
|
The rules are the published base game. Where the published rules leave room, the
|
|
engine picks one reading and records it here.
|
|
|
|
**Followed from the published rules:** the board and its component mix; the
|
|
number tokens; 6s and 8s not adjacent; snake-order setup and second-settlement
|
|
income; production weighting; the robber on a seven; discard half over seven;
|
|
domestic and maritime trade; build costs; the 25-card development deck and its
|
|
timing rules; Longest Road and Largest Army worth five; victory at ten.
|
|
|
|
**Choices made here, and why:**
|
|
|
|
- **A bank short of a resource pays nobody that roll.** This is the official
|
|
ruling, and it is implemented per-resource rather than per-player.
|
|
- **Longest Road and Largest Army are kept on a tie**, and stolen only by a
|
|
strictly longer road or a strictly larger army. The alternative reading lets a
|
|
tie steal the award; "strictly better" is the common tournament reading and
|
|
keeps the award from flickering.
|
|
- **A ring road counts as a simple path.** No vertex is revisited, so six
|
|
segments around one hex are worth five, not six.
|
|
- **The win is checked when a seat's turn arrives**, not only on that seat's own
|
|
action, so points that move on another player's turn still win.
|
|
- **A bought Victory Point card does not win immediately.** It is counted when
|
|
the win is next checked (a build or the end of the turn), which is when the
|
|
holder would reveal it.
|
|
- **Year of Plenty two-of-a-kind needs the bank to hold both cards**, not one.
|
|
- **The robber must move.** Some printings allow staying put; this engine always
|
|
requires a different hex.
|
|
- **Offers may be open to the whole table** and can be countered; the first accept wins (see Trading).
|
|
|
|
## 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 timer or cancel on offers.** An offer waits for each asked seat in turn; the offerer cannot withdraw it mid-round, and a player who never answers stalls the table.
|
|
- **The AI never counters.** It accepts, declines, and picks among counters it is sent, but does not propose them.
|
|
- **No trading away a development card.** Not part of the base game.
|
|
- **The screen is built, not yet verified on two devices.** The Katan screen and
|
|
setup options exist (see [UI-catan.md](UI-catan.md)); the catalogue entry is a
|
|
dev-channel preview until two-device play has been verified.
|
|
- **The AI does not look ahead.** It scores one move deep with a fixed resource
|
|
valuation. It plays legally and plausibly; it is not a strong opponent, and no
|
|
strength claim is made.
|
|
- **The board is the standard published layout, generated.** Unlike the Sequence
|
|
board there is no unverified substitute here — the geometry is the real one —
|
|
but the *island* (terrain and token placement) is freshly generated each game,
|
|
so it will not match any particular physical set-up. That is the intent.
|