Katan: no roads past an opponent's settlement, piece limits, stable layout
Rules - A road may only start from a crossing where the player has a building, or where one of their roads ends AND no opponent's settlement or city stands. roadConnects ignored opponents' buildings, so a road (the computer's included) could be laid straight past a settlement. Also applies to Road Building. - Enforce the piece supply: 15 roads, 5 settlements, 4 cities. It was unlimited. - Tests: building past a settlement and a city, free roads, own settlement not blocking, and running out of each piece. UI - The layout rescaled the board whenever it became the player's turn, because the action buttons only existed then. Everything outside the board now has a fixed height: the action bar always shows every button (disabled, not absent), the prompt is pinned to two lines, player cards always draw every line, and the dice total is always present. Rule recorded in docs/UI-catan.md. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
This commit is contained in:
@@ -340,6 +340,89 @@ class CatanRulesTest {
|
||||
)
|
||||
}
|
||||
|
||||
/** Seat 0 owns a settlement at vertex 0 and a two-road chain v0 - w - x; `beyond` continues past x. */
|
||||
private data class Chain(val state: CatanState, val x: Int, val beyond: Int, val spur: Int?)
|
||||
|
||||
private fun chain(): Chain {
|
||||
val e1 = CatanBoard.VERTEX_EDGES[0][0]
|
||||
val w = CatanBoard.otherEnd(e1, 0)
|
||||
val e2 = CatanBoard.VERTEX_EDGES[w].first { it != e1 }
|
||||
val x = CatanBoard.otherEnd(e2, w)
|
||||
val beyond = CatanBoard.VERTEX_EDGES[x].first { it != e2 }
|
||||
val spur = CatanBoard.VERTEX_EDGES[w].firstOrNull { it != e1 && it != e2 }
|
||||
val state = craft()
|
||||
.copy(resources = hands(hand(Resource.LUMBER to 5, Resource.BRICK to 5)))
|
||||
.withBuilding(0, BuildingType.SETTLEMENT, 0)
|
||||
.withRoad(e1, 0)
|
||||
.withRoad(e2, 0)
|
||||
return Chain(state, x, beyond, spur)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a road cannot be built past an opponent's settlement`() {
|
||||
val (open, x, beyond, spur) = chain()
|
||||
assertTrue("open crossing: the road continues", BuildRoad(beyond) in CatanRules.legalMoves(open, 0))
|
||||
|
||||
val blocked = open.withBuilding(x, BuildingType.SETTLEMENT, 1)
|
||||
assertFalse(
|
||||
"an opponent's settlement on the crossing breaks the network",
|
||||
BuildRoad(beyond) in CatanRules.legalMoves(blocked, 0),
|
||||
)
|
||||
// The same through a city, and while laying free roads from Road Building.
|
||||
val city = open.withBuilding(x, BuildingType.CITY, 1)
|
||||
assertFalse(BuildRoad(beyond) in CatanRules.legalMoves(city, 0))
|
||||
val free = blocked.copy(phase = CatanPhase.FREE_ROADS, pendingFreeRoads = 2)
|
||||
assertFalse(BuildRoad(beyond) in CatanRules.legalMoves(free, 0))
|
||||
// The rest of the network is untouched: the spur at the other crossing still works.
|
||||
if (spur != null) assertTrue(BuildRoad(spur) in CatanRules.legalMoves(blocked, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `your own settlement on the crossing does not block your road`() {
|
||||
val (open, x, beyond, _) = chain()
|
||||
val own = open.withBuilding(x, BuildingType.SETTLEMENT, 0)
|
||||
assertTrue(BuildRoad(beyond) in CatanRules.legalMoves(own, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a player runs out of pieces`() {
|
||||
val rich = hands(
|
||||
hand(
|
||||
Resource.LUMBER to 9, Resource.BRICK to 9, Resource.WOOL to 9,
|
||||
Resource.GRAIN to 9, Resource.ORE to 9,
|
||||
),
|
||||
)
|
||||
|
||||
// Roads: fifteen, and no sixteenth - in the main phase or from Road Building.
|
||||
var roads = craft().copy(resources = rich).withBuilding(0, BuildingType.SETTLEMENT, 0)
|
||||
repeat(14) {
|
||||
roads = roads.withRoad(CatanRules.legalMoves(roads, 0).filterIsInstance<BuildRoad>().first().edge, 0)
|
||||
}
|
||||
assertTrue("fourteen laid: one more is allowed", CatanRules.legalMoves(roads, 0).any { it is BuildRoad })
|
||||
roads = roads.withRoad(CatanRules.legalMoves(roads, 0).filterIsInstance<BuildRoad>().first().edge, 0)
|
||||
assertEquals(15, roads.roads.count { it == 0 })
|
||||
assertTrue("fifteen laid: none left", CatanRules.legalMoves(roads, 0).none { it is BuildRoad })
|
||||
val free = roads.copy(phase = CatanPhase.FREE_ROADS, pendingFreeRoads = 2)
|
||||
assertEquals(listOf<CatanMove>(EndFreeRoads), CatanRules.legalMoves(free, 0))
|
||||
|
||||
// Settlements: five.
|
||||
val homes = scattered(7)
|
||||
var settled = craft().copy(resources = rich)
|
||||
for (i in 0 until 4) settled = settled.withBuilding(homes[i], BuildingType.SETTLEMENT, 0)
|
||||
settled = settled.withRoad(CatanBoard.VERTEX_EDGES[homes[5]][0], 0)
|
||||
assertTrue("four standing: a fifth is allowed", BuildSettlement(homes[5]) in CatanRules.legalMoves(settled, 0))
|
||||
settled = settled.withBuilding(homes[4], BuildingType.SETTLEMENT, 0)
|
||||
assertFalse("five standing: no sixth", BuildSettlement(homes[5]) in CatanRules.legalMoves(settled, 0))
|
||||
|
||||
// Cities: four. A raised settlement returns to the supply, so five settlements stays possible.
|
||||
var raised = craft().copy(resources = rich)
|
||||
for (i in 0 until 3) raised = raised.withBuilding(homes[i], BuildingType.CITY, 0)
|
||||
raised = raised.withBuilding(homes[3], BuildingType.SETTLEMENT, 0)
|
||||
assertTrue("three cities: a fourth is allowed", BuildCity(homes[3]) in CatanRules.legalMoves(raised, 0))
|
||||
raised = raised.withBuilding(homes[3], BuildingType.CITY, 0).withBuilding(homes[4], BuildingType.SETTLEMENT, 0)
|
||||
assertFalse("four cities: no fifth", BuildCity(homes[4]) in CatanRules.legalMoves(raised, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a settlement needs an open vertex on the network, a city one of yours`() {
|
||||
val base = craft()
|
||||
|
||||
Reference in New Issue
Block a user