From 6c311ad3047700bad5198b91bff4bd8236e35a82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:50:24 +0000 Subject: [PATCH] Katan: expansions as toggles, scenario chosen in options Replace the single CatanRuleSet with a set of expansions and add a scenario field; saves with the old ruleSet still decode. Setup shows a toggle per expansion and a map picker for Seafarers. Combining expansions is rejected for now (lifted in a later phase). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01TWwLCrXa9aFVg8dZsCdLBZ --- CLAUDE.md | 4 +- .../games/game/catan/CatanModel.kt | 63 +++++++++++-- .../games/game/catan/CatanRules.kt | 6 +- .../prolibertate/games/ui/GameSetupScreen.kt | 90 +++++++++++-------- .../games/game/catan/CatanAiProfilesTest.kt | 22 ++--- .../games/game/catan/CatanCkRulesTest.kt | 20 ++++- .../games/game/catan/CatanCkSelfPlayTest.kt | 4 +- .../games/game/catan/CatanSeafarersTest.kt | 2 +- .../games/net/SaveRoundTripAllGamesTest.kt | 2 +- .../ui/game/catan/CatanCkInteractionTest.kt | 4 +- .../games/ui/game/catan/CatanCkTextTest.kt | 26 +++--- .../catan/CatanSeafarersInteractionTest.kt | 4 +- docs/RULES-catan-seafarers.md | 4 +- 13 files changed, 165 insertions(+), 86 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bbee649..c28dd54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,8 +51,8 @@ integration: ## Katan -The island game is catalogue id `catan`, shown as **Katan**. It has two rule -sets chosen at setup: the base game (`docs/RULES-catan.md`), Cities & Knights +The island game is catalogue id `catan`, shown as **Katan**. It has two expansions +(toggles, `CatanOptions.expansions`; none on is the base game): the base game (`docs/RULES-catan.md`), Cities & Knights (`docs/RULES-catan-ck.md`, code in `game/catan/CatanCk*.kt`, state behind `CatanState.ck`, which is null in the base game) and Seafarers (`docs/RULES-catan-seafarers.md`, code in `game/catan/CatanSea*.kt`, state behind diff --git a/app/src/main/java/org/prolibertate/games/game/catan/CatanModel.kt b/app/src/main/java/org/prolibertate/games/game/catan/CatanModel.kt index 1aeb977..023275f 100644 --- a/app/src/main/java/org/prolibertate/games/game/catan/CatanModel.kt +++ b/app/src/main/java/org/prolibertate/games/game/catan/CatanModel.kt @@ -1,7 +1,16 @@ package org.prolibertate.games.game.catan +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject /** Sentinel stored in [CatanState.roads] for an unclaimed road position. */ const val NO_PLAYER = -1 @@ -151,12 +160,9 @@ enum class CatanPhase { CHORE, } -/** Which rules a table plays. The expansion is a whole rule set, not a toggle. */ +/** An expansion a table can switch on. The base game is what is left with none selected. */ @Serializable -enum class CatanRuleSet { - /** The base game: development cards, Largest Army, a robber from the start. */ - BASE, - +enum class CatanExpansion { /** Cities & Knights: commodities, knights, barbarians, city improvements. */ CITIES_AND_KNIGHTS, @@ -164,15 +170,58 @@ enum class CatanRuleSet { SEAFARERS, } -@Serializable +/** + * What a table plays: the expansions switched on, and the Seafarers map when that one is. + * + * Saves written before expansions became toggles carry `"ruleSet"` instead of `"expansions"`; + * [CatanOptionsSerializer] reads either. + */ +@Serializable(with = CatanOptionsSerializer::class) data class CatanOptions( val playerCount: Int = 3, val pointsToWin: Int = 10, - val ruleSet: CatanRuleSet = CatanRuleSet.BASE, + val expansions: Set = emptySet(), + val scenario: SeaScenario = SeaScenario.NEW_SHORES, ) { init { require(playerCount in 3..4) { "The island seats three or four players" } require(pointsToWin >= 2) { "pointsToWin must be at least 2" } + // Lifted once Cities & Knights runs on a Seafarers map. + require(expansions.size <= 1) { "Combining expansions is not supported yet" } + } + + val seafarers: Boolean get() = CatanExpansion.SEAFARERS in expansions + val citiesAndKnights: Boolean get() = CatanExpansion.CITIES_AND_KNIGHTS in expansions +} + +/** The wire shape of [CatanOptions]. */ +@Serializable +private class CatanOptionsWire( + val playerCount: Int = 3, + val pointsToWin: Int = 10, + val expansions: Set = emptySet(), + val scenario: SeaScenario = SeaScenario.NEW_SHORES, +) + +/** Reads the legacy single `ruleSet` field as an expansion set. */ +object CatanOptionsSerializer : KSerializer { + override val descriptor: SerialDescriptor = CatanOptionsWire.serializer().descriptor + + override fun serialize(encoder: Encoder, value: CatanOptions) = encoder.encodeSerializableValue( + CatanOptionsWire.serializer(), + CatanOptionsWire(value.playerCount, value.pointsToWin, value.expansions, value.scenario), + ) + + override fun deserialize(decoder: Decoder): CatanOptions { + val input = decoder as JsonDecoder + var obj = input.decodeJsonElement().jsonObject + val legacy = (obj["ruleSet"] as? JsonPrimitive)?.content + if (legacy != null) { + val expansions = if (legacy == "BASE") emptyList() else listOf(JsonPrimitive(legacy)) + obj = JsonObject(obj - "ruleSet" + ("expansions" to JsonArray(expansions))) + } + val wire = input.json.decodeFromJsonElement(CatanOptionsWire.serializer(), obj) + return CatanOptions(wire.playerCount, wire.pointsToWin, wire.expansions, wire.scenario) } } diff --git a/app/src/main/java/org/prolibertate/games/game/catan/CatanRules.kt b/app/src/main/java/org/prolibertate/games/game/catan/CatanRules.kt index 5c45806..018c966 100644 --- a/app/src/main/java/org/prolibertate/games/game/catan/CatanRules.kt +++ b/app/src/main/java/org/prolibertate/games/game/catan/CatanRules.kt @@ -54,15 +54,15 @@ object CatanRules : GameRules { "Seat count ${config.seats.size} does not match ${options.playerCount} players" } - val sea = options.ruleSet == CatanRuleSet.SEAFARERS - val scenario = SeaScenario.NEW_SHORES + val sea = options.seafarers + val scenario = options.scenario val seaBoard = if (sea) SeaBoards.generate(scenario, config.seed) else null val topo = if (seaBoard != null) SeaBoards.topology(scenario) else CatanBoard.base val board = seaBoard?.let { CatanBoard.Generated(it.terrains, it.numbers, it.robber, it.ports) } ?: CatanBoard.generate(config.seed) // A different random stream than the island, so the same seed still // produces both a fixed island and a fixed deck. - val ck = options.ruleSet == CatanRuleSet.CITIES_AND_KNIGHTS + val ck = options.citiesAndKnights // Cities & Knights has no development cards: knights and progress // cards take their place. val devDeck = if (ck) { diff --git a/app/src/main/java/org/prolibertate/games/ui/GameSetupScreen.kt b/app/src/main/java/org/prolibertate/games/ui/GameSetupScreen.kt index 5fcbb2c..15a8dbb 100644 --- a/app/src/main/java/org/prolibertate/games/ui/GameSetupScreen.kt +++ b/app/src/main/java/org/prolibertate/games/ui/GameSetupScreen.kt @@ -59,7 +59,8 @@ import org.prolibertate.games.game.morris.MorrisOptions import org.prolibertate.games.game.pirates.PiratesOptions import org.prolibertate.games.game.president.PresidentOptions import org.prolibertate.games.game.catan.CatanOptions -import org.prolibertate.games.game.catan.CatanRuleSet +import org.prolibertate.games.game.catan.CatanExpansion +import org.prolibertate.games.game.catan.SeaScenario import org.prolibertate.games.game.sequence.SequenceOptions import org.prolibertate.games.game.tayu.TayuLevel import org.prolibertate.games.game.tayu.TayuOptions @@ -373,47 +374,48 @@ fun teamForSeat(gameId: String, seat: Int, seatCount: Int): Int = when (gameId) else -> seat } -/** The targets the setup screen offers for a Katan rule set. */ -internal fun catanPointsChoices(ruleSet: CatanRuleSet): List = when (ruleSet) { - // Five is a short game for trying things out; ten is the published target. - CatanRuleSet.BASE -> listOf(5, 8, 10, 12) - // Cities & Knights is played to thirteen, which is also why it runs longer. - CatanRuleSet.CITIES_AND_KNIGHTS -> listOf(8, 10, 13, 15) +/** The targets the setup screen offers for a Katan game with [expansions] on. */ +internal fun catanPointsChoices(expansions: Set): List = when { // Seafarers adds two points for each new island, so the target is higher. - CatanRuleSet.SEAFARERS -> listOf(8, 10, 12, 14) + CatanExpansion.SEAFARERS in expansions -> listOf(8, 10, 12, 14) + // Cities & Knights is played to thirteen, which is also why it runs longer. + CatanExpansion.CITIES_AND_KNIGHTS in expansions -> listOf(8, 10, 13, 15) + // Five is a short game for trying things out; ten is the published target. + else -> listOf(5, 8, 10, 12) } -/** The published target for a rule set. */ -internal fun catanDefaultPoints(ruleSet: CatanRuleSet): Int = when (ruleSet) { - CatanRuleSet.BASE -> 10 - CatanRuleSet.CITIES_AND_KNIGHTS -> 13 - CatanRuleSet.SEAFARERS -> 12 +/** The published target for a game with [expansions] on. */ +internal fun catanDefaultPoints(expansions: Set): Int = when { + CatanExpansion.SEAFARERS in expansions -> 12 + CatanExpansion.CITIES_AND_KNIGHTS in expansions -> 13 + else -> 10 } /** - * [options] switched to [ruleSet]. The target moves to the new rule set's own - * default unless the one already chosen is on its menu too. + * [options] with [expansion] switched on or off. The target moves to the new combination's own + * default unless the one already chosen is on its menu too. Expansions cannot be combined yet, + * so switching one on switches the other off. */ -internal fun catanSwitchRules(options: CatanOptions, ruleSet: CatanRuleSet): CatanOptions = - options.copy( - ruleSet = ruleSet, - pointsToWin = options.pointsToWin.takeIf { it in catanPointsChoices(ruleSet) && it != catanDefaultPoints(options.ruleSet) } - ?: catanDefaultPoints(ruleSet), +internal fun catanToggleExpansion(options: CatanOptions, expansion: CatanExpansion, on: Boolean): CatanOptions { + val expansions = if (on) setOf(expansion) else options.expansions - expansion + return options.copy( + expansions = expansions, + pointsToWin = options.pointsToWin.takeIf { + it in catanPointsChoices(expansions) && it != catanDefaultPoints(options.expansions) + } ?: catanDefaultPoints(expansions), ) - -private fun catanRuleSetName(ruleSet: CatanRuleSet): String = when (ruleSet) { - CatanRuleSet.BASE -> "Base game" - CatanRuleSet.CITIES_AND_KNIGHTS -> "Cities & Knights" - CatanRuleSet.SEAFARERS -> "Seafarers" } -private fun catanRuleSetBlurb(ruleSet: CatanRuleSet): String = when (ruleSet) { - CatanRuleSet.BASE -> - "Settle the island, trade, and play development cards. Longest Road and Largest Army score." - CatanRuleSet.CITIES_AND_KNIGHTS -> +private fun catanExpansionName(expansion: CatanExpansion): String = when (expansion) { + CatanExpansion.CITIES_AND_KNIGHTS -> "Cities & Knights" + CatanExpansion.SEAFARERS -> "Seafarers" +} + +private fun catanExpansionBlurb(expansion: CatanExpansion): String = when (expansion) { + CatanExpansion.CITIES_AND_KNIGHTS -> "Cities make commodities, knights hold off the barbarians, and improvements earn progress " + "cards and metropolises. No development cards; the robber sleeps until the first attack." - CatanRuleSet.SEAFARERS -> + CatanExpansion.SEAFARERS -> "Build ships to reach new islands. Gold fields pay any resource, a pirate guards the sea, " + "and the first settlement on each new island scores two extra points." } @@ -421,14 +423,24 @@ private fun catanRuleSetBlurb(ruleSet: CatanRuleSet): String = when (ruleSet) { @Composable private fun CatanOptionsEditor(options: CatanOptions, onChange: (CatanOptions) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - ChipRow( - label = "Rules", - values = CatanRuleSet.entries, - selected = options.ruleSet, - display = ::catanRuleSetName, - onSelect = { onChange(catanSwitchRules(options, it)) }, - ) - Text(catanRuleSetBlurb(options.ruleSet), style = MaterialTheme.typography.bodySmall) + CatanExpansion.entries.forEach { expansion -> + ToggleRow( + title = catanExpansionName(expansion), + subtitle = catanExpansionBlurb(expansion), + checked = expansion in options.expansions, + onChange = { onChange(catanToggleExpansion(options, expansion, it)) }, + ) + } + if (options.seafarers) { + ChipRow( + label = "Map", + values = SeaScenario.entries, + selected = options.scenario, + display = { it.title }, + onSelect = { onChange(options.copy(scenario = it)) }, + ) + Text(options.scenario.blurb, style = MaterialTheme.typography.bodySmall) + } ChipRow( label = "Players", values = listOf(3, 4), @@ -438,7 +450,7 @@ private fun CatanOptionsEditor(options: CatanOptions, onChange: (CatanOptions) - ) ChipRow( label = "Game to", - values = catanPointsChoices(options.ruleSet), + values = catanPointsChoices(options.expansions), selected = options.pointsToWin, display = { "$it points" }, onSelect = { onChange(options.copy(pointsToWin = it)) }, diff --git a/app/src/test/java/org/prolibertate/games/game/catan/CatanAiProfilesTest.kt b/app/src/test/java/org/prolibertate/games/game/catan/CatanAiProfilesTest.kt index c270dfa..14d0d2b 100644 --- a/app/src/test/java/org/prolibertate/games/game/catan/CatanAiProfilesTest.kt +++ b/app/src/test/java/org/prolibertate/games/game/catan/CatanAiProfilesTest.kt @@ -55,15 +55,15 @@ class CatanAiProfilesTest { (0 until 3).any { p.of(it).style == style } } - private fun forEveryProfile(rules: CatanRuleSet, cap: Int) { + private fun forEveryProfile(rules: Set, cap: Int) { val options = CatanOptions( playerCount = 3, - pointsToWin = when (rules) { - CatanRuleSet.BASE -> 10 - CatanRuleSet.CITIES_AND_KNIGHTS -> 13 - else -> 12 + pointsToWin = when { + CatanExpansion.CITIES_AND_KNIGHTS in rules -> 13 + CatanExpansion.SEAFARERS in rules -> 12 + else -> 10 }, - ruleSet = rules, + expansions = rules, ) for (difficulty in listOf(AiDifficulty.EASY, AiDifficulty.HARD)) { for (style in AiStyle.entries) { @@ -78,14 +78,14 @@ class CatanAiProfilesTest { } @Test - fun `easy and hard seats of every style play the base game`() = forEveryProfile(CatanRuleSet.BASE, 1_500) + fun `easy and hard seats of every style play the base game`() = forEveryProfile(emptySet(), 1_500) @Test fun `easy and hard seats of every style play Cities and Knights`() = - forEveryProfile(CatanRuleSet.CITIES_AND_KNIGHTS, 1_500) + forEveryProfile(setOf(CatanExpansion.CITIES_AND_KNIGHTS), 1_500) @Test - fun `easy and hard seats of every style play Seafarers`() = forEveryProfile(CatanRuleSet.SEAFARERS, 1_500) + fun `easy and hard seats of every style play Seafarers`() = forEveryProfile(setOf(CatanExpansion.SEAFARERS), 1_500) @Test fun `a mixed table of four finishes a base game`() { @@ -95,8 +95,8 @@ class CatanAiProfilesTest { @Test fun `the default profile plays exactly as an explicit medium table does`() { - for (rules in listOf(CatanRuleSet.BASE, CatanRuleSet.CITIES_AND_KNIGHTS)) { - val options = CatanOptions(playerCount = 3, pointsToWin = 10, ruleSet = rules) + for (rules in listOf(emptySet(), setOf(CatanExpansion.CITIES_AND_KNIGHTS))) { + val options = CatanOptions(playerCount = 3, pointsToWin = 10, expansions = rules) val plain = play(options, 9L, AiProfiles.DEFAULT, 600) val medium = play(options, 9L, AiProfiles(AiDifficulty.MEDIUM, 1234L), 600) assertEquals("$rules", plain, medium) diff --git a/app/src/test/java/org/prolibertate/games/game/catan/CatanCkRulesTest.kt b/app/src/test/java/org/prolibertate/games/game/catan/CatanCkRulesTest.kt index 9fca90a..4241174 100644 --- a/app/src/test/java/org/prolibertate/games/game/catan/CatanCkRulesTest.kt +++ b/app/src/test/java/org/prolibertate/games/game/catan/CatanCkRulesTest.kt @@ -25,7 +25,7 @@ class CatanCkRulesTest { CatanOptions( playerCount = players, pointsToWin = 13, - ruleSet = if (ck) CatanRuleSet.CITIES_AND_KNIGHTS else CatanRuleSet.BASE, + expansions = if (ck) setOf(CatanExpansion.CITIES_AND_KNIGHTS) else emptySet(), ), ), seed = seed, @@ -138,7 +138,7 @@ class CatanCkRulesTest { val base = CatanRules.initialState(config(ck = false)) assertNull(base.ck) assertEquals(25, base.devDeck.size) - assertEquals(CatanRuleSet.BASE, base.options.ruleSet) + assertEquals(emptySet(), base.options.expansions) } @Test @@ -157,7 +157,21 @@ class CatanCkRulesTest { fun `the options without a rule set still read as the base game`() { val old = """{"playerCount":4,"pointsToWin":10}""" val options = Json { ignoreUnknownKeys = true }.decodeFromString(old) - assertEquals(CatanRuleSet.BASE, options.ruleSet) + assertEquals(emptySet(), options.expansions) + } + + @Test + fun `options saved with a rule set still read as the expansion they named`() { + val read = { text: String -> Json { ignoreUnknownKeys = true }.decodeFromString(text) } + assertEquals( + setOf(CatanExpansion.CITIES_AND_KNIGHTS), + read("""{"playerCount":3,"pointsToWin":13,"ruleSet":"CITIES_AND_KNIGHTS"}""").expansions, + ) + assertEquals( + setOf(CatanExpansion.SEAFARERS), + read("""{"playerCount":3,"pointsToWin":12,"ruleSet":"SEAFARERS"}""").expansions, + ) + assertEquals(emptySet(), read("""{"ruleSet":"BASE"}""").expansions) } @Test diff --git a/app/src/test/java/org/prolibertate/games/game/catan/CatanCkSelfPlayTest.kt b/app/src/test/java/org/prolibertate/games/game/catan/CatanCkSelfPlayTest.kt index b11e954..cb17d36 100644 --- a/app/src/test/java/org/prolibertate/games/game/catan/CatanCkSelfPlayTest.kt +++ b/app/src/test/java/org/prolibertate/games/game/catan/CatanCkSelfPlayTest.kt @@ -152,7 +152,7 @@ class CatanCkSelfPlayTest { fun `three engines play Cities and Knights to a finish`() { val reports = (1L..12L).map { seed -> val (state, report) = playOut( - CatanOptions(playerCount = 3, pointsToWin = 13, ruleSet = CatanRuleSet.CITIES_AND_KNIGHTS), + CatanOptions(playerCount = 3, pointsToWin = 13, expansions = setOf(CatanExpansion.CITIES_AND_KNIGHTS)), seed, ) assertEquals("seed $seed", CatanPhase.GAME_OVER, state.phase) @@ -171,7 +171,7 @@ class CatanCkSelfPlayTest { fun `four engines play Cities and Knights to a finish`() { val reports = (101L..108L).map { seed -> val (state, report) = playOut( - CatanOptions(playerCount = 4, pointsToWin = 13, ruleSet = CatanRuleSet.CITIES_AND_KNIGHTS), + CatanOptions(playerCount = 4, pointsToWin = 13, expansions = setOf(CatanExpansion.CITIES_AND_KNIGHTS)), seed, ) assertEquals("seed $seed", CatanPhase.GAME_OVER, state.phase) diff --git a/app/src/test/java/org/prolibertate/games/game/catan/CatanSeafarersTest.kt b/app/src/test/java/org/prolibertate/games/game/catan/CatanSeafarersTest.kt index 770f3af..afcbfeb 100644 --- a/app/src/test/java/org/prolibertate/games/game/catan/CatanSeafarersTest.kt +++ b/app/src/test/java/org/prolibertate/games/game/catan/CatanSeafarersTest.kt @@ -17,7 +17,7 @@ class CatanSeafarersTest { private val json = Json { encodeDefaults = true } private fun options(players: Int = 3) = - CatanOptions(playerCount = players, pointsToWin = 12, ruleSet = CatanRuleSet.SEAFARERS) + CatanOptions(playerCount = players, pointsToWin = 12, expansions = setOf(CatanExpansion.SEAFARERS)) private fun config(options: CatanOptions, seed: Long) = TableConfig( gameId = GameCatalog.CATAN, diff --git a/app/src/test/java/org/prolibertate/games/net/SaveRoundTripAllGamesTest.kt b/app/src/test/java/org/prolibertate/games/net/SaveRoundTripAllGamesTest.kt index 80af138..72db3b4 100644 --- a/app/src/test/java/org/prolibertate/games/net/SaveRoundTripAllGamesTest.kt +++ b/app/src/test/java/org/prolibertate/games/net/SaveRoundTripAllGamesTest.kt @@ -171,7 +171,7 @@ class SaveRoundTripAllGamesTest { GameCatalog.CATAN, CatanRules, CatanAi(), - opts(CatanOptions(3, 13, org.prolibertate.games.game.catan.CatanRuleSet.CITIES_AND_KNIGHTS)), + opts(CatanOptions(3, 13, setOf(org.prolibertate.games.game.catan.CatanExpansion.CITIES_AND_KNIGHTS))), steps = 600, ) } 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 085736f..78a2373 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 @@ -21,7 +21,7 @@ import org.prolibertate.games.game.catan.CatanBoard import org.prolibertate.games.game.catan.CatanMove import org.prolibertate.games.game.catan.CatanOptions import org.prolibertate.games.game.catan.CatanPhase -import org.prolibertate.games.game.catan.CatanRuleSet +import org.prolibertate.games.game.catan.CatanExpansion import org.prolibertate.games.game.catan.CatanRules import org.prolibertate.games.game.catan.CatanState import org.prolibertate.games.game.catan.ChaseRobber @@ -69,7 +69,7 @@ class CatanCkInteractionTest { gameId = GameCatalog.CATAN, seats = (0 until players).map { PlayerSlot(seat = it, name = "P$it", kind = PlayerKind.AI, team = it) }, optionsJson = json.encodeToString( - CatanOptions(players, pointsToWin = 13, ruleSet = CatanRuleSet.CITIES_AND_KNIGHTS), + CatanOptions(players, pointsToWin = 13, expansions = setOf(CatanExpansion.CITIES_AND_KNIGHTS)), ), seed = seed, ) diff --git a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkTextTest.kt b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkTextTest.kt index 8a8d6f7..3ed5400 100644 --- a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkTextTest.kt +++ b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanCkTextTest.kt @@ -8,7 +8,7 @@ import org.junit.Test import org.prolibertate.games.game.GameCatalog import org.prolibertate.games.game.catan.CatanAi import org.prolibertate.games.game.catan.CatanOptions -import org.prolibertate.games.game.catan.CatanRuleSet +import org.prolibertate.games.game.catan.CatanExpansion import org.prolibertate.games.game.catan.CatanRules import org.prolibertate.games.game.catan.ProgressType import org.prolibertate.games.game.engine.PlayerKind @@ -16,7 +16,7 @@ import org.prolibertate.games.game.engine.PlayerSlot import org.prolibertate.games.game.engine.TableConfig import org.prolibertate.games.ui.catanDefaultPoints import org.prolibertate.games.ui.catanPointsChoices -import org.prolibertate.games.ui.catanSwitchRules +import org.prolibertate.games.ui.catanToggleExpansion class CatanCkTextTest { @@ -28,7 +28,7 @@ class CatanCkTextTest { gameId = GameCatalog.CATAN, seats = (0 until 3).map { PlayerSlot(seat = it, name = "P$it", kind = PlayerKind.AI, team = it) }, optionsJson = json.encodeToString( - CatanOptions(3, 13, CatanRuleSet.CITIES_AND_KNIGHTS), + CatanOptions(3, 13, setOf(CatanExpansion.CITIES_AND_KNIGHTS)), ), seed = 31L, ) @@ -56,20 +56,24 @@ class CatanCkTextTest { @Test fun `the setup screen offers each rule set its own targets and switches sensibly`() { - assertEquals(listOf(5, 8, 10, 12), catanPointsChoices(CatanRuleSet.BASE)) - assertEquals(listOf(8, 10, 13, 15), catanPointsChoices(CatanRuleSet.CITIES_AND_KNIGHTS)) - assertEquals(10, catanDefaultPoints(CatanRuleSet.BASE)) - assertEquals(13, catanDefaultPoints(CatanRuleSet.CITIES_AND_KNIGHTS)) + assertEquals(listOf(5, 8, 10, 12), catanPointsChoices(emptySet())) + assertEquals(listOf(8, 10, 13, 15), catanPointsChoices(setOf(CatanExpansion.CITIES_AND_KNIGHTS))) + assertEquals(10, catanDefaultPoints(emptySet())) + assertEquals(13, catanDefaultPoints(setOf(CatanExpansion.CITIES_AND_KNIGHTS))) val base = CatanOptions() - val ck = catanSwitchRules(base, CatanRuleSet.CITIES_AND_KNIGHTS) - assertEquals(CatanRuleSet.CITIES_AND_KNIGHTS, ck.ruleSet) + val ck = catanToggleExpansion(base, CatanExpansion.CITIES_AND_KNIGHTS, true) + assertEquals(setOf(CatanExpansion.CITIES_AND_KNIGHTS), ck.expansions) assertEquals("the published target for the new rules", 13, ck.pointsToWin) - assertEquals(10, catanSwitchRules(ck, CatanRuleSet.BASE).pointsToWin) + assertEquals(10, catanToggleExpansion(ck, CatanExpansion.CITIES_AND_KNIGHTS, false).pointsToWin) // A short game picked on purpose is kept when both menus have it. - val short = catanSwitchRules(base.copy(pointsToWin = 8), CatanRuleSet.CITIES_AND_KNIGHTS) + val short = catanToggleExpansion(base.copy(pointsToWin = 8), CatanExpansion.CITIES_AND_KNIGHTS, true) assertEquals(8, short.pointsToWin) assertEquals(3, ck.playerCount) + + // Expansions cannot be combined yet: switching one on switches the other off. + val sea = catanToggleExpansion(ck, CatanExpansion.SEAFARERS, true) + assertEquals(setOf(CatanExpansion.SEAFARERS), sea.expansions) } } diff --git a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanSeafarersInteractionTest.kt b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanSeafarersInteractionTest.kt index bca9a79..b2bfc3b 100644 --- a/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanSeafarersInteractionTest.kt +++ b/app/src/test/java/org/prolibertate/games/ui/game/catan/CatanSeafarersInteractionTest.kt @@ -12,7 +12,7 @@ import org.prolibertate.games.game.catan.CatanAi import org.prolibertate.games.game.catan.CatanBuilding import org.prolibertate.games.game.catan.CatanOptions import org.prolibertate.games.game.catan.CatanPhase -import org.prolibertate.games.game.catan.CatanRuleSet +import org.prolibertate.games.game.catan.CatanExpansion import org.prolibertate.games.game.catan.CatanRules import org.prolibertate.games.game.catan.CatanState import org.prolibertate.games.game.catan.MovePirate @@ -29,7 +29,7 @@ import org.prolibertate.games.game.engine.TableConfig class CatanSeafarersInteractionTest { private fun fresh(): CatanState { - val options = CatanOptions(playerCount = 3, pointsToWin = 12, ruleSet = CatanRuleSet.SEAFARERS) + val options = CatanOptions(playerCount = 3, pointsToWin = 12, expansions = setOf(CatanExpansion.SEAFARERS)) return CatanRules.initialState( TableConfig( gameId = GameCatalog.CATAN, diff --git a/docs/RULES-catan-seafarers.md b/docs/RULES-catan-seafarers.md index e6ea2ac..e4f5941 100644 --- a/docs/RULES-catan-seafarers.md +++ b/docs/RULES-catan-seafarers.md @@ -1,10 +1,10 @@ # Katan: Seafarers -The Seafarers rule set is chosen at setup next to *Base game* and *Cities & Knights*. It is the base +Seafarers is an expansion switched on at setup (`CatanOptions.expansions`; the old single `ruleSet` in saves is still read) and its map is chosen beside it (`CatanOptions.scenario`). It is the base game ([RULES-catan.md](RULES-catan.md)) with a sea, ships, a pirate, gold fields and islands to settle. Code: `game/catan/CatanSeaRules.kt` (the additions), `CatanSeaBoard.kt` (the map), `CatanBoard.kt` (`Topology`, the geometry shared by every map), state behind `CatanState.sea` -(null in the other rule sets). It cannot be combined with Cities & Knights. +(null in the other rule sets). It cannot yet be combined with Cities & Knights; the setup screen switches one off when the other is turned on. Where the published rules leave room, or where this game differs, it says so below. -- 2.47.3