Files
ProLibertateGames/docs/RULES-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

15 KiB

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.

Table

  • 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).

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

  • 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); 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.