From 14dbbceffa6194d60790ec38bb759d07c5acbdea Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 22 Apr 2024 04:34:12 +0300 Subject: [PATCH 01/38] feat(plugin24): repeat gameplay XML protocol changes This reverts commit 4d3a95da36c629f0342514daee963606b9c11b73. --- plugin/src/main/kotlin/sc/plugin2024/Move.kt | 5 +++-- .../src/main/kotlin/sc/plugin2024/Segment.kt | 2 +- .../test/kotlin/sc/plugin2024/BoardTest.kt | 20 ++++++++--------- .../kotlin/sc/plugin2024/GameStateTest.kt | 6 ++--- .../src/test/kotlin/sc/plugin2024/MoveTest.kt | 22 +++++++------------ 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2024/Move.kt b/plugin/src/main/kotlin/sc/plugin2024/Move.kt index 6335373b3..847278876 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Move.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/Move.kt @@ -1,6 +1,7 @@ package sc.plugin2024 import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.IMove @XStreamAlias("move") @@ -12,11 +13,11 @@ import sc.api.plugins.IMove * @property actions The list of actions in the move. */ data class Move( - //@XStreamImplicit + @XStreamImplicit val actions: List, ): IMove, Comparable { - constructor(vararg actions: Action) : this(ArrayList(actions.asList())) + constructor(vararg actions: Action) : this(actions.asList()) /** * Compares this Move instance with the specified Move for order. diff --git a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt b/plugin/src/main/kotlin/sc/plugin2024/Segment.kt index 571e6fba2..09eba46e7 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/Segment.kt @@ -38,7 +38,7 @@ data class Segment( @XStreamAsAttribute val direction: CubeDirection, // could be omitted but helpful since ships also come with cubecoords @XStreamOmitField val center: CubeCoordinates, - @XStreamImplicit/*(itemFieldName = "column")*/ val fields: SegmentFields, + @XStreamImplicit(itemFieldName = "column") val fields: SegmentFields, ): PublicCloneable { val tip: CubeCoordinates diff --git a/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt b/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt index 2fe031449..986d0240b 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt @@ -167,34 +167,34 @@ class BoardTest: FunSpec({ val serializedSegment = """
- + - - + + - - + + - - + + - + """ val serialized = """ $serializedSegment @@ -235,13 +235,13 @@ class BoardTest: FunSpec({
- + - + """ } diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt index 5034a65b3..c2d7d6876 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt @@ -338,10 +338,8 @@ class GameStateTest: FunSpec({ - - - - + + """ } diff --git a/plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt index 3094bcc05..c15ad47c5 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt @@ -16,22 +16,18 @@ class MoveTest: FunSpec({ test("deserializes gracefully") { testXStream.fromXML(""" - - - - + + """.trimIndent()) shouldBe Move(Accelerate(1), Advance(2)) } test("serializes well") { Move(Accelerate(2), Advance(2), Turn(CubeDirection.UP_LEFT), Advance(1), Push(CubeDirection.LEFT)) shouldSerializeTo """ - - - - - - - + + + + + """.trimIndent() } @@ -39,9 +35,7 @@ class MoveTest: FunSpec({ RoomPacket("lol", Move(Advance(1))) shouldSerializeTo """ - - - + """.trimIndent() From b44a547d71dec6334b50df1b6c60f11cef789cfd Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 22 Apr 2024 04:37:21 +0300 Subject: [PATCH 02/38] fix(plugin24): implement post-finale changes --- CHANGELOG.md | 2 +- plugin/src/main/kotlin/sc/plugin2024/GameState.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2e17d3d..7f6df1b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The version should always be in sync with the [GUI](https://github.com/software- ### 25.0.0 - 2024 - Allow other player to move on when one is disqualified -### 24.X - Post-Finale +### 24 Post-Finale - Allow one player to move on when other is stuck or finished (add points depending on speed of reaching goal, do not require passengers?) - Improve XML protocol diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt index b9ff219b7..9c193f56e 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt @@ -417,9 +417,7 @@ data class GameState @JvmOverloads constructor( // Bedingung 4: das Rundenlimit von 30 Runden ist erreicht turn / 2 >= PluginConstants.ROUND_LIMIT -> true // Bedingung 5: beide Spieler können sich nicht mehr bewegen - // lastMove == null && !canMove() -> true - // Bedingung 5: Ein Spieler kann sich nicht mehr bewegen - ships.any { it.stuck } -> true + lastMove == null && !canMove() -> true // ansonsten geht das Spiel weiter else -> false } From 39f08bcb749906cf626786181cff8d05c9fcd5d5 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 22 Apr 2024 04:37:21 +0300 Subject: [PATCH 03/38] fix(plugin24): implement post-finale updates to game mechanics --- .../test/kotlin/sc/plugin2024/GameResultTest.kt | 16 ++++++++-------- .../test/kotlin/sc/plugin2024/GameStateTest.kt | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt index d6ed192b9..8ac0870fd 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt @@ -172,34 +172,34 @@ class GameResultTest: WordSpec({
- + - - + + - - + + - - + + - + """.trimIndent() diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt index c2d7d6876..4763badaf 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt @@ -279,7 +279,7 @@ class GameStateTest: FunSpec({ it.coal = 0 } gameState.performMoveDirectly(Move(Advance(1))) - gameState.isOver shouldBe true //false + gameState.isOver shouldBe false gameState.turn shouldBe 2 gameState.board.segments.first().fields[2][1] = Field.ISLAND gameState.ships.forEach { From 7843024b4490678cb62639dc78278808a645ffb7 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 22 Apr 2024 11:36:26 +0300 Subject: [PATCH 04/38] refactor: generify Game instance from plugin to sdk --- plugin/src/main/kotlin/sc/plugin2023/Game.kt | 31 --------------- .../kotlin/sc/plugin2023/util/GamePlugin.kt | 11 ++++-- plugin/src/main/kotlin/sc/plugin2024/Game.kt | 39 ------------------- .../kotlin/sc/plugin2024/util/GamePlugin.kt | 11 ++++-- plugin/src/test/kotlin/sc/GamePlayTest.kt | 2 +- .../kotlin/sc/plugin2024/GameResultTest.kt | 6 ++- .../server-api/sc/api/plugins/IGamePlugin.kt | 10 +++-- .../plugins/{AbstractGame.kt => Game.kt} | 26 ++++++++++++- .../sc/networking/clients/XStreamClient.java | 3 +- .../server-api/sc/protocol/LobbyProtocol.kt | 1 + .../test/java/sc/server/plugins/TestGame.kt | 2 +- .../test/java/sc/server/plugins/TestPlugin.kt | 4 +- 12 files changed, 56 insertions(+), 90 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2023/Game.kt delete mode 100644 plugin/src/main/kotlin/sc/plugin2024/Game.kt rename sdk/src/main/server-api/sc/framework/plugins/{AbstractGame.kt => Game.kt} (90%) diff --git a/plugin/src/main/kotlin/sc/plugin2023/Game.kt b/plugin/src/main/kotlin/sc/plugin2023/Game.kt deleted file mode 100644 index 72991b047..000000000 --- a/plugin/src/main/kotlin/sc/plugin2023/Game.kt +++ /dev/null @@ -1,31 +0,0 @@ -package sc.plugin2023 - -import sc.api.plugins.IMove -import sc.framework.plugins.AbstractGame -import sc.shared.MoveMistake -import sc.plugin2023.util.GamePlugin -import sc.shared.InvalidMoveException - -class Game(override val currentState: GameState = GameState()): AbstractGame(GamePlugin()) { - val isGameOver: Boolean - get() = currentState.isOver - - override fun onRoundBasedAction(move: IMove) { - if (move !is Move) - throw InvalidMoveException(MoveMistake.INVALID_FORMAT) - - logger.debug("Performing {}", move) - currentState.performMoveDirectly(move) - logger.debug("Current State: {}", currentState.longString()) - } - - override fun toString(): String = - "Game(${ - when { - isGameOver -> "OVER, " - isPaused -> "PAUSED, " - else -> "" - } - }players=$players, gameState=$currentState)" - -} diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt index e767f34d0..5fa1ff3c6 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt @@ -3,14 +3,15 @@ package sc.plugin2023.util import sc.api.plugins.IGameInstance import sc.api.plugins.IGamePlugin import sc.api.plugins.IGameState -import sc.plugin2023.Game +import sc.framework.plugins.TwoPlayerGame import sc.plugin2023.GameState +import sc.plugin2023.Move import sc.shared.ScoreAggregation import sc.shared.ScoreDefinition import sc.shared.ScoreFragment import sc.shared.WinReason -class GamePlugin: IGamePlugin { +class GamePlugin: IGamePlugin { companion object { const val PLUGIN_ID = "swc_2023_penguins" val scoreDefinition: ScoreDefinition = @@ -28,10 +29,12 @@ class GamePlugin: IGamePlugin { override val turnLimit: Int = PluginConstants.BOARD_SIZE * PluginConstants.BOARD_SIZE + override val moveClass: Class = Move::class.java + override fun createGame(): IGameInstance = - Game() + TwoPlayerGame(this, GameState()) override fun createGameFromState(state: IGameState): IGameInstance = - Game(state as GameState) + TwoPlayerGame(this, state as GameState) } diff --git a/plugin/src/main/kotlin/sc/plugin2024/Game.kt b/plugin/src/main/kotlin/sc/plugin2024/Game.kt deleted file mode 100644 index 14d7298d5..000000000 --- a/plugin/src/main/kotlin/sc/plugin2024/Game.kt +++ /dev/null @@ -1,39 +0,0 @@ -package sc.plugin2024 - -import sc.api.plugins.IMove -import sc.framework.plugins.AbstractGame -import sc.shared.MoveMistake -import sc.plugin2024.util.GamePlugin -import sc.shared.InvalidMoveException - -fun Collection.maxByNoEqual(selector: (T) -> Int): T? = - fold(Int.MIN_VALUE to (null as T?)) { acc, pos -> - val value = selector(pos) - when { - value > acc.first -> value to pos - value == acc.first -> value to null - else -> acc - } - }.second - -class Game(override val currentState: GameState = GameState()): AbstractGame(GamePlugin()) { - - override fun onRoundBasedAction(move: IMove) { - if(move !is Move) - throw InvalidMoveException(MoveMistake.INVALID_FORMAT) - - logger.debug("Performing {}", move) - currentState.performMoveDirectly(move) - logger.debug("Current State: ${currentState.longString()}") - } - - override fun toString(): String = - "Game(${ - when { - currentState.isOver -> "OVER, " - isPaused -> "PAUSED, " - else -> "" - } - }players=$players, gameState=$currentState)" - -} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt index c966c9a81..e1e178e11 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt @@ -4,8 +4,9 @@ import com.thoughtworks.xstream.annotations.XStreamAlias import sc.api.plugins.IGameInstance import sc.api.plugins.IGamePlugin import sc.api.plugins.IGameState -import sc.plugin2024.Game +import sc.framework.plugins.TwoPlayerGame import sc.plugin2024.GameState +import sc.plugin2024.Move import sc.shared.* @XStreamAlias(value = "winreason") @@ -17,7 +18,7 @@ enum class MQWinReason(override val message: String, override val isRegular: Boo STUCK("%s kann sich nicht mehr bewegen.", false); } -class GamePlugin: IGamePlugin { +class GamePlugin: IGamePlugin { companion object { const val PLUGIN_ID = "swc_2024_mississippi_queen" val scoreDefinition: ScoreDefinition = @@ -36,10 +37,12 @@ class GamePlugin: IGamePlugin { override val turnLimit: Int = PluginConstants.ROUND_LIMIT * 2 + override val moveClass = Move::class.java + override fun createGame(): IGameInstance = - Game() + TwoPlayerGame(this, GameState()) override fun createGameFromState(state: IGameState): IGameInstance = - Game(state as GameState) + TwoPlayerGame(this, state as GameState) } diff --git a/plugin/src/test/kotlin/sc/GamePlayTest.kt b/plugin/src/test/kotlin/sc/GamePlayTest.kt index 34bf36eb4..9959af79b 100644 --- a/plugin/src/test/kotlin/sc/GamePlayTest.kt +++ b/plugin/src/test/kotlin/sc/GamePlayTest.kt @@ -27,7 +27,7 @@ class GamePlayTest: WordSpec({ val logger = LoggerFactory.getLogger(GamePlayTest::class.java) isolationMode = IsolationMode.SingleInstance val plugin = IGamePlugin.loadPlugin() - fun createGame() = plugin.createGame() as AbstractGame + fun createGame() = plugin.createGame() as AbstractGame<*> "A Game" should { val game = createGame() "let players join" { diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt index 8ac0870fd..c41938006 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt @@ -5,12 +5,14 @@ import io.kotest.matchers.* import sc.api.plugins.CubeCoordinates import sc.api.plugins.CubeDirection import sc.api.plugins.Team +import sc.framework.plugins.TwoPlayerGame import sc.helpers.shouldSerializeTo import sc.helpers.testXStream import sc.plugin2024.actions.Accelerate import sc.plugin2024.actions.Advance import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.MoveMistake +import sc.plugin2024.util.GamePlugin import sc.shared.InvalidMoveException import sc.shared.Violation import sc.shared.WinCondition @@ -18,7 +20,7 @@ import sc.shared.WinReasonTie class GameResultTest: WordSpec({ "Result XML" should { - val game = Game() + val game = TwoPlayerGame(GamePlugin(), GameState()) "work when empty" { game.getResult() shouldSerializeTo """ @@ -204,7 +206,7 @@ class GameResultTest: WordSpec({ """.trimIndent() val state = GameState(testXStream.fromXML(boardXML) as Board) - val game = Game(state) + val game = TwoPlayerGame(GamePlugin(), state) game.onPlayerJoined() game.onPlayerJoined() "be correct on finish" { diff --git a/sdk/src/main/server-api/sc/api/plugins/IGamePlugin.kt b/sdk/src/main/server-api/sc/api/plugins/IGamePlugin.kt index 878d03cc9..58d27fcc7 100644 --- a/sdk/src/main/server-api/sc/api/plugins/IGamePlugin.kt +++ b/sdk/src/main/server-api/sc/api/plugins/IGamePlugin.kt @@ -5,7 +5,7 @@ import sc.framework.plugins.Constants import sc.shared.ScoreDefinition import java.util.ServiceLoader -interface IGamePlugin { +interface IGamePlugin { /** Plugin identifier for the protocol. */ val id: String /** Arrangement of ScoreFragments in the GameResult. */ @@ -19,6 +19,8 @@ interface IGamePlugin { val gameTimeout get() = turnLimit * Constants.SOFT_TIMEOUT + val moveClass: Class + /** @return ein neues Spiel. */ fun createGame(): IGameInstance /** @return ein neues Spiel mit dem gegebenen GameState. */ @@ -26,7 +28,7 @@ interface IGamePlugin { companion object { @JvmStatic - fun loadPlugins(): Iterator = + fun loadPlugins(): Iterator> = ServiceLoader.load(IGamePlugin::class.java).iterator().takeIf { it.hasNext() } ?: throw PluginLoaderException("Could not find any game plugin") @@ -34,13 +36,13 @@ interface IGamePlugin { /** @param gameType id of the plugin, if null return any * @return The plugin with an id equal to [gameType]. */ @JvmStatic - fun loadPlugin(gameType: String?): IGamePlugin = + fun loadPlugin(gameType: String?): IGamePlugin<*> = loadPlugins().asSequence().find { gameType == null || it.id == gameType } ?: throw PluginLoaderException("Could not find game of type '$gameType'") @JvmStatic - fun loadPlugin(): IGamePlugin = + fun loadPlugin(): IGamePlugin<*> = loadPlugins().next() @JvmStatic diff --git a/sdk/src/main/server-api/sc/framework/plugins/AbstractGame.kt b/sdk/src/main/server-api/sc/framework/plugins/Game.kt similarity index 90% rename from sdk/src/main/server-api/sc/framework/plugins/AbstractGame.kt rename to sdk/src/main/server-api/sc/framework/plugins/Game.kt index 71851d3d5..16691a1af 100644 --- a/sdk/src/main/server-api/sc/framework/plugins/AbstractGame.kt +++ b/sdk/src/main/server-api/sc/framework/plugins/Game.kt @@ -19,8 +19,20 @@ fun Iterable.maxByNoEqual(selector: (T) -> Int): T? = } }.second -abstract class AbstractGame(val plugin: IGamePlugin): IGameInstance, Pausable { - val logger = LoggerFactory.getLogger(this::class.java) + +class TwoPlayerGame>(plugin: IGamePlugin, override val currentState: GameState): AbstractGame(plugin) { + override fun onRoundBasedAction(move: IMove) { + if(!plugin.moveClass.isInstance(move)) + throw InvalidMoveException(MoveMistake.INVALID_FORMAT) + + logger.debug("Performing {}", move) + currentState.performMoveDirectly(plugin.moveClass.cast(move)) + logger.debug("Current State: ${currentState.longString()}") + } +} + +abstract class AbstractGame(protected val plugin: IGamePlugin): IGameInstance, Pausable { + protected val logger = LoggerFactory.getLogger(this::class.java) override val pluginUUID: String = plugin.id @@ -247,4 +259,14 @@ abstract class AbstractGame(val plugin: IGamePlugin): IGameInstance, Pausable { } return GameResult(plugin.scoreDefinition, scores, winCondition) } + + override fun toString(): String = + "Game(${ + when { + currentState.isOver -> "OVER, " + isPaused -> "PAUSED, " + else -> "" + } + }players=$players, gameState=$currentState)" + } \ No newline at end of file diff --git a/sdk/src/main/server-api/sc/networking/clients/XStreamClient.java b/sdk/src/main/server-api/sc/networking/clients/XStreamClient.java index 1ef699766..0ad0e05ee 100644 --- a/sdk/src/main/server-api/sc/networking/clients/XStreamClient.java +++ b/sdk/src/main/server-api/sc/networking/clients/XStreamClient.java @@ -120,7 +120,8 @@ public void receiveThread() { } } catch (EOFException e) { logger.info("End of input reached, disconnecting {}", this); - logger.trace("Disconnected with", e); + logger.trace("{} disconnected with {}", this.getClass().getSimpleName(), e.toString(), e); + handleDisconnect(DisconnectCause.RECEIVED_DISCONNECT); } catch (IOException e) { // The other side closed the connection. // It is better when the other side sends a CloseConnection message beforehand, diff --git a/sdk/src/main/server-api/sc/protocol/LobbyProtocol.kt b/sdk/src/main/server-api/sc/protocol/LobbyProtocol.kt index f0159867a..d1b23268f 100644 --- a/sdk/src/main/server-api/sc/protocol/LobbyProtocol.kt +++ b/sdk/src/main/server-api/sc/protocol/LobbyProtocol.kt @@ -15,6 +15,7 @@ object LobbyProtocol { registerAdditionalMessages(xStream, listOf( AuthenticateRequest::class.java, CancelRequest::class.java, + CloseConnection::class.java, JoinGameRequest::class.java, JoinPreparedRoomRequest::class.java, JoinRoomRequest::class.java, diff --git a/server/src/test/java/sc/server/plugins/TestGame.kt b/server/src/test/java/sc/server/plugins/TestGame.kt index 22363eb4d..0182a4377 100644 --- a/server/src/test/java/sc/server/plugins/TestGame.kt +++ b/server/src/test/java/sc/server/plugins/TestGame.kt @@ -11,7 +11,7 @@ import sc.shared.* data class TestGame( override val currentState: TestGameState = TestGameState(), -): AbstractGame(TestPlugin()) { +): AbstractGame(TestPlugin()) { override fun onRoundBasedAction(move: IMove) { if(move !is TestMove) diff --git a/server/src/test/java/sc/server/plugins/TestPlugin.kt b/server/src/test/java/sc/server/plugins/TestPlugin.kt index d52067de8..ddec1cdd1 100644 --- a/server/src/test/java/sc/server/plugins/TestPlugin.kt +++ b/server/src/test/java/sc/server/plugins/TestPlugin.kt @@ -5,7 +5,7 @@ import sc.api.plugins.IGamePlugin import sc.api.plugins.IGameState import sc.shared.ScoreDefinition -class TestPlugin: IGamePlugin { +class TestPlugin: IGamePlugin { companion object { const val TEST_PLUGIN_UUID = "012345-norris" } @@ -18,6 +18,8 @@ class TestPlugin: IGamePlugin { override val turnLimit get() = throw NotImplementedError() + override val moveClass: Class = TestMove::class.java + override fun createGame(): IGameInstance = TestGame() From bd141910d5f4cb322d08cfdab73ce887f8d029a8 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Wed, 24 Apr 2024 21:53:20 +0300 Subject: [PATCH 05/38] feat(plugin25): add and partially convert HuI --- .dev/scopes.txt | 2 + plugin/src/main/kotlin/sc/plugin2023/Board.kt | 2 +- .../main/kotlin/sc/plugin2023/GameState.kt | 10 +- .../kotlin/sc/plugin2023/util/Constants.kt | 2 +- .../kotlin/sc/plugin2023/util/GamePlugin.kt | 2 +- .../main/kotlin/sc/plugin2024/GameState.kt | 16 +- .../src/main/kotlin/sc/plugin2024/Segment.kt | 20 +- plugin/src/main/kotlin/sc/plugin2024/Ship.kt | 30 +- .../kotlin/sc/plugin2024/actions/Advance.kt | 6 +- .../plugin2024/mistake/AccelerationProblem.kt | 6 +- .../sc/plugin2024/mistake/MoveMistake.kt | 2 +- .../kotlin/sc/plugin2024/util/Constants.kt | 2 +- .../kotlin/sc/plugin2024/util/GamePlugin.kt | 2 +- .../plugin2024/{ => util}/XStreamClasses.kt | 3 +- .../src/main/kotlin/sc/plugin2025/Action.kt | 10 + .../main/kotlin/sc/plugin2025/Advance.java | 72 ++ plugin/src/main/kotlin/sc/plugin2025/Board.kt | 167 ++++ .../src/main/kotlin/sc/plugin2025/Card.java | 132 ++++ plugin/src/main/kotlin/sc/plugin2025/Card.kt | 32 + .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 32 + .../kotlin/sc/plugin2025/ExchangeCarrots.java | 60 ++ .../main/kotlin/sc/plugin2025/FallBack.java | 49 ++ plugin/src/main/kotlin/sc/plugin2025/Field.kt | 27 + .../kotlin/sc/plugin2025/GameRuleLogic.kt | 400 ++++++++++ .../main/kotlin/sc/plugin2025/GameState.java | 707 +++++++++++++++++ .../main/kotlin/sc/plugin2025/GameState.kt | 78 ++ plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 29 + plugin/src/main/kotlin/sc/plugin2025/Move.kt | 46 ++ .../main/kotlin/sc/plugin2025/MoveMistake.kt | 9 + .../src/main/kotlin/sc/plugin2025/Player.java | 309 ++++++++ .../src/main/kotlin/sc/plugin2025/Skip.java | 49 ++ .../kotlin/sc/plugin2025/util/Constants.kt | 9 + .../kotlin/sc/plugin2025/util/GamePlugin.kt | 46 ++ .../sc/plugin2025/util/XStreamClasses.kt | 13 + .../services/sc.networking.XStreamProvider | 2 +- .../test/kotlin/sc/plugin2023/BoardTest.kt | 12 +- .../kotlin/sc/plugin2023/GameStateTest.kt | 10 +- .../test/kotlin/sc/plugin2024/BoardTest.kt | 10 +- .../kotlin/sc/plugin2024/GameStateTest.kt | 6 +- .../test/kotlin/sc/plugin2024/SegmentTest.kt | 4 +- .../kotlin/sc/plugin2025/GamePlayTest.java | 746 ++++++++++++++++++ .../test/kotlin/sc/plugin2025/PlayerTest.java | 34 + .../sc/plugin2025/util/TestGameUtil.java | 35 + .../main/framework/sc/shared/MoveIterator.kt | 33 + 44 files changed, 3200 insertions(+), 73 deletions(-) rename plugin/src/main/kotlin/sc/plugin2024/{ => util}/XStreamClasses.kt (94%) create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Action.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Advance.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Board.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Card.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Card.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/FallBack.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Field.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/GameState.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/GameState.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Hare.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Move.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Player.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/Skip.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt create mode 100644 plugin/src/test/kotlin/sc/plugin2025/GamePlayTest.java create mode 100644 plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java create mode 100644 plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java create mode 100644 sdk/src/main/framework/sc/shared/MoveIterator.kt diff --git a/.dev/scopes.txt b/.dev/scopes.txt index e090cfd4f..471fcfa48 100644 --- a/.dev/scopes.txt +++ b/.dev/scopes.txt @@ -15,6 +15,8 @@ server plugin plugin23 plugin24 +plugin25 +plugin26 gamerules sdk diff --git a/plugin/src/main/kotlin/sc/plugin2023/Board.kt b/plugin/src/main/kotlin/sc/plugin2023/Board.kt index b4485b0c0..97fb69305 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/Board.kt @@ -3,7 +3,7 @@ package sc.plugin2023 import com.thoughtworks.xstream.annotations.XStreamAlias import sc.api.plugins.* import kotlin.random.Random -import sc.plugin2023.util.PluginConstants as Constants +import sc.plugin2023.util.PenguinConstants as Constants /** * Klasse welche eine Spielbrett darstellt. Bestehend aus einem diff --git a/plugin/src/main/kotlin/sc/plugin2023/GameState.kt b/plugin/src/main/kotlin/sc/plugin2023/GameState.kt index 804e4272c..2143bd95f 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/GameState.kt @@ -7,7 +7,7 @@ import sc.api.plugins.ITeam import sc.api.plugins.Team import sc.api.plugins.TwoPlayerGameState import sc.plugin2023.util.PenguinMoveMistake -import sc.plugin2023.util.PluginConstants +import sc.plugin2023.util.PenguinConstants import sc.shared.InvalidMoveException import sc.shared.MoveMistake @@ -37,7 +37,7 @@ data class GameState @JvmOverloads constructor( if(move.from != null) { if(board[move.from].penguin != currentTeam) throw InvalidMoveException(MoveMistake.WRONG_COLOR, move) - if(currentPieces.size < PluginConstants.PENGUINS) + if(currentPieces.size < PenguinConstants.PENGUINS) throw InvalidMoveException(PenguinMoveMistake.PENGUINS, move) if(!move.to.minus(move.from).straightHex) throw InvalidMoveException(MoveMistake.INVALID_MOVE, move) @@ -46,7 +46,7 @@ data class GameState @JvmOverloads constructor( throw InvalidMoveException(MoveMistake.INVALID_MOVE, move) board[move.from] = null } else { - if(currentPieces.size >= PluginConstants.PENGUINS) + if(currentPieces.size >= PenguinConstants.PENGUINS) throw InvalidMoveException(PenguinMoveMistake.MAX_PENGUINS, move) if(board[move.to].fish != 1) throw InvalidMoveException(PenguinMoveMistake.SINGLE_FISH, move) @@ -60,7 +60,7 @@ data class GameState @JvmOverloads constructor( get() = board.filterValues { it.penguin == currentTeam } val penguinsPlaced - get() = currentPieces.size == PluginConstants.PENGUINS + get() = currentPieces.size == PenguinConstants.PENGUINS override fun getSensibleMoves(): List = if(penguinsPlaced) { @@ -77,7 +77,7 @@ data class GameState @JvmOverloads constructor( fun immovable(team: Team? = null) = board.getPenguins() .filter { team == null || it.second == team } - .takeIf { it.size == PluginConstants.PENGUINS * (if(team == null) Team.values().size else 1) } + .takeIf { it.size == PenguinConstants.PENGUINS * (if(team == null) Team.values().size else 1) } ?.all { pair -> pair.first.hexNeighbors.all { board.getOrEmpty(it).fish == 0 } } ?: false override val isOver: Boolean diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt b/plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt index a8721f38d..c38403629 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt @@ -1,7 +1,7 @@ package sc.plugin2023.util /** Eine Sammlung an verschiedenen Konstanten, die im Spiel verwendet werden. */ -object PluginConstants { +object PenguinConstants { /** Seitenlänge des quadratischen Spielfelds als Anzahl der Felder. */ const val BOARD_SIZE = 8 diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt index 5fa1ff3c6..efc03998d 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt @@ -27,7 +27,7 @@ class GamePlugin: IGamePlugin { Companion.scoreDefinition override val turnLimit: Int = - PluginConstants.BOARD_SIZE * PluginConstants.BOARD_SIZE + PenguinConstants.BOARD_SIZE * PenguinConstants.BOARD_SIZE override val moveClass: Class = Move::class.java diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt index 9c193f56e..5e89fcb3e 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt @@ -11,8 +11,8 @@ import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.AdvanceProblem import sc.plugin2024.mistake.MoveMistake import sc.plugin2024.util.MQWinReason -import sc.plugin2024.util.PluginConstants -import sc.plugin2024.util.PluginConstants.POINTS_PER_SEGMENT +import sc.plugin2024.util.MQConstants +import sc.plugin2024.util.MQConstants.POINTS_PER_SEGMENT import sc.shared.InvalidMoveException import sc.shared.WinCondition import kotlin.math.absoluteValue @@ -79,7 +79,7 @@ data class GameState @JvmOverloads constructor( } fun calculatePoints(ship: Ship) = - shipAdvancePoints(ship) + ship.passengers * PluginConstants.POINTS_PER_PASSENGER + shipAdvancePoints(ship) + ship.passengers * MQConstants.POINTS_PER_PASSENGER fun isCurrentShipOnCurrent() = board.doesFieldHaveCurrent(currentShip.position) @@ -338,7 +338,7 @@ data class GameState @JvmOverloads constructor( var currentPosition = start var totalCost = 0 var hasCurrent = false - val maxMovement = maxMovementPoints.coerceIn(0, PluginConstants.MAX_SPEED) + val maxMovement = maxMovementPoints.coerceIn(0, MQConstants.MAX_SPEED) val result = ArrayList(maxMovement) fun result(condition: AdvanceProblem) = @@ -386,8 +386,8 @@ data class GameState @JvmOverloads constructor( return (1..maxCoal + currentShip.freeAcc).flatMap { i -> listOfNotNull( - Accelerate(i).takeIf { PluginConstants.MAX_SPEED >= currentShip.speed + i }, - Accelerate(-i).takeIf { PluginConstants.MIN_SPEED <= currentShip.speed - i } + Accelerate(i).takeIf { MQConstants.MAX_SPEED >= currentShip.speed + i }, + Accelerate(-i).takeIf { MQConstants.MIN_SPEED <= currentShip.speed - i } ) } } @@ -415,7 +415,7 @@ data class GameState @JvmOverloads constructor( // Bedingung 3: am Ende einer Runde liegt ein Dampfer mehr als 3 Spielsegmente zurück board.segmentDistance(ships.first().position, ships.last().position).absoluteValue > 3 -> true // Bedingung 4: das Rundenlimit von 30 Runden ist erreicht - turn / 2 >= PluginConstants.ROUND_LIMIT -> true + turn / 2 >= MQConstants.ROUND_LIMIT -> true // Bedingung 5: beide Spieler können sich nicht mehr bewegen lastMove == null && !canMove() -> true // ansonsten geht das Spiel weiter @@ -435,7 +435,7 @@ data class GameState @JvmOverloads constructor( override fun getPointsForTeamExtended(team: ITeam): IntArray = ships[team.index].let { ship -> - intArrayOf(*getPointsForTeam(team), ship.coal * 2, if(inGoal(ship)) PluginConstants.FINISH_POINTS else 0) + intArrayOf(*getPointsForTeam(team), ship.coal * 2, if(inGoal(ship)) MQConstants.FINISH_POINTS else 0) } override fun teamStats(team: ITeam): List> = diff --git a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt b/plugin/src/main/kotlin/sc/plugin2024/Segment.kt index 09eba46e7..c48db7b6e 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/Segment.kt @@ -9,7 +9,7 @@ import sc.api.plugins.CubeDirection import sc.api.plugins.deepCopy import sc.framework.PublicCloneable import sc.framework.shuffledIndices -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants import kotlin.math.absoluteValue import kotlin.random.Random import kotlin.random.nextInt @@ -92,7 +92,7 @@ data class Segment( companion object { fun inDirection(previousCenter: CubeCoordinates, direction: CubeDirection, fields: SegmentFields) = - Segment(direction, previousCenter + direction.vector * PluginConstants.SEGMENT_FIELDS_WIDTH, fields) + Segment(direction, previousCenter + direction.vector * MQConstants.SEGMENT_FIELDS_WIDTH, fields) fun empty(center: CubeCoordinates = CubeCoordinates.ORIGIN) = Segment(CubeDirection.RIGHT, center, generateSegment(false, arrayOf())) @@ -143,7 +143,7 @@ internal fun generateSegment( end: Boolean, fieldsToPlace: Array, ): SegmentFields { - val fields: SegmentFields = Array(PluginConstants.SEGMENT_FIELDS_WIDTH) { Array(PluginConstants.SEGMENT_FIELDS_HEIGHT) { Field.WATER } } + val fields: SegmentFields = Array(MQConstants.SEGMENT_FIELDS_WIDTH) { Array(MQConstants.SEGMENT_FIELDS_HEIGHT) { Field.WATER } } val columnsButLast = fields.size - 1 var currentField = 0 @@ -173,23 +173,23 @@ internal fun generateSegment( } internal fun generateBoard(): Segments { - val segments = ArrayList(PluginConstants.NUMBER_OF_SEGMENTS) + val segments = ArrayList(MQConstants.NUMBER_OF_SEGMENTS) segments.add(Segment( CubeDirection.RIGHT, CubeCoordinates.ORIGIN, generateSegment(false, arrayOf()) )) - val passengerTiles = shuffledIndices(PluginConstants.NUMBER_OF_SEGMENTS - 2, PluginConstants.NUMBER_OF_PASSENGERS).toArray() - (2..PluginConstants.NUMBER_OF_SEGMENTS).forEach { index -> + val passengerTiles = shuffledIndices(MQConstants.NUMBER_OF_SEGMENTS - 2, MQConstants.NUMBER_OF_PASSENGERS).toArray() + (2..MQConstants.NUMBER_OF_SEGMENTS).forEach { index -> val previous = segments.last() val direction = if(index == 2) CubeDirection.RIGHT else previous.direction.withNeighbors().filter { it != segments.takeLast(3).first().direction.opposite() }.random() // Do not allow three consecutive turns in one direction to prevent clashes val segment = - generateSegment(index == PluginConstants.NUMBER_OF_SEGMENTS, - Array(Random.nextInt(PluginConstants.MIN_ISLANDS..PluginConstants.MAX_ISLANDS)) { Field.ISLAND } + - Array(Random.nextInt(PluginConstants.MIN_SPECIAL..PluginConstants.MAX_SPECIAL)) { Field.SANDBANK } + - Array(if(passengerTiles.contains(index - 2)) 1 else 0) { Field.PASSENGER() } + generateSegment(index == MQConstants.NUMBER_OF_SEGMENTS, + Array(Random.nextInt(MQConstants.MIN_ISLANDS..MQConstants.MAX_ISLANDS)) { Field.ISLAND } + + Array(Random.nextInt(MQConstants.MIN_SPECIAL..MQConstants.MAX_SPECIAL)) { Field.SANDBANK } + + Array(if(passengerTiles.contains(index - 2)) 1 else 0) { Field.PASSENGER() } ) segment.forEachField { c, f -> // Turn local passenger field rotation into global diff --git a/plugin/src/main/kotlin/sc/plugin2024/Ship.kt b/plugin/src/main/kotlin/sc/plugin2024/Ship.kt index cb5c03a8a..25d1a3ef1 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Ship.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/Ship.kt @@ -8,8 +8,8 @@ import sc.api.plugins.CubeDirection import sc.api.plugins.Team import sc.framework.PublicCloneable import sc.plugin2024.actions.Accelerate -import sc.plugin2024.util.PluginConstants -import sc.plugin2024.util.PluginConstants.START_COAL +import sc.plugin2024.util.MQConstants +import sc.plugin2024.util.MQConstants.START_COAL /** * Repräsentiert das Schiff eines Spielers. @@ -48,17 +48,17 @@ import sc.plugin2024.util.PluginConstants.START_COAL */ @XStreamAlias(value = "ship") data class Ship( - var position: CubeCoordinates, - @XStreamAsAttribute val team: Team, - @XStreamAsAttribute var direction: CubeDirection = CubeDirection.RIGHT, - @XStreamAsAttribute var speed: Int = PluginConstants.MIN_SPEED, - @XStreamAsAttribute var coal: Int = START_COAL, - @XStreamAsAttribute var passengers: Int = 0, - @XStreamAsAttribute var freeTurns: Int = 1, - @XStreamAsAttribute var points: Int = 0, // TODO don't track points here - @XStreamAsAttribute var stuck: Boolean = false, // TODO consider tracking as -1 points - @XStreamOmitField var freeAcc: Int = PluginConstants.FREE_ACC, - @XStreamOmitField var movement: Int = speed, + var position: CubeCoordinates, + @XStreamAsAttribute val team: Team, + @XStreamAsAttribute var direction: CubeDirection = CubeDirection.RIGHT, + @XStreamAsAttribute var speed: Int = MQConstants.MIN_SPEED, + @XStreamAsAttribute var coal: Int = START_COAL, + @XStreamAsAttribute var passengers: Int = 0, + @XStreamAsAttribute var freeTurns: Int = 1, + @XStreamAsAttribute var points: Int = 0, // TODO don't track points here + @XStreamAsAttribute var stuck: Boolean = false, // TODO consider tracking as -1 points + @XStreamOmitField var freeAcc: Int = MQConstants.FREE_ACC, + @XStreamOmitField var movement: Int = speed, ): PublicCloneable { override fun clone(): Ship = this.copy() @@ -68,7 +68,7 @@ data class Ship( /** The maximum count of points this speed is able and allowed to accelerate by. */ val maxAcc: Int - get() = (coal + freeAcc).coerceAtMost(PluginConstants.MAX_SPEED - speed) + get() = (coal + freeAcc).coerceAtMost(MQConstants.MAX_SPEED - speed) /** Adjust speed and movement simultaneously. */ fun accelerateBy(diff: Int) { @@ -77,7 +77,7 @@ data class Ship( } fun readResolve(): Ship { - freeAcc = PluginConstants.FREE_ACC + freeAcc = MQConstants.FREE_ACC movement = speed return this } diff --git a/plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt b/plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt index c196c44e5..3b5319a63 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt @@ -5,7 +5,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute import sc.api.plugins.CubeDirection import sc.plugin2024.* import sc.plugin2024.mistake.AdvanceProblem -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants import kotlin.math.absoluteValue /** @@ -30,9 +30,9 @@ data class Advance( ): Action, Addable { override fun perform(state: GameState): AdvanceProblem? { - if(distance < PluginConstants.MIN_SPEED && + if(distance < MQConstants.MIN_SPEED && state.board[state.currentShip.position] != Field.SANDBANK || - distance > PluginConstants.MAX_SPEED) + distance > MQConstants.MAX_SPEED) return AdvanceProblem.INVALID_DISTANCE if(distance > state.currentShip.movement) return AdvanceProblem.MOVEMENT_POINTS_MISSING diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt b/plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt index 1e0ca74c5..9eb7fe7c4 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt @@ -1,12 +1,12 @@ package sc.plugin2024.mistake -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants import sc.shared.IMoveMistake enum class AccelerationProblem(override val message: String) : IMoveMistake { ZERO_ACC("Es kann nicht um den Wert 0 beschleunigt werden."), - ABOVE_MAX_SPEED("Die maximale Geschwindigkeit von ${PluginConstants.MAX_SPEED} darf nicht überschritten werden."), - BELOW_MIN_SPEED("Die minimale Geschwindigkeit von ${PluginConstants.MIN_SPEED} darf nicht unterschritten werden."), + ABOVE_MAX_SPEED("Die maximale Geschwindigkeit von ${MQConstants.MAX_SPEED} darf nicht überschritten werden."), + BELOW_MIN_SPEED("Die minimale Geschwindigkeit von ${MQConstants.MIN_SPEED} darf nicht unterschritten werden."), INSUFFICIENT_COAL("Nicht genug Kohle für die Aktion vorhanden."), ON_SANDBANK("Auf einer Sandbank kann nicht beschleunigt werden."); } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt index 9ee14d0ff..39a8eb2da 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt @@ -9,4 +9,4 @@ enum class MoveMistake(override val message: String) : IMoveMistake { FIRST_ACTION_ACCELERATE("Du kannst nur in der ersten Aktion beschleunigen."), MOVEMENT_POINTS_LEFT("Es sind noch Bewegungspunkte übrig."), MOVEMENT_POINTS_MISSING("Nicht genug Bewegungspunkte."); -} +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt b/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt index 2d042dc55..6bd0b9f85 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt @@ -1,7 +1,7 @@ package sc.plugin2024.util /** Eine Sammlung an verschiedenen Konstanten, die im Spiel verwendet werden. */ -object PluginConstants { +object MQConstants { const val ROUND_LIMIT = 30 // Ship Properties diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt index e1e178e11..e5568f13b 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt @@ -35,7 +35,7 @@ class GamePlugin: IGamePlugin { Companion.scoreDefinition override val turnLimit: Int = - PluginConstants.ROUND_LIMIT * 2 + MQConstants.ROUND_LIMIT * 2 override val moveClass = Move::class.java diff --git a/plugin/src/main/kotlin/sc/plugin2024/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt similarity index 94% rename from plugin/src/main/kotlin/sc/plugin2024/XStreamClasses.kt rename to plugin/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt index 195dfca51..838418906 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/XStreamClasses.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt @@ -1,6 +1,7 @@ -package sc.plugin2024 +package sc.plugin2024.util import sc.networking.XStreamProvider +import sc.plugin2024.* import sc.plugin2024.actions.Accelerate import sc.plugin2024.actions.Advance import sc.plugin2024.actions.Push diff --git a/plugin/src/main/kotlin/sc/plugin2025/Action.kt b/plugin/src/main/kotlin/sc/plugin2025/Action.kt new file mode 100644 index 000000000..a7f57bf4e --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Action.kt @@ -0,0 +1,10 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.shared.IMoveMistake + +@XStreamAlias(value = "action") +interface Action { + fun perform(state: GameState): IMoveMistake? +} + diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.java b/plugin/src/main/kotlin/sc/plugin2025/Advance.java new file mode 100644 index 000000000..f188f5298 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.java @@ -0,0 +1,72 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidMoveException; + +/** + * Ein Vorwärtszug, um spezifizierte Distanz. Verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 + * berechnet (Gaußsche Summe) + */ +@XStreamAlias(value = "advance") +public class Advance extends Action{ + + @XStreamAsAttribute + private int distance; + + public Advance(int distance) { + super(); + this.distance = distance; + } + + public Advance(int distance, int order) { + this.order = order; + this.distance = distance; + } + + @Override + public void perform(GameState state) throws InvalidMoveException { + if (GameRuleLogic.isValidToAdvance(state, this.distance)) { + state.getCurrentPlayer().changeCarrotsBy(- GameRuleLogic.calculateCarrots(this.distance)); + state.getCurrentPlayer().setFieldIndex(state.getCurrentPlayer().getFieldIndex() + distance); + if (state.getTypeAt(state.getCurrentPlayer().getFieldIndex()) == Field.HARE) { + state.getCurrentPlayer().setMustPlayCard(true); + } + // Setze letzte Aktion + state.setLastAction(this); + } else { + throw new InvalidMoveException("Vorwärtszug um " + this.distance + " Felder ist nicht möglich"); + } + } + + /** + * Gibt das Dinstanzattribut zurück + * @return Distanz + */ + public int getDistance() { + return distance; + } + + @Override + public Advance clone() { + return new Advance(this.distance, this.order); + } + + @Override + public boolean equals(Object o) { + if(o instanceof Advance) { + return (this.distance == ((Advance) o).distance); + } + return false; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder("Advance: distance "); + b.append(this.distance); + b.append(" order "); + b.append(this.order); + return b.toString(); + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt new file mode 100644 index 000000000..bec6b4c38 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -0,0 +1,167 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamImplicit +import sc.api.plugins.IBoard +import sc.plugin2025.util.HuIConstants + +@XStreamAlias("board") +data class Board( + @XStreamImplicit(itemFieldName = "fields") private val track: Array = generateTrack().toTypedArray(), +): IBoard { + fun findField(field: Field, range: Iterable = 0 until HuIConstants.NUM_FIELDS) = + range.find { track[it] == field } + + fun getField(index: Int) = track[index] + + fun getPreviousField(field: Field, index: Int = 0) = + findField(field, (index - 1) downTo (0)) + + fun getNextField(field: Field, index: Int = 0) = + findField(field, (index + 1).until(HuIConstants.NUM_FIELDS)) + + override fun toString() = + track.joinToString(prefix = "Board[", postfix = "]") { it.unicode } + + override fun clone(): Board = Board(Array(track.size) { track[it] }) + + companion object { + /** + * Erstellt eine zufällige Rennstrecke. + * Die Indizes der Salat- und Igelfelder bleiben unverändert - + * nur die Felder zwischen zwei Igelfeldern werden permutiert. + * Außerdem werden auch die Abschnitte zwischen Start- und Ziel + * und dem ersten bzw. letzten Igelfeld permutiert. + */ + private fun generateTrack(): List { + val track = ArrayList() + val segment = ArrayList() + + track.add(Field.START) + segment.addAll( + listOf( + Field.HARE, + Field.CARROT, Field.HARE, Field.CARROT, + Field.CARROT, Field.HARE, Field.POSITION_1, + Field.POSITION_2, Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.SALAD) + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.CARROT, + Field.CARROT, Field.HARE + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.POSITION_1, + Field.POSITION_2, Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.CARROT, + Field.CARROT, Field.POSITION_2 + ) + ) + segment.shuffle() + + track.add(segment.removeAt(0)) + track.add(segment.removeAt(0)) + track.add(Field.SALAD) + track.add(segment.removeAt(0)) + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.HARE, + Field.CARROT, Field.CARROT, Field.CARROT, + Field.POSITION_2 + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.HARE, + Field.POSITION_1, Field.CARROT, Field.HARE, + Field.POSITION_2, Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.CARROT, + Field.HARE, Field.CARROT, Field.POSITION_2 + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.SALAD) + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.CARROT, + Field.CARROT, Field.HARE, Field.POSITION_2, + Field.POSITION_1, Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.HEDGEHOG) + segment.addAll( + listOf( + Field.HARE, + Field.CARROT, Field.POSITION_2, Field.CARROT, + Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + track.add(Field.HEDGEHOG) + track.add(Field.SALAD) + + segment.clear() + segment.addAll( + listOf( + Field.HARE, + Field.CARROT, Field.POSITION_1, Field.CARROT, + Field.HARE, Field.CARROT + ) + ) + segment.shuffle() + track.addAll(segment) + + segment.clear() + track.add(Field.GOAL) + return track + } + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.java b/plugin/src/main/kotlin/sc/plugin2025/Card.java new file mode 100644 index 000000000..10befb3c5 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.java @@ -0,0 +1,132 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidMoveException; + +/** + * Eine Karte die auf einem Hasenfeld gespielt werden kann. + */ +@XStreamAlias(value = "card") +public class Card extends Action { + + @XStreamAsAttribute + private CardType type; + + /** + * Nur für TAKE_OR_DROP_CARROTS genutzt. Muss 20, 0 oder -20 sein. + */ + @XStreamAsAttribute + private int value; + + /** + * Default Konstruktor. Setzt value auf 0 und order auf 0. + * @param type Art der Karte + */ + public Card(CardType type) { + this.order = 0; + this.value = 0; + this.type = type; + } + + /** + * Konstruktor für eine Karte + * @param type Art der Karte + * @param order Index in der Aktionsliste des Zuges + */ + public Card(CardType type, int order) { + this.order = order; + this.value = 0; + this.type = type; + } + + /** + * + * @param type Art der Karte + * @param value Wert einer Karte nur für TAKE_OR_DROP_CARROTS genutzt (-20,0,20) + * @param order Index in der Aktionsliste des Zuges + */ + public Card(CardType type, int value, int order) { + this.order = order; + this.value = 0; // default value + this.value = value; + this.type = type; + } + + + @Override + public void perform(GameState state) throws InvalidMoveException { + state.getCurrentPlayer().setMustPlayCard(false); // player played a card + switch (type) { // when entering a HARE field with fall_back or hurry ahead, player has to play another card + case EAT_SALAD: + if (GameRuleLogic.isValidToPlayEatSalad(state)) { + state.getCurrentPlayer().eatSalad(); + if (state.isFirst(state.getCurrentPlayer())) { + state.getCurrentPlayer().changeCarrotsBy(10); + } else { + state.getCurrentPlayer().changeCarrotsBy(30); + } + } else { + throw new InvalidMoveException("Das Ausspielen der EAT_SALAD Karte ist nicht möglich."); + } + break; + case FALL_BACK: + if (GameRuleLogic.isValidToPlayFallBack(state)) { + state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() - 1); + if (state.fieldOfCurrentPlayer() == Field.HARE) { + state.getCurrentPlayer().setMustPlayCard(true); + } + } else { + throw new InvalidMoveException("Das Ausspielen der FALL_BACK Karte ist nicht möglich."); + } + break; + case HURRY_AHEAD: + if (GameRuleLogic.isValidToPlayHurryAhead(state)) { + state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() + 1); + if (state.fieldOfCurrentPlayer() == Field.HARE) { + state.getCurrentPlayer().setMustPlayCard(true); + } + } else { + throw new InvalidMoveException("Das Ausspielen der HURRY_AHEAD Karte ist nicht möglich."); + } + break; + case TAKE_OR_DROP_CARROTS: + if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(state, this.getValue())) { + state.getCurrentPlayer().changeCarrotsBy(this.getValue()); + } else { + throw new InvalidMoveException("Das Ausspielen der TAKE_OR_DROP_CARROTS Karte ist nicht möglich."); + } + break; + } + state.setLastAction(this); + // remove player card + state.getCurrentPlayer().setCards(state.getCurrentPlayer().getCardsWithout(this.type)); + } + + public CardType getType() { + return type; + } + + public int getValue() { + return value; + } + + @Override + public Card clone() { + return new Card(this.type, this.value, this.order); + } + + @Override + public boolean equals(Object o) { + return o instanceof Card && (this.value == ((Card) o).value) && (this.type == ((Card) o).type); + } + + @Override + public String toString() { + return "Card " + + this.getType() + + ((this.getType() == CardType.TAKE_OR_DROP_CARROTS)?(" " + this.value):"") + + " order " + this.order; + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt new file mode 100644 index 000000000..91a03d76c --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -0,0 +1,32 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.shared.IMoveMistake + +/** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ +@XStreamAlias(value = "card") +enum class Card { + /** Nehme Karotten auf, oder leg sie ab. */ + TAKE_OR_DROP_CARROTS, + /** Iß sofort einen Salat. */ + EAT_SALAD, + /** Falle eine Position zurück. */ + FALL_BACK, + /** Rücke eine Position vor. */ + HURRY_AHEAD +} + +sealed class CardAction: Action { + abstract val card: Card + data class PlayCard(override val card: Card): CardAction() { + override fun perform(state: GameState): IMoveMistake? { + TODO("Not yet implemented") + } + } + class CarrotCard(val value: Int): CardAction() { + override val card = Card.TAKE_OR_DROP_CARROTS + override fun perform(state: GameState): IMoveMistake? { + TODO("Not yet implemented") + } + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt new file mode 100644 index 000000000..55751647b --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -0,0 +1,32 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.shared.IMoveMistake + +/** + * Eine Salatessen-Aktion. + * Kann nur auf einem Salatfeld ausgeführt werden. + * Muss ausgeführt werden, wenn ein Salatfeld betreten wird. + * Nachdem die Aktion ausgeführt wurde, muss das Salatfeld verlassen werden, oder es muss ausgesetzt werden. + * Durch eine Salatessen-Aktion wird ein Salat verbraucht + * und es werden je nachdem ob der Spieler führt oder nicht 10 oder 30 Karotten aufgenommen. + */ +@XStreamAlias(value = "Salad") +object EatSalad: Action { + override fun perform(state: GameState): IMoveMistake? { + if(state.canEatSalad()) { + val player = state.currentPlayer + player.eatSalad() + if(player == state.aheadPlayer) { + player.carrots += 10 + } else { + player.carrots += 30 + } + return null + } else { + return MoveMistake.CANNOT_EAT_SALAD + } + } + + override fun equals(other: Any?): Boolean = other is EatSalad +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java new file mode 100644 index 000000000..b17223c2b --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java @@ -0,0 +1,60 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidMoveException; + +/** + * Karottentauschaktion. Es können auf einem Karottenfeld 10 Karotten abgegeben oder aufgenommen werden. + * Dies kann beliebig oft hintereinander ausgeführt werden. + */ +@XStreamAlias(value = "exchangeCarrots") +public class ExchangeCarrots extends Action { + + @XStreamAsAttribute + private int value; + + public ExchangeCarrots(int value) { + this.order = 0; + this.value = value; + } + + public ExchangeCarrots(int value, int order) { + this.order = order; + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public void perform(GameState state) throws InvalidMoveException { + if (GameRuleLogic.isValidToExchangeCarrots(state, this.getValue())) { + state.getCurrentPlayer().changeCarrotsBy(this.getValue()); + state.setLastAction(this); + } else { + throw new InvalidMoveException("Es können nicht " + this.getValue() + " Karotten aufgenommen werden."); + } + } + + @Override + public ExchangeCarrots clone() { + return new ExchangeCarrots(this.getValue(), this.order); + } + + @Override + public boolean equals(Object o) { + if(o instanceof ExchangeCarrots) { + return (this.getValue() == ((ExchangeCarrots) o).getValue()); + } + return false; + } + + @Override + public String toString() { + return "ExchangeCarrots value " + this.getValue() + " order " + this.order; + } + +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.java b/plugin/src/main/kotlin/sc/plugin2025/FallBack.java new file mode 100644 index 000000000..53238ee16 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.java @@ -0,0 +1,49 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidMoveException; + +/** + * Rückzugaktion. Sollte das nächste Igelfeld hinter einem Spieler nicht belegt sein, darf anstatt nach + * vorne zu ziehen ein Rückzug gemacht werden. Dabei werden die zurückgezogene Distanz * 10 Karotten aufgenommen. + */ +@XStreamAlias(value = "fallBack") +public class FallBack extends Action { + + public FallBack() { + this.order = 0; + } + + public FallBack(int order) { + this.order = order; + } + + @Override + public void perform(GameState state) throws InvalidMoveException { + if (GameRuleLogic.isValidToFallBack(state)) { + int previousFieldIndex = state.getCurrentPlayer().getFieldIndex(); + state.getCurrentPlayer().setFieldIndex(state.getPreviousFieldByType(Field.HEDGEHOG, state.getCurrentPlayer() + .getFieldIndex())); + state.getCurrentPlayer().changeCarrotsBy(10 * (previousFieldIndex - state.getCurrentPlayer().getFieldIndex())); + state.setLastAction(this); + } else { + throw new InvalidMoveException("Es kann gerade kein Rückzug gemacht werden."); + } + } + + @Override + public FallBack clone() { + return new FallBack(this.order); + } + + @Override + public boolean equals(Object o) { + return o instanceof FallBack; + } + + @Override + public String toString() { + return "FallBack order " + this.order; + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt new file mode 100644 index 000000000..631066593 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -0,0 +1,27 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias + +/** Die unterschiedlichen Spielfelder aus dem Hase und Igel Original. */ +@XStreamAlias(value = "field") +enum class Field(val short: String, val unicode: String = short) { + /** + * Zahl- und Flaggenfelder. + * Die veränderten Spielregeln sehen nur noch die Felder 1,2 vor. + * Die Positionsfelder 3 und 4 wurden in Möhrenfelder umgewandelt, + * und (1,5,6) sind jetzt Position-1-Felder. + */ + POSITION_1("P1"), POSITION_2("P2"), + /** Igelfeld */ + HEDGEHOG("I", "\uD83E\uDD94"), + /** Salatfeld */ + SALAD("S", "\uD83E\uDD57"), + /** Karottenfeld */ + CARROT("K", "\uD83E\uDD55"), + /** Hasenfeld */ + HARE("H"), + /** Das Zielfeld */ + GOAL("Z", "🏁"), + /** Das Startfeld */ + START("0", "▶"), +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt new file mode 100644 index 000000000..2d9f8e663 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -0,0 +1,400 @@ +package sc.plugin2025 + +import kotlin.math.sqrt + +object GameRuleLogic { + /** + * Berechnet wie viele Karotten für einen Zug der Länge + * `moveCount` benötigt werden. + * + * @param moveCount Anzahl der Felder, um die bewegt wird + * @return Anzahl der benötigten Karotten + */ + fun calculateCarrots(moveCount: Int): Int = + (moveCount * (moveCount + 1)) / 2 + + /** + * Berechnet, wie viele Züge mit `carrots` Karotten möglich sind. + * + * @param carrots maximal ausgegebene Karotten + * @return Felder um die maximal bewegt werden kann + */ + fun calculateMoveableFields(carrots: Int): Int { + return when { + carrots >= 990 -> 44 + carrots < 1 -> 0 + //-0.48 anstelle von -0.5 um Rundungsfehler zu vermeiden + else -> (sqrt((2.0 * carrots) + 0.25) - 0.48).toInt() + } + } + + /* + /** + * Überprüft `Advance` Aktionen auf ihre Korrektheit. + * Folgende Spielregeln werden beachtet: + * + * - Der Spieler muss genügend Karotten für den Zug besitzen + * - Wenn das Ziel erreicht wird, darf der Spieler nach dem Zug maximal 10 Karotten übrig haben + * - Man darf nicht auf Igelfelder ziehen + * - Salatfelder dürfen nur betreten werden, wenn man noch Salate essen muss + * - Hasenfelder dürfen nur betreten werden, wenn man noch Karte ausspielen kann + * + * @param state GameState + * @param distance relativer Abstand zur aktuellen Position des Spielers + * @return true, falls ein Vorwärtszug möglich ist + */ + fun isValidToAdvance(state: GameState, distance: Int): Boolean { + if(distance <= 0) { + return false + } + val player: Hare = state.currentPlayer + if(mustEatSalad(state)) { + return false + } + var valid = true + val requiredCarrots = calculateCarrots(distance) + valid = valid && (requiredCarrots <= player.getCarrots()) + + val newPosition: Int = player.getFieldIndex() + distance + valid = valid && !state.isOccupied(newPosition) + val type: Field = state.board.getTypeAt(newPosition) + when(type) { + INVALID -> valid = false + Field.SALAD -> valid = valid && player.getSalads() > 0 + Field.HARE -> { + var state2: GameState? = null + try { + state2 = state.clone() + } catch(e: CloneNotSupportedException) { + e.printStackTrace() + } + state2.setLastAction(Advance(distance)) + state2!!.currentPlayer.setFieldIndex(newPosition) + state2.currentPlayer.changeCarrotsBy(-requiredCarrots) + valid = valid && canPlayAnyCard(state2) + } + + Field.GOAL -> { + val carrotsLeft: Int = player.getCarrots() - requiredCarrots + valid = valid && carrotsLeft <= 10 + valid = valid && player.getSalads() === 0 + } + + Field.HEDGEHOG -> valid = false + Field.CARROT, Field.POSITION_1, Field.START, Field.POSITION_2 -> {} + else -> throw IllegalStateException("Unknown Type $type") + + } + return valid + } + + /** + * Überprüft, ob ein Spieler aussetzen darf. Er darf dies, wenn kein anderer Zug möglich ist. + * @param state GameState + * @return true, falls der derzeitige Spieler keine andere Aktion machen kann. + */ + fun isValidToSkip(state: GameState): Boolean { + return !canDoAnything(state) + } + + /** + * Überprüft, ob ein Spieler einen Zug (keinen Aussetzug) + * @param state GameState + * @return true, falls ein Zug möglich ist. + */ + private fun canDoAnything(state: GameState): Boolean { + return (canPlayAnyCard(state) || isValidToFallBack(state) + || isValidToExchangeCarrots(state, 10) + || isValidToExchangeCarrots(state, -10) + || isValidToEat(state) || canAdvanceToAnyField(state)) + } + + /** + * Überprüft ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. + * @param state GameState + * @return true, falls der Spieler irgendeinen Vorwärtszug machen kann + */ + private fun canAdvanceToAnyField(state: GameState): Boolean { + val fields = calculateMoveableFields(state.currentPlayer.carrots) + for(i in 0..fields) { + if(isValidToAdvance(state, i)) { + return true + } + } + + return false + } + + /** + * Überprüft `EatSalad` Züge auf Korrektheit. Um einen Salat + * zu verzehren muss der Spieler sich: + * + * - auf einem Salatfeld befinden + * - noch mindestens einen Salat besitzen + * - vorher kein Salat auf diesem Feld verzehrt wurde + * + * @param state GameState + * @return true, falls ein Salat gegessen werden darf + */ + fun isValidToEat(state: GameState): Boolean { + val player: Hare = state.currentPlayer + var valid = true + val currentField: Field = state.getTypeAt(player.getFieldIndex()) + + valid = valid && (currentField == Field.SALAD) + valid = valid && (player.getSalads() > 0) + valid = valid && !playerMustAdvance(state) + + return valid + } + + /** + * Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss. + * @param state GameState + * @return true, falls der derzeitige Spieler einen Vorwärtszug gemacht werden muss + */ + fun playerMustAdvance(state: GameState?): Boolean { + val player: Hare = state!!.currentPlayer + val type: Field = state.getTypeAt(player.getFieldIndex()) + + if(type == Field.HEDGEHOG || type == Field.START) { + return true + } + + val lastAction: Action = state.getLastNonSkipAction(player) + + if(lastAction != null) { + if(lastAction is EatSalad) { + return true + } else if(lastAction is Card) { + // the player has to leave a hare field in next turn + if((lastAction as Card).getType() === Card.EAT_SALAD) { + return true + } else if((lastAction as Card).getType() === Card.TAKE_OR_DROP_CARROTS) // the player has to leave the hare field + { + return true + } + } + } + + return false + } + + /** + * Überprüft ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. + * @param state GameState + * @param n 10 oder -10 je nach Fragestellung + * @return true, falls die durch n spezifizierte Aktion möglich ist. + */ + fun isValidToExchangeCarrots(state: GameState, n: Int): Boolean { + val player: Hare = state.currentPlayer + val valid: Boolean = state.getTypeAt(player.getFieldIndex()).equals(Field.CARROT) + if(n == 10) { + return valid + } + if(n == -10) { + return if(player.getCarrots() >= 10) { + valid + } else { + false + } + } + return false + } + + /** + * Überprüft `FallBack` Züge auf Korrektheit + * + * @param state GameState + * @return true, falls der currentPlayer einen Rückzug machen darf + */ + fun isValidToFallBack(state: GameState): Boolean { + if(mustEatSalad(state)) { + return false + } + var valid = true + val newPosition: Int = state.getPreviousFieldByType( + Field.HEDGEHOG, state.currentPlayer + .getFieldIndex() + ) + valid = valid && (newPosition != -1) + valid = valid && !state.isOccupied(newPosition) + return valid + } + + /** + * Überprüft ob der derzeitige Spieler die `FALL_BACK` Karte spielen darf. + * @param state GameState + * @return true, falls die `FALL_BACK` Karte gespielt werden darf + */ + fun isValidToPlayFallBack(state: GameState?): Boolean { + val player: Hare = state!!.currentPlayer + var valid = (!playerMustAdvance(state) && state.isOnHareField() + && state.isFirst(player)) + + valid = valid && player.ownsCardOfType(Card.FALL_BACK) + + val o: Hare = state.getOpponent(player) + val nextPos: Int = o.getFieldIndex() - 1 + if(nextPos == 0) { + return false + } + + val type: Field = state.getTypeAt(nextPos) + when(type) { + INVALID, Field.HEDGEHOG -> valid = false + Field.START -> {} + Field.SALAD -> valid = valid && player.getSalads() > 0 + Field.HARE -> { + var state2: GameState? = null + try { + state2 = state.clone() + } catch(e: CloneNotSupportedException) { + e.printStackTrace() + } + state2.setLastAction(Card(Card.FALL_BACK)) + state2!!.currentPlayer.setFieldIndex(nextPos) + state2.currentPlayer.setCards(player.getCardsWithout(Card.FALL_BACK)) + valid = valid && canPlayAnyCard(state2) + } + + Field.CARROT, Field.POSITION_1, Field.POSITION_2 -> {} + else -> throw IllegalStateException("Unknown Type $type") + } + return valid + } + + /** + * Überprüft ob der derzeitige Spieler die `HURRY_AHEAD` Karte spielen darf. + * @param state GameState + * @return true, falls die `HURRY_AHEAD` Karte gespielt werden darf + */ + fun isValidToPlayHurryAhead(state: GameState?): Boolean { + val player: Hare = state!!.currentPlayer + var valid = (!playerMustAdvance(state) && state.isOnHareField() + && !state.isFirst(player)) + valid = valid && player.ownsCardOfType(Card.HURRY_AHEAD) + + val o: Hare = state.getOpponent(player) + val nextPos: Int = o.getFieldIndex() + 1 + + val type: Field = state.getTypeAt(nextPos) + when(type) { + INVALID, Field.HEDGEHOG -> valid = false + Field.SALAD -> valid = valid && player.getSalads() > 0 + Field.HARE -> { + var state2: GameState? = null + try { + state2 = state.clone() + } catch(e: CloneNotSupportedException) { + e.printStackTrace() + } + state2.setLastAction(Card(Card.HURRY_AHEAD)) + state2!!.currentPlayer.setFieldIndex(nextPos) + state2.currentPlayer.setCards(player.getCardsWithout(Card.HURRY_AHEAD)) + valid = valid && canPlayAnyCard(state2) + } + + Field.GOAL -> valid = valid && canEnterGoal(state) + Field.CARROT, Field.POSITION_1, Field.POSITION_2, Field.START -> {} + else -> throw IllegalStateException("Unknown Type $type") + } + return valid + } + + /** + * Überprüft ob der derzeitige Spieler die `TAKE_OR_DROP_CARROTS` Karte spielen darf. + * @param state GameState + * @param n 20 für nehmen, -20 für abgeben, 0 für nichts tun + * @return true, falls die `TAKE_OR_DROP_CARROTS` Karte gespielt werden darf + */ + fun isValidToPlayTakeOrDropCarrots(state: GameState?, n: Int): Boolean { + val player: Hare = state!!.currentPlayer + var valid = (!playerMustAdvance(state) && state.isOnHareField() + && player.ownsCardOfType(Card.TAKE_OR_DROP_CARROTS)) + + valid = valid && (n == 20 || n == -20 || n == 0) + if(n < 0) { + valid = valid && ((player.getCarrots() + n) >= 0) + } + return valid + } + + /** + * Überprüft ob der derzeitige Spieler die `EAT_SALAD` Karte spielen darf. + * @param state GameState + * @return true, falls die `EAT_SALAD` Karte gespielt werden darf + */ + fun isValidToPlayEatSalad(state: GameState?): Boolean { + val player: Hare = state!!.currentPlayer + return (!playerMustAdvance(state) && state.isOnHareField() + && player.ownsCardOfType(Card.EAT_SALAD)) && player.getSalads() > 0 + } + + /** + * Überprüft ob der derzeitige Spieler irgendeine Karte spielen kann. + * TAKE_OR_DROP_CARROTS wird nur mit 20 überprüft + * @param state GameState + * @return true, falls das Spielen einer Karte möglich ist + */ + private fun canPlayAnyCard(state: GameState?): Boolean { + for(card in state!!.currentPlayer.getCards()) { + if(canPlayCard(state, card)) return true + } + + return false + } + + private fun canPlayCard(state: GameState?, card: Card): Boolean { + return when(card) { + Card.EAT_SALAD -> isValidToPlayEatSalad(state) + Card.FALL_BACK -> isValidToPlayFallBack(state) + Card.HURRY_AHEAD -> isValidToPlayHurryAhead(state) + Card.TAKE_OR_DROP_CARROTS -> isValidToPlayTakeOrDropCarrots(state, 20) + else -> throw IllegalArgumentException("Unknown CardType $card") + } + } + + /** + * Überprüft ob der derzeitige Spieler die Karte spielen kann. + * @param state derzeitiger GameState + * @param c Karte die gespielt werden soll + * @param n Wert fuer TAKE_OR_DROP_CARROTS + * @return true, falls das Spielen der entsprechenden Karte möglich ist + */ + fun isValidToPlayCard(state: GameState?, c: Card, n: Int): Boolean { + return if(c == Card.TAKE_OR_DROP_CARROTS) isValidToPlayTakeOrDropCarrots( + state, + n + ) + else canPlayCard(state, c) + } + + fun mustEatSalad(state: GameState): Boolean { + val player: Hare = state.currentPlayer + // check whether player just moved to salad field and must eat salad + val field: Field = state.board.getTypeAt(player.getFieldIndex()) + if(field == Field.SALAD) { + if(player.getLastNonSkipAction() is Advance) { + return true + } else if(player.getLastNonSkipAction() is Card) { + if((player.getLastNonSkipAction() as Card).getType() === Card.FALL_BACK || + (player.getLastNonSkipAction() as Card).getType() === Card.HURRY_AHEAD + ) { + return true + } + } + } + return false + } + + /** + * Gibt zurück, ob der derzeitige Spieler eine Karte spielen kann. + * @param state derzeitiger GameState + * @return true, falls eine Karte gespielt werden kann + */ + fun canPlayCard(state: GameState): Boolean { + return state.fieldOfCurrentPlayer() === Field.HARE && canPlayAnyCard(state) + } + */ +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.java b/plugin/src/main/kotlin/sc/plugin2025/GameState.java new file mode 100644 index 000000000..7f344e565 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.java @@ -0,0 +1,707 @@ +package sc.plugin2018; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamOmitField; + +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidGameStateException; +import sc.shared.InvalidMoveException; +import sc.shared.PlayerColor; +import sc.plugin2018.util.Constants; + +import java.util.ArrayList; + +/** + * Ein {@code GameState} beinhaltet alle Informationen, die den Spielstand zu + * einem gegebenen Zeitpunkt, das heisst zwischen zwei Spielzuegen, beschreiben. + * Dies umfasst eine fortlaufende Zugnummer ({@link #getTurn() getRound()}), die + * der Spielserver als Antwort von einem der beiden Spieler ( + * {@link #getCurrentPlayer() getCurrentPlayer()}) erwartet. Weiterhin gehoeren + * die Informationen ueber die beiden Spieler und das Spielfeld zum Zustand. + * Zuseatzlich wird ueber den zuletzt getaetigeten Spielzung und ggf. ueber das + * Spielende informiert. + * + * + * Der {@code GameState} ist damit das zentrale Objekt ueber das auf alle + * wesentlichen Informationen des aktuellen Spiels zugegriffen werden kann. + * + * + * Der Spielserver sendet an beide teilnehmenden Spieler nach jedem getaetigten + * Zug eine neue Kopie des {@code GameState}, in dem der dann aktuelle Zustand + * beschrieben wird. Informationen ueber den Spielverlauf sind nur bedingt ueber + * den {@code GameState} erfragbar und muessen von einem Spielclient daher bei + * Bedarf selbst mitgeschrieben werden. + * + * + * Zusaetzlich zu den eigentlichen Informationen koennen bestimmte + * Teilinformationen abgefragt werden. + * + * @author Niklas, Sören + */ +@XStreamAlias(value = "state") +public class GameState implements Cloneable { + + @XStreamOmitField + private static final Logger logger = LoggerFactory + .getLogger(GameState.class); + /** + * momentane Rundenzahl + */ + @XStreamAsAttribute + private int turn; + + /** + * Farbe des Startspielers + */ + @XStreamAsAttribute + private PlayerColor startPlayer; + + /** + * Farbe des aktuellen Spielers + */ + @XStreamAsAttribute + private PlayerColor currentPlayer; + + /** + * der rote Spieler + */ + private Player red; + /** + * der blaue Spieler + */ + private Player blue; + + /** + * Das Spielbrett + */ + private Board board; + + /** + * letzter getaetigter Zug + */ + private Move lastMove; + + /** + * Erzeugt einen neuen {@code GameState}, in dem alle Informationen so gesetzt + * sind, wie sie zu Beginn eines Spiels, bevor die Spieler beigetreten sind, + * gueltig sind. + * + * + * Dieser Konstruktor ist nur fuer den Spielserver relevant und sollte vom + * Spielclient i.A. nicht aufgerufen werden! + * + * Das Spielfeld wird zufällig aufgebaut. + */ + public GameState() { + this.turn = 0; + this.currentPlayer = PlayerColor.RED; + this.startPlayer = PlayerColor.RED; + this.board = new Board(); + this.red = new Player(PlayerColor.RED); + this.blue = new Player(PlayerColor.BLUE); + } + + /** + * Erzeugt einen neuen {@code GameState} mit denselben Eigenschaften von + * stateToClone. Fuer eigene Implementierungen. + */ + protected GameState(GameState stateToClone) throws CloneNotSupportedException { + GameState clone = stateToClone.clone(); + setRedPlayer(clone.getPlayer(PlayerColor.RED)); + setBluePlayer(clone.getPlayer(PlayerColor.BLUE)); + setLastMove(clone.getLastMove()); + setBoard(clone.getBoard()); + setCurrentPlayer(clone.getCurrentPlayerColor()); + } + + /** + * erzeugt eine Deepcopy dieses Objekts + * + * @return ein neues Objekt mit gleichen Eigenschaften + * @throws CloneNotSupportedException falls das Klonen fehlschlaegt + */ + @Override + public GameState clone() throws CloneNotSupportedException { + GameState clone = (GameState) super.clone(); + if (red != null) + clone.red = this.red.clone(); + if (blue != null) + clone.blue = this.blue.clone(); + if (lastMove != null) + clone.lastMove = (Move) this.lastMove.clone(); + if (board != null) + clone.board = this.board.clone(); + if (currentPlayer != null) + clone.currentPlayer = this.currentPlayer; + clone.startPlayer = this.startPlayer; + clone.turn = this.turn; + return clone; + } + + /** + * Fuegt einem Spiel einen weiteren Spieler hinzu. + * + * + * Diese Methode ist nur fuer den Spielserver relevant und sollte vom + * Spielclient i.A. nicht aufgerufen werden! + * + * @param player + * Der hinzuzufuegende Spieler. + */ + public void addPlayer(Player player) { + if (player.getPlayerColor() == PlayerColor.RED) { + red = player; + } else if (player.getPlayerColor() == PlayerColor.BLUE) { + blue = player; + } + } + + /** + * Gibt das Spielfeld zurueck + * + * @return das Spielfeld + */ + public Board getBoard() { + return this.board; + } + + /** + * Liefert den Spieler als {@code Player}-Objekt, der als die entsprechende Farbe spielt + * @param color die Farbe des gefragten Spielers + */ + public Player getPlayer(PlayerColor color) { + return color == PlayerColor.RED ? red : blue; + } + + /** + * Liefert den Spieler, also ein {@code Player}-Objekt, der momentan am Zug + * ist. + * + * @return Der Spieler, der momentan am Zug ist. + */ + public Player getCurrentPlayer() { + return getPlayer(currentPlayer); + } + + /** + * Liefert die {@code PlayerColor}-Farbe des Spielers, der momentan am Zug + * ist. Dies ist aequivalent zum Aufruf + * {@code getCurrentPlayer().getPlayerColor()}, aber schneller. + * + * @return Die Farbe des Spielers, der momentan am Zug ist. + */ + public PlayerColor getCurrentPlayerColor() { + return currentPlayer; + } + + /** + * Nur für den Server relevant + * @param playerColor PlayerColor of new currentPlayer + */ + protected void setCurrentPlayer(PlayerColor playerColor) { + this.currentPlayer = playerColor; + } + + /** + * Liefert den Spieler, also ein {@code Player}-Objekt, der momentan nicht am + * Zug ist. + * + * @return Der Spieler, der momentan nicht am Zug ist. + */ + public Player getOtherPlayer() { + return getPlayer(getOtherPlayerColor()); + } + + /** + * Liefert die {@code PlayerColor}-Farbe des Spielers, der momentan nicht am + * Zug ist. Dies ist aequivalent zum Aufruf @ + * {@code getCurrentPlayerColor.opponent()} oder + * {@code getOtherPlayer().getPlayerColor()}, aber etwas effizienter. + * + * @return Die Farbe des Spielers, der momentan nicht am Zug ist. + */ + public PlayerColor getOtherPlayerColor() { + return currentPlayer.opponent(); + } + + /** + * @deprecated ersetzt durch {@link #getPlayer(PlayerColor)} + */ + public Player getRedPlayer() { + return red; + } + + /** + * @deprecated ersetzt durch {@link #getPlayer(PlayerColor)} + */ + public Player getBluePlayer() { + return blue; + } + + /** + * Nur für den Server relevant + * @param red roter Spieler + */ + protected void setRedPlayer(Player red) { + this.red = red; + } + + /** + * Nur für den Server relevant + * @param blue blauer Spieler + */ + protected void setBluePlayer(Player blue) { + this.blue = blue; + } + + /** + * Liefert den Spieler, also eine {@code Player}-Objekt, der das Spiel + * begonnen hat. + * + * @return Der Spieler, der momentan Startspieler ist. + */ + public Player getStartPlayer() { + return startPlayer == PlayerColor.RED ? red : blue; + } + + /** + * Liefert die {@code PlayerColor}-Farbe des Spielers, der den aktuellen + * Abschnitt begonnen hat. Dies ist aequivalent zum Aufruf + * {@code getStartPlayer().getPlayerColor()}, aber etwas effizienter. + * + * @return Die Farbe des Spielers, der den aktuellen Abschnitt begonnen + * hat. + */ + public PlayerColor getStartPlayerColor() { + return startPlayer; + } + + /** + * wechselt den Spieler, der aktuell an der Reihe ist anhand der Anzahl der Züge turn + */ + public void switchCurrentPlayer() { + if (turn % 2 == 0) { + currentPlayer = PlayerColor.RED; + } else { + currentPlayer = PlayerColor.BLUE; + } + } + + /** + * Überprüft ob ein Feld durch einen Spieler belegt ist, sodass niemand darauf ziehen kann. + * (da Zielfeld von mehreren betretbar, bei Zielfeld immer false) + * + * @param index + * der Index auf der Rennstrecke + * @return Gibt true zurück, falls sich ein Spieler auf dem Feld befindet und es nicht das Zielfeld ist + */ + public final boolean isOccupied(final int index) + { + return (red.getFieldIndex() == index || blue.getFieldIndex() == index) + && (index != Constants.NUM_FIELDS - 1); + } + + /** + * Überprüft ob der angegebene Spieler an erster Stelle ist. Wenn sich beide + * Spieler im Ziel befinden wird zusätzlich überprüft, ob player + * weniger Karotten besitzt als der Gegenspieler. + * + * @param player überprüfter Spieler + * @return true, falls Spieler an erster Stelle + */ + public final boolean isFirst(final Player player) + { + Player o = this.getOpponent(player); + boolean isFirst = o.getFieldIndex() <= player.getFieldIndex(); + if (player.inGoal() && o.getFieldIndex() == player.getFieldIndex()) + isFirst = isFirst + && player.getCarrots() < o.getCarrots(); + return isFirst; + } + + /** + * Gibt den Feldtypen an einem bestimmten Index zurück. Liegt der + * gewählte Index vor dem Startpunkt oder hinter dem Ziel, so wird + * INVALID zurückgegeben. + * + * @param index die Index auf der Rennstrecke + * @return Feldtyp an Index + */ + public final sc.plugin2018.Field getTypeAt(final int index) + { + return board.getTypeAt(index); + } + + /** + * Findet das nächste Spielfeld vom Typ type beginnend an + * Index index auf diesem Spielbrett. + * + * @param type Feldtyp + * @param index Index + * @return Index des nächsten Feldes genannten Typs + */ + public final int getNextFieldByType(sc.plugin2018.Field type, int index) + { + return this.board.getNextFieldByType(type, index); + } + + /** + * Findet das vorherige Spielfeld vom Typ type beginnend an Index + * index auf diesem Spielbrett. + * @param type Feldtyp + * @param index Index + * @return Index des vorherigen Feldes genannten Typs + */ + public final int getPreviousFieldByType(sc.plugin2018.Field type, int index) + { + return this.board.getPreviousFieldByType(type, index); + } + + /** + * liefert die aktuelle Zugzahl + * @return Nummer des aktuellen Zuges (Zaehlung beginnt mit 0) + */ + public int getTurn() { + return turn; + } + + /** + * Setzt die aktuelle Zugzahl. Nur für den Server relevant + * @param turn neue Zugzahl + */ + public void setTurn(int turn) throws InvalidGameStateException { + int turnLimit = Constants.ROUND_LIMIT * 2; + if (turn > turnLimit) { + throw new InvalidGameStateException("Turn " + turn + " exceeded maxTurn " + turnLimit); + } + this.turn = turn; + } + + /** + * liefert die aktuelle Rundenzahl + * + * @return aktuelle Rundenzahl + */ + public int getRound() { + return turn / 2; + } + + /** + * Liefert Statusinformationen zu einem Spieler als Array mit folgenden + * Einträgen: + *
    + *
  • [0] - Punktekonto des Spielers (Flussfortschritt und Passagiere) + *
  • [1] - Anzahl eingesammelter Passagiere + *
+ * + * @param player + * Spieler + * @return Array mit Statistiken + */ + public int[] getPlayerStats(Player player) { + assert player != null; + return getPlayerStats(player.getPlayerColor()); + } + + /** + * Liefert Statusinformationen zu einem Spieler als Array mit folgenden + * Einträgen: + *
    + *
  • [0] - Punktekonto des Spielers (Flussfortschritt und Passagiere) + *
  • [1] - Anzahl eingesammelter Passagiere + *
+ * + * @param playerColor + * Farbe des Spielers + * @return Array mit Statistiken + */ + public int[] getPlayerStats(PlayerColor playerColor) { + assert playerColor != null; + + if (playerColor == PlayerColor.RED) { + return getGameStats()[Constants.GAME_STATS_RED_INDEX]; + } else { + return getGameStats()[Constants.GAME_STATS_BLUE_INDEX]; + } + } + + /** + * Liefert Statusinformationen zum Spiel. Diese sind ein Array der + * {@link #getPlayerStats(PlayerColor) Spielerstats}, wobei getGameStats()[0], + * einem Aufruf von getPlayerStats(PlayerColor.RED) entspricht. + * + * @see #getPlayerStats(PlayerColor) + * @return Statusinformationen beider Spieler + */ + public int[][] getGameStats() { + + int[][] stats = new int[2][2]; + + stats[Constants.GAME_STATS_RED_INDEX][Constants.GAME_STATS_FIELD_INDEX] = this.red.getFieldIndex(); + stats[Constants.GAME_STATS_RED_INDEX][Constants.GAME_STATS_CARROTS] = this.red.getCarrots(); + stats[Constants.GAME_STATS_BLUE_INDEX][Constants.GAME_STATS_FIELD_INDEX] = this.blue.getFieldIndex(); + stats[Constants.GAME_STATS_BLUE_INDEX][Constants.GAME_STATS_CARROTS] = this.blue.getCarrots(); + return stats; + + } + + /** + * liefert die Namen den beiden Spieler + * @return Namen der Spieler + */ + public String[] getPlayerNames() { + return new String[] { red.getDisplayName(), blue.getDisplayName() }; + + } + + /** + * Gibt die angezeigte Punktzahl des Spielers zurueck. + * @param playerColor Farbe des Spielers + * @return Punktzahl des Spielers + */ + public int getPointsForPlayer(PlayerColor playerColor) { + return getPlayer(playerColor).getFieldIndex(); + } + + /** + * Ueberschreibt das aktuelle Spielbrett. Fuer eigene Implementierungen. + */ + protected void setBoard(Board newValue) { + board = newValue; + } + + public Player getOpponent(Player player) { + return getPlayer(player.getPlayerColor().opponent()); + } + + /** + * Setzt letzten Zug. Nur für den Server relevant. + * @param lastMove letzter Zug + */ + protected void setLastMove(Move lastMove) { + this.lastMove = lastMove; + } + + /** + * Setzt letzte Aktion eines Spielers. Für den Server in der Zugvalidierung relevant. + * @param action letzte Aktion + */ + public void setLastAction(Action action) { + if (action instanceof sc.plugin2018.Skip) { + return; + } + getCurrentPlayer().setLastNonSkipAction(action); + } + + /** + * Gibt den letzten Zugzurück + * @return letzter Zug + */ + public Move getLastMove() { + return this.lastMove; + } + + + /** + * Gibt die letzte Aktion des Spielers zurück. Nötig für das erkennen von ungültigen Zügen. + * @param player Spieler + * @return letzte Aktion die nicht Skip war + */ + public Action getLastNonSkipAction(Player player) { + return getLastNonSkipAction(player.getPlayerColor()); + } + + /** + * Gibt die letzte Aktion des Spielers der entsprechenden Farbe zurück. Nötig für das erkennen von ungültigen Zügen. + * @param playerColor Spielerfarbe + * @return letzte Aktion die nicht Skip war + */ + public Action getLastNonSkipAction(PlayerColor playerColor) { + return getPlayer(playerColor).getLastNonSkipAction(); + } + + /** + * Git das Feld des derzeitigen Spielers zurück + * @return Feldtyp + */ + public sc.plugin2018.Field fieldOfCurrentPlayer() { + return this.getTypeAt(this.getCurrentPlayer().getFieldIndex()); + } + + /** + * Überprüft ob sich der derzeitige Spieler auf einem Hasenfeld befindet. + * @return true, falls auf Hasenfeld + */ + public boolean isOnHareField() + { + return fieldOfCurrentPlayer().equals(sc.plugin2018.Field.HARE); + } + + public ArrayList getPossibleMoves() { + ArrayList possibleMove = new ArrayList<>(); + ArrayList actions = new ArrayList<>(); + if (GameRuleLogic.isValidToEat(this)) { + // Wenn ein Salat gegessen werden kann, muss auch ein Salat gegessen werden + actions.add(new sc.plugin2018.EatSalad()); + Move move = new Move(actions); + possibleMove.add(move); + return possibleMove; + } + if (GameRuleLogic.isValidToExchangeCarrots(this, 10)) { + actions.add(new sc.plugin2018.ExchangeCarrots(10)); + possibleMove.add(new Move(actions)); + actions.clear(); + } + if (GameRuleLogic.isValidToExchangeCarrots(this, -10)) { + actions.add(new sc.plugin2018.ExchangeCarrots(-10)); + possibleMove.add(new Move(actions)); + actions.clear(); + } + if (GameRuleLogic.isValidToFallBack(this)) { + actions.add(new sc.plugin2018.FallBack()); + possibleMove.add(new Move(actions)); + actions.clear(); + } + // Generiere mögliche Vorwärtszüge + for (int i = 1; i <= GameRuleLogic.calculateMoveableFields(this.getCurrentPlayer().getCarrots()); i++) { + GameState clone = null; + try { + clone = this.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + // Überrüfe ob Vorwärtszug möglich ist + if (GameRuleLogic.isValidToAdvance(clone, i)) { + Advance tryAdvance = new Advance(i); + try { + tryAdvance.perform(clone); + } catch (InvalidMoveException e) { + // Sollte nicht passieren, da Zug valide ist + e.printStackTrace(); + break; + } + actions.add(tryAdvance); + // überprüfe, ob eine Karte gespielt werden muss/kann + if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { + possibleMove.addAll(clone.checkForPlayableCards(actions)); + } else { + // Füge möglichen Vorwärtszug hinzu + possibleMove.add(new Move(actions)); + } + } + actions.clear(); + } + if (possibleMove.isEmpty()) { + Move move; + logger.warn("Muss aussetzen"); + actions.add(new sc.plugin2018.Skip()); + move = new Move(actions); + possibleMove.add(move); + } + return possibleMove; + } + + /** + * Überprüft für übergebenen GameState und bisher getätigte Züge, + * ob das Ausspielen einer Karte nötig/möglich ist + * @param actions bisherige Aktionenliste + * @return mögliche Züge + */ + private ArrayList checkForPlayableCards(ArrayList actions) { + ArrayList possibleMove = new ArrayList<>(); + if (this.getCurrentPlayer().mustPlayCard()) { // überprüfe, ob eine Karte gespielt werden muss + if (GameRuleLogic.isValidToPlayEatSalad(this)) { + actions.add(new Card(sc.plugin2018.CardType.EAT_SALAD, actions.size())); + possibleMove.add(new Move(actions)); + + actions.remove(new Card(sc.plugin2018.CardType.EAT_SALAD, 1)); + } + if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 20)) { + actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); + possibleMove.add(new Move(actions)); + + actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); + } + if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, -20)) { + actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); + possibleMove.add(new Move(actions)); + + actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); + } + if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 0)) { + actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); + possibleMove.add(new Move(actions)); + + actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); + } + if (GameRuleLogic.isValidToPlayHurryAhead(this)) { + Card card = new Card(sc.plugin2018.CardType.HURRY_AHEAD, actions.size()); + actions.add(card); + // Überprüfe ob wieder auf Hasenfeld gelandet: + GameState clone = null; + try { + clone = this.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + try { + card.perform(clone); + } catch (InvalidMoveException e) { + // Sollte nie passieren + e.printStackTrace(); + } + if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { + ArrayList moves = clone.checkForPlayableCards(actions); + if (!moves.isEmpty()) { + possibleMove.addAll(moves); + } + } else { + possibleMove.add(new Move(actions)); + } + + actions.remove(new Card(sc.plugin2018.CardType.HURRY_AHEAD, actions.size())); + } + if (GameRuleLogic.isValidToPlayFallBack(this)) { + Card card = new Card(sc.plugin2018.CardType.FALL_BACK, actions.size()); + actions.add(card); + // Überprüfe ob wieder auf Hasenfeld gelandet: + GameState clone = null; + try { + clone = this.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + try { + card.perform(clone); + } catch (InvalidMoveException e) { + // Sollte nie passieren + e.printStackTrace(); + } + if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { + ArrayList moves = clone.checkForPlayableCards(actions); + if (!moves.isEmpty()) { + possibleMove.addAll(moves); + } + } else { + possibleMove.add(new Move(actions)); + } + actions.remove(new Card(sc.plugin2018.CardType.FALL_BACK, actions.size())); + } + } + return possibleMove; + } + + @Override + public String toString() { + return "GameState:\n" + + "turn=" + this.getTurn() + this.getCurrentPlayer() + + this.red + this.blue + + this.board + + this.getLastMove(); + + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt new file mode 100644 index 000000000..fd519b102 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -0,0 +1,78 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import com.thoughtworks.xstream.annotations.XStreamImplicit +import sc.api.plugins.* +import sc.shared.InvalidMoveException + +/** + * The GameState class represents the current state of the game. + * + * It holds all the information about the current round, which is used + * to calculate the next move. + * + * @property board The current game board. + * @property turn The number of turns already made in the game. + * @property lastMove The last move made in the game. + */ +@XStreamAlias(value = "state") +data class GameState @JvmOverloads constructor( + /** Das aktuelle Spielfeld. */ + override val board: Board = Board(), + /** Die Anzahl an bereits getätigten Zügen. + * Modifikation nur via [advanceTurn]. */ + @XStreamAsAttribute override var turn: Int = 0, + @XStreamImplicit + val players: List = Team.values().map { Hare(it) }, + /** Der zuletzt gespielte Zug. */ + override var lastMove: Move? = null, +): TwoPlayerGameState(players.first().team) { + + val currentPlayer + get() = getHare(currentTeam) + + val aheadPlayer + get() = players.maxByOrNull { it.position }!! + + fun getHare(team: ITeam) = + players.find { it.team == team }!! + + val currentField + get() = board.getField(currentPlayer.position) + + /** Das [Team], das am Zug ist. */ + override val currentTeam: Team + get() = currentTeamFromTurn() + + override val isOver: Boolean + get() = players.any { it.inGoal } + + override fun performMoveDirectly(move: Move) { + move.actions.forEach { + if(mustPlayCard() && it !is CardAction) + throw InvalidMoveException(MoveMistake.MUST_PLAY_CARD) + it.perform(this) + } + if(mustPlayCard()) + throw InvalidMoveException(MoveMistake.MUST_PLAY_CARD) + } + + fun mustPlayCard(player: Hare = currentPlayer) = + currentField == Field.HARE && + player.lastAction !is CardAction + + fun canEatSalad(player: Hare = currentPlayer) = + player.salads > 0 && + board.getField(player.position) == Field.SALAD && + player.lastAction != EatSalad + + override fun moveIterator(): Iterator = TODO() + + override fun clone(): GameState = + copy(board = board.clone(), players = players.clone()) + + override fun getPointsForTeam(team: ITeam): IntArray = + getHare(team).let { intArrayOf(it.position, it.salads) } + +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt new file mode 100644 index 000000000..0fa0f1232 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -0,0 +1,29 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import sc.api.plugins.Team +import sc.framework.PublicCloneable +import sc.plugin2025.util.HuIConstants + +data class Hare( + @XStreamAsAttribute val team: Team, + @XStreamAsAttribute val position: Int = 0, + @XStreamAsAttribute var salads: Int = HuIConstants.INITIAL_SALADS, + @XStreamAsAttribute var carrots: Int = HuIConstants.INITIAL_CARROTS, + @XStreamAsAttribute var lastAction: Action? = null, + private val cards: ArrayList = arrayListOf(*Card.values()), +): PublicCloneable { + fun getCards(): List = cards + fun hasCard(card: Card) = cards.contains(card) + fun removeCard(card: Card) = cards.remove(card) + + val inGoal: Boolean + get() = position == HuIConstants.NUM_FIELDS - 1 + + val canEnterGoal: Boolean + get() = carrots <= 10 && salads == 0 + + fun eatSalad() = salads-- + + override fun clone(): Hare = copy() +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Move.kt b/plugin/src/main/kotlin/sc/plugin2025/Move.kt new file mode 100644 index 000000000..969d2f4a6 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Move.kt @@ -0,0 +1,46 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamImplicit +import sc.api.plugins.IMove + +@XStreamAlias("move") +/** + * Represents a move in a game. + * + * A move consists of a list of actions. + * + * @property actions The list of actions in the move. + */ +data class Move( + @XStreamImplicit + val actions: List, +): IMove, Comparable { + + constructor(vararg actions: Action) : this(actions.asList()) + + /** + * Compares this Move instance with the specified Move for order. + * + * The comparison is based on the size of the actions list. + * The Move with a smaller size will be considered smaller, + * the Move with a larger size will be considered larger, + * and if the sizes are equal, the Moves are considered equal. + * + * @param other the Move to be compared. + * @return a negative integer if this Move is smaller than the specified Move, + * zero if they are equal in length, + * or a positive integer if this Move is larger than the specified Move. + */ + override fun compareTo(other: Move): Int = + actions.size.compareTo(other.actions.size) + + /** @return true if the specified object is a Move and contains the same actions as this move, false otherwise */ + override fun equals(other: Any?): Boolean = other is Move && actions == other.actions + + override fun hashCode(): Int = actions.hashCode() + + override fun toString(): String = + actions.joinToString(separator = ", ", prefix = "Zug[", postfix = "]") + +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt new file mode 100644 index 000000000..67dd39293 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -0,0 +1,9 @@ +package sc.plugin2025 + +import sc.shared.IMoveMistake + +enum class MoveMistake(override val message: String) : IMoveMistake { + NO_ACTIONS("Der Zug enthält keine Aktionen"), + CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), + MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden.") +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Player.java b/plugin/src/main/kotlin/sc/plugin2025/Player.java new file mode 100644 index 000000000..de8907cb2 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Player.java @@ -0,0 +1,309 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamOmitField; +import sc.framework.plugins.SimplePlayer; +import sc.shared.PlayerColor; +import sc.plugin2018.util.Constants; +import sc.shared.PlayerScore; +import sc.shared.ScoreCause; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * Ein Spieler aus Hase- und Igel. + * + */ +@XStreamAlias(value = "player") +public class Player extends SimplePlayer implements Cloneable +{ + // Farbe der Spielfigure + @XStreamAsAttribute + private PlayerColor color; + + // Position auf dem Spielbrett + @XStreamAsAttribute + private int index; + + // Anzahl der Karotten des Spielers + @XStreamAsAttribute + private int carrots; + + // Anzahl der bisher verspeisten Salate + @XStreamAsAttribute + private int salads; + + // verfügbare Hasenkarten + private ArrayList cards; + + // letzte Aktion, die kein Skip war + private Action lastNonSkipAction; + + @XStreamOmitField + private boolean mustPlayCard; + + /** + * Nur für den Server relevant. Wird innerhalb eines Zuges genutzt, um zu überpüfen, ob eine + * Karte gespielt werden muss. Muss am nach einem Zug immer false sein, sonst war Zug ungültig. + * @param mustPlayCard zu setzender Wert + */ + public void setMustPlayCard(boolean mustPlayCard) + { + this.mustPlayCard = mustPlayCard; + } + + /** + * Nur für den Server relevant. Wird innerhalb eines Zuges genutzt, um zu überpüfen, ob eine + * Karte gespielt werden muss. Muss am nach einem Zug immer false sein, sonst war Zug ungültig. + * @return true, falls eine Karte gespielt werden muss + */ + public boolean mustPlayCard() + { + return mustPlayCard; + } + + protected Player() + { + cards = new ArrayList<>(); + // only for XStream + } + + protected Player(PlayerColor color) + { + this(); + initialize(color, 0); + } + + protected Player(PlayerColor color, int position) + { + this(); + initialize(color, position); + } + + private void initialize(PlayerColor color, int index) + { + this.index = index; + this.color = color; + this.carrots = Constants.INITIAL_CARROTS; + this.salads = Constants.SALADS_TO_EAT; + + cards.add(CardType.TAKE_OR_DROP_CARROTS); + cards.add(CardType.EAT_SALAD); + cards.add(CardType.HURRY_AHEAD); + cards.add(CardType.FALL_BACK); + } + + /** + * Überprüft ob Spieler bestimmte Karte noch besitzt + * @param type Karte + * @return true, falls Karte noch vorhanden + */ + public boolean ownsCardOfType(CardType type) + { + return getCards().contains(type); + } + + /** + * Die Anzahl an Karotten die der Spieler zur Zeit auf der Hand hat. + * + * @return Anzahl der Karotten + */ + public final int getCarrots() + { + return carrots; + } + + /** + * Setzt die Karotten initial + * @param carrots Anzahl der Karotten + */ + protected final void setCarrots(int carrots) + { + this.carrots = carrots; + } + + /** + * Ändert Karottenanzahl um angegebenen Wert + * @param amount Wert um den geändert wird + */ + public final void changeCarrotsBy(int amount) + { + this.carrots = this.carrots + amount; + } + + /** + * Die Anzahl der Salate, die dieser Spieler noch verspeisen muss. + * + * @return Anzahl der übrigen Salate + */ + public final int getSalads() + { + return salads; + } + + /** + * Setzt Salate, nur für den Server relevant. Nur für Tests genutzt. + * @param salads Salate + */ + protected final void setSalads(int salads) + { + this.salads = salads; + } + + /** + * Verringert Salate um eins. Das essen eines Salats ist nicht erlaubt, sollte keiner mehr vorhanden sein. + */ + protected final void eatSalad() + { + this.salads = this.salads - 1; + } + + /** + * Gibt die für diesen Spieler verfügbaren Hasenkarten zurück. + * + * @return übrige Karten + */ + public List getCards() + { + if (this.cards == null) + { + this.cards = new ArrayList<>(); + } + + return cards; + } + + /** + * Gibt Karten ohne bestimmten Typ zurück. + * @param type Typ der zu entfernenden Karte + * @return Liste der übrigen Karten + */ + public List getCardsWithout(CardType type) + { + List res = new ArrayList<>(4); + for (CardType b : cards) + { + if (!b.equals(type)) + res.add(b); + } + return res; + } + + /** + * Setzt verfügbare Karten es Spielers. Wird vom Server beim ausführen eines Zuges verwendet. + * @param cards verfügbare Karten + */ + public void setCards(List cards) + { + this.cards = new ArrayList<>(cards); + } + + /** + * Die aktuelle Position der Figure auf dem Spielfeld. Vor dem ersten Zug + * steht eine Figure immer auf Spielfeld 0 + * + * @return Spielfeldpositionsindex + */ + public final int getFieldIndex() + { + return index; + } + + /** + * Setzt die Spielfeldposition eines Spielers. Nur für den Server relevant. + * @param pos neuer Positionsindex eines Spielers + */ + public final void setFieldIndex(final int pos) + { + index = pos; + } + + /** + * Die Farbe dieses Spielers auf dem Spielbrett + * + * @return Spielerfarbe + */ + public final PlayerColor getPlayerColor() + { + return color; + } + + /** + * Nur für den Server relevant. Setzt Spielerfarbe des Spielers. + * @param playerColor Spielerfarbe + */ + public void setPlayerColor(PlayerColor playerColor) { + this.color = playerColor; + } + + /** + * Gibt letzte Aktion des Spielers zurück. Wird vom Server zum validieren von Zügen genutzt. + * @return letzte Aktion + */ + public Action getLastNonSkipAction() { + return lastNonSkipAction; + } + + /** + * Setzt letzte Aktion des Spielers. Nur für den Server relevant beim ausführen von perform + * Es wird hier nicht überprüft, ob die Aktion Skip ist. + * @param lastNonSkipAction letzte Aktion + */ + public void setLastNonSkipAction(Action lastNonSkipAction) { + this.lastNonSkipAction = lastNonSkipAction; + } + + + /** + * Erzeugt eine deep copy eines Spielers + * @return Spieler + */ + public Player clone() + { + Player clone = null; + try + { + clone = (Player) super.clone(); + clone.cards = new ArrayList<>(); + clone.cards.addAll(this.cards); + clone.mustPlayCard = this.mustPlayCard; + clone.salads = this.salads; + clone.carrots = this.carrots; + clone.index = this.index; + if (this.lastNonSkipAction != null) { + clone.lastNonSkipAction = this.lastNonSkipAction.clone(); + } + } + catch (CloneNotSupportedException e) + { + e.printStackTrace(); + } + return clone; + } + + /** + * Überprüft, ob Spieler im Ziel. Für den Server für das Überprüfen der WinCondition relevant + * @return true, falls Spieler auf Zielfeld steht, Sekundärkriterien werden nicht geprüft. + */ + public boolean inGoal() + { + return index == Constants.NUM_FIELDS - 1; + } + + @Override + public String toString() { + String toString = "Player " + this.getDisplayName() + " (color,index,carrots,salads) " + "(" + + this.color + "," + + this.index + "," + + this.carrots + "," + + this.salads + ")\n"; + for (CardType type: this.cards) { + toString += type + "\n"; + } + toString += "LastAction " + this.lastNonSkipAction; + return toString; + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Skip.java b/plugin/src/main/kotlin/sc/plugin2025/Skip.java new file mode 100644 index 000000000..197793cd8 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/Skip.java @@ -0,0 +1,49 @@ +package sc.plugin2018; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidMoveException; + +/** + * Ein Aussetzzug. Ist nur erlaubt, sollten keine anderen Züge möglich sein. + */ +@XStreamAlias(value = "skip") +public class Skip extends Action { + + /** + * Konstruktor für einen Aussetzzug. Ein Aussetzzug sollte immer die einzige und erste Aktion eines + * Zuges sein. + */ + public Skip() { + this.order = 0; + } + + public Skip(int order) { + this.order = 0; + } + + public void perform(GameState state) throws InvalidMoveException { + // this methods does literally nothing + if(this.order > 0) { + throw new InvalidMoveException("Nur das ausspielen von Karten ist nach der ersten Aktion erlaubt."); + } + if (!GameRuleLogic.isValidToSkip(state)) { + throw new InvalidMoveException("Spieler kann noch einen anderen Zug ausführen, aussetzen ist nicht erlaubt."); + } + } + + @Override + public Skip clone() { + return new Skip(this.order); + } + + @Override + public boolean equals(Object o) { + return o instanceof Skip; + } + + @Override + public String toString() { + return "Skip order " + this.order; + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt b/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt new file mode 100644 index 000000000..a245e39dd --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt @@ -0,0 +1,9 @@ +package sc.plugin2025.util + +/** Eine Sammlung an verschiedenen Konstanten, die im Spiel verwendet werden. */ +object HuIConstants { + const val NUM_FIELDS: Int = 65 + const val INITIAL_SALADS: Int = 5 + const val INITIAL_CARROTS: Int = 68 + const val ROUND_LIMIT: Int = 30 +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt new file mode 100644 index 000000000..a938bd749 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt @@ -0,0 +1,46 @@ +package sc.plugin2025.util + +import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.api.plugins.IGameInstance +import sc.api.plugins.IGamePlugin +import sc.api.plugins.IGameState +import sc.framework.plugins.TwoPlayerGame +import sc.plugin2024.GameState +import sc.plugin2024.Move +import sc.shared.* + +@XStreamAlias(value = "winreason") +enum class HuIWinReason(override val message: String, override val isRegular: Boolean = true): IWinReason { + DIFFERING_SCORES("%s ist weiter vorne."), + DIFFERING_CARROTS("%S hat mehr Karotten übrig."), + GOAL("%s hat das Ziel zuerst erreicht."), +} + +class GamePlugin: IGamePlugin { + companion object { + const val PLUGIN_ID = "swc_2025_hase_und_igel" + val scoreDefinition: ScoreDefinition = + ScoreDefinition(arrayOf( + ScoreFragment("Siegpunkte", WinReason("%s hat gewonnen"), ScoreAggregation.SUM), + ScoreFragment("Feld", HuIWinReason.DIFFERING_SCORES, ScoreAggregation.AVERAGE), + ScoreFragment("Karotten", HuIWinReason.DIFFERING_CARROTS, ScoreAggregation.AVERAGE), + )) + } + + override val id = PLUGIN_ID + + override val scoreDefinition = + Companion.scoreDefinition + + override val turnLimit: Int = + HuIConstants.ROUND_LIMIT * 2 + + override val moveClass = Move::class.java + + override fun createGame(): IGameInstance = + TwoPlayerGame(this, GameState()) + + override fun createGameFromState(state: IGameState): IGameInstance = + TwoPlayerGame(this, state as GameState) + +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt new file mode 100644 index 000000000..e15d07131 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt @@ -0,0 +1,13 @@ +package sc.plugin2025.util + +import sc.networking.XStreamProvider +import sc.plugin2025.* + +class XStreamClasses: XStreamProvider { + + override val classesToRegister = + listOf( + Move::class.java + ) + +} \ No newline at end of file diff --git a/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider b/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider index 367d61f7b..804559dc4 100644 --- a/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider +++ b/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider @@ -1 +1 @@ -sc.plugin2024.XStreamClasses +sc.plugin2024.util.XStreamClasses diff --git a/plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt b/plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt index 6de58fc6a..747670f3f 100644 --- a/plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt @@ -12,14 +12,14 @@ import sc.api.plugins.Coordinates import sc.api.plugins.Team import sc.helpers.shouldSerializeTo import sc.helpers.testXStream -import sc.plugin2023.util.PluginConstants +import sc.plugin2023.util.PenguinConstants import sc.y class BoardTest: FunSpec({ context("Board") { val generatedBoard = Board() test("generates properly") { - generatedBoard shouldHaveSize PluginConstants.BOARD_SIZE * PluginConstants.BOARD_SIZE + generatedBoard shouldHaveSize PenguinConstants.BOARD_SIZE * PenguinConstants.BOARD_SIZE generatedBoard.forAll { it.penguin.shouldBeNull() it.fish shouldBeInRange 0..4 @@ -33,7 +33,7 @@ class BoardTest: FunSpec({ arrayOf(-1 y 1, -2 y 0, -1 y 3, -1 y 0).forAll { generatedBoard.getOrNull(it).shouldBeNull() } - (0 until PluginConstants.BOARD_SIZE).map { (it * 2) y 2 }.forAll { + (0 until PenguinConstants.BOARD_SIZE).map { (it * 2) y 2 }.forAll { val field = generatedBoard[it] field.fish shouldBeInRange 0..4 generatedBoard[it.x, it.y] shouldBe field @@ -53,11 +53,11 @@ class BoardTest: FunSpec({ val board = makeBoard(0 y 0 to 0) test("many possible moves") { // right, right down - board.possibleMovesFrom(0 y 0) shouldHaveSize 2 * (PluginConstants.BOARD_SIZE - 1) + board.possibleMovesFrom(0 y 0) shouldHaveSize 2 * (PenguinConstants.BOARD_SIZE - 1) } test("restricted moves") { board[1 y 1] = Team.ONE - board.possibleMovesFrom(0 y 0) shouldHaveSize PluginConstants.BOARD_SIZE - 1 + board.possibleMovesFrom(0 y 0) shouldHaveSize PenguinConstants.BOARD_SIZE - 1 } } xcontext("XML Serialization") { @@ -82,6 +82,6 @@ fun makeSimpleBoard(vararg fields: Field) = Board(arrayOf(arrayOf(*fields))) fun makeBoard(vararg list: Pair) = - Board(Array(PluginConstants.BOARD_SIZE) { Array(PluginConstants.BOARD_SIZE) { Field(1) } }).apply { + Board(Array(PenguinConstants.BOARD_SIZE) { Array(PenguinConstants.BOARD_SIZE) { Field(1) } }).apply { list.forEach { set(it.first, Team.values().getOrNull(it.second)) } } diff --git a/plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt index c877784ea..bfdf2a728 100644 --- a/plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt @@ -9,7 +9,7 @@ import io.kotest.matchers.string.* import sc.api.plugins.Team import sc.helpers.shouldSerializeTo import sc.helpers.testXStream -import sc.plugin2023.util.PluginConstants +import sc.plugin2023.util.PenguinConstants import sc.y class GameStateTest: FunSpec({ @@ -58,18 +58,18 @@ class GameStateTest: FunSpec({ context("move calculation") { context("initial placement") { val emptyBoard = makeBoard() - GameState(Board()).getSensibleMoves().size shouldBeInRange (PluginConstants.PENGUINS * 2)..emptyBoard.size + GameState(Board()).getSensibleMoves().size shouldBeInRange (PenguinConstants.PENGUINS * 2)..emptyBoard.size GameState(emptyBoard).getSensibleMoves() shouldHaveSize emptyBoard.size } test("first moves") { // Board with max penguins for one player - GameState(makeBoard(*Array(PluginConstants.PENGUINS) { it y it to 0 })).getSensibleMoves() shouldHaveAtLeastSize PluginConstants.PENGUINS * 2 + GameState(makeBoard(*Array(PenguinConstants.PENGUINS) { it y it to 0 })).getSensibleMoves() shouldHaveAtLeastSize PenguinConstants.PENGUINS * 2 } test("immovable") { // Board with max penguins for both players val state = GameState(Board(arrayOf( - Array(PluginConstants.PENGUINS) { Field(penguin = Team.ONE) }, - Array(PluginConstants.PENGUINS) { Field(penguin = Team.TWO) }))) + Array(PenguinConstants.PENGUINS) { Field(penguin = Team.ONE) }, + Array(PenguinConstants.PENGUINS) { Field(penguin = Team.TWO) }))) state.getSensibleMoves().shouldBeEmpty() state.board.toString() shouldBe "RRRR\nBBBB" state.immovable(Team.ONE).shouldBeTrue() diff --git a/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt b/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt index 986d0240b..42bf26431 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt @@ -14,7 +14,7 @@ import sc.api.plugins.Team import sc.helpers.checkSerialization import sc.helpers.shouldSerializeTo import sc.helpers.testXStream -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants class BoardTest: FunSpec({ context("get field by CubeCoordinates") { @@ -41,10 +41,10 @@ class BoardTest: FunSpec({ test("end of second segment") { val board = Board() board.getCoordinateByIndex(1, - PluginConstants.SEGMENT_FIELDS_WIDTH - 1, 0) shouldBe CubeCoordinates(6, -2) + MQConstants.SEGMENT_FIELDS_WIDTH - 1, 0) shouldBe CubeCoordinates(6, -2) board.getCoordinateByIndex(1, - PluginConstants.SEGMENT_FIELDS_WIDTH - 1, - PluginConstants.SEGMENT_FIELDS_HEIGHT - 1) shouldBe CubeCoordinates(4, 2) + MQConstants.SEGMENT_FIELDS_WIDTH - 1, + MQConstants.SEGMENT_FIELDS_HEIGHT - 1) shouldBe CubeCoordinates(4, 2) board[CubeCoordinates(6, -2)] shouldBe Field.WATER // top right board[CubeCoordinates(4, 2)] shouldBe Field.WATER // bottom right } @@ -219,7 +219,7 @@ class BoardTest: FunSpec({ } test("random Board has correct length") { testXStream.toXML(board) shouldHaveLineCount 64 - board.visibleSegments = PluginConstants.NUMBER_OF_SEGMENTS + board.visibleSegments = MQConstants.NUMBER_OF_SEGMENTS val full = testXStream.toXML(board) full shouldHaveLineCount 250 testXStream.fromXML(full) shouldBe board diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt index 4763badaf..0968435bb 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt @@ -20,7 +20,7 @@ import sc.plugin2024.actions.Push import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.AdvanceProblem import sc.plugin2024.mistake.MoveMistake -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants import sc.shared.InvalidMoveException class GameStateTest: FunSpec({ @@ -117,8 +117,8 @@ class GameStateTest: FunSpec({ context("getPossibleActions") { test("getPossibleAccelerations") { - gameState.getPossibleAccelerations(0).size shouldBe PluginConstants.FREE_ACC - gameState.getPossibleAccelerations(1).size shouldBe PluginConstants.FREE_ACC + 1 + gameState.getPossibleAccelerations(0).size shouldBe MQConstants.FREE_ACC + gameState.getPossibleAccelerations(1).size shouldBe MQConstants.FREE_ACC + 1 gameState.getPossibleAccelerations().size shouldBe 5 } test("getPossibleTurns") { diff --git a/plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt b/plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt index 50a3cc87d..80be154ce 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt @@ -7,7 +7,7 @@ import io.kotest.matchers.collections.* import sc.api.plugins.CubeCoordinates import sc.api.plugins.CubeDirection import sc.helpers.shouldSerializeTo -import sc.plugin2024.util.PluginConstants +import sc.plugin2024.util.MQConstants import kotlin.random.Random class SegmentTest: FunSpec({ @@ -29,7 +29,7 @@ class SegmentTest: FunSpec({ val segment = generateSegment(true, arrayOf()) segment.sumOf { it.count { it == Field.WATER } } shouldBe 17 (1..3).toList().forAll { - segment[PluginConstants.SEGMENT_FIELDS_WIDTH - 1, it] shouldBe Field.GOAL + segment[MQConstants.SEGMENT_FIELDS_WIDTH - 1, it] shouldBe Field.GOAL } } test("proper board start") { diff --git a/plugin/src/test/kotlin/sc/plugin2025/GamePlayTest.java b/plugin/src/test/kotlin/sc/plugin2025/GamePlayTest.java new file mode 100644 index 000000000..fe68ae99c --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/GamePlayTest.java @@ -0,0 +1,746 @@ +package sc.plugin2018; + +import junit.framework.Assert; +import org.junit.Before; +import org.junit.Test; +import sc.plugin2018.util.Constants; +import sc.plugin2025.GameRuleLogic; +import sc.shared.InvalidGameStateException; +import sc.shared.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GamePlayTest +{ + private Game game; + private GameState state; + private Player red; + private Player blue; + + // TODO test multiply cards (etc: fall_back->hurry_ahead->EatSalad) may need to construct board to test this + // TODO check the mustMove criteria? + + @Before + public void beforeEveryTest() { + game = new Game(); + state = game.getGameState(); + red = state.getRedPlayer(); + blue = state.getBluePlayer(); + } + + /** + * Both players start on the start field (index 0), red has to make the first move + */ + @Test + public void firstRound() + { + Assert.assertEquals(red.getPlayerColor(), PlayerColor.RED); + Assert.assertEquals(blue.getPlayerColor(), PlayerColor.BLUE); + + Assert.assertEquals(0, red.getFieldIndex()); + Assert.assertEquals(0, blue.getFieldIndex()); + Assert.assertEquals(red, state.getStartPlayer()); + Assert.assertEquals(null, game.checkGoalReached()); + Assert.assertEquals(null, game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, null, 1, 0, 68), game.getScoreFor(red)); + } + + /** + * There is only one possible move at the start of a game + */ + @Test + public void justStarted() + { + // player cannot fall_back from start field + Assert.assertFalse(GameRuleLogic.isValidToFallBack(state)); + // player cannot take carrots at start field + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + // player cannot eat salad at start field + Assert.assertFalse(GameRuleLogic.isValidToPlayEatSalad(state)); + + // the play must be able to get to a carrot field + Assert.assertTrue(GameRuleLogic.isValidToAdvance(state, state.getNextFieldByType(Field.CARROT, 0))); + } + + /** + * Tests the cases in which takeOrDrop10Carrots is invalid + */ + @Test + public void takeOrDropCarrots() + { + red.setFieldIndex(0); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + + int hareAt = state.getNextFieldByType(Field.HARE, 0); + red.setFieldIndex(hareAt); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 9)); + + int saladAt = state.getNextFieldByType(Field.SALAD, 0); + red.setFieldIndex(saladAt); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + + int pos1 = state.getNextFieldByType(Field.POSITION_1, 0); + red.setFieldIndex(pos1); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + + int pos2 = state.getNextFieldByType(Field.POSITION_2, 0); + red.setFieldIndex(pos2); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + + Assert.assertEquals(null, game.checkGoalReached()); + Assert.assertEquals(null, game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, null, 1, pos2, red.getCarrots()), game.getScoreFor(red)); + } +// + /** + * Checks if round is set correctly and currentPlayer is updated + * + */ + @Test + public void turnCounting() throws InvalidMoveException, InvalidGameStateException { + // red moves + List actions = new ArrayList<>(); + Assert.assertEquals(0, state.getRound()); + int firstCarrot = state.getNextFieldByType(Field.CARROT, red + .getFieldIndex()); + actions.add(new Advance(firstCarrot)); + Move move = new Move(actions); + move.perform(state); + + // blue moves + Assert.assertEquals(0, state.getRound()); + int nextCarrot = state.getNextFieldByType(Field.CARROT, red + .getFieldIndex()); + actions.clear(); + actions.add(new Advance(nextCarrot - blue.getFieldIndex())); + move = new Move(actions); + move.perform(state); + + // red moves + Assert.assertEquals(1, state.getRound()); + int hareAt = state.getNextFieldByType(Field.HARE, red + .getFieldIndex()); + actions.clear(); + actions.add(new Advance(hareAt - red.getFieldIndex())); + actions.add(new Card(CardType.TAKE_OR_DROP_CARROTS, 20, 1)); + move = new Move(actions); + move.perform(state); + + // blue moves + Assert.assertEquals(1, state.getRound()); + nextCarrot = state + .getNextFieldByType(Field.CARROT, blue.getFieldIndex()); + actions.clear(); + actions.add(new Advance(nextCarrot - blue.getFieldIndex())); + move = new Move(actions); + move.perform(state); + + Assert.assertEquals(state.getCurrentPlayer(), red); + + Assert.assertEquals(2, state.getRound()); + Assert.assertEquals(null, game.checkGoalReached()); + Assert.assertEquals(null, game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, null, 1, hareAt, red.getCarrots()), game.getScoreFor(red)); + } + + /** + * Checks reaching of goal + * + */ + @Test + public void enterGoalCycle() throws InvalidMoveException, InvalidGameStateException { + int lastCarrot = state.getPreviousFieldByType(Field.CARROT, 64); + int preLastCarrot = state + .getPreviousFieldByType(Field.CARROT, lastCarrot); + red.setFieldIndex(lastCarrot); + blue.setFieldIndex(preLastCarrot); + + red.setCarrots(GameRuleLogic.calculateCarrots(64 - lastCarrot)); + blue + .setCarrots(GameRuleLogic + .calculateCarrots(64 - preLastCarrot) + 1); + red.setSalads(0); + blue.setSalads(0); + List actions = new ArrayList<>(); + actions.add(new Advance(64 - red.getFieldIndex())); + Move move = new Move(actions); + + move.perform(state); + Assert.assertTrue(red.inGoal()); + Assert.assertEquals(null, game.checkGoalReached()); + Assert.assertEquals(null, game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, null, 1, 64, 0), game.getScoreFor(red)); + + actions.clear(); + actions.add(new Advance(64 - blue.getFieldIndex())); + move = new Move(actions); + move.perform(state); + Assert.assertTrue(blue.inGoal()); + Assert.assertTrue(state.isFirst(red)); + Assert.assertEquals(PlayerColor.RED, game.checkGoalReached().getPlayerColor()); + Assert.assertEquals(new WinCondition(PlayerColor.RED, Constants.IN_GOAL_MESSAGE), game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, Constants.IN_GOAL_MESSAGE, 2, 64, 0), game.getScoreFor(red)); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, Constants.IN_GOAL_MESSAGE, 0, 64, 1), game.getScoreFor(blue)); + } + + @Test + public void enterGoalOnRoundLimit() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + state.setTurn((Constants.ROUND_LIMIT-1)*2); + int lastCarrot = state.getPreviousFieldByType(Field.CARROT, 64); + int firstCarrot = state.getNextFieldByType(Field.CARROT, 0); + red.setFieldIndex(lastCarrot); + red.setSalads(0); + red.setCarrots(GameRuleLogic.calculateCarrots(64-lastCarrot)); + actions.add(new Advance(64-lastCarrot)); + Move move = new Move(actions); + // red advances to goal + move.perform(state); + actions.clear(); + // red hasn't won yet + Assert.assertTrue(red.inGoal()); + Assert.assertEquals(null, game.checkGoalReached()); + Assert.assertEquals(null, game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, null, 1, 64, 0), game.getScoreFor(red)); + // blues turn + actions.add(new Advance(firstCarrot)); + move = new Move(actions); + move.perform(state); + + // red should have won now by entering goal (not round limit) + Assert.assertTrue(state.isFirst(red)); + Assert.assertEquals(PlayerColor.RED, game.checkGoalReached().getPlayerColor()); + Assert.assertEquals(new WinCondition(PlayerColor.RED, Constants.IN_GOAL_MESSAGE), game.checkWinCondition()); + Assert.assertEquals(new PlayerScore(ScoreCause.REGULAR, Constants.IN_GOAL_MESSAGE, 2, 64, 0), game.getScoreFor(red)); + + } + + /** + * Only when newly entering a hare field a card has to be played + * + */ + @Test + public void mustPlayCard() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + actions.add(new Advance(state.getNextFieldByType(Field.HARE,0))); + actions.add(new Card(CardType.TAKE_OR_DROP_CARROTS, 1)); + Move move = new Move(actions); + move.perform(state); + + actions.clear(); + actions.add(new Advance(state.getNextFieldByType(Field.CARROT,0))); + move = new Move(actions); + move.perform(state); + + Assert.assertFalse(red.mustPlayCard()); + } + + /** + * Checks whether a player is allowed to Skip + * Testing with: + * - 0 carrots and opponent is on previous hedgehog field + */ + @Test + public void canSkip() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int redPos = state.getNextFieldByType(Field.POSITION_2, red.getFieldIndex()); + red.setFieldIndex(redPos); + red.setCarrots(0); + + int bluePos = state.getPreviousFieldByType(Field.HEDGEHOG, red.getFieldIndex()); + blue.setFieldIndex(bluePos); + actions.add(new Skip()); + + Move move = new Move(actions); + move.perform(state); // test whether red can Skip + try { + move.perform(state); // test whether blue can Skip + Assert.fail("Blue is not allowed to Skip"); + } catch(InvalidMoveException e) { + // everything is fine + } + } + + /** + * Checks all possibilities of a player to get carrots from position fields + * (as first or as second) + */ + @Test + public void onPositionField() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + red.setCarrots(5000); + blue.setCarrots(5000); + int redCarrotsBefore = red.getCarrots(); + int pos1At = state.getPreviousFieldByType(Field.POSITION_1, + state.getPreviousFieldByType(Field.POSITION_1, 64)); + actions.add(new Advance(pos1At)); + int redMoveCosts = GameRuleLogic.calculateCarrots(pos1At); + Move move = new Move(actions); + move.perform(state); + + Assert.assertEquals(redCarrotsBefore - redMoveCosts, red + .getCarrots()); + actions.clear(); + + int blueCarrotsBefore = blue.getCarrots(); + int pos2At = state.getPreviousFieldByType(Field.POSITION_2, pos1At); + actions.add(new Advance(pos2At)); + move = new Move(actions); + int blueMoveCosts = GameRuleLogic.calculateCarrots(pos2At); + move.perform(state); + actions.clear(); + + Assert.assertEquals(state.getCurrentPlayer(), red); + Assert.assertEquals(redCarrotsBefore - redMoveCosts + 10, red + .getCarrots()); // assert that red got 10 carrots for being first + + actions.add(new Advance(state.getNextFieldByType(Field.CARROT, + red.getFieldIndex()) - red.getFieldIndex())); // random valid move from red + move = new Move(actions); + move.perform(state); + + Assert.assertEquals(state.getCurrentPlayer(), blue); + Assert.assertEquals(blueCarrotsBefore - blueMoveCosts + 30, blue + .getCarrots()); // assert that red got 30 carrots for being second + } + + /** + * Checks whether it is only allowed to drop 20 carrots iff player has at least 20 + */ + @Test + public void playDropCarrotsCard() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + red.setFieldIndex(state.getNextFieldByType(Field.HARE, 0)); + actions.add(new Card(CardType.TAKE_OR_DROP_CARROTS, -20 ,0)); + + Assert.assertTrue(red.getCarrots() > 20); + Move move = new Move(actions); + move.perform(state); + + blue.setCarrots(19); + try { + move.perform(state); + Assert.fail("Not enough carrots"); + } catch (InvalidMoveException e) { + // everything is fine + } + } + + /** + * Checks the conditions for advancing to the goal field + */ + @Test + public void enterGoal() + { + int carrotAt = state.getPreviousFieldByType(Field.CARROT, 64); + red.setFieldIndex(carrotAt); + int toGoal = 64 - red.getFieldIndex(); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, toGoal)); + + red.setCarrots(10); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, toGoal)); + + red.setSalads(0); + Assert.assertTrue(red.getSalads() == 0); + Assert.assertTrue(red.getCarrots() <= 10); + Assert.assertTrue(GameRuleLogic.isValidToAdvance(state, toGoal)); + } + + /** + * Checks whether game ends only after a round (blue has last move) + */ + @Test + public void blueHasLastMove() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int carrotAt = state.getPreviousFieldByType(Field.CARROT, 64); + red.setFieldIndex(carrotAt); + int toGoal = 64 - red.getFieldIndex(); + actions.add(new Advance(toGoal)); + red.setCarrots(10); + red.setSalads(0); + Move move = new Move(actions); + move.perform(state); + Assert.assertEquals(null, game.checkWinCondition()); + } + + /** + * Checks whether game ends only after a round (red has no last move) + */ + @Test + public void redHasNoLastMove() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int firstCarrot = state.getNextFieldByType(Field.CARROT, 0); + actions.add(new Advance(firstCarrot)); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + int carrotAt = state.getPreviousFieldByType(Field.CARROT, 64); + blue.setFieldIndex(carrotAt); + int toGoal = 64 - blue.getFieldIndex(); + actions.add(new Advance(toGoal)); + blue.setCarrots(10); + blue.setSalads(0); + move = new Move(actions); + move.perform(state); + Assert.assertEquals(blue.getPlayerColor(), game.checkWinCondition().getWinner()); + } + + /** + * Checks the conditions for eating a salad on a salad field + */ + @Test + public void eatSalad() + { + int saladAt = state.getNextFieldByType(Field.SALAD, 0); + red.setFieldIndex(saladAt); + Assert.assertTrue(GameRuleLogic.isValidToEat(state)); + red.setSalads(0); + Assert.assertFalse(GameRuleLogic.isValidToEat(state)); + } + + /** + * Checks the conditions for eating a salad on a salad field + */ + @Test + public void mustEatSalad() + { + int hedgehog = state.getNextFieldByType(Field.HEDGEHOG, 0); + int saladAt = state.getNextFieldByType(Field.SALAD, hedgehog); + int carrot = state.getNextFieldByType(Field.CARROT, saladAt); + red.setFieldIndex(saladAt); + red.setLastNonSkipAction(new Advance(1)); + Assert.assertTrue(GameRuleLogic.isValidToEat(state)); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, carrot - saladAt)); + Assert.assertFalse(GameRuleLogic.isValidToFallBack(state)); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + Assert.assertFalse(GameRuleLogic.isValidToSkip(state)); + red.setLastNonSkipAction(new Card(CardType.HURRY_AHEAD)); + Assert.assertTrue(GameRuleLogic.isValidToEat(state)); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, carrot - saladAt)); + Assert.assertFalse(GameRuleLogic.isValidToFallBack(state)); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + Assert.assertFalse(GameRuleLogic.isValidToSkip(state)); + red.setLastNonSkipAction(new Card(CardType.FALL_BACK)); + Assert.assertTrue(GameRuleLogic.isValidToEat(state)); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, carrot - saladAt)); + Assert.assertFalse(GameRuleLogic.isValidToFallBack(state)); + Assert.assertFalse(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + Assert.assertFalse(GameRuleLogic.isValidToSkip(state)); + } + + /** + * Checks the perform method for eating salad + * + */ + @Test + public void eatSaladCycle() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + red.setCarrots(100); + int saladAt = state.getNextFieldByType(Field.SALAD, 0); + int carrotAt = state.getNextFieldByType(Field.CARROT, 0); + actions.add(new Advance(saladAt)); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new Advance(carrotAt)); + move = new Move(actions); + move.perform(state); + actions.clear(); + + int saladsBefore = red.getSalads(); + int carrotsBefore = red.getCarrots(); + actions.add(new EatSalad()); + move = new Move(actions); + move.perform(state); + Assert.assertEquals(saladsBefore - 1, red.getSalads()); + Assert.assertEquals(carrotsBefore + 10, red.getCarrots()); + } + + /** + * Checks the perform method when using a hare joker + */ + @Test + public void playCardCycle() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int hareAt = state.getNextFieldByType(Field.HARE, 0); + actions.add(new Advance(hareAt)); + actions.add(new Card(CardType.TAKE_OR_DROP_CARROTS, 20, 1)); + Move move = new Move(actions); + Assert.assertTrue(red.getCards().contains(CardType.TAKE_OR_DROP_CARROTS)); + move.perform(state); + actions.clear(); + Assert.assertFalse(red.getCards().contains(CardType.TAKE_OR_DROP_CARROTS)); + } + + /** + * Checks the perform method when taking carrots + * + */ + @Test + public void takeCarrotsCycle() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int carrotsAt = state.getNextFieldByType(Field.CARROT, 0); + actions.add(new Advance(carrotsAt)); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + + carrotsAt = state.getNextFieldByType(Field.CARROT, red.getFieldIndex()); + actions.add(new Advance(carrotsAt)); + move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new ExchangeCarrots(10)); + Assert.assertTrue(GameRuleLogic.isValidToExchangeCarrots(state, 10)); + int carrotsBefore = red.getCarrots(); + move = new Move(actions); + move.perform(state); + Assert.assertEquals(carrotsBefore + 10, red.getCarrots()); + } + + /** + * Checks the perform method when dropping carrots + * + */ + @Test + public void dropCarrotsCycle() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int carrotsAt = state.getNextFieldByType(Field.CARROT, 0); + actions.add(new Advance(carrotsAt)); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + + carrotsAt = state.getNextFieldByType(Field.CARROT, red.getFieldIndex()); + actions.add(new Advance(carrotsAt)); + move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new ExchangeCarrots(-10)); + move = new Move(actions); + Assert.assertTrue(GameRuleLogic.isValidToExchangeCarrots(state,-10)); + int carrotsBefore = red.getCarrots(); + + move.perform(state); + Assert.assertEquals(carrotsBefore - 10, red.getCarrots()); + } + + /** + * Checks that a hare joker can only be played on a hare field + */ + @Test + public void actioncardOnField() + { + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + int carrotAt = state.getNextFieldByType(Field.CARROT, 0); + int pos1At = state.getNextFieldByType(Field.POSITION_1, 0); + int pos2At = state.getNextFieldByType(Field.POSITION_2, 0); + int hedgehogAt = state.getNextFieldByType(Field.HEDGEHOG, 0); + int goalAt = state.getNextFieldByType(Field.GOAL, 0); + int saladAt = state.getNextFieldByType(Field.SALAD, 0); + + red.setFieldIndex(carrotAt); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + red.setFieldIndex(pos1At); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + red.setFieldIndex(pos2At); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + red.setFieldIndex(hedgehogAt); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + red.setFieldIndex(goalAt); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + red.setFieldIndex(saladAt); + Assert.assertFalse(GameRuleLogic.canPlayCard(state)); + + } + + /** + * It is not allowed to advance to a hedgehog field or to use a card to move to it + */ + @Test + public void directMoveOntoHedgehog() { + int hedgehog = state.getNextFieldByType(Field.HEDGEHOG, 0); + + // advance + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, hedgehog)); + + // fall back + blue.setFieldIndex(hedgehog + 1); + int hare = state.getNextFieldByType(Field.HARE, blue.getFieldIndex()); + red.setFieldIndex(hare); + + Assert.assertFalse(GameRuleLogic.isValidToPlayFallBack(state)); + + + // hurry ahead + blue.setFieldIndex(hedgehog - 1); + hare = state.getNextFieldByType(Field.HARE, 0); + red.setFieldIndex(hare); + Assert.assertFalse(GameRuleLogic.isValidToPlayHurryAhead(state)); + } + + /** + * It is not allowed to enter a hare field without a card + */ + @Test + public void moveOntoHareWithoutCard() + { + int hare = state.getNextFieldByType(Field.HARE, 0); + red.setCards(Collections.emptyList()); + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, hare)); + } + + /** + * It is not allowed a use a hare koker to enter a hare field, if no other hare card is available + */ + @Test + public void indirectHurryAheadOntoHare() + { + int firstHare = state.getNextFieldByType(Field.HARE, 0); + int secondHare = state.getNextFieldByType(Field.HARE, firstHare + 1); + + blue.setFieldIndex(secondHare - 1); + red.setCards(Collections.singletonList(CardType.HURRY_AHEAD)); + + Assert.assertFalse(GameRuleLogic.isValidToAdvance(state, firstHare)); + ArrayList cards = new ArrayList<>(); + cards.add(CardType.HURRY_AHEAD); + cards.add(CardType.EAT_SALAD); + red.setCards(cards); + Assert.assertTrue(GameRuleLogic.isValidToAdvance(state, firstHare)); + } + + /** + * Checks if a player is allowed to fall back to a hedgehog field + */ + @Test + public void fallback() + { + int firstHedgehog = state.getNextFieldByType(Field.HEDGEHOG, 0); + + int carrotAfter = state.getNextFieldByType(Field.CARROT,firstHedgehog + 1); + red.setFieldIndex(carrotAfter); + + Assert.assertTrue(GameRuleLogic.isValidToFallBack(state)); + } + + /** + * Checks to perform method when falling back + */ + @Test + public void fallbackCycle() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int firstHedgehog = state.getNextFieldByType(Field.HEDGEHOG, 0); + int carrotAfter = state.getNextFieldByType(Field.CARROT, + firstHedgehog + 1); + actions.add(new Advance(carrotAfter)); + red.setCarrots(200); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new Advance(state.getNextFieldByType(Field.CARROT, 0))); + + move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new FallBack()); + int carrotsBefore = red.getCarrots(); + int diff = red.getFieldIndex() - firstHedgehog; + move = new Move(actions); + move.perform(state); + Assert.assertEquals(carrotsBefore + diff * 10, red.getCarrots()); + } + + /** + * A player is allowed to fall back, even if he did the same last turn + */ + @Test + public void fallbackTwice() throws InvalidMoveException, InvalidGameStateException { + List actions = new ArrayList<>(); + int firstHedgehog = state.getNextFieldByType(Field.HEDGEHOG, red + .getFieldIndex()); + int carrotAt = state.getNextFieldByType(Field.CARROT, firstHedgehog); + int secondHedgehog = state.getNextFieldByType(Field.HEDGEHOG, carrotAt); + carrotAt = state.getNextFieldByType(Field.CARROT, secondHedgehog); + + red.setFieldIndex(carrotAt); + actions.add(new FallBack()); + Move move = new Move(actions); + move.perform(state); + actions.clear(); + + Assert.assertEquals(red.getFieldIndex(), secondHedgehog); + + actions.add(new Advance(state.getNextFieldByType(Field.POSITION_2, 0))); + move = new Move(actions); + move.perform(state); + actions.clear(); + + actions.add(new FallBack()); + Assert.assertTrue(GameRuleLogic.isValidToFallBack(state)); + move = new Move(actions); + move.perform(state); + Assert.assertEquals(red.getFieldIndex(), firstHedgehog); + } + + @Test + public void redWinsDrawTest() throws InvalidMoveException, InvalidGameStateException { + red.setSalads(0); + blue.setSalads(0); + int redIndex = state.getPreviousFieldByType(Field.CARROT, 64); + red.setFieldIndex(redIndex); + red.setCarrots(GameRuleLogic.calculateCarrots(64 - redIndex)); + int blueIndex = state.getPreviousFieldByType(Field.CARROT, redIndex); + blue.setFieldIndex(blueIndex); + blue.setCarrots(GameRuleLogic.calculateCarrots(64 - blueIndex)); + { // red Move + List actions = new ArrayList<>(); + actions.add(new Advance(64 - redIndex)); + Move move = new Move(actions); + move.perform(state); + } + + { // blue Move + List actions = new ArrayList<>(); + actions.add(new Advance(64 - blueIndex)); + Move move = new Move(actions); + move.perform(state); + } + Assert.assertEquals(new WinCondition(PlayerColor.RED, Constants.IN_GOAL_MESSAGE), game.checkWinCondition()); + + } + + @Test + public void doubleCardFromSaladToSalad() throws InvalidMoveException, InvalidGameStateException { + Board board = new Board(0); + state.setBoard(board); + red.setFieldIndex(42); + red.setLastNonSkipAction(new EatSalad()); + red.setSalads(1); + red.setCarrots(34); + List cardTypeList = new ArrayList<>(); + cardTypeList.add(CardType.HURRY_AHEAD); + cardTypeList.add(CardType.FALL_BACK); + red.setCards(cardTypeList); + blue.setFieldIndex(41); + { // red Moves and plays 2 cards + Move move = new Move(new Advance(3, 0), new Card(CardType.FALL_BACK, 1), new Card(CardType.HURRY_AHEAD, 2)); + move.perform(state); + } + Assert.assertEquals(42, red.getFieldIndex()); + } + +} diff --git a/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java b/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java new file mode 100644 index 000000000..d44de4ab5 --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java @@ -0,0 +1,34 @@ +package sc.plugin2018; + +import junit.framework.Assert; +import org.junit.Test; +import sc.shared.PlayerColor; + +import java.util.List; + +/** + * @author rra + * @since Jul 4, 2009 + * + */ +public class PlayerTest +{ + /** + * Überprüft das ein Spieler mit den richtigen Anfangswerten erstellt wird. + */ + @Test + public void testPlayer() + { + Player red = new Player(PlayerColor.RED); + Assert.assertEquals(PlayerColor.RED, red.getPlayerColor()); + Assert.assertEquals(68, red.getCarrots()); + Assert.assertEquals(5, red.getSalads()); + Assert.assertEquals(false, red.inGoal()); + Assert.assertEquals(false, red.mustPlayCard()); + Assert.assertEquals(0, red.getFieldIndex()); + Assert.assertEquals(true, red.ownsCardOfType(CardType.EAT_SALAD)); + Assert.assertEquals(true, red.ownsCardOfType(CardType.HURRY_AHEAD)); + Assert.assertEquals(true, red.ownsCardOfType(CardType.FALL_BACK)); + Assert.assertEquals(true, red.ownsCardOfType(CardType.TAKE_OR_DROP_CARROTS)); + } +} diff --git a/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java b/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java new file mode 100644 index 000000000..671943bc6 --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java @@ -0,0 +1,35 @@ +package sc.plugin2018.util; + +import junit.framework.Assert; + +import org.junit.Test; + +/** + * @author rra + * @since Jul 4, 2009 + * + */ +public class TestGameUtil +{ + /** + * Überprüft die Berechnungen der calculateCarrots() Hilfsfunktion + */ + @Test + public void testCalculateCarrots() + { + Assert.assertEquals(1, GameRuleLogic.calculateCarrots(1)); + Assert.assertEquals(55, GameRuleLogic.calculateCarrots(10)); + } + + /** + * Überprüft die Berechnung der calculateMoveableFields() Hilfsfunktion + */ + @Test + public void testCalculateMoveableFields() { + Assert.assertEquals(0, GameRuleLogic.calculateMoveableFields(0)); + Assert.assertEquals(1, GameRuleLogic.calculateMoveableFields(1)); + Assert.assertEquals(2, GameRuleLogic.calculateMoveableFields(5)); + Assert.assertEquals(3, GameRuleLogic.calculateMoveableFields(6)); + Assert.assertEquals(3, GameRuleLogic.calculateMoveableFields(7)); + } +} diff --git a/sdk/src/main/framework/sc/shared/MoveIterator.kt b/sdk/src/main/framework/sc/shared/MoveIterator.kt new file mode 100644 index 000000000..c8096e540 --- /dev/null +++ b/sdk/src/main/framework/sc/shared/MoveIterator.kt @@ -0,0 +1,33 @@ +package sc.shared + +import sc.api.plugins.IGameState +import sc.api.plugins.IMove + +class MoveIterator(state: GameState, val makeMove: (List) -> Move, val process: (GameState, Move) -> List): Iterator { + val queue = ArrayDeque>>(64) + + init { + queue.add(state to listOf()) + } + + fun process(): List { + val (state, move) = queue.removeFirst() + process() + return move + } + + fun findNext() { + while(queue.isNotEmpty()) + process() + } + + override fun hasNext(): Boolean { + findNext() + return queue.isNotEmpty() + } + + override fun next(): Move { + findNext() + return makeMove(process()) + } +} \ No newline at end of file From 7cdf574359ee8a81fc240ed5fdda3a6535fa41aa Mon Sep 17 00:00:00 2001 From: maxblan Date: Sat, 4 May 2024 18:41:55 +0200 Subject: [PATCH 06/38] Rename .java to .kt --- plugin/src/main/kotlin/sc/plugin2025/{Advance.java => Advance.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugin/src/main/kotlin/sc/plugin2025/{Advance.java => Advance.kt} (100%) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.java b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2025/Advance.java rename to plugin/src/main/kotlin/sc/plugin2025/Advance.kt From 6bcda798c539846d84f521156b68a8ad2101636f Mon Sep 17 00:00:00 2001 From: maxblan Date: Sat, 4 May 2024 18:41:55 +0200 Subject: [PATCH 07/38] feat(plugin25): adapt GameRuleLogic to new plugin structure --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 80 +--- plugin/src/main/kotlin/sc/plugin2025/Field.kt | 2 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 411 +++++++----------- .../main/kotlin/sc/plugin2025/GameState.kt | 5 +- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- .../main/kotlin/sc/plugin2025/MoveMistake.kt | 3 +- 6 files changed, 184 insertions(+), 319 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index f188f5298..f9404ec7f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -1,72 +1,28 @@ -package sc.plugin2018; +package sc.plugin2025 -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidMoveException; +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import sc.plugin2025.GameRuleLogic.calculateCarrots +import sc.plugin2025.GameRuleLogic.isValidToAdvance +import sc.shared.IMoveMistake /** * Ein Vorwärtszug, um spezifizierte Distanz. Verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 * berechnet (Gaußsche Summe) */ @XStreamAlias(value = "advance") -public class Advance extends Action{ - - @XStreamAsAttribute - private int distance; - - public Advance(int distance) { - super(); - this.distance = distance; - } - - public Advance(int distance, int order) { - this.order = order; - this.distance = distance; - } - - @Override - public void perform(GameState state) throws InvalidMoveException { - if (GameRuleLogic.isValidToAdvance(state, this.distance)) { - state.getCurrentPlayer().changeCarrotsBy(- GameRuleLogic.calculateCarrots(this.distance)); - state.getCurrentPlayer().setFieldIndex(state.getCurrentPlayer().getFieldIndex() + distance); - if (state.getTypeAt(state.getCurrentPlayer().getFieldIndex()) == Field.HARE) { - state.getCurrentPlayer().setMustPlayCard(true); - } - // Setze letzte Aktion - state.setLastAction(this); - } else { - throw new InvalidMoveException("Vorwärtszug um " + this.distance + " Felder ist nicht möglich"); - } - } - - /** - * Gibt das Dinstanzattribut zurück - * @return Distanz - */ - public int getDistance() { - return distance; - } - - @Override - public Advance clone() { - return new Advance(this.distance, this.order); - } - - @Override - public boolean equals(Object o) { - if(o instanceof Advance) { - return (this.distance == ((Advance) o).distance); +class Advance(@XStreamAsAttribute val distance: Int) : Action { + + override fun perform(state: GameState): IMoveMistake? { + if (isValidToAdvance(state, this.distance)) { + state.currentPlayer.carrots -= calculateCarrots(this.distance) + state.currentPlayer.position += distance + return null + } else { + return MoveMistake.CANNOT_MOVE_FORWARD + } } - return false; - } - @Override - public String toString() { - StringBuilder b = new StringBuilder("Advance: distance "); - b.append(this.distance); - b.append(" order "); - b.append(this.order); - return b.toString(); - } + override fun equals(other: Any?) = other is Advance && this.distance == other.distance + override fun hashCode(): Int = distance } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt index 631066593..1053dc278 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Field.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -2,7 +2,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias -/** Die unterschiedlichen Spielfelder aus dem Hase und Igel Original. */ +/** Die unterschiedlichen Spielfelder aus dem Hasen und Igel Original. */ @XStreamAlias(value = "field") enum class Field(val short: String, val unicode: String = short) { /** diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 2d9f8e663..260c54be8 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -10,9 +10,8 @@ object GameRuleLogic { * @param moveCount Anzahl der Felder, um die bewegt wird * @return Anzahl der benötigten Karotten */ - fun calculateCarrots(moveCount: Int): Int = - (moveCount * (moveCount + 1)) / 2 - + fun calculateCarrots(moveCount: Int): Int = (moveCount * (moveCount + 1)) / 2 + /** * Berechnet, wie viele Züge mit `carrots` Karotten möglich sind. * @@ -27,8 +26,7 @@ object GameRuleLogic { else -> (sqrt((2.0 * carrots) + 0.25) - 0.48).toInt() } } - - /* + /** * Überprüft `Advance` Aktionen auf ihre Korrektheit. * Folgende Spielregeln werden beachtet: @@ -44,90 +42,60 @@ object GameRuleLogic { * @return true, falls ein Vorwärtszug möglich ist */ fun isValidToAdvance(state: GameState, distance: Int): Boolean { - if(distance <= 0) { - return false - } + if (distance <= 0) return false + val player: Hare = state.currentPlayer - if(mustEatSalad(state)) { - return false - } - var valid = true + if (mustEatSalad(state)) return false + val requiredCarrots = calculateCarrots(distance) - valid = valid && (requiredCarrots <= player.getCarrots()) - - val newPosition: Int = player.getFieldIndex() + distance - valid = valid && !state.isOccupied(newPosition) - val type: Field = state.board.getTypeAt(newPosition) - when(type) { - INVALID -> valid = false - Field.SALAD -> valid = valid && player.getSalads() > 0 + val hasEnoughCarrots = requiredCarrots <= player.carrots + + val newPosition: Int = player.position + distance + val isNotOnOtherPlayer = state.otherPlayer.position != newPosition + + return when (state.board.getField(newPosition)) { + Field.SALAD -> player.salads > 0 Field.HARE -> { - var state2: GameState? = null - try { - state2 = state.clone() - } catch(e: CloneNotSupportedException) { - e.printStackTrace() - } - state2.setLastAction(Advance(distance)) - state2!!.currentPlayer.setFieldIndex(newPosition) - state2.currentPlayer.changeCarrotsBy(-requiredCarrots) - valid = valid && canPlayAnyCard(state2) - } - - Field.GOAL -> { - val carrotsLeft: Int = player.getCarrots() - requiredCarrots - valid = valid && carrotsLeft <= 10 - valid = valid && player.getSalads() === 0 + val advanceMove = Move(listOf(Advance(distance))) + val nextState: GameState = state.performMove(advanceMove) as GameState + canPlayAnyCard(nextState) } - - Field.HEDGEHOG -> valid = false - Field.CARROT, Field.POSITION_1, Field.START, Field.POSITION_2 -> {} - else -> throw IllegalStateException("Unknown Type $type") - - } - return valid + + Field.GOAL -> player.carrots - requiredCarrots <= 10 && player.salads == 0 + Field.HEDGEHOG -> false + else -> true + } && hasEnoughCarrots && isNotOnOtherPlayer } - + /** * Überprüft, ob ein Spieler aussetzen darf. Er darf dies, wenn kein anderer Zug möglich ist. * @param state GameState * @return true, falls der derzeitige Spieler keine andere Aktion machen kann. */ - fun isValidToSkip(state: GameState): Boolean { - return !canDoAnything(state) - } - + fun isValidToSkip(state: GameState): Boolean = !canDoAnything(state) + /** - * Überprüft, ob ein Spieler einen Zug (keinen Aussetzug) + * Überprüft, ob ein Spieler einen Zug (keine Aussetzung) * @param state GameState * @return true, falls ein Zug möglich ist. */ - private fun canDoAnything(state: GameState): Boolean { - return (canPlayAnyCard(state) || isValidToFallBack(state) - || isValidToExchangeCarrots(state, 10) - || isValidToExchangeCarrots(state, -10) - || isValidToEat(state) || canAdvanceToAnyField(state)) - } - + private fun canDoAnything(state: GameState): Boolean = + (canPlayAnyCard(state) || isValidToFallBack(state) || isValidToExchangeCarrots(state, 10) + || isValidToExchangeCarrots(state, -10) || isValidToEat(state) || canAdvanceToAnyField(state)) + /** - * Überprüft ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. + * Überprüft, ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. * @param state GameState * @return true, falls der Spieler irgendeinen Vorwärtszug machen kann */ - private fun canAdvanceToAnyField(state: GameState): Boolean { - val fields = calculateMoveableFields(state.currentPlayer.carrots) - for(i in 0..fields) { - if(isValidToAdvance(state, i)) { - return true - } - } - - return false - } - + private fun canAdvanceToAnyField(state: GameState): Boolean = + (0..calculateMoveableFields(state.currentPlayer.carrots)) + .any { isValidToAdvance(state, it) } + + /** * Überprüft `EatSalad` Züge auf Korrektheit. Um einen Salat - * zu verzehren muss der Spieler sich: + * zu verzehren, muss der Spieler sich: * * - auf einem Salatfeld befinden * - noch mindestens einen Salat besitzen @@ -138,16 +106,14 @@ object GameRuleLogic { */ fun isValidToEat(state: GameState): Boolean { val player: Hare = state.currentPlayer - var valid = true - val currentField: Field = state.getTypeAt(player.getFieldIndex()) - - valid = valid && (currentField == Field.SALAD) - valid = valid && (player.getSalads() > 0) - valid = valid && !playerMustAdvance(state) - - return valid + + val isSaladField = state.board.getField(player.position) == Field.SALAD + val hasSalad = player.salads > 0 + val mustNotAdvance = !playerMustAdvance(state) + + return isSaladField && hasSalad && mustNotAdvance } - + /** * Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss. * @param state GameState @@ -155,53 +121,36 @@ object GameRuleLogic { */ fun playerMustAdvance(state: GameState?): Boolean { val player: Hare = state!!.currentPlayer - val type: Field = state.getTypeAt(player.getFieldIndex()) - - if(type == Field.HEDGEHOG || type == Field.START) { - return true - } - - val lastAction: Action = state.getLastNonSkipAction(player) - - if(lastAction != null) { - if(lastAction is EatSalad) { - return true - } else if(lastAction is Card) { - // the player has to leave a hare field in next turn - if((lastAction as Card).getType() === Card.EAT_SALAD) { - return true - } else if((lastAction as Card).getType() === Card.TAKE_OR_DROP_CARROTS) // the player has to leave the hare field - { - return true - } - } + val type: Field = state.board.getField(player.position) + + if (type == Field.HEDGEHOG || type == Field.START) return true + + val lastAction: Action? = player.lastAction + + if (lastAction is EatSalad) return true + else if (lastAction is CardAction) { + // the player has to leave a hare field in next turn + if (lastAction.card === Card.EAT_SALAD) return true + // the player has to leave the hare field + else if (lastAction.card === Card.TAKE_OR_DROP_CARROTS) return true } - return false } - + + /** - * Überprüft ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. + * Überprüft, ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. * @param state GameState * @param n 10 oder -10 je nach Fragestellung * @return true, falls die durch n spezifizierte Aktion möglich ist. */ - fun isValidToExchangeCarrots(state: GameState, n: Int): Boolean { - val player: Hare = state.currentPlayer - val valid: Boolean = state.getTypeAt(player.getFieldIndex()).equals(Field.CARROT) - if(n == 10) { - return valid - } - if(n == -10) { - return if(player.getCarrots() >= 10) { - valid - } else { - false - } - } - return false + fun isValidToExchangeCarrots(state: GameState, n: Int) = with(state) { + val player = currentPlayer + val valid = board.getField(player.position) == Field.CARROT + n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) } - + + /** * Überprüft `FallBack` Züge auf Korrektheit * @@ -209,192 +158,148 @@ object GameRuleLogic { * @return true, falls der currentPlayer einen Rückzug machen darf */ fun isValidToFallBack(state: GameState): Boolean { - if(mustEatSalad(state)) { - return false - } - var valid = true - val newPosition: Int = state.getPreviousFieldByType( - Field.HEDGEHOG, state.currentPlayer - .getFieldIndex() - ) - valid = valid && (newPosition != -1) - valid = valid && !state.isOccupied(newPosition) - return valid + if (mustEatSalad(state)) return false + val newPosition: Int? = state.board.getPreviousField(Field.HEDGEHOG, state.currentPlayer.position) + return (newPosition != -1) && state.otherPlayer.position != newPosition } - + /** - * Überprüft ob der derzeitige Spieler die `FALL_BACK` Karte spielen darf. + * Überprüft, ob der derzeitige Spieler die `FALL_BACK` Karte spielen darf. * @param state GameState * @return true, falls die `FALL_BACK` Karte gespielt werden darf */ - fun isValidToPlayFallBack(state: GameState?): Boolean { - val player: Hare = state!!.currentPlayer - var valid = (!playerMustAdvance(state) && state.isOnHareField() - && state.isFirst(player)) - - valid = valid && player.ownsCardOfType(Card.FALL_BACK) - - val o: Hare = state.getOpponent(player) - val nextPos: Int = o.getFieldIndex() - 1 - if(nextPos == 0) { - return false - } - - val type: Field = state.getTypeAt(nextPos) - when(type) { - INVALID, Field.HEDGEHOG -> valid = false - Field.START -> {} - Field.SALAD -> valid = valid && player.getSalads() > 0 + fun isValidToPlayFallBack(state: GameState): Boolean { + val player: Hare = state.currentPlayer + + val mustNotAdvance = !playerMustAdvance(state) + val isOnHare = state.board.getField(player.position) == Field.HARE + // TODO() val isFirst = state.players.firstOrNull() == player // das ist denke falsch, aber ich war mir nicht sicher was `isFirst` war + val hasFallback = player.getCards().any { it == Card.FALL_BACK } + + val nextPos: Int = state.otherPlayer.position - 1 + if (nextPos == 0) return false + + return when (state.board.getField(nextPos)) { + Field.SALAD -> player.salads > 0 Field.HARE -> { - var state2: GameState? = null - try { - state2 = state.clone() - } catch(e: CloneNotSupportedException) { - e.printStackTrace() - } - state2.setLastAction(Card(Card.FALL_BACK)) - state2!!.currentPlayer.setFieldIndex(nextPos) - state2.currentPlayer.setCards(player.getCardsWithout(Card.FALL_BACK)) - valid = valid && canPlayAnyCard(state2) + val fallBack = Move(listOf(CardAction.PlayCard(Card.FALL_BACK))) + val nextState: GameState = state.performMove(fallBack) as GameState + canPlayAnyCard(nextState) } - - Field.CARROT, Field.POSITION_1, Field.POSITION_2 -> {} - else -> throw IllegalStateException("Unknown Type $type") - } - return valid + + Field.HEDGEHOG -> false + else -> true + } && mustNotAdvance && isOnHare && hasFallback } - + /** - * Überprüft ob der derzeitige Spieler die `HURRY_AHEAD` Karte spielen darf. + * Überprüft, ob der derzeitige Spieler die `HURRY_AHEAD` Karte spielen darf. * @param state GameState * @return true, falls die `HURRY_AHEAD` Karte gespielt werden darf */ - fun isValidToPlayHurryAhead(state: GameState?): Boolean { - val player: Hare = state!!.currentPlayer - var valid = (!playerMustAdvance(state) && state.isOnHareField() - && !state.isFirst(player)) - valid = valid && player.ownsCardOfType(Card.HURRY_AHEAD) - - val o: Hare = state.getOpponent(player) - val nextPos: Int = o.getFieldIndex() + 1 - - val type: Field = state.getTypeAt(nextPos) - when(type) { - INVALID, Field.HEDGEHOG -> valid = false - Field.SALAD -> valid = valid && player.getSalads() > 0 + fun isValidToPlayHurryAhead(state: GameState): Boolean { + val player: Hare = state.currentPlayer + + val mustNotAdvance = !playerMustAdvance(state) + val isOnHare = state.board.getField(player.position) == Field.HARE + // TODO() val isFirst = state.players.firstOrNull() == player // das ist denke falsch, aber ich war mir nicht sicher was `isFirst` war + val hasHurry = player.getCards().any { it == Card.HURRY_AHEAD } + + val nextPos: Int = state.otherPlayer.position + 1 + if (nextPos == 0) return false + + return when (state.board.getField(nextPos)) { + Field.SALAD -> player.salads > 0 Field.HARE -> { - var state2: GameState? = null - try { - state2 = state.clone() - } catch(e: CloneNotSupportedException) { - e.printStackTrace() - } - state2.setLastAction(Card(Card.HURRY_AHEAD)) - state2!!.currentPlayer.setFieldIndex(nextPos) - state2.currentPlayer.setCards(player.getCardsWithout(Card.HURRY_AHEAD)) - valid = valid && canPlayAnyCard(state2) + val fallBack = Move(listOf(CardAction.PlayCard(Card.HURRY_AHEAD))) + val nextState: GameState = state.performMove(fallBack) as GameState + canPlayAnyCard(nextState) } - - Field.GOAL -> valid = valid && canEnterGoal(state) - Field.CARROT, Field.POSITION_1, Field.POSITION_2, Field.START -> {} - else -> throw IllegalStateException("Unknown Type $type") - } - return valid + + Field.GOAL -> player.carrots - calculateCarrots(nextPos - player.position) <= 10 && player.salads == 0 + Field.HEDGEHOG -> false + else -> true + } && mustNotAdvance && isOnHare && hasHurry } - + /** - * Überprüft ob der derzeitige Spieler die `TAKE_OR_DROP_CARROTS` Karte spielen darf. + * Überprüft, ob der derzeitige Spieler die `TAKE_OR_DROP_CARROTS` Karte spielen darf. * @param state GameState - * @param n 20 für nehmen, -20 für abgeben, 0 für nichts tun + * @param n 20 für Nehmen, -20 für Abgeben, 0 für nichts tun * @return true, falls die `TAKE_OR_DROP_CARROTS` Karte gespielt werden darf */ - fun isValidToPlayTakeOrDropCarrots(state: GameState?, n: Int): Boolean { - val player: Hare = state!!.currentPlayer - var valid = (!playerMustAdvance(state) && state.isOnHareField() - && player.ownsCardOfType(Card.TAKE_OR_DROP_CARROTS)) - - valid = valid && (n == 20 || n == -20 || n == 0) - if(n < 0) { - valid = valid && ((player.getCarrots() + n) >= 0) - } - return valid + fun isValidToPlayTakeOrDropCarrots(state: GameState, n: Int): Boolean { + val player: Hare = state.currentPlayer + + val mustNotAdvance = !playerMustAdvance(state) + val isOnHare = state.board.getField(player.position) == Field.HARE + val hasCarrots = player.getCards().any { it == Card.TAKE_OR_DROP_CARROTS } + + return mustNotAdvance && isOnHare && hasCarrots && (n == 20 || n == -20 || n == 0) && + if (n < 0) (player.carrots + n) >= 0 else true } - + /** - * Überprüft ob der derzeitige Spieler die `EAT_SALAD` Karte spielen darf. + * Überprüft, ob der derzeitige Spieler die `EAT_SALAD` Karte spielen darf. * @param state GameState * @return true, falls die `EAT_SALAD` Karte gespielt werden darf */ - fun isValidToPlayEatSalad(state: GameState?): Boolean { - val player: Hare = state!!.currentPlayer - return (!playerMustAdvance(state) && state.isOnHareField() - && player.ownsCardOfType(Card.EAT_SALAD)) && player.getSalads() > 0 + fun isValidToPlayEatSalad(state: GameState): Boolean { + val player: Hare = state.currentPlayer + + val isOnHare = state.board.getField(player.position) == Field.HARE + val hasEatSalad = player.getCards().any { it == Card.TAKE_OR_DROP_CARROTS } + val hasCarrots = player.salads > 0 + + return isOnHare && hasEatSalad && hasCarrots } - + /** - * Überprüft ob der derzeitige Spieler irgendeine Karte spielen kann. + * Überprüft, ob der derzeitige Spieler irgendeine Karte spielen kann. * TAKE_OR_DROP_CARROTS wird nur mit 20 überprüft * @param state GameState * @return true, falls das Spielen einer Karte möglich ist */ - private fun canPlayAnyCard(state: GameState?): Boolean { - for(card in state!!.currentPlayer.getCards()) { - if(canPlayCard(state, card)) return true - } - - return false - } - - private fun canPlayCard(state: GameState?, card: Card): Boolean { - return when(card) { + private fun canPlayAnyCard(state: GameState): Boolean = + state.currentPlayer.getCards().any { canPlayCard(state, it) } ?: false + + private fun canPlayCard(state: GameState, card: Card): Boolean = + when (card) { Card.EAT_SALAD -> isValidToPlayEatSalad(state) Card.FALL_BACK -> isValidToPlayFallBack(state) Card.HURRY_AHEAD -> isValidToPlayHurryAhead(state) Card.TAKE_OR_DROP_CARROTS -> isValidToPlayTakeOrDropCarrots(state, 20) - else -> throw IllegalArgumentException("Unknown CardType $card") } - } - + /** - * Überprüft ob der derzeitige Spieler die Karte spielen kann. + * Überprüft, ob der derzeitige Spieler die Karte spielen kann. * @param state derzeitiger GameState * @param c Karte die gespielt werden soll * @param n Wert fuer TAKE_OR_DROP_CARROTS * @return true, falls das Spielen der entsprechenden Karte möglich ist */ - fun isValidToPlayCard(state: GameState?, c: Card, n: Int): Boolean { - return if(c == Card.TAKE_OR_DROP_CARROTS) isValidToPlayTakeOrDropCarrots( - state, - n - ) + fun isValidToPlayCard(state: GameState, c: Card, n: Int): Boolean { + return if (c == Card.TAKE_OR_DROP_CARROTS) isValidToPlayTakeOrDropCarrots(state, n) else canPlayCard(state, c) } - + fun mustEatSalad(state: GameState): Boolean { - val player: Hare = state.currentPlayer // check whether player just moved to salad field and must eat salad - val field: Field = state.board.getTypeAt(player.getFieldIndex()) - if(field == Field.SALAD) { - if(player.getLastNonSkipAction() is Advance) { - return true - } else if(player.getLastNonSkipAction() is Card) { - if((player.getLastNonSkipAction() as Card).getType() === Card.FALL_BACK || - (player.getLastNonSkipAction() as Card).getType() === Card.HURRY_AHEAD - ) { - return true - } - } - } - return false + val player: Hare = state.currentPlayer + val field: Field = state.board.getField(player.position) + + val isSalad = field == Field.SALAD + val wasLastAdvance = player.lastAction is Advance + val wasFallBackOrHurry = (player.lastAction as? CardAction)?.card in listOf(Card.FALL_BACK, Card.HURRY_AHEAD) + + return isSalad && (wasLastAdvance || wasFallBackOrHurry) } - + /** * Gibt zurück, ob der derzeitige Spieler eine Karte spielen kann. * @param state derzeitiger GameState * @return true, falls eine Karte gespielt werden kann */ - fun canPlayCard(state: GameState): Boolean { - return state.fieldOfCurrentPlayer() === Field.HARE && canPlayAnyCard(state) - } - */ + fun canPlayCard(state: GameState): Boolean = + state.board.getField(state.currentPlayer.position) === Field.HARE && canPlayAnyCard(state) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index fd519b102..2f109ec82 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -31,7 +31,10 @@ data class GameState @JvmOverloads constructor( val currentPlayer get() = getHare(currentTeam) - + + val otherPlayer + get() = if (currentPlayer == players[0]) players[1] else players[0] + val aheadPlayer get() = players.maxByOrNull { it.position }!! diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 0fa0f1232..d54e5fce1 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -7,7 +7,7 @@ import sc.plugin2025.util.HuIConstants data class Hare( @XStreamAsAttribute val team: Team, - @XStreamAsAttribute val position: Int = 0, + @XStreamAsAttribute var position: Int = 0, @XStreamAsAttribute var salads: Int = HuIConstants.INITIAL_SALADS, @XStreamAsAttribute var carrots: Int = HuIConstants.INITIAL_CARROTS, @XStreamAsAttribute var lastAction: Action? = null, diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index 67dd39293..9d3b7aa8b 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -5,5 +5,6 @@ import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { NO_ACTIONS("Der Zug enthält keine Aktionen"), CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), - MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden.") + MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), + CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich") } From 88b9168ead9a757c7c3291cc154f4648bc795925 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sat, 4 May 2024 12:13:19 +0300 Subject: [PATCH 08/38] refactor(plugin24): rename stuck to crashed --- CHANGELOG.md | 3 +-- plugin/src/main/kotlin/sc/plugin2024/GameState.kt | 10 +++++----- plugin/src/main/kotlin/sc/plugin2024/Ship.kt | 2 +- .../src/main/kotlin/sc/plugin2024/util/GamePlugin.kt | 2 +- plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt | 4 ++-- plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt | 3 +-- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f6df1b06..cb3cf6a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,7 @@ The version should always be in sync with the [GUI](https://github.com/software- - Allow other player to move on when one is disqualified ### 24 Post-Finale -- Allow one player to move on when other is stuck or finished - (add points depending on speed of reaching goal, do not require passengers?) +- Allow one player to move on when other is stuck (now for clarification: crashed) or finished - Improve XML protocol ### 24.2.5 No points for stuck ship - 2024-03-27 diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt index 5e89fcb3e..b261f7f71 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt @@ -10,9 +10,9 @@ import sc.plugin2024.actions.Push import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.AdvanceProblem import sc.plugin2024.mistake.MoveMistake -import sc.plugin2024.util.MQWinReason import sc.plugin2024.util.MQConstants import sc.plugin2024.util.MQConstants.POINTS_PER_SEGMENT +import sc.plugin2024.util.MQWinReason import sc.shared.InvalidMoveException import sc.shared.WinCondition import kotlin.math.absoluteValue @@ -140,7 +140,7 @@ data class GameState @JvmOverloads constructor( currentTeam = if(turn % 2 == 0) determineAheadTeam() else currentTeam.opponent() if(!canMove() && !isOver) { lastMove = null - currentShip.stuck = true + currentShip.crashed = true advanceTurn() } } @@ -394,12 +394,12 @@ data class GameState @JvmOverloads constructor( // In rare cases this returns true on the server even though the player cannot move // because the target tile is not revealed yet - fun canMove() = !currentShip.stuck && moveIterator().hasNext() + fun canMove() = !currentShip.crashed && moveIterator().hasNext() override val winCondition: WinCondition? get() = arrayOf({ ships.singleOrNull { inGoal(it) }?.let { WinCondition(it.team, MQWinReason.GOAL) } }, - { ships.singleOrNull { it.stuck }?.let { WinCondition(it.team.opponent(), MQWinReason.STUCK) } }, + { ships.singleOrNull { it.crashed }?.let { WinCondition(it.team.opponent(), MQWinReason.CRASHED) } }, { val dist = board.segmentDistance(ships.first().position, ships.last().position) WinCondition(ships[if(dist > 0) 0 else 1].team, MQWinReason.SEGMENT_DISTANCE).takeIf { dist.absoluteValue > 3 } @@ -427,7 +427,7 @@ data class GameState @JvmOverloads constructor( override fun getPointsForTeam(team: ITeam): IntArray = ships[team.index].let { ship -> - if(ship.stuck) + if(ship.crashed) intArrayOf(0, 0) else intArrayOf(ship.points, ship.passengers) diff --git a/plugin/src/main/kotlin/sc/plugin2024/Ship.kt b/plugin/src/main/kotlin/sc/plugin2024/Ship.kt index 25d1a3ef1..0b8d310dc 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Ship.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/Ship.kt @@ -56,7 +56,7 @@ data class Ship( @XStreamAsAttribute var passengers: Int = 0, @XStreamAsAttribute var freeTurns: Int = 1, @XStreamAsAttribute var points: Int = 0, // TODO don't track points here - @XStreamAsAttribute var stuck: Boolean = false, // TODO consider tracking as -1 points + @XStreamAsAttribute var crashed: Boolean = false, // TODO consider tracking as -1 points @XStreamOmitField var freeAcc: Int = MQConstants.FREE_ACC, @XStreamOmitField var movement: Int = speed, ): PublicCloneable { diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt index e5568f13b..e2abd52cc 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt @@ -15,7 +15,7 @@ enum class MQWinReason(override val message: String, override val isRegular: Boo DIFFERING_PASSENGERS("%S hat mehr Passagiere befördert."), SEGMENT_DISTANCE("%s liegt 3 Segmente vorne."), GOAL("%s hat das Ziel zuerst erreicht."), - STUCK("%s kann sich nicht mehr bewegen.", false); + CRASHED("%s kann sich nicht mehr bewegen.", false); } class GamePlugin: IGamePlugin { diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt index 0968435bb..f17714e56 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt @@ -331,10 +331,10 @@ class GameStateTest: FunSpec({ GameState(Board(listOf()), lastMove = Move(Accelerate(1), Advance(2))) shouldSerializeTo """ - + - + diff --git a/plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt b/plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt index fa13f4a05..42eb9f3d4 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt @@ -1,6 +1,5 @@ package sc.plugin2024 -import com.thoughtworks.xstream.XStream import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.* import sc.api.plugins.CubeCoordinates @@ -16,7 +15,7 @@ class ShipTest: FunSpec({ } test("serializes nicely") { shipOne shouldSerializeTo """ - + """ } From 9e2869c5d554db355656c8e0b0620aae3e4d3d9e Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sun, 5 May 2024 23:14:35 +0300 Subject: [PATCH 09/38] refactor(plugin25): start some hui overhaul - Move is not a list of actions, but an action with optionally cards to play after a move ahead - Probably no need to track lastAction except for eating salad on salat field --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 3 ++- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 2 +- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 9 ++++--- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 24 +------------------ .../main/kotlin/sc/plugin2025/GameState.kt | 11 +++++++++ plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- .../sc/plugin2025/{Action.kt => HuIMove.kt} | 3 ++- plugin/src/main/kotlin/sc/plugin2025/Move.kt | 6 ++--- 8 files changed, 25 insertions(+), 35 deletions(-) rename plugin/src/main/kotlin/sc/plugin2025/{Action.kt => HuIMove.kt} (78%) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index f9404ec7f..a3a3ea0ca 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -2,6 +2,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import sc.api.plugins.IMove import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.plugin2025.GameRuleLogic.isValidToAdvance import sc.shared.IMoveMistake @@ -11,7 +12,7 @@ import sc.shared.IMoveMistake * berechnet (Gaußsche Summe) */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int) : Action { +class Advance(@XStreamAsAttribute val distance: Int) : IMove { override fun perform(state: GameState): IMoveMistake? { if (isValidToAdvance(state, this.distance)) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 91a03d76c..8ed93af02 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -16,7 +16,7 @@ enum class Card { HURRY_AHEAD } -sealed class CardAction: Action { +sealed class CardAction: HuIMove { abstract val card: Card data class PlayCard(override val card: Card): CardAction() { override fun perform(state: GameState): IMoveMistake? { diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index 55751647b..b6e72f1ab 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -4,15 +4,14 @@ import com.thoughtworks.xstream.annotations.XStreamAlias import sc.shared.IMoveMistake /** - * Eine Salatessen-Aktion. - * Kann nur auf einem Salatfeld ausgeführt werden. - * Muss ausgeführt werden, wenn ein Salatfeld betreten wird. - * Nachdem die Aktion ausgeführt wurde, muss das Salatfeld verlassen werden, oder es muss ausgesetzt werden. + * Eine Salatessen-Aktion auf einem Salatfeld. + * Muss im Zug nach Betreten eines Salatfeldes ausgeführt werden. + * Nachdem die Aktion ausgeführt wurde, muss das Salatfeld verlassen werden. * Durch eine Salatessen-Aktion wird ein Salat verbraucht * und es werden je nachdem ob der Spieler führt oder nicht 10 oder 30 Karotten aufgenommen. */ @XStreamAlias(value = "Salad") -object EatSalad: Action { +object EatSalad: HuIMove { override fun perform(state: GameState): IMoveMistake? { if(state.canEatSalad()) { val player = state.currentPlayer diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 260c54be8..0edb3d65f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -92,28 +92,6 @@ object GameRuleLogic { (0..calculateMoveableFields(state.currentPlayer.carrots)) .any { isValidToAdvance(state, it) } - - /** - * Überprüft `EatSalad` Züge auf Korrektheit. Um einen Salat - * zu verzehren, muss der Spieler sich: - * - * - auf einem Salatfeld befinden - * - noch mindestens einen Salat besitzen - * - vorher kein Salat auf diesem Feld verzehrt wurde - * - * @param state GameState - * @return true, falls ein Salat gegessen werden darf - */ - fun isValidToEat(state: GameState): Boolean { - val player: Hare = state.currentPlayer - - val isSaladField = state.board.getField(player.position) == Field.SALAD - val hasSalad = player.salads > 0 - val mustNotAdvance = !playerMustAdvance(state) - - return isSaladField && hasSalad && mustNotAdvance - } - /** * Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss. * @param state GameState @@ -125,7 +103,7 @@ object GameRuleLogic { if (type == Field.HEDGEHOG || type == Field.START) return true - val lastAction: Action? = player.lastAction + val lastAction: HuIMove? = player.lastAction if (lastAction is EatSalad) return true else if (lastAction is CardAction) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 2f109ec82..f20b889c5 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -65,6 +65,17 @@ data class GameState @JvmOverloads constructor( currentField == Field.HARE && player.lastAction !is CardAction + + /** + * Überprüft `EatSalad` Zug auf Korrektheit. + * Um einen Salat zu verzehren, muss der Spieler sich: + * + * - auf einem Salatfeld befinden + * - noch mindestens einen Salat besitzen + * - vorher kein Salat auf diesem Feld verzehrt wurde + * + * @return true, falls ein Salat gegessen werden darf + */ fun canEatSalad(player: Hare = currentPlayer) = player.salads > 0 && board.getField(player.position) == Field.SALAD && diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index d54e5fce1..37e35dfde 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -10,7 +10,7 @@ data class Hare( @XStreamAsAttribute var position: Int = 0, @XStreamAsAttribute var salads: Int = HuIConstants.INITIAL_SALADS, @XStreamAsAttribute var carrots: Int = HuIConstants.INITIAL_CARROTS, - @XStreamAsAttribute var lastAction: Action? = null, + @XStreamAsAttribute var lastAction: HuIMove? = null, private val cards: ArrayList = arrayListOf(*Card.values()), ): PublicCloneable { fun getCards(): List = cards diff --git a/plugin/src/main/kotlin/sc/plugin2025/Action.kt b/plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt similarity index 78% rename from plugin/src/main/kotlin/sc/plugin2025/Action.kt rename to plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt index a7f57bf4e..ffc2ae960 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Action.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt @@ -1,10 +1,11 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.api.plugins.IMove import sc.shared.IMoveMistake @XStreamAlias(value = "action") -interface Action { +interface HuIMove: IMove { fun perform(state: GameState): IMoveMistake? } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Move.kt b/plugin/src/main/kotlin/sc/plugin2025/Move.kt index 969d2f4a6..cc82e875a 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Move.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Move.kt @@ -13,11 +13,11 @@ import sc.api.plugins.IMove * @property actions The list of actions in the move. */ data class Move( - @XStreamImplicit - val actions: List, + @XStreamImplicit + val actions: List, ): IMove, Comparable { - constructor(vararg actions: Action) : this(actions.asList()) + constructor(vararg actions: HuIMove) : this(actions.asList()) /** * Compares this Move instance with the specified Move for order. From 2a90e6c2dcf8add561a7d3121bc769a4e984f060 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 6 May 2024 12:28:21 +0300 Subject: [PATCH 10/38] fix(plugin25): make playing cards part of Advance --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 23 +-- .../src/main/kotlin/sc/plugin2025/Card.java | 132 ------------------ plugin/src/main/kotlin/sc/plugin2025/Card.kt | 52 +++++++ .../kotlin/sc/plugin2025/GameRuleLogic.kt | 2 +- 4 files changed, 68 insertions(+), 141 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/Card.java diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index a3a3ea0ca..53e3ce8f4 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -8,22 +8,29 @@ import sc.plugin2025.GameRuleLogic.isValidToAdvance import sc.shared.IMoveMistake /** - * Ein Vorwärtszug, um spezifizierte Distanz. Verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 - * berechnet (Gaußsche Summe) + * Ein Vorwärtszug, um spezifizierte Distanz. + * Verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 berechnet (Gaußsche Summe). + * Falls der Zug auf einem Hasenfeld endet, müssen auszuführende Hasenkarten mitgegeben werden. */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int) : IMove { +data class Advance(@XStreamAsAttribute val distance: Int, val cards: Array = arrayOf()) : HuIMove { + + constructor(distance: Int, vararg cards: CardAction) : this(distance, cards) override fun perform(state: GameState): IMoveMistake? { if (isValidToAdvance(state, this.distance)) { - state.currentPlayer.carrots -= calculateCarrots(this.distance) - state.currentPlayer.position += distance + val player = state.currentPlayer + player.carrots -= calculateCarrots(this.distance) + player.position += distance + if(state.currentField == Field.HARE) { + if(cards.isEmpty()) + return MoveMistake.MUST_PLAY_CARD + return cards.firstNotNullOfOrNull { it.perform(state) } + } return null } else { return MoveMistake.CANNOT_MOVE_FORWARD } } - - override fun equals(other: Any?) = other is Advance && this.distance == other.distance - override fun hashCode(): Int = distance + } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.java b/plugin/src/main/kotlin/sc/plugin2025/Card.java deleted file mode 100644 index 10befb3c5..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.java +++ /dev/null @@ -1,132 +0,0 @@ -package sc.plugin2018; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidMoveException; - -/** - * Eine Karte die auf einem Hasenfeld gespielt werden kann. - */ -@XStreamAlias(value = "card") -public class Card extends Action { - - @XStreamAsAttribute - private CardType type; - - /** - * Nur für TAKE_OR_DROP_CARROTS genutzt. Muss 20, 0 oder -20 sein. - */ - @XStreamAsAttribute - private int value; - - /** - * Default Konstruktor. Setzt value auf 0 und order auf 0. - * @param type Art der Karte - */ - public Card(CardType type) { - this.order = 0; - this.value = 0; - this.type = type; - } - - /** - * Konstruktor für eine Karte - * @param type Art der Karte - * @param order Index in der Aktionsliste des Zuges - */ - public Card(CardType type, int order) { - this.order = order; - this.value = 0; - this.type = type; - } - - /** - * - * @param type Art der Karte - * @param value Wert einer Karte nur für TAKE_OR_DROP_CARROTS genutzt (-20,0,20) - * @param order Index in der Aktionsliste des Zuges - */ - public Card(CardType type, int value, int order) { - this.order = order; - this.value = 0; // default value - this.value = value; - this.type = type; - } - - - @Override - public void perform(GameState state) throws InvalidMoveException { - state.getCurrentPlayer().setMustPlayCard(false); // player played a card - switch (type) { // when entering a HARE field with fall_back or hurry ahead, player has to play another card - case EAT_SALAD: - if (GameRuleLogic.isValidToPlayEatSalad(state)) { - state.getCurrentPlayer().eatSalad(); - if (state.isFirst(state.getCurrentPlayer())) { - state.getCurrentPlayer().changeCarrotsBy(10); - } else { - state.getCurrentPlayer().changeCarrotsBy(30); - } - } else { - throw new InvalidMoveException("Das Ausspielen der EAT_SALAD Karte ist nicht möglich."); - } - break; - case FALL_BACK: - if (GameRuleLogic.isValidToPlayFallBack(state)) { - state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() - 1); - if (state.fieldOfCurrentPlayer() == Field.HARE) { - state.getCurrentPlayer().setMustPlayCard(true); - } - } else { - throw new InvalidMoveException("Das Ausspielen der FALL_BACK Karte ist nicht möglich."); - } - break; - case HURRY_AHEAD: - if (GameRuleLogic.isValidToPlayHurryAhead(state)) { - state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() + 1); - if (state.fieldOfCurrentPlayer() == Field.HARE) { - state.getCurrentPlayer().setMustPlayCard(true); - } - } else { - throw new InvalidMoveException("Das Ausspielen der HURRY_AHEAD Karte ist nicht möglich."); - } - break; - case TAKE_OR_DROP_CARROTS: - if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(state, this.getValue())) { - state.getCurrentPlayer().changeCarrotsBy(this.getValue()); - } else { - throw new InvalidMoveException("Das Ausspielen der TAKE_OR_DROP_CARROTS Karte ist nicht möglich."); - } - break; - } - state.setLastAction(this); - // remove player card - state.getCurrentPlayer().setCards(state.getCurrentPlayer().getCardsWithout(this.type)); - } - - public CardType getType() { - return type; - } - - public int getValue() { - return value; - } - - @Override - public Card clone() { - return new Card(this.type, this.value, this.order); - } - - @Override - public boolean equals(Object o) { - return o instanceof Card && (this.value == ((Card) o).value) && (this.type == ((Card) o).type); - } - - @Override - public String toString() { - return "Card " - + this.getType() - + ((this.getType() == CardType.TAKE_OR_DROP_CARROTS)?(" " + this.value):"") - + " order " + this.order; - } -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 8ed93af02..3fd48ec7d 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -29,4 +29,56 @@ sealed class CardAction: HuIMove { TODO("Not yet implemented") } } + + + /* + @Override + public void perform(GameState state) throws InvalidMoveException { + state.getCurrentPlayer().setMustPlayCard(false); // player played a card + switch (type) { // when entering a HARE field with fall_back or hurry ahead, player has to play another card + case EAT_SALAD: + if (GameRuleLogic.isValidToPlayEatSalad(state)) { + state.getCurrentPlayer().eatSalad(); + if (state.isFirst(state.getCurrentPlayer())) { + state.getCurrentPlayer().changeCarrotsBy(10); + } else { + state.getCurrentPlayer().changeCarrotsBy(30); + } + } else { + throw new InvalidMoveException("Das Ausspielen der EAT_SALAD Karte ist nicht möglich."); + } + break; + case FALL_BACK: + if (GameRuleLogic.isValidToPlayFallBack(state)) { + state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() - 1); + if (state.fieldOfCurrentPlayer() == Field.HARE) { + state.getCurrentPlayer().setMustPlayCard(true); + } + } else { + throw new InvalidMoveException("Das Ausspielen der FALL_BACK Karte ist nicht möglich."); + } + break; + case HURRY_AHEAD: + if (GameRuleLogic.isValidToPlayHurryAhead(state)) { + state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() + 1); + if (state.fieldOfCurrentPlayer() == Field.HARE) { + state.getCurrentPlayer().setMustPlayCard(true); + } + } else { + throw new InvalidMoveException("Das Ausspielen der HURRY_AHEAD Karte ist nicht möglich."); + } + break; + case TAKE_OR_DROP_CARROTS: + if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(state, this.getValue())) { + state.getCurrentPlayer().changeCarrotsBy(this.getValue()); + } else { + throw new InvalidMoveException("Das Ausspielen der TAKE_OR_DROP_CARROTS Karte ist nicht möglich."); + } + break; + } + state.setLastAction(this); + // remove player card + state.getCurrentPlayer().setCards(state.getCurrentPlayer().getCardsWithout(this.type)); + } + */ } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 0edb3d65f..574f884df 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -81,7 +81,7 @@ object GameRuleLogic { */ private fun canDoAnything(state: GameState): Boolean = (canPlayAnyCard(state) || isValidToFallBack(state) || isValidToExchangeCarrots(state, 10) - || isValidToExchangeCarrots(state, -10) || isValidToEat(state) || canAdvanceToAnyField(state)) + || isValidToExchangeCarrots(state, -10) || state.canEatSalad() || canAdvanceToAnyField(state)) /** * Überprüft, ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. From 583fa962d3a06f98465bc67acb5f47b6b03ad32b Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 6 May 2024 12:48:49 +0300 Subject: [PATCH 11/38] fix(plugin25): migrate to HuIMove --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 6 +-- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 6 +-- .../main/kotlin/sc/plugin2025/GameState.java | 46 +++++++++---------- .../main/kotlin/sc/plugin2025/GameState.kt | 16 ++----- plugin/src/main/kotlin/sc/plugin2025/Move.kt | 46 ------------------- .../kotlin/sc/plugin2025/util/GamePlugin.kt | 8 ++-- .../sc/plugin2025/util/XStreamClasses.kt | 2 +- 7 files changed, 38 insertions(+), 92 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/Move.kt diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 53e3ce8f4..4e22a024e 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -13,10 +13,8 @@ import sc.shared.IMoveMistake * Falls der Zug auf einem Hasenfeld endet, müssen auszuführende Hasenkarten mitgegeben werden. */ @XStreamAlias(value = "advance") -data class Advance(@XStreamAsAttribute val distance: Int, val cards: Array = arrayOf()) : HuIMove { +class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction) : HuIMove { - constructor(distance: Int, vararg cards: CardAction) : this(distance, cards) - override fun perform(state: GameState): IMoveMistake? { if (isValidToAdvance(state, this.distance)) { val player = state.currentPlayer @@ -33,4 +31,4 @@ data class Advance(@XStreamAsAttribute val distance: Int, val cards: Array player.salads > 0 Field.HARE -> { - val advanceMove = Move(listOf(Advance(distance))) + val advanceMove = Advance(distance) val nextState: GameState = state.performMove(advanceMove) as GameState canPlayAnyCard(nextState) } @@ -160,7 +160,7 @@ object GameRuleLogic { return when (state.board.getField(nextPos)) { Field.SALAD -> player.salads > 0 Field.HARE -> { - val fallBack = Move(listOf(CardAction.PlayCard(Card.FALL_BACK))) + val fallBack = CardAction.PlayCard(Card.FALL_BACK) val nextState: GameState = state.performMove(fallBack) as GameState canPlayAnyCard(nextState) } @@ -189,7 +189,7 @@ object GameRuleLogic { return when (state.board.getField(nextPos)) { Field.SALAD -> player.salads > 0 Field.HARE -> { - val fallBack = Move(listOf(CardAction.PlayCard(Card.HURRY_AHEAD))) + val fallBack = CardAction.PlayCard(Card.HURRY_AHEAD) val nextState: GameState = state.performMove(fallBack) as GameState canPlayAnyCard(nextState) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.java b/plugin/src/main/kotlin/sc/plugin2025/GameState.java index 7f344e565..325a5e69f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.java +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.java @@ -83,7 +83,7 @@ public class GameState implements Cloneable { /** * letzter getaetigter Zug */ - private Move lastMove; + private HuIMove lastMove; /** * Erzeugt einen neuen {@code GameState}, in dem alle Informationen so gesetzt @@ -132,7 +132,7 @@ public GameState clone() throws CloneNotSupportedException { if (blue != null) clone.blue = this.blue.clone(); if (lastMove != null) - clone.lastMove = (Move) this.lastMove.clone(); + clone.lastMove = (HuIMove) this.lastMove.clone(); if (board != null) clone.board = this.board.clone(); if (currentPlayer != null) @@ -482,7 +482,7 @@ public Player getOpponent(Player player) { * Setzt letzten Zug. Nur für den Server relevant. * @param lastMove letzter Zug */ - protected void setLastMove(Move lastMove) { + protected void setLastMove(HuIMove lastMove) { this.lastMove = lastMove; } @@ -501,7 +501,7 @@ public void setLastAction(Action action) { * Gibt den letzten Zugzurück * @return letzter Zug */ - public Move getLastMove() { + public HuIMove getLastMove() { return this.lastMove; } @@ -541,29 +541,29 @@ public boolean isOnHareField() return fieldOfCurrentPlayer().equals(sc.plugin2018.Field.HARE); } - public ArrayList getPossibleMoves() { - ArrayList possibleMove = new ArrayList<>(); + public ArrayList getPossibleMoves() { + ArrayList possibleMove = new ArrayList<>(); ArrayList actions = new ArrayList<>(); if (GameRuleLogic.isValidToEat(this)) { // Wenn ein Salat gegessen werden kann, muss auch ein Salat gegessen werden actions.add(new sc.plugin2018.EatSalad()); - Move move = new Move(actions); + HuIMove move = new HuIMove(actions); possibleMove.add(move); return possibleMove; } if (GameRuleLogic.isValidToExchangeCarrots(this, 10)) { actions.add(new sc.plugin2018.ExchangeCarrots(10)); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.clear(); } if (GameRuleLogic.isValidToExchangeCarrots(this, -10)) { actions.add(new sc.plugin2018.ExchangeCarrots(-10)); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.clear(); } if (GameRuleLogic.isValidToFallBack(this)) { actions.add(new sc.plugin2018.FallBack()); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.clear(); } // Generiere mögliche Vorwärtszüge @@ -590,16 +590,16 @@ public ArrayList getPossibleMoves() { possibleMove.addAll(clone.checkForPlayableCards(actions)); } else { // Füge möglichen Vorwärtszug hinzu - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); } } actions.clear(); } if (possibleMove.isEmpty()) { - Move move; + HuIMove move; logger.warn("Muss aussetzen"); actions.add(new sc.plugin2018.Skip()); - move = new Move(actions); + move = new HuIMove(actions); possibleMove.add(move); } return possibleMove; @@ -611,30 +611,30 @@ public ArrayList getPossibleMoves() { * @param actions bisherige Aktionenliste * @return mögliche Züge */ - private ArrayList checkForPlayableCards(ArrayList actions) { - ArrayList possibleMove = new ArrayList<>(); + private ArrayList checkForPlayableCards(ArrayList actions) { + ArrayList possibleMove = new ArrayList<>(); if (this.getCurrentPlayer().mustPlayCard()) { // überprüfe, ob eine Karte gespielt werden muss if (GameRuleLogic.isValidToPlayEatSalad(this)) { actions.add(new Card(sc.plugin2018.CardType.EAT_SALAD, actions.size())); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.remove(new Card(sc.plugin2018.CardType.EAT_SALAD, 1)); } if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 20)) { actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); } if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, -20)) { actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); } if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 0)) { actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); } @@ -655,12 +655,12 @@ private ArrayList checkForPlayableCards(ArrayList actions) { e.printStackTrace(); } if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { - ArrayList moves = clone.checkForPlayableCards(actions); + ArrayList moves = clone.checkForPlayableCards(actions); if (!moves.isEmpty()) { possibleMove.addAll(moves); } } else { - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); } actions.remove(new Card(sc.plugin2018.CardType.HURRY_AHEAD, actions.size())); @@ -682,12 +682,12 @@ private ArrayList checkForPlayableCards(ArrayList actions) { e.printStackTrace(); } if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { - ArrayList moves = clone.checkForPlayableCards(actions); + ArrayList moves = clone.checkForPlayableCards(actions); if (!moves.isEmpty()) { possibleMove.addAll(moves); } } else { - possibleMove.add(new Move(actions)); + possibleMove.add(new HuIMove(actions)); } actions.remove(new Card(sc.plugin2018.CardType.FALL_BACK, actions.size())); } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index f20b889c5..20eecfd8a 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -26,8 +26,8 @@ data class GameState @JvmOverloads constructor( @XStreamImplicit val players: List = Team.values().map { Hare(it) }, /** Der zuletzt gespielte Zug. */ - override var lastMove: Move? = null, -): TwoPlayerGameState(players.first().team) { + override var lastMove: HuIMove? = null, +): TwoPlayerGameState(players.first().team) { val currentPlayer get() = getHare(currentTeam) @@ -51,14 +51,8 @@ data class GameState @JvmOverloads constructor( override val isOver: Boolean get() = players.any { it.inGoal } - override fun performMoveDirectly(move: Move) { - move.actions.forEach { - if(mustPlayCard() && it !is CardAction) - throw InvalidMoveException(MoveMistake.MUST_PLAY_CARD) - it.perform(this) - } - if(mustPlayCard()) - throw InvalidMoveException(MoveMistake.MUST_PLAY_CARD) + override fun performMoveDirectly(move: HuIMove) { + move.perform(this) } fun mustPlayCard(player: Hare = currentPlayer) = @@ -81,7 +75,7 @@ data class GameState @JvmOverloads constructor( board.getField(player.position) == Field.SALAD && player.lastAction != EatSalad - override fun moveIterator(): Iterator = TODO() + override fun moveIterator(): Iterator = TODO() override fun clone(): GameState = copy(board = board.clone(), players = players.clone()) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Move.kt b/plugin/src/main/kotlin/sc/plugin2025/Move.kt deleted file mode 100644 index cc82e875a..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/Move.kt +++ /dev/null @@ -1,46 +0,0 @@ -package sc.plugin2025 - -import com.thoughtworks.xstream.annotations.XStreamAlias -import com.thoughtworks.xstream.annotations.XStreamImplicit -import sc.api.plugins.IMove - -@XStreamAlias("move") -/** - * Represents a move in a game. - * - * A move consists of a list of actions. - * - * @property actions The list of actions in the move. - */ -data class Move( - @XStreamImplicit - val actions: List, -): IMove, Comparable { - - constructor(vararg actions: HuIMove) : this(actions.asList()) - - /** - * Compares this Move instance with the specified Move for order. - * - * The comparison is based on the size of the actions list. - * The Move with a smaller size will be considered smaller, - * the Move with a larger size will be considered larger, - * and if the sizes are equal, the Moves are considered equal. - * - * @param other the Move to be compared. - * @return a negative integer if this Move is smaller than the specified Move, - * zero if they are equal in length, - * or a positive integer if this Move is larger than the specified Move. - */ - override fun compareTo(other: Move): Int = - actions.size.compareTo(other.actions.size) - - /** @return true if the specified object is a Move and contains the same actions as this move, false otherwise */ - override fun equals(other: Any?): Boolean = other is Move && actions == other.actions - - override fun hashCode(): Int = actions.hashCode() - - override fun toString(): String = - actions.joinToString(separator = ", ", prefix = "Zug[", postfix = "]") - -} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt index a938bd749..919ae57c8 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt @@ -5,8 +5,8 @@ import sc.api.plugins.IGameInstance import sc.api.plugins.IGamePlugin import sc.api.plugins.IGameState import sc.framework.plugins.TwoPlayerGame -import sc.plugin2024.GameState -import sc.plugin2024.Move +import sc.plugin2025.GameState +import sc.plugin2025.HuIMove import sc.shared.* @XStreamAlias(value = "winreason") @@ -16,7 +16,7 @@ enum class HuIWinReason(override val message: String, override val isRegular: Bo GOAL("%s hat das Ziel zuerst erreicht."), } -class GamePlugin: IGamePlugin { +class GamePlugin: IGamePlugin { companion object { const val PLUGIN_ID = "swc_2025_hase_und_igel" val scoreDefinition: ScoreDefinition = @@ -35,7 +35,7 @@ class GamePlugin: IGamePlugin { override val turnLimit: Int = HuIConstants.ROUND_LIMIT * 2 - override val moveClass = Move::class.java + override val moveClass = HuIMove::class.java override fun createGame(): IGameInstance = TwoPlayerGame(this, GameState()) diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt index e15d07131..ab28a7355 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt @@ -7,7 +7,7 @@ class XStreamClasses: XStreamProvider { override val classesToRegister = listOf( - Move::class.java + HuIMove::class.java ) } \ No newline at end of file From 89dfbe117e3a65241823a7b840264cae61f87cbc Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 6 May 2024 16:25:28 +0300 Subject: [PATCH 12/38] test(plugin25): convert test for GameRuleLogic --- .../kotlin/sc/plugin2025/GameRuleLogicTest.kt | 23 ++++++++++++ .../sc/plugin2025/util/TestGameUtil.java | 35 ------------------- 2 files changed, 23 insertions(+), 35 deletions(-) create mode 100644 plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt delete mode 100644 plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java diff --git a/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt b/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt new file mode 100644 index 000000000..6f5631bdd --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt @@ -0,0 +1,23 @@ +package sc.plugin2025 + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.* +import sc.plugin2025.GameRuleLogic.calculateCarrots +import sc.plugin2025.GameRuleLogic.calculateMoveableFields + +class GameRuleLogicTest: StringSpec({ + /** Überprüft die Berechnungen der `calculateCarrots()` Hilfsfunktion */ + "testCalculateCarrots" { + calculateCarrots(1) shouldBe 1 + calculateCarrots(10) shouldBe 55 + } + + /** Überprüft die Berechnung der `calculateMoveableFields()` Hilfsfunktion */ + "testCalculateMoveableFields" { + calculateMoveableFields(0) shouldBe 0 + calculateMoveableFields(1) shouldBe 1 + calculateMoveableFields(5) shouldBe 2 + calculateMoveableFields(6) shouldBe 3 + calculateMoveableFields(7) shouldBe 3 + } +}) diff --git a/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java b/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java deleted file mode 100644 index 671943bc6..000000000 --- a/plugin/src/test/kotlin/sc/plugin2025/util/TestGameUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package sc.plugin2018.util; - -import junit.framework.Assert; - -import org.junit.Test; - -/** - * @author rra - * @since Jul 4, 2009 - * - */ -public class TestGameUtil -{ - /** - * Überprüft die Berechnungen der calculateCarrots() Hilfsfunktion - */ - @Test - public void testCalculateCarrots() - { - Assert.assertEquals(1, GameRuleLogic.calculateCarrots(1)); - Assert.assertEquals(55, GameRuleLogic.calculateCarrots(10)); - } - - /** - * Überprüft die Berechnung der calculateMoveableFields() Hilfsfunktion - */ - @Test - public void testCalculateMoveableFields() { - Assert.assertEquals(0, GameRuleLogic.calculateMoveableFields(0)); - Assert.assertEquals(1, GameRuleLogic.calculateMoveableFields(1)); - Assert.assertEquals(2, GameRuleLogic.calculateMoveableFields(5)); - Assert.assertEquals(3, GameRuleLogic.calculateMoveableFields(6)); - Assert.assertEquals(3, GameRuleLogic.calculateMoveableFields(7)); - } -} From 0cb980db10d8bc3131251b09a889511511f6fbe4 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Thu, 9 May 2024 15:08:16 +0300 Subject: [PATCH 13/38] feat(plugin25): implement all move types --- gradle.properties | 8 +- .../src/main/kotlin/sc/plugin2025/Advance.kt | 9 +- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 2 +- .../kotlin/sc/plugin2025/ExchangeCarrots.java | 60 ---- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 22 ++ .../main/kotlin/sc/plugin2025/FallBack.java | 49 --- .../src/main/kotlin/sc/plugin2025/FallBack.kt | 26 ++ plugin/src/main/kotlin/sc/plugin2025/Field.kt | 16 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 38 +-- .../main/kotlin/sc/plugin2025/GameState.kt | 74 ++++- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- .../main/kotlin/sc/plugin2025/MoveMistake.kt | 5 +- .../src/main/kotlin/sc/plugin2025/Player.java | 309 ------------------ .../src/main/kotlin/sc/plugin2025/Skip.java | 49 --- .../services/sc.api.plugins.IGamePlugin | 2 +- .../services/sc.networking.XStreamProvider | 2 +- .../kotlin/sc/plugin2025/GameRuleLogicTest.kt | 2 +- .../test/kotlin/sc/plugin2025/PlayerTest.java | 34 -- .../sc/api/plugins/TwoPlayerGameState.kt | 2 +- 19 files changed, 149 insertions(+), 562 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/FallBack.java create mode 100644 plugin/src/main/kotlin/sc/plugin2025/FallBack.kt delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/Player.java delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/Skip.java delete mode 100644 plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java diff --git a/gradle.properties b/gradle.properties index 9814d2250..91db40fbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -socha.gameName=mississippi -socha.version.year=24 -socha.version.minor=02 -socha.version.patch=05 \ No newline at end of file +socha.gameName=hui +socha.version.year=25 +socha.version.minor=00 +socha.version.patch=00 \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 4e22a024e..169e9fd85 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -2,15 +2,16 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute -import sc.api.plugins.IMove import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.plugin2025.GameRuleLogic.isValidToAdvance import sc.shared.IMoveMistake /** - * Ein Vorwärtszug, um spezifizierte Distanz. - * Verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 berechnet (Gaußsche Summe). - * Falls der Zug auf einem Hasenfeld endet, müssen auszuführende Hasenkarten mitgegeben werden. + * Ein Vorwärtszug um die angegebene Distanz. + * - verbrauchte Karotten werden mit k = (distance * (distance + 1)) / 2 berechnet (Gaußsche Summe). + * - falls der Zug auf einem Hasenfeld endet, müssen auszuführende Hasenkarten mitgegeben werden. + * - falls der Zug auf einem Marktfeld endet, wird eine zu kaufende Hasenkarte mitgegeben. + * Der Wert der Karottentauschkarte spielt dann keine Rolle. */ @XStreamAlias(value = "advance") class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction) : HuIMove { diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index b6e72f1ab..5fe1a1c25 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -10,7 +10,7 @@ import sc.shared.IMoveMistake * Durch eine Salatessen-Aktion wird ein Salat verbraucht * und es werden je nachdem ob der Spieler führt oder nicht 10 oder 30 Karotten aufgenommen. */ -@XStreamAlias(value = "Salad") +@XStreamAlias(value = "EatSalad") object EatSalad: HuIMove { override fun perform(state: GameState): IMoveMistake? { if(state.canEatSalad()) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java deleted file mode 100644 index b17223c2b..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.java +++ /dev/null @@ -1,60 +0,0 @@ -package sc.plugin2018; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidMoveException; - -/** - * Karottentauschaktion. Es können auf einem Karottenfeld 10 Karotten abgegeben oder aufgenommen werden. - * Dies kann beliebig oft hintereinander ausgeführt werden. - */ -@XStreamAlias(value = "exchangeCarrots") -public class ExchangeCarrots extends Action { - - @XStreamAsAttribute - private int value; - - public ExchangeCarrots(int value) { - this.order = 0; - this.value = value; - } - - public ExchangeCarrots(int value, int order) { - this.order = order; - this.value = value; - } - - public int getValue() { - return value; - } - - @Override - public void perform(GameState state) throws InvalidMoveException { - if (GameRuleLogic.isValidToExchangeCarrots(state, this.getValue())) { - state.getCurrentPlayer().changeCarrotsBy(this.getValue()); - state.setLastAction(this); - } else { - throw new InvalidMoveException("Es können nicht " + this.getValue() + " Karotten aufgenommen werden."); - } - } - - @Override - public ExchangeCarrots clone() { - return new ExchangeCarrots(this.getValue(), this.order); - } - - @Override - public boolean equals(Object o) { - if(o instanceof ExchangeCarrots) { - return (this.getValue() == ((ExchangeCarrots) o).getValue()); - } - return false; - } - - @Override - public String toString() { - return "ExchangeCarrots value " + this.getValue() + " order " + this.order; - } - -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt new file mode 100644 index 000000000..4d334182f --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -0,0 +1,22 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias +import sc.plugin2025.GameRuleLogic.isValidToExchangeCarrots +import sc.shared.IMoveMistake + +/** + * Karottentauschaktion. + * Auf einem Karottenfeld können 10 Karotten abgegeben oder aufgenommen werden. + * Dies kann beliebig oft hintereinander ausgeführt werden. + */ +@XStreamAlias(value = "ExchangeCarrots") +data class ExchangeCarrots(val value: Int): HuIMove { + override fun perform(state: GameState): IMoveMistake? { + if(isValidToExchangeCarrots(state, this.value)) { + state.currentPlayer.carrots += value + return null + } else { + return MoveMistake.CANNOT_CARD_EXCHANGE_CARROTS + } + } +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.java b/plugin/src/main/kotlin/sc/plugin2025/FallBack.java deleted file mode 100644 index 53238ee16..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.java +++ /dev/null @@ -1,49 +0,0 @@ -package sc.plugin2018; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidMoveException; - -/** - * Rückzugaktion. Sollte das nächste Igelfeld hinter einem Spieler nicht belegt sein, darf anstatt nach - * vorne zu ziehen ein Rückzug gemacht werden. Dabei werden die zurückgezogene Distanz * 10 Karotten aufgenommen. - */ -@XStreamAlias(value = "fallBack") -public class FallBack extends Action { - - public FallBack() { - this.order = 0; - } - - public FallBack(int order) { - this.order = order; - } - - @Override - public void perform(GameState state) throws InvalidMoveException { - if (GameRuleLogic.isValidToFallBack(state)) { - int previousFieldIndex = state.getCurrentPlayer().getFieldIndex(); - state.getCurrentPlayer().setFieldIndex(state.getPreviousFieldByType(Field.HEDGEHOG, state.getCurrentPlayer() - .getFieldIndex())); - state.getCurrentPlayer().changeCarrotsBy(10 * (previousFieldIndex - state.getCurrentPlayer().getFieldIndex())); - state.setLastAction(this); - } else { - throw new InvalidMoveException("Es kann gerade kein Rückzug gemacht werden."); - } - } - - @Override - public FallBack clone() { - return new FallBack(this.order); - } - - @Override - public boolean equals(Object o) { - return o instanceof FallBack; - } - - @Override - public String toString() { - return "FallBack order " + this.order; - } -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt new file mode 100644 index 000000000..dacc5db31 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -0,0 +1,26 @@ +package sc.plugin2025 + +import com.thoughtworks.xstream.annotations.XStreamAlias + +/** + * Rückwärtszug. + * Sollte das nächste Igelfeld hinter einem Spieler nicht belegt sein, + * darf anstatt nach vorne zu ziehen ein Rückzug gemacht werden. + * Dabei werden die zurückgezogene Distanz * 10 Karotten aufgenommen. + */ +@XStreamAlias(value = "fallBack") +object FallBack: HuIMove { + override fun perform(state: GameState): MoveMistake? { + if(state.isValidToFallBack()) { + val previousFieldIndex: Int = state.currentPlayer.position + state.currentPlayer.position = + state.board.getPreviousField(Field.HEDGEHOG, previousFieldIndex) ?: return MoveMistake.CANNOT_FALL_BACK + state.currentPlayer.carrots += 10 * (previousFieldIndex - state.currentPlayer.position) + return null + } else { + return MoveMistake.CANNOT_FALL_BACK + } + } + + override fun equals(other: Any?): Boolean = other is FallBack +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt index 1053dc278..27688a6d0 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Field.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -7,19 +7,21 @@ import com.thoughtworks.xstream.annotations.XStreamAlias enum class Field(val short: String, val unicode: String = short) { /** * Zahl- und Flaggenfelder. - * Die veränderten Spielregeln sehen nur noch die Felder 1,2 vor. - * Die Positionsfelder 3 und 4 wurden in Möhrenfelder umgewandelt, - * und (1,5,6) sind jetzt Position-1-Felder. + * Die veränderten Spielregeln sehen nur noch die Felder 1 und 2 vor. + * Die Positionsfelder 3 und 4 wurden umgewandelt. + * und (1/5/6) sind jetzt Position-1-Felder. */ POSITION_1("P1"), POSITION_2("P2"), - /** Igelfeld */ + /** Igelfeld: Hierauf kann nur rückwärts gezogen werden. */ HEDGEHOG("I", "\uD83E\uDD94"), - /** Salatfeld */ + /** Salatfeld: Beim Betreteten wird im nächsten Zug ein Salat gegessen. */ SALAD("S", "\uD83E\uDD57"), - /** Karottenfeld */ + /** Karottenfeld: Hier dürfen Karotten getauscht werden. */ CARROT("K", "\uD83E\uDD55"), - /** Hasenfeld */ + /** Hasenfeld: Hier wird sofort eine Hasenkarte gespielt. */ HARE("H"), + /** Marktfeld: Hier wird eine Hasenkarte gekauft (Variation). */ + MARKET("M"), /** Das Zielfeld */ GOAL("Z", "🏁"), /** Das Startfeld */ diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index f8de7ce45..5c1f7ddd6 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -10,7 +10,8 @@ object GameRuleLogic { * @param moveCount Anzahl der Felder, um die bewegt wird * @return Anzahl der benötigten Karotten */ - fun calculateCarrots(moveCount: Int): Int = (moveCount * (moveCount + 1)) / 2 + fun calculateCarrots(moveCount: Int): Int = + (moveCount * (moveCount + 1)) / 2 /** * Berechnet, wie viele Züge mit `carrots` Karotten möglich sind. @@ -72,7 +73,8 @@ object GameRuleLogic { * @param state GameState * @return true, falls der derzeitige Spieler keine andere Aktion machen kann. */ - fun isValidToSkip(state: GameState): Boolean = !canDoAnything(state) + fun isValidToSkip(state: GameState): Boolean = + !canDoAnything(state) /** * Überprüft, ob ein Spieler einen Zug (keine Aussetzung) @@ -129,18 +131,6 @@ object GameRuleLogic { } - /** - * Überprüft `FallBack` Züge auf Korrektheit - * - * @param state GameState - * @return true, falls der currentPlayer einen Rückzug machen darf - */ - fun isValidToFallBack(state: GameState): Boolean { - if (mustEatSalad(state)) return false - val newPosition: Int? = state.board.getPreviousField(Field.HEDGEHOG, state.currentPlayer.position) - return (newPosition != -1) && state.otherPlayer.position != newPosition - } - /** * Überprüft, ob der derzeitige Spieler die `FALL_BACK` Karte spielen darf. * @param state GameState @@ -213,7 +203,8 @@ object GameRuleLogic { val isOnHare = state.board.getField(player.position) == Field.HARE val hasCarrots = player.getCards().any { it == Card.TAKE_OR_DROP_CARROTS } - return mustNotAdvance && isOnHare && hasCarrots && (n == 20 || n == -20 || n == 0) && + return mustNotAdvance && isOnHare && hasCarrots && + (n == 20 || n == -20 || n == 0) && if (n < 0) (player.carrots + n) >= 0 else true } @@ -222,15 +213,10 @@ object GameRuleLogic { * @param state GameState * @return true, falls die `EAT_SALAD` Karte gespielt werden darf */ - fun isValidToPlayEatSalad(state: GameState): Boolean { - val player: Hare = state.currentPlayer - - val isOnHare = state.board.getField(player.position) == Field.HARE - val hasEatSalad = player.getCards().any { it == Card.TAKE_OR_DROP_CARROTS } - val hasCarrots = player.salads > 0 - - return isOnHare && hasEatSalad && hasCarrots - } + fun isValidToPlayEatSalad(state: GameState, player: Hare = state.currentPlayer): Boolean = + state.board.getField(player.position) == Field.HARE && + player.getCards().any { it == Card.EAT_SALAD } && + player.salads > 0 /** * Überprüft, ob der derzeitige Spieler irgendeine Karte spielen kann. @@ -238,8 +224,8 @@ object GameRuleLogic { * @param state GameState * @return true, falls das Spielen einer Karte möglich ist */ - private fun canPlayAnyCard(state: GameState): Boolean = - state.currentPlayer.getCards().any { canPlayCard(state, it) } ?: false + fun canPlayAnyCard(state: GameState): Boolean = + state.currentPlayer.getCards().any { canPlayCard(state, it) } private fun canPlayCard(state: GameState, card: Card): Boolean = when (card) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 20eecfd8a..770e6ba77 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -4,7 +4,8 @@ import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* -import sc.shared.InvalidMoveException +import sc.plugin2025.GameRuleLogic.calculateCarrots +import sc.plugin2025.GameRuleLogic.mustEatSalad /** * The GameState class represents the current state of the game. @@ -33,11 +34,14 @@ data class GameState @JvmOverloads constructor( get() = getHare(currentTeam) val otherPlayer - get() = if (currentPlayer == players[0]) players[1] else players[0] + get() = getHare(otherTeam) val aheadPlayer get() = players.maxByOrNull { it.position }!! + val Hare.opponent: Hare + get() = getHare(team) + fun getHare(team: ITeam) = players.find { it.team == team }!! @@ -51,15 +55,69 @@ data class GameState @JvmOverloads constructor( override val isOver: Boolean get() = players.any { it.inGoal } + override fun clone(): GameState = + copy(board = board.clone(), players = players.clone()) + + override fun getPointsForTeam(team: ITeam): IntArray = + getHare(team).let { intArrayOf(it.position, it.salads) } + + override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) + + fun getSensibleMoves(player: Hare): List { + if(currentField == Field.SALAD && player.lastAction != EatSalad) + return listOf(EatSalad) + return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).mapNotNull { distance -> + val newPos = player.position + distance + Advance(distance).takeIf { + board.getField(newPos) != Field.HEDGEHOG && canEnterField(newPos) + } + } + listOf(FallBack).takeIf { isValidToFallBack() }.orEmpty() + + listOf() + } + + override fun moveIterator(): Iterator = getSensibleMoves().iterator() + override fun performMoveDirectly(move: HuIMove) { move.perform(this) + turn++ + if(GameRuleLogic.isValidToSkip(this)) { + turn++ + } } + /** Basic validation whether a field may be entered via a jump that is not backward. + * Does not validate whether a Hare card can be played on hare field. */ + fun canEnterField(newPosition: Int, player: Hare = currentPlayer): Boolean { + val field = board.getField(newPosition) + if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) + return false + return when (field) { + Field.SALAD -> player.salads > 0 + Field.MARKET -> player.carrots >= 10 + Field.HARE -> player.getCards().isNotEmpty() + Field.GOAL -> player.carrots - calculateCarrots(newPosition - player.position) <= 10 && player.salads == 0 + Field.HEDGEHOG -> false + else -> true + } + } + + /** + * Überprüft `FallBack` Züge auf Korrektheit + * + * @param state GameState + * @return true, falls der currentPlayer einen Rückzug machen darf + */ + fun isValidToFallBack(): Boolean { + if (mustEatSalad(this)) return false + val newPosition: Int? = this.board.getPreviousField(Field.HEDGEHOG, this.currentPlayer.position) + return (newPosition != -1) && this.otherPlayer.position != newPosition + } + + fun mustPlayCard(player: Hare = currentPlayer) = currentField == Field.HARE && player.lastAction !is CardAction - /** * Überprüft `EatSalad` Zug auf Korrektheit. * Um einen Salat zu verzehren, muss der Spieler sich: @@ -75,12 +133,4 @@ data class GameState @JvmOverloads constructor( board.getField(player.position) == Field.SALAD && player.lastAction != EatSalad - override fun moveIterator(): Iterator = TODO() - - override fun clone(): GameState = - copy(board = board.clone(), players = players.clone()) - - override fun getPointsForTeam(team: ITeam): IntArray = - getHare(team).let { intArrayOf(it.position, it.salads) } - -} +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 37e35dfde..fc67c1c22 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -26,4 +26,4 @@ data class Hare( fun eatSalad() = salads-- override fun clone(): Hare = copy() -} +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index 9d3b7aa8b..9e78e0b91 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -3,8 +3,9 @@ package sc.plugin2025 import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { - NO_ACTIONS("Der Zug enthält keine Aktionen"), CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), - CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich") + CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), + CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), + CANNOT_CARD_EXCHANGE_CARROTS("Karottentauschkarte kann nicht für %s Karotten gespielt werden."), } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Player.java b/plugin/src/main/kotlin/sc/plugin2025/Player.java deleted file mode 100644 index de8907cb2..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/Player.java +++ /dev/null @@ -1,309 +0,0 @@ -package sc.plugin2018; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import com.thoughtworks.xstream.annotations.XStreamOmitField; -import sc.framework.plugins.SimplePlayer; -import sc.shared.PlayerColor; -import sc.plugin2018.util.Constants; -import sc.shared.PlayerScore; -import sc.shared.ScoreCause; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -/** - * Ein Spieler aus Hase- und Igel. - * - */ -@XStreamAlias(value = "player") -public class Player extends SimplePlayer implements Cloneable -{ - // Farbe der Spielfigure - @XStreamAsAttribute - private PlayerColor color; - - // Position auf dem Spielbrett - @XStreamAsAttribute - private int index; - - // Anzahl der Karotten des Spielers - @XStreamAsAttribute - private int carrots; - - // Anzahl der bisher verspeisten Salate - @XStreamAsAttribute - private int salads; - - // verfügbare Hasenkarten - private ArrayList cards; - - // letzte Aktion, die kein Skip war - private Action lastNonSkipAction; - - @XStreamOmitField - private boolean mustPlayCard; - - /** - * Nur für den Server relevant. Wird innerhalb eines Zuges genutzt, um zu überpüfen, ob eine - * Karte gespielt werden muss. Muss am nach einem Zug immer false sein, sonst war Zug ungültig. - * @param mustPlayCard zu setzender Wert - */ - public void setMustPlayCard(boolean mustPlayCard) - { - this.mustPlayCard = mustPlayCard; - } - - /** - * Nur für den Server relevant. Wird innerhalb eines Zuges genutzt, um zu überpüfen, ob eine - * Karte gespielt werden muss. Muss am nach einem Zug immer false sein, sonst war Zug ungültig. - * @return true, falls eine Karte gespielt werden muss - */ - public boolean mustPlayCard() - { - return mustPlayCard; - } - - protected Player() - { - cards = new ArrayList<>(); - // only for XStream - } - - protected Player(PlayerColor color) - { - this(); - initialize(color, 0); - } - - protected Player(PlayerColor color, int position) - { - this(); - initialize(color, position); - } - - private void initialize(PlayerColor color, int index) - { - this.index = index; - this.color = color; - this.carrots = Constants.INITIAL_CARROTS; - this.salads = Constants.SALADS_TO_EAT; - - cards.add(CardType.TAKE_OR_DROP_CARROTS); - cards.add(CardType.EAT_SALAD); - cards.add(CardType.HURRY_AHEAD); - cards.add(CardType.FALL_BACK); - } - - /** - * Überprüft ob Spieler bestimmte Karte noch besitzt - * @param type Karte - * @return true, falls Karte noch vorhanden - */ - public boolean ownsCardOfType(CardType type) - { - return getCards().contains(type); - } - - /** - * Die Anzahl an Karotten die der Spieler zur Zeit auf der Hand hat. - * - * @return Anzahl der Karotten - */ - public final int getCarrots() - { - return carrots; - } - - /** - * Setzt die Karotten initial - * @param carrots Anzahl der Karotten - */ - protected final void setCarrots(int carrots) - { - this.carrots = carrots; - } - - /** - * Ändert Karottenanzahl um angegebenen Wert - * @param amount Wert um den geändert wird - */ - public final void changeCarrotsBy(int amount) - { - this.carrots = this.carrots + amount; - } - - /** - * Die Anzahl der Salate, die dieser Spieler noch verspeisen muss. - * - * @return Anzahl der übrigen Salate - */ - public final int getSalads() - { - return salads; - } - - /** - * Setzt Salate, nur für den Server relevant. Nur für Tests genutzt. - * @param salads Salate - */ - protected final void setSalads(int salads) - { - this.salads = salads; - } - - /** - * Verringert Salate um eins. Das essen eines Salats ist nicht erlaubt, sollte keiner mehr vorhanden sein. - */ - protected final void eatSalad() - { - this.salads = this.salads - 1; - } - - /** - * Gibt die für diesen Spieler verfügbaren Hasenkarten zurück. - * - * @return übrige Karten - */ - public List getCards() - { - if (this.cards == null) - { - this.cards = new ArrayList<>(); - } - - return cards; - } - - /** - * Gibt Karten ohne bestimmten Typ zurück. - * @param type Typ der zu entfernenden Karte - * @return Liste der übrigen Karten - */ - public List getCardsWithout(CardType type) - { - List res = new ArrayList<>(4); - for (CardType b : cards) - { - if (!b.equals(type)) - res.add(b); - } - return res; - } - - /** - * Setzt verfügbare Karten es Spielers. Wird vom Server beim ausführen eines Zuges verwendet. - * @param cards verfügbare Karten - */ - public void setCards(List cards) - { - this.cards = new ArrayList<>(cards); - } - - /** - * Die aktuelle Position der Figure auf dem Spielfeld. Vor dem ersten Zug - * steht eine Figure immer auf Spielfeld 0 - * - * @return Spielfeldpositionsindex - */ - public final int getFieldIndex() - { - return index; - } - - /** - * Setzt die Spielfeldposition eines Spielers. Nur für den Server relevant. - * @param pos neuer Positionsindex eines Spielers - */ - public final void setFieldIndex(final int pos) - { - index = pos; - } - - /** - * Die Farbe dieses Spielers auf dem Spielbrett - * - * @return Spielerfarbe - */ - public final PlayerColor getPlayerColor() - { - return color; - } - - /** - * Nur für den Server relevant. Setzt Spielerfarbe des Spielers. - * @param playerColor Spielerfarbe - */ - public void setPlayerColor(PlayerColor playerColor) { - this.color = playerColor; - } - - /** - * Gibt letzte Aktion des Spielers zurück. Wird vom Server zum validieren von Zügen genutzt. - * @return letzte Aktion - */ - public Action getLastNonSkipAction() { - return lastNonSkipAction; - } - - /** - * Setzt letzte Aktion des Spielers. Nur für den Server relevant beim ausführen von perform - * Es wird hier nicht überprüft, ob die Aktion Skip ist. - * @param lastNonSkipAction letzte Aktion - */ - public void setLastNonSkipAction(Action lastNonSkipAction) { - this.lastNonSkipAction = lastNonSkipAction; - } - - - /** - * Erzeugt eine deep copy eines Spielers - * @return Spieler - */ - public Player clone() - { - Player clone = null; - try - { - clone = (Player) super.clone(); - clone.cards = new ArrayList<>(); - clone.cards.addAll(this.cards); - clone.mustPlayCard = this.mustPlayCard; - clone.salads = this.salads; - clone.carrots = this.carrots; - clone.index = this.index; - if (this.lastNonSkipAction != null) { - clone.lastNonSkipAction = this.lastNonSkipAction.clone(); - } - } - catch (CloneNotSupportedException e) - { - e.printStackTrace(); - } - return clone; - } - - /** - * Überprüft, ob Spieler im Ziel. Für den Server für das Überprüfen der WinCondition relevant - * @return true, falls Spieler auf Zielfeld steht, Sekundärkriterien werden nicht geprüft. - */ - public boolean inGoal() - { - return index == Constants.NUM_FIELDS - 1; - } - - @Override - public String toString() { - String toString = "Player " + this.getDisplayName() + " (color,index,carrots,salads) " + "(" - + this.color + "," - + this.index + "," - + this.carrots + "," - + this.salads + ")\n"; - for (CardType type: this.cards) { - toString += type + "\n"; - } - toString += "LastAction " + this.lastNonSkipAction; - return toString; - } -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/Skip.java b/plugin/src/main/kotlin/sc/plugin2025/Skip.java deleted file mode 100644 index 197793cd8..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/Skip.java +++ /dev/null @@ -1,49 +0,0 @@ -package sc.plugin2018; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidMoveException; - -/** - * Ein Aussetzzug. Ist nur erlaubt, sollten keine anderen Züge möglich sein. - */ -@XStreamAlias(value = "skip") -public class Skip extends Action { - - /** - * Konstruktor für einen Aussetzzug. Ein Aussetzzug sollte immer die einzige und erste Aktion eines - * Zuges sein. - */ - public Skip() { - this.order = 0; - } - - public Skip(int order) { - this.order = 0; - } - - public void perform(GameState state) throws InvalidMoveException { - // this methods does literally nothing - if(this.order > 0) { - throw new InvalidMoveException("Nur das ausspielen von Karten ist nach der ersten Aktion erlaubt."); - } - if (!GameRuleLogic.isValidToSkip(state)) { - throw new InvalidMoveException("Spieler kann noch einen anderen Zug ausführen, aussetzen ist nicht erlaubt."); - } - } - - @Override - public Skip clone() { - return new Skip(this.order); - } - - @Override - public boolean equals(Object o) { - return o instanceof Skip; - } - - @Override - public String toString() { - return "Skip order " + this.order; - } -} diff --git a/plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin b/plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin index d66bf875a..0ec671ead 100644 --- a/plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin +++ b/plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin @@ -1 +1 @@ -sc.plugin2024.util.GamePlugin +sc.plugin2025.util.GamePlugin diff --git a/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider b/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider index 804559dc4..c2f0bf0cd 100644 --- a/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider +++ b/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider @@ -1 +1 @@ -sc.plugin2024.util.XStreamClasses +sc.plugin2025.util.XStreamClasses diff --git a/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt b/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt index 6f5631bdd..eb06a1319 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt @@ -20,4 +20,4 @@ class GameRuleLogicTest: StringSpec({ calculateMoveableFields(6) shouldBe 3 calculateMoveableFields(7) shouldBe 3 } -}) +}) \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java b/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java deleted file mode 100644 index d44de4ab5..000000000 --- a/plugin/src/test/kotlin/sc/plugin2025/PlayerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package sc.plugin2018; - -import junit.framework.Assert; -import org.junit.Test; -import sc.shared.PlayerColor; - -import java.util.List; - -/** - * @author rra - * @since Jul 4, 2009 - * - */ -public class PlayerTest -{ - /** - * Überprüft das ein Spieler mit den richtigen Anfangswerten erstellt wird. - */ - @Test - public void testPlayer() - { - Player red = new Player(PlayerColor.RED); - Assert.assertEquals(PlayerColor.RED, red.getPlayerColor()); - Assert.assertEquals(68, red.getCarrots()); - Assert.assertEquals(5, red.getSalads()); - Assert.assertEquals(false, red.inGoal()); - Assert.assertEquals(false, red.mustPlayCard()); - Assert.assertEquals(0, red.getFieldIndex()); - Assert.assertEquals(true, red.ownsCardOfType(CardType.EAT_SALAD)); - Assert.assertEquals(true, red.ownsCardOfType(CardType.HURRY_AHEAD)); - Assert.assertEquals(true, red.ownsCardOfType(CardType.FALL_BACK)); - Assert.assertEquals(true, red.ownsCardOfType(CardType.TAKE_OR_DROP_CARROTS)); - } -} diff --git a/sdk/src/main/server-api/sc/api/plugins/TwoPlayerGameState.kt b/sdk/src/main/server-api/sc/api/plugins/TwoPlayerGameState.kt index e864d9878..50209b393 100644 --- a/sdk/src/main/server-api/sc/api/plugins/TwoPlayerGameState.kt +++ b/sdk/src/main/server-api/sc/api/plugins/TwoPlayerGameState.kt @@ -24,7 +24,7 @@ abstract class TwoPlayerGameState( abstract val lastMove: M? /** Führe den gegebenen Zug in einer Kopie dieses Gamestate aus und gib ihn zurück. */ - fun performMove(move: M): IGameState = + fun performMove(move: M): TwoPlayerGameState = clone().also { it.performMoveDirectly(move) } /** Performs the Move on this GameState. From 9176b4583b0c3fe5839d578080718fc770ee201c Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Thu, 9 May 2024 19:49:38 +0300 Subject: [PATCH 14/38] test(plugin): provide all XStreamClasses for tests --- .../kotlin/sc/plugin2023/XStreamClasses.kt | 14 -------------- .../sc/plugin2023/util/XStreamClasses.kt | 19 +++++++++++++++++++ .../kotlin/sc/plugin2024/AccelerationTest.kt | 2 +- .../services/sc.api.plugins.IGamePlugin | 1 + .../services/sc.networking.XStreamProvider | 3 +++ 5 files changed, 24 insertions(+), 15 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2023/XStreamClasses.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt create mode 100644 plugin/src/test/resources/META-INF/services/sc.api.plugins.IGamePlugin create mode 100644 plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider diff --git a/plugin/src/main/kotlin/sc/plugin2023/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2023/XStreamClasses.kt deleted file mode 100644 index e8d6bb93c..000000000 --- a/plugin/src/main/kotlin/sc/plugin2023/XStreamClasses.kt +++ /dev/null @@ -1,14 +0,0 @@ -package sc.plugin2023 - -import sc.api.plugins.Coordinates -import sc.api.plugins.Team -import sc.networking.XStreamProvider - -class XStreamClasses: XStreamProvider { - - override val classesToRegister = - listOf( - Board::class.java, Coordinates::class.java, GameState::class.java, - Move::class.java, Team::class.java) - -} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt new file mode 100644 index 000000000..ba92462f0 --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt @@ -0,0 +1,19 @@ +package sc.plugin2023.util + +import sc.api.plugins.Coordinates +import sc.networking.XStreamProvider +import sc.plugin2023.Board +import sc.plugin2023.GameState +import sc.plugin2023.Move + +class XStreamClasses: XStreamProvider { + + override val classesToRegister = + listOf( + Board::class.java, + GameState::class.java, + Move::class.java, + Coordinates::class.java, + ) + +} \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt b/plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt index 6aabe731c..e0b3f2e84 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt @@ -9,7 +9,7 @@ import sc.plugin2024.mistake.AccelerationProblem class AccelerationTest: FunSpec({ test("serializes nicely") { - Accelerate(5) shouldSerializeTo """""" + Accelerate(5) shouldSerializeTo """""" } val gameState = GameState() diff --git a/plugin/src/test/resources/META-INF/services/sc.api.plugins.IGamePlugin b/plugin/src/test/resources/META-INF/services/sc.api.plugins.IGamePlugin new file mode 100644 index 000000000..471343c51 --- /dev/null +++ b/plugin/src/test/resources/META-INF/services/sc.api.plugins.IGamePlugin @@ -0,0 +1 @@ +sc.plugin2025.util.GamePlugin \ No newline at end of file diff --git a/plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider b/plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider new file mode 100644 index 000000000..db0c70efa --- /dev/null +++ b/plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider @@ -0,0 +1,3 @@ +sc.plugin2023.util.XStreamClasses +sc.plugin2024.util.XStreamClasses +sc.plugin2025.util.XStreamClasses \ No newline at end of file From 36f736b73632b4cdbc131592fa054bf8cfa3b4cd Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Thu, 9 May 2024 19:55:49 +0300 Subject: [PATCH 15/38] refactor(plugin25): unify some move validation methods --- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 2 +- .../src/main/kotlin/sc/plugin2025/FallBack.kt | 2 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 4 +- .../main/kotlin/sc/plugin2025/GameState.kt | 41 ++++++++++--------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index 5fe1a1c25..031e95647 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -13,7 +13,7 @@ import sc.shared.IMoveMistake @XStreamAlias(value = "EatSalad") object EatSalad: HuIMove { override fun perform(state: GameState): IMoveMistake? { - if(state.canEatSalad()) { + if(state.mayEatSalad()) { val player = state.currentPlayer player.eatSalad() if(player == state.aheadPlayer) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt index dacc5db31..73b44a89f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -11,7 +11,7 @@ import com.thoughtworks.xstream.annotations.XStreamAlias @XStreamAlias(value = "fallBack") object FallBack: HuIMove { override fun perform(state: GameState): MoveMistake? { - if(state.isValidToFallBack()) { + if(state.mayFallBack()) { val previousFieldIndex: Int = state.currentPlayer.position state.currentPlayer.position = state.board.getPreviousField(Field.HEDGEHOG, previousFieldIndex) ?: return MoveMistake.CANNOT_FALL_BACK diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 5c1f7ddd6..8814a6044 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -82,8 +82,8 @@ object GameRuleLogic { * @return true, falls ein Zug möglich ist. */ private fun canDoAnything(state: GameState): Boolean = - (canPlayAnyCard(state) || isValidToFallBack(state) || isValidToExchangeCarrots(state, 10) - || isValidToExchangeCarrots(state, -10) || state.canEatSalad() || canAdvanceToAnyField(state)) + (canPlayAnyCard(state) || state.mayFallBack() || isValidToExchangeCarrots(state, 10) + || isValidToExchangeCarrots(state, -10) || state.mayEatSalad() || canAdvanceToAnyField(state)) /** * Überprüft, ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 770e6ba77..ec56d1a7e 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -39,15 +39,18 @@ data class GameState @JvmOverloads constructor( val aheadPlayer get() = players.maxByOrNull { it.position }!! + val currentField: Field + get() = currentPlayer.field + + val Hare.field: Field + get() = board.getField(position) + val Hare.opponent: Hare get() = getHare(team) fun getHare(team: ITeam) = players.find { it.team == team }!! - val currentField - get() = board.getField(currentPlayer.position) - /** Das [Team], das am Zug ist. */ override val currentTeam: Team get() = currentTeamFromTurn() @@ -64,14 +67,14 @@ data class GameState @JvmOverloads constructor( override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) fun getSensibleMoves(player: Hare): List { - if(currentField == Field.SALAD && player.lastAction != EatSalad) + if(mustEatSalad()) return listOf(EatSalad) return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).mapNotNull { distance -> val newPos = player.position + distance Advance(distance).takeIf { - board.getField(newPos) != Field.HEDGEHOG && canEnterField(newPos) + board.getField(newPos) != Field.HEDGEHOG && mayEnterField(newPos) } - } + listOf(FallBack).takeIf { isValidToFallBack() }.orEmpty() + + } + listOf(FallBack).takeIf { mayFallBack() }.orEmpty() + listOf() } @@ -87,7 +90,7 @@ data class GameState @JvmOverloads constructor( /** Basic validation whether a field may be entered via a jump that is not backward. * Does not validate whether a Hare card can be played on hare field. */ - fun canEnterField(newPosition: Int, player: Hare = currentPlayer): Boolean { + fun mayEnterField(newPosition: Int, player: Hare = currentPlayer): Boolean { val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) return false @@ -107,17 +110,12 @@ data class GameState @JvmOverloads constructor( * @param state GameState * @return true, falls der currentPlayer einen Rückzug machen darf */ - fun isValidToFallBack(): Boolean { + fun mayFallBack(): Boolean { if (mustEatSalad(this)) return false - val newPosition: Int? = this.board.getPreviousField(Field.HEDGEHOG, this.currentPlayer.position) - return (newPosition != -1) && this.otherPlayer.position != newPosition + val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, currentPlayer.position) + return lastHedgehog != null && otherPlayer.position != lastHedgehog } - - fun mustPlayCard(player: Hare = currentPlayer) = - currentField == Field.HARE && - player.lastAction !is CardAction - /** * Überprüft `EatSalad` Zug auf Korrektheit. * Um einen Salat zu verzehren, muss der Spieler sich: @@ -128,9 +126,14 @@ data class GameState @JvmOverloads constructor( * * @return true, falls ein Salat gegessen werden darf */ - fun canEatSalad(player: Hare = currentPlayer) = - player.salads > 0 && - board.getField(player.position) == Field.SALAD && - player.lastAction != EatSalad + fun mayEatSalad(player: Hare = currentPlayer) = + player.salads > 0 && mustEatSalad(player) + + fun mustEatSalad(player: Hare = currentPlayer) = + player.field == Field.SALAD && player.lastAction != EatSalad + + fun mustPlayCard(player: Hare = currentPlayer) = + player.field == Field.HARE && + player.lastAction !is CardAction } \ No newline at end of file From 1556474af590603b5c1b9ebac04d8913f930d91a Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Fri, 10 May 2024 01:22:10 +0300 Subject: [PATCH 16/38] feat(plugin25): add new card mechanics --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 32 +++++++++++++------ plugin/src/main/kotlin/sc/plugin2025/Card.kt | 20 ++++++++++-- .../main/kotlin/sc/plugin2025/GameState.kt | 16 +++++++--- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 9 ++++++ .../main/kotlin/sc/plugin2025/MoveMistake.kt | 5 +++ 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 169e9fd85..fd6bbaed2 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -3,7 +3,6 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute import sc.plugin2025.GameRuleLogic.calculateCarrots -import sc.plugin2025.GameRuleLogic.isValidToAdvance import sc.shared.IMoveMistake /** @@ -17,18 +16,31 @@ import sc.shared.IMoveMistake class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction) : HuIMove { override fun perform(state: GameState): IMoveMistake? { - if (isValidToAdvance(state, this.distance)) { - val player = state.currentPlayer - player.carrots -= calculateCarrots(this.distance) - player.position += distance - if(state.currentField == Field.HARE) { + val player = state.currentPlayer + val check = state.checkAdvance(distance) + if(check != null) + return check + player.carrots -= calculateCarrots(distance) + player.position += distance + return when(state.currentField) { + Field.HARE -> { if(cards.isEmpty()) return MoveMistake.MUST_PLAY_CARD - return cards.firstNotNullOfOrNull { it.perform(state) } + cards.firstNotNullOfOrNull { + it.perform(state) + } } - return null - } else { - return MoveMistake.CANNOT_MOVE_FORWARD + Field.MARKET -> { + if(cards.size > 1) + return MoveMistake.CANNOT_BUY_MULTIPLE_CARDS + cards.firstNotNullOfOrNull { card -> + return player.consumeCarrots(10) ?: run { + player.addCard(card.card) + null + } + } + } + else -> MoveMistake.CANNOT_PLAY_CARD.takeIf { cards.isNotEmpty() } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 3fd48ec7d..66f2c3eea 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -13,20 +13,34 @@ enum class Card { /** Falle eine Position zurück. */ FALL_BACK, /** Rücke eine Position vor. */ - HURRY_AHEAD + HURRY_AHEAD, + /** Karottenvorrat mit dem Gegner tauschen. */ + SWAP_CARROTS, } sealed class CardAction: HuIMove { abstract val card: Card + override fun perform(state: GameState): IMoveMistake? { + if(state.currentField != Field.HARE) + return MoveMistake.CANNOT_PLAY_CARD + if(state.currentPlayer.hasCard(card)) + return MoveMistake.CARD_NOT_OWNED + return null + } data class PlayCard(override val card: Card): CardAction() { override fun perform(state: GameState): IMoveMistake? { - TODO("Not yet implemented") + return super.perform(state) ?: run { + when(card) { + Card.TAKE_OR_DROP_CARROTS -> null + else -> MoveMistake.CANNOT_PLAY_CARD + } + } } } class CarrotCard(val value: Int): CardAction() { override val card = Card.TAKE_OR_DROP_CARROTS override fun perform(state: GameState): IMoveMistake? { - TODO("Not yet implemented") + return super.perform(state) } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index ec56d1a7e..b211cdfed 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -70,10 +70,7 @@ data class GameState @JvmOverloads constructor( if(mustEatSalad()) return listOf(EatSalad) return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).mapNotNull { distance -> - val newPos = player.position + distance - Advance(distance).takeIf { - board.getField(newPos) != Field.HEDGEHOG && mayEnterField(newPos) - } + Advance(distance).takeIf { mayEnterField(player.position + distance) } } + listOf(FallBack).takeIf { mayFallBack() }.orEmpty() + listOf() } @@ -88,8 +85,17 @@ data class GameState @JvmOverloads constructor( } } + /** Basic validation whether a player may move forward by that distance. + * Does not validate whether a card can be played on hare field. */ + fun checkAdvance(distance: Int, player: Hare = currentPlayer) = + when { + !mayEnterField(player.position + distance, player) -> MoveMistake.CANNOT_ENTER_FIELD + player.carrots < calculateCarrots(distance) -> MoveMistake.MISSING_CARROTS + else -> null + } + /** Basic validation whether a field may be entered via a jump that is not backward. - * Does not validate whether a Hare card can be played on hare field. */ + * Does not validate whether a card can be played on hare field. */ fun mayEnterField(newPosition: Int, player: Hare = currentPlayer): Boolean { val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index fc67c1c22..07a7bf869 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -14,6 +14,7 @@ data class Hare( private val cards: ArrayList = arrayListOf(*Card.values()), ): PublicCloneable { fun getCards(): List = cards + fun addCard(card: Card) = cards.add(card) fun hasCard(card: Card) = cards.contains(card) fun removeCard(card: Card) = cards.remove(card) @@ -25,5 +26,13 @@ data class Hare( fun eatSalad() = salads-- + fun consumeCarrots(count: Int): MoveMistake? = + if(carrots < count) { + MoveMistake.MISSING_CARROTS + } else { + carrots -= count + null + } + override fun clone(): Hare = copy() } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index 9e78e0b91..d180ba162 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -5,7 +5,12 @@ import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), + CARD_NOT_OWNED("Karte kann nicht gespielt werden, da nicht im Besitz."), + CANNOT_PLAY_CARD("Karte kann nicht gespielt werden."), + CANNOT_BUY_MULTIPLE_CARDS("Auf einem Marktfeld kann maximal eine Karte gekauft werden."), + CANNOT_ENTER_FIELD("Vorwärtszug ist nicht möglich auf dieses Feld."), CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), CANNOT_CARD_EXCHANGE_CARROTS("Karottentauschkarte kann nicht für %s Karotten gespielt werden."), + MISSING_CARROTS("Nicht genügend Karotten"), } From 3bad86bb5949a826cb2b96a1f45f2d0dc0183031 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Fri, 10 May 2024 11:34:07 +0300 Subject: [PATCH 17/38] feat(plugin25): implement card actions --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 38 +++++++++-------- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 37 +++++++++++++---- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 8 +--- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 2 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 1 + .../main/kotlin/sc/plugin2025/GameState.kt | 41 +++++++++++++++---- .../main/kotlin/sc/plugin2025/MoveMistake.kt | 16 +++++--- 7 files changed, 94 insertions(+), 49 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index fd6bbaed2..2f16b2b33 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -13,7 +13,7 @@ import sc.shared.IMoveMistake * Der Wert der Karottentauschkarte spielt dann keine Rolle. */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction) : HuIMove { +class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction): HuIMove { override fun perform(state: GameState): IMoveMistake? { val player = state.currentPlayer @@ -22,25 +22,27 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardActio return check player.carrots -= calculateCarrots(distance) player.position += distance - return when(state.currentField) { - Field.HARE -> { - if(cards.isEmpty()) - return MoveMistake.MUST_PLAY_CARD - cards.firstNotNullOfOrNull { - it.perform(state) - } + + if(state.currentField == Field.MARKET) { + if(cards.size != 1) + return MoveMistake.MUST_BUY_ONE_CARD + return player.consumeCarrots(10) ?: run { + player.addCard(cards.single().card) + null } - Field.MARKET -> { - if(cards.size > 1) - return MoveMistake.CANNOT_BUY_MULTIPLE_CARDS - cards.firstNotNullOfOrNull { card -> - return player.consumeCarrots(10) ?: run { - player.addCard(card.card) - null - } - } + } + + var lastCard: Card? = null + return cards.firstNotNullOfOrNull { + if(state.currentField != Field.HARE || lastCard?.moves == false) + return MoveMistake.CANNOT_PLAY_CARD + lastCard = it.card + it.perform(state) + } ?: run { + MoveMistake.MUST_PLAY_CARD.takeIf { + // On Hare field and no card played or just moved there through card + state.currentField == Field.HARE || lastCard?.moves != false } - else -> MoveMistake.CANNOT_PLAY_CARD.takeIf { cards.isNotEmpty() } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 66f2c3eea..660e5a257 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -5,17 +5,17 @@ import sc.shared.IMoveMistake /** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ @XStreamAlias(value = "card") -enum class Card { +enum class Card(val moves: Boolean) { /** Nehme Karotten auf, oder leg sie ab. */ - TAKE_OR_DROP_CARROTS, + TAKE_OR_DROP_CARROTS(false), /** Iß sofort einen Salat. */ - EAT_SALAD, + EAT_SALAD(false), /** Falle eine Position zurück. */ - FALL_BACK, + FALL_BACK(true), /** Rücke eine Position vor. */ - HURRY_AHEAD, + HURRY_AHEAD(true), /** Karottenvorrat mit dem Gegner tauschen. */ - SWAP_CARROTS, + SWAP_CARROTS(false), } sealed class CardAction: HuIMove { @@ -31,8 +31,29 @@ sealed class CardAction: HuIMove { override fun perform(state: GameState): IMoveMistake? { return super.perform(state) ?: run { when(card) { - Card.TAKE_OR_DROP_CARROTS -> null - else -> MoveMistake.CANNOT_PLAY_CARD + Card.EAT_SALAD -> { + if(state.currentPlayer.salads == 0) + return MoveMistake.CANNOT_EAT_SALAD + state.eatSalad() + null + } + Card.FALL_BACK -> { + if(!state.isAhead()) + return MoveMistake.CANNOT_PLAY_FALL_BACK + state.moveToField(state.otherPlayer.position - 1) + } + Card.HURRY_AHEAD -> { + if(state.isAhead()) + return MoveMistake.CANNOT_PLAY_HURRY_AHEAD + state.moveToField(state.otherPlayer.position + 1) + } + Card.SWAP_CARROTS -> { + val car = state.currentPlayer.carrots + state.currentPlayer.carrots = state.otherPlayer.carrots + state.otherPlayer.carrots = car + null + } + Card.TAKE_OR_DROP_CARROTS -> throw NoWhenBranchMatchedException() } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index 031e95647..abcbab02e 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -14,13 +14,7 @@ import sc.shared.IMoveMistake object EatSalad: HuIMove { override fun perform(state: GameState): IMoveMistake? { if(state.mayEatSalad()) { - val player = state.currentPlayer - player.eatSalad() - if(player == state.aheadPlayer) { - player.carrots += 10 - } else { - player.carrots += 30 - } + state.eatSalad() return null } else { return MoveMistake.CANNOT_EAT_SALAD diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index 4d334182f..8b12318fe 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -16,7 +16,7 @@ data class ExchangeCarrots(val value: Int): HuIMove { state.currentPlayer.carrots += value return null } else { - return MoveMistake.CANNOT_CARD_EXCHANGE_CARROTS + return MoveMistake.CANNOT_PLAY_EXCHANGE_CARROTS } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 8814a6044..70013daec 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -233,6 +233,7 @@ object GameRuleLogic { Card.FALL_BACK -> isValidToPlayFallBack(state) Card.HURRY_AHEAD -> isValidToPlayHurryAhead(state) Card.TAKE_OR_DROP_CARROTS -> isValidToPlayTakeOrDropCarrots(state, 20) + else -> TODO() } /** diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index b211cdfed..cd53f04b3 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -6,6 +6,7 @@ import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.plugin2025.GameRuleLogic.mustEatSalad +import sc.shared.InvalidMoveException /** * The GameState class represents the current state of the game. @@ -32,13 +33,16 @@ data class GameState @JvmOverloads constructor( val currentPlayer get() = getHare(currentTeam) - + val otherPlayer get() = getHare(otherTeam) - + val aheadPlayer get() = players.maxByOrNull { it.position }!! + fun isAhead(player: Hare = currentPlayer) = + player.position > player.opponent.position + val currentField: Field get() = currentPlayer.field @@ -68,7 +72,7 @@ data class GameState @JvmOverloads constructor( fun getSensibleMoves(player: Hare): List { if(mustEatSalad()) - return listOf(EatSalad) + return listOf(EatSalad) return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).mapNotNull { distance -> Advance(distance).takeIf { mayEnterField(player.position + distance) } } + listOf(FallBack).takeIf { mayFallBack() }.orEmpty() + @@ -78,13 +82,26 @@ data class GameState @JvmOverloads constructor( override fun moveIterator(): Iterator = getSensibleMoves().iterator() override fun performMoveDirectly(move: HuIMove) { - move.perform(this) + val mist = + MoveMistake.MUST_EAT_SALAD.takeIf { + mustEatSalad() && move != EatSalad + } ?: move.perform(this) + if(mist != null) + throw InvalidMoveException(mist, move) turn++ if(GameRuleLogic.isValidToSkip(this)) { turn++ } } + fun moveToField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? = + if(mayEnterField(newPosition, player)) { + player.position = newPosition + null + } else { + MoveMistake.CANNOT_ENTER_FIELD + } + /** Basic validation whether a player may move forward by that distance. * Does not validate whether a card can be played on hare field. */ fun checkAdvance(distance: Int, player: Hare = currentPlayer) = @@ -100,7 +117,7 @@ data class GameState @JvmOverloads constructor( val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) return false - return when (field) { + return when(field) { Field.SALAD -> player.salads > 0 Field.MARKET -> player.carrots >= 10 Field.HARE -> player.getCards().isNotEmpty() @@ -117,7 +134,7 @@ data class GameState @JvmOverloads constructor( * @return true, falls der currentPlayer einen Rückzug machen darf */ fun mayFallBack(): Boolean { - if (mustEatSalad(this)) return false + if(mustEatSalad(this)) return false val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, currentPlayer.position) return lastHedgehog != null && otherPlayer.position != lastHedgehog } @@ -138,8 +155,14 @@ data class GameState @JvmOverloads constructor( fun mustEatSalad(player: Hare = currentPlayer) = player.field == Field.SALAD && player.lastAction != EatSalad - fun mustPlayCard(player: Hare = currentPlayer) = - player.field == Field.HARE && - player.lastAction !is CardAction + /** Isst einen Salat, keine Überprüfung der Regelkonformität. */ + fun eatSalad(player: Hare = currentPlayer) { + player.eatSalad() + if(isAhead(player)) { + player.carrots += 10 + } else { + player.carrots += 30 + } + } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index d180ba162..167b8931c 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -3,14 +3,18 @@ package sc.plugin2025 import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { + MISSING_CARROTS("Nicht genügend Karotten"), + MUST_EAT_SALAD("Auf einem Salatfeld muss ein Salat gegessen werden"), CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), + CANNOT_ENTER_FIELD("Feld kann nicht betreten werden."), + CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), + CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), + + MUST_BUY_ONE_CARD("Auf einem Marktfeld muss genau eine Karte gekauft werden."), MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), CARD_NOT_OWNED("Karte kann nicht gespielt werden, da nicht im Besitz."), CANNOT_PLAY_CARD("Karte kann nicht gespielt werden."), - CANNOT_BUY_MULTIPLE_CARDS("Auf einem Marktfeld kann maximal eine Karte gekauft werden."), - CANNOT_ENTER_FIELD("Vorwärtszug ist nicht möglich auf dieses Feld."), - CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), - CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), - CANNOT_CARD_EXCHANGE_CARROTS("Karottentauschkarte kann nicht für %s Karotten gespielt werden."), - MISSING_CARROTS("Nicht genügend Karotten"), + CANNOT_PLAY_FALL_BACK("Rückzugskarte nicht spielbar."), + CANNOT_PLAY_HURRY_AHEAD("Vorwärtssprungkarte nicht spielbar."), + CANNOT_PLAY_EXCHANGE_CARROTS("Karottentauschkarte kann nicht mit dieser Karottenanzahl gespielt werden."), } From f2d2b28d1851fe1969711ee43e6aa6e915422dca Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Fri, 10 May 2024 23:16:17 +0300 Subject: [PATCH 18/38] feat(plugin25): better field accessibility for gui --- plugin/src/main/kotlin/sc/plugin2025/Board.kt | 48 ++++++++++--------- plugin/src/main/kotlin/sc/plugin2025/Field.kt | 2 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 2 +- .../main/kotlin/sc/plugin2025/GameState.kt | 21 +++++--- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt index bec6b4c38..7a66cb2f0 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -9,11 +9,15 @@ import sc.plugin2025.util.HuIConstants data class Board( @XStreamImplicit(itemFieldName = "fields") private val track: Array = generateTrack().toTypedArray(), ): IBoard { - fun findField(field: Field, range: Iterable = 0 until HuIConstants.NUM_FIELDS) = - range.find { track[it] == field } + + val fields + get() = track.iterator() fun getField(index: Int) = track[index] + fun findField(field: Field, range: Iterable = 0 until HuIConstants.NUM_FIELDS) = + range.find { track[it] == field } + fun getPreviousField(field: Field, index: Int = 0) = findField(field, (index - 1) downTo (0)) @@ -41,9 +45,9 @@ data class Board( segment.addAll( listOf( Field.HARE, - Field.CARROT, Field.HARE, Field.CARROT, - Field.CARROT, Field.HARE, Field.POSITION_1, - Field.POSITION_2, Field.CARROT + Field.CARROTS, Field.HARE, Field.CARROTS, + Field.CARROTS, Field.HARE, Field.POSITION_1, + Field.POSITION_2, Field.CARROTS ) ) segment.shuffle() @@ -54,8 +58,8 @@ data class Board( track.add(Field.HEDGEHOG) segment.addAll( listOf( - Field.CARROT, - Field.CARROT, Field.HARE + Field.CARROTS, + Field.CARROTS, Field.HARE ) ) segment.shuffle() @@ -66,7 +70,7 @@ data class Board( segment.addAll( listOf( Field.POSITION_1, - Field.POSITION_2, Field.CARROT + Field.POSITION_2, Field.CARROTS ) ) segment.shuffle() @@ -76,8 +80,8 @@ data class Board( track.add(Field.HEDGEHOG) segment.addAll( listOf( - Field.CARROT, - Field.CARROT, Field.POSITION_2 + Field.CARROTS, + Field.CARROTS, Field.POSITION_2 ) ) segment.shuffle() @@ -90,7 +94,7 @@ data class Board( segment.addAll( listOf( Field.HARE, - Field.CARROT, Field.CARROT, Field.CARROT, + Field.CARROTS, Field.CARROTS, Field.CARROTS, Field.POSITION_2 ) ) @@ -102,8 +106,8 @@ data class Board( segment.addAll( listOf( Field.HARE, - Field.POSITION_1, Field.CARROT, Field.HARE, - Field.POSITION_2, Field.CARROT + Field.POSITION_1, Field.CARROTS, Field.HARE, + Field.POSITION_2, Field.CARROTS ) ) segment.shuffle() @@ -113,8 +117,8 @@ data class Board( track.add(Field.HEDGEHOG) segment.addAll( listOf( - Field.CARROT, - Field.HARE, Field.CARROT, Field.POSITION_2 + Field.CARROTS, + Field.HARE, Field.CARROTS, Field.POSITION_2 ) ) segment.shuffle() @@ -125,9 +129,9 @@ data class Board( track.add(Field.HEDGEHOG) segment.addAll( listOf( - Field.CARROT, - Field.CARROT, Field.HARE, Field.POSITION_2, - Field.POSITION_1, Field.CARROT + Field.CARROTS, + Field.CARROTS, Field.HARE, Field.POSITION_2, + Field.POSITION_1, Field.CARROTS ) ) segment.shuffle() @@ -138,8 +142,8 @@ data class Board( segment.addAll( listOf( Field.HARE, - Field.CARROT, Field.POSITION_2, Field.CARROT, - Field.CARROT + Field.CARROTS, Field.POSITION_2, Field.CARROTS, + Field.CARROTS ) ) segment.shuffle() @@ -152,8 +156,8 @@ data class Board( segment.addAll( listOf( Field.HARE, - Field.CARROT, Field.POSITION_1, Field.CARROT, - Field.HARE, Field.CARROT + Field.CARROTS, Field.POSITION_1, Field.CARROTS, + Field.HARE, Field.CARROTS ) ) segment.shuffle() diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt index 27688a6d0..834804eec 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Field.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -17,7 +17,7 @@ enum class Field(val short: String, val unicode: String = short) { /** Salatfeld: Beim Betreteten wird im nächsten Zug ein Salat gegessen. */ SALAD("S", "\uD83E\uDD57"), /** Karottenfeld: Hier dürfen Karotten getauscht werden. */ - CARROT("K", "\uD83E\uDD55"), + CARROTS("K", "\uD83E\uDD55"), /** Hasenfeld: Hier wird sofort eine Hasenkarte gespielt. */ HARE("H"), /** Marktfeld: Hier wird eine Hasenkarte gekauft (Variation). */ diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 70013daec..66e4ed8e1 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -126,7 +126,7 @@ object GameRuleLogic { */ fun isValidToExchangeCarrots(state: GameState, n: Int) = with(state) { val player = currentPlayer - val valid = board.getField(player.position) == Field.CARROT + val valid = board.getField(player.position) == Field.CARROTS n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index cd53f04b3..01b907d83 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -5,7 +5,6 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* import sc.plugin2025.GameRuleLogic.calculateCarrots -import sc.plugin2025.GameRuleLogic.mustEatSalad import sc.shared.InvalidMoveException /** @@ -73,10 +72,20 @@ data class GameState @JvmOverloads constructor( fun getSensibleMoves(player: Hare): List { if(mustEatSalad()) return listOf(EatSalad) - return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).mapNotNull { distance -> - Advance(distance).takeIf { mayEnterField(player.position + distance) } - } + listOf(FallBack).takeIf { mayFallBack() }.orEmpty() + - listOf() + return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).flatMap { distance -> + val newField = player.position + distance + if(!mayEnterField(newField)) + return emptyList() + when(board.getField(newField)) { + //Field.HARE -> Card.values().map { Advance(distance, it) } + //Field.MARKET -> Card.values().map { Advance(distance, it) } + else -> listOf(Advance(distance)) + } + } + listOfNotNull( + FallBack.takeIf { mayFallBack() }, + ExchangeCarrots(10).takeIf { GameRuleLogic.isValidToExchangeCarrots(this, 10) }, + ExchangeCarrots(-10).takeIf { GameRuleLogic.isValidToExchangeCarrots(this, -10) }, + ) } override fun moveIterator(): Iterator = getSensibleMoves().iterator() @@ -134,7 +143,7 @@ data class GameState @JvmOverloads constructor( * @return true, falls der currentPlayer einen Rückzug machen darf */ fun mayFallBack(): Boolean { - if(mustEatSalad(this)) return false + if(mustEatSalad()) return false val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, currentPlayer.position) return lastHedgehog != null && otherPlayer.position != lastHedgehog } From ada838155f83c47909bea4bc59513113239b7a96 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Fri, 10 May 2024 23:51:04 +0300 Subject: [PATCH 19/38] fix(plugin25): remove exchange carrots card and streamline card validation --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 29 +++-- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 123 +++--------------- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 2 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 51 +------- .../main/kotlin/sc/plugin2025/GameState.kt | 4 + .../main/kotlin/sc/plugin2025/MoveMistake.kt | 4 +- 6 files changed, 43 insertions(+), 170 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 2f16b2b33..8da14deb7 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -13,7 +13,7 @@ import sc.shared.IMoveMistake * Der Wert der Karottentauschkarte spielt dann keine Rolle. */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardAction): HuIMove { +class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): HuIMove { override fun perform(state: GameState): IMoveMistake? { val player = state.currentPlayer @@ -23,25 +23,30 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: CardActio player.carrots -= calculateCarrots(distance) player.position += distance - if(state.currentField == Field.MARKET) { - if(cards.size != 1) - return MoveMistake.MUST_BUY_ONE_CARD - return player.consumeCarrots(10) ?: run { - player.addCard(cards.single().card) - null - } - } - var lastCard: Card? = null + var bought = false return cards.firstNotNullOfOrNull { + if(bought) + return MoveMistake.MUST_BUY_ONE_CARD + if(state.currentField == Field.MARKET) { + return player.consumeCarrots(10) ?: run { + bought = true + player.addCard(it) + null + } + } if(state.currentField != Field.HARE || lastCard?.moves == false) return MoveMistake.CANNOT_PLAY_CARD - lastCard = it.card + lastCard = it it.perform(state) } ?: run { + MoveMistake.MUST_BUY_ONE_CARD.takeIf { + // On Market field and no card bought or just moved there through card + state.currentField == Field.MARKET && !bought + } MoveMistake.MUST_PLAY_CARD.takeIf { // On Hare field and no card played or just moved there through card - state.currentField == Field.HARE || lastCard?.moves != false + state.currentField == Field.HARE && lastCard?.moves != false } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 660e5a257..061c2e539 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -5,115 +5,28 @@ import sc.shared.IMoveMistake /** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ @XStreamAlias(value = "card") -enum class Card(val moves: Boolean) { - /** Nehme Karotten auf, oder leg sie ab. */ - TAKE_OR_DROP_CARROTS(false), - /** Iß sofort einen Salat. */ - EAT_SALAD(false), - /** Falle eine Position zurück. */ - FALL_BACK(true), - /** Rücke eine Position vor. */ - HURRY_AHEAD(true), +enum class Card(val moves: Boolean, val error: MoveMistake, val playable: (GameState) -> Boolean, val play: (GameState) -> Unit): HuIMove { + /** Falle hinter den Gegenspieler. */ + FALL_BACK(true, MoveMistake.CANNOT_PLAY_FALL_BACK, { it.isAhead() && it.mayEnterField(it.otherPlayer.position + 1) }, { it.moveToField(it.otherPlayer.position - 1) }), + /** Rücke vor den Gegenspieler. */ + HURRY_AHEAD(true, MoveMistake.CANNOT_PLAY_HURRY_AHEAD, { !it.isAhead() && it.mayEnterField(it.otherPlayer.position + 1) }, { it.moveToField(it.otherPlayer.position + 1) }), + /** Friss sofort einen Salat. */ + EAT_SALAD(false, MoveMistake.CANNOT_EAT_SALAD, { it.currentPlayer.salads > 0 }, { it.eatSalad() }), /** Karottenvorrat mit dem Gegner tauschen. */ - SWAP_CARROTS(false), -} - -sealed class CardAction: HuIMove { - abstract val card: Card + SWAP_CARROTS(false, MoveMistake.CARD_NOT_OWNED, { true }, { + val car = it.currentPlayer.carrots + it.currentPlayer.carrots = it.otherPlayer.carrots + it.otherPlayer.carrots = car + }); + override fun perform(state: GameState): IMoveMistake? { if(state.currentField != Field.HARE) return MoveMistake.CANNOT_PLAY_CARD - if(state.currentPlayer.hasCard(card)) + if(!state.currentPlayer.removeCard(this)) return MoveMistake.CARD_NOT_OWNED + if(!playable(state)) + return error + play(state) return null } - data class PlayCard(override val card: Card): CardAction() { - override fun perform(state: GameState): IMoveMistake? { - return super.perform(state) ?: run { - when(card) { - Card.EAT_SALAD -> { - if(state.currentPlayer.salads == 0) - return MoveMistake.CANNOT_EAT_SALAD - state.eatSalad() - null - } - Card.FALL_BACK -> { - if(!state.isAhead()) - return MoveMistake.CANNOT_PLAY_FALL_BACK - state.moveToField(state.otherPlayer.position - 1) - } - Card.HURRY_AHEAD -> { - if(state.isAhead()) - return MoveMistake.CANNOT_PLAY_HURRY_AHEAD - state.moveToField(state.otherPlayer.position + 1) - } - Card.SWAP_CARROTS -> { - val car = state.currentPlayer.carrots - state.currentPlayer.carrots = state.otherPlayer.carrots - state.otherPlayer.carrots = car - null - } - Card.TAKE_OR_DROP_CARROTS -> throw NoWhenBranchMatchedException() - } - } - } - } - class CarrotCard(val value: Int): CardAction() { - override val card = Card.TAKE_OR_DROP_CARROTS - override fun perform(state: GameState): IMoveMistake? { - return super.perform(state) - } - } - - - /* - @Override - public void perform(GameState state) throws InvalidMoveException { - state.getCurrentPlayer().setMustPlayCard(false); // player played a card - switch (type) { // when entering a HARE field with fall_back or hurry ahead, player has to play another card - case EAT_SALAD: - if (GameRuleLogic.isValidToPlayEatSalad(state)) { - state.getCurrentPlayer().eatSalad(); - if (state.isFirst(state.getCurrentPlayer())) { - state.getCurrentPlayer().changeCarrotsBy(10); - } else { - state.getCurrentPlayer().changeCarrotsBy(30); - } - } else { - throw new InvalidMoveException("Das Ausspielen der EAT_SALAD Karte ist nicht möglich."); - } - break; - case FALL_BACK: - if (GameRuleLogic.isValidToPlayFallBack(state)) { - state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() - 1); - if (state.fieldOfCurrentPlayer() == Field.HARE) { - state.getCurrentPlayer().setMustPlayCard(true); - } - } else { - throw new InvalidMoveException("Das Ausspielen der FALL_BACK Karte ist nicht möglich."); - } - break; - case HURRY_AHEAD: - if (GameRuleLogic.isValidToPlayHurryAhead(state)) { - state.getCurrentPlayer().setFieldIndex(state.getOtherPlayer().getFieldIndex() + 1); - if (state.fieldOfCurrentPlayer() == Field.HARE) { - state.getCurrentPlayer().setMustPlayCard(true); - } - } else { - throw new InvalidMoveException("Das Ausspielen der HURRY_AHEAD Karte ist nicht möglich."); - } - break; - case TAKE_OR_DROP_CARROTS: - if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(state, this.getValue())) { - state.getCurrentPlayer().changeCarrotsBy(this.getValue()); - } else { - throw new InvalidMoveException("Das Ausspielen der TAKE_OR_DROP_CARROTS Karte ist nicht möglich."); - } - break; - } - state.setLastAction(this); - // remove player card - state.getCurrentPlayer().setCards(state.getCurrentPlayer().getCardsWithout(this.type)); - } - */ -} +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index 8b12318fe..d999c9518 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -16,7 +16,7 @@ data class ExchangeCarrots(val value: Int): HuIMove { state.currentPlayer.carrots += value return null } else { - return MoveMistake.CANNOT_PLAY_EXCHANGE_CARROTS + return MoveMistake.CANNOT_EXCHANGE_CARROTS } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index 66e4ed8e1..f79850417 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -217,54 +217,5 @@ object GameRuleLogic { state.board.getField(player.position) == Field.HARE && player.getCards().any { it == Card.EAT_SALAD } && player.salads > 0 - - /** - * Überprüft, ob der derzeitige Spieler irgendeine Karte spielen kann. - * TAKE_OR_DROP_CARROTS wird nur mit 20 überprüft - * @param state GameState - * @return true, falls das Spielen einer Karte möglich ist - */ - fun canPlayAnyCard(state: GameState): Boolean = - state.currentPlayer.getCards().any { canPlayCard(state, it) } - - private fun canPlayCard(state: GameState, card: Card): Boolean = - when (card) { - Card.EAT_SALAD -> isValidToPlayEatSalad(state) - Card.FALL_BACK -> isValidToPlayFallBack(state) - Card.HURRY_AHEAD -> isValidToPlayHurryAhead(state) - Card.TAKE_OR_DROP_CARROTS -> isValidToPlayTakeOrDropCarrots(state, 20) - else -> TODO() - } - - /** - * Überprüft, ob der derzeitige Spieler die Karte spielen kann. - * @param state derzeitiger GameState - * @param c Karte die gespielt werden soll - * @param n Wert fuer TAKE_OR_DROP_CARROTS - * @return true, falls das Spielen der entsprechenden Karte möglich ist - */ - fun isValidToPlayCard(state: GameState, c: Card, n: Int): Boolean { - return if (c == Card.TAKE_OR_DROP_CARROTS) isValidToPlayTakeOrDropCarrots(state, n) - else canPlayCard(state, c) - } - - fun mustEatSalad(state: GameState): Boolean { - // check whether player just moved to salad field and must eat salad - val player: Hare = state.currentPlayer - val field: Field = state.board.getField(player.position) - - val isSalad = field == Field.SALAD - val wasLastAdvance = player.lastAction is Advance - val wasFallBackOrHurry = (player.lastAction as? CardAction)?.card in listOf(Card.FALL_BACK, Card.HURRY_AHEAD) - - return isSalad && (wasLastAdvance || wasFallBackOrHurry) - } - - /** - * Gibt zurück, ob der derzeitige Spieler eine Karte spielen kann. - * @param state derzeitiger GameState - * @return true, falls eine Karte gespielt werden kann - */ - fun canPlayCard(state: GameState): Boolean = - state.board.getField(state.currentPlayer.position) === Field.HARE && canPlayAnyCard(state) + } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 01b907d83..efa20da9e 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -161,6 +161,10 @@ data class GameState @JvmOverloads constructor( fun mayEatSalad(player: Hare = currentPlayer) = player.salads > 0 && mustEatSalad(player) + /** Gibt zurück, ob der Spieler eine Karte spielen kann. */ + fun canPlayCard(player: Hare = currentPlayer): Boolean = + board.getField(player.position) === Field.HARE && player.getCards().any { it.playable(this) } + fun mustEatSalad(player: Hare = currentPlayer) = player.field == Field.SALAD && player.lastAction != EatSalad diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index 167b8931c..0afc06fd6 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -5,10 +5,11 @@ import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { MISSING_CARROTS("Nicht genügend Karotten"), MUST_EAT_SALAD("Auf einem Salatfeld muss ein Salat gegessen werden"), - CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), CANNOT_ENTER_FIELD("Feld kann nicht betreten werden."), CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), + CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), + CANNOT_EXCHANGE_CARROTS("Karottentauschen kann nicht mit dieser Karottenanzahl gespielt werden."), MUST_BUY_ONE_CARD("Auf einem Marktfeld muss genau eine Karte gekauft werden."), MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), @@ -16,5 +17,4 @@ enum class MoveMistake(override val message: String) : IMoveMistake { CANNOT_PLAY_CARD("Karte kann nicht gespielt werden."), CANNOT_PLAY_FALL_BACK("Rückzugskarte nicht spielbar."), CANNOT_PLAY_HURRY_AHEAD("Vorwärtssprungkarte nicht spielbar."), - CANNOT_PLAY_EXCHANGE_CARROTS("Karottentauschkarte kann nicht mit dieser Karottenanzahl gespielt werden."), } From a6276fc065488abdbcabf649db3a9681cbc48a37 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sat, 11 May 2024 01:02:50 +0300 Subject: [PATCH 20/38] fix(plugin25): clean out legacy logic and start testing --- .../sc/plugin2024/mistake/MoveMistake.kt | 2 +- .../src/main/kotlin/sc/plugin2025/Advance.kt | 25 +- plugin/src/main/kotlin/sc/plugin2025/Board.kt | 5 +- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 27 +- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 3 +- .../kotlin/sc/plugin2025/GameRuleLogic.kt | 190 ----- .../main/kotlin/sc/plugin2025/GameState.java | 707 ------------------ .../main/kotlin/sc/plugin2025/GameState.kt | 86 ++- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 8 +- .../main/kotlin/sc/plugin2025/MoveMistake.kt | 7 +- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 32 + 11 files changed, 131 insertions(+), 961 deletions(-) delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/GameState.java create mode 100644 plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt index 39a8eb2da..c6a0df35c 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt @@ -3,7 +3,7 @@ package sc.plugin2024.mistake import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { - NO_ACTIONS("Der Zug enthält keine Aktionen"), + NO_ACTIONS("Der Zug enthält keine Aktionen."), PUSH_ACTION_REQUIRED("Wenn du auf einem gegnerischen Schiff landest, muss darauf eine Abdrängaktion folgen."), SAND_BANK_END("Zug auf eine Sandbank muss letzte Aktion sein."), FIRST_ACTION_ACCELERATE("Du kannst nur in der ersten Aktion beschleunigen."), diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 8da14deb7..39083a7c7 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -2,7 +2,6 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute -import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.shared.IMoveMistake /** @@ -20,8 +19,7 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu val check = state.checkAdvance(distance) if(check != null) return check - player.carrots -= calculateCarrots(distance) - player.position += distance + player.advanceBy(distance) var lastCard: Card? = null var bought = false @@ -29,7 +27,7 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu if(bought) return MoveMistake.MUST_BUY_ONE_CARD if(state.currentField == Field.MARKET) { - return player.consumeCarrots(10) ?: run { + return@firstNotNullOfOrNull player.consumeCarrots(10) ?: run { bought = true player.addCard(it) null @@ -39,16 +37,17 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu return MoveMistake.CANNOT_PLAY_CARD lastCard = it it.perform(state) - } ?: run { - MoveMistake.MUST_BUY_ONE_CARD.takeIf { - // On Market field and no card bought or just moved there through card - state.currentField == Field.MARKET && !bought - } - MoveMistake.MUST_PLAY_CARD.takeIf { - // On Hare field and no card played or just moved there through card - state.currentField == Field.HARE && lastCard?.moves != false - } + } ?: MoveMistake.MUST_BUY_ONE_CARD.takeIf { + // On Market field and no card bought or just moved there through card + state.currentField == Field.MARKET && !bought + } ?: MoveMistake.MUST_PLAY_CARD.takeIf { + // On Hare field and no card played or just moved there through card + state.currentField == Field.HARE && lastCard?.moves != false } } + override fun toString(): String { + return "Vorwärts um $distance${cards.joinToString(prefix = " mit Karten [", postfix = "]")}" + } + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt index 7a66cb2f0..0e6cf9832 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -7,13 +7,14 @@ import sc.plugin2025.util.HuIConstants @XStreamAlias("board") data class Board( - @XStreamImplicit(itemFieldName = "fields") private val track: Array = generateTrack().toTypedArray(), + @XStreamImplicit(itemFieldName = "fields") private val track: Array = generateTrack().toTypedArray(), ): IBoard { val fields get() = track.iterator() - fun getField(index: Int) = track[index] + fun getField(index: Int): Field? = + track.getOrNull(index) fun findField(field: Field, range: Iterable = 0 until HuIConstants.NUM_FIELDS) = range.find { track[it] == field } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 061c2e539..03b959c96 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -5,15 +5,24 @@ import sc.shared.IMoveMistake /** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ @XStreamAlias(value = "card") -enum class Card(val moves: Boolean, val error: MoveMistake, val playable: (GameState) -> Boolean, val play: (GameState) -> Unit): HuIMove { +enum class Card(val moves: Boolean, val playable: (GameState) -> MoveMistake?, val play: (GameState) -> Unit): HuIMove { /** Falle hinter den Gegenspieler. */ - FALL_BACK(true, MoveMistake.CANNOT_PLAY_FALL_BACK, { it.isAhead() && it.mayEnterField(it.otherPlayer.position + 1) }, { it.moveToField(it.otherPlayer.position - 1) }), + FALL_BACK(true, { state -> + MoveMistake.CANNOT_PLAY_FALL_BACK.takeUnless { state.isAhead() } + ?: state.validateTargetField(state.otherPlayer.position + 1) + }, { it.moveToField(it.otherPlayer.position - 1) }), /** Rücke vor den Gegenspieler. */ - HURRY_AHEAD(true, MoveMistake.CANNOT_PLAY_HURRY_AHEAD, { !it.isAhead() && it.mayEnterField(it.otherPlayer.position + 1) }, { it.moveToField(it.otherPlayer.position + 1) }), + HURRY_AHEAD(true, { state -> + MoveMistake.CANNOT_PLAY_HURRY_AHEAD.takeIf { state.isAhead() } + ?: state.validateTargetField(state.otherPlayer.position + 1) + }, { it.moveToField(it.otherPlayer.position + 1) }), /** Friss sofort einen Salat. */ - EAT_SALAD(false, MoveMistake.CANNOT_EAT_SALAD, { it.currentPlayer.salads > 0 }, { it.eatSalad() }), + EAT_SALAD( + false, + { state -> MoveMistake.NO_SALAD.takeUnless { state.currentPlayer.salads > 0 } }, + { it.eatSalad() }), /** Karottenvorrat mit dem Gegner tauschen. */ - SWAP_CARROTS(false, MoveMistake.CARD_NOT_OWNED, { true }, { + SWAP_CARROTS(false, { null }, { val car = it.currentPlayer.carrots it.currentPlayer.carrots = it.otherPlayer.carrots it.otherPlayer.carrots = car @@ -24,9 +33,9 @@ enum class Card(val moves: Boolean, val error: MoveMistake, val playable: (GameS return MoveMistake.CANNOT_PLAY_CARD if(!state.currentPlayer.removeCard(this)) return MoveMistake.CARD_NOT_OWNED - if(!playable(state)) - return error - play(state) - return null + return playable(state) ?: run { + play(state) + null + } } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index d999c9518..011808ad2 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -1,7 +1,6 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias -import sc.plugin2025.GameRuleLogic.isValidToExchangeCarrots import sc.shared.IMoveMistake /** @@ -12,7 +11,7 @@ import sc.shared.IMoveMistake @XStreamAlias(value = "ExchangeCarrots") data class ExchangeCarrots(val value: Int): HuIMove { override fun perform(state: GameState): IMoveMistake? { - if(isValidToExchangeCarrots(state, this.value)) { + if(state.mayExchangeCarrots(this.value)) { state.currentPlayer.carrots += value return null } else { diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt index f79850417..84c750a99 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt @@ -27,195 +27,5 @@ object GameRuleLogic { else -> (sqrt((2.0 * carrots) + 0.25) - 0.48).toInt() } } - - /** - * Überprüft `Advance` Aktionen auf ihre Korrektheit. - * Folgende Spielregeln werden beachtet: - * - * - Der Spieler muss genügend Karotten für den Zug besitzen - * - Wenn das Ziel erreicht wird, darf der Spieler nach dem Zug maximal 10 Karotten übrig haben - * - Man darf nicht auf Igelfelder ziehen - * - Salatfelder dürfen nur betreten werden, wenn man noch Salate essen muss - * - Hasenfelder dürfen nur betreten werden, wenn man noch Karte ausspielen kann - * - * @param state GameState - * @param distance relativer Abstand zur aktuellen Position des Spielers - * @return true, falls ein Vorwärtszug möglich ist - */ - fun isValidToAdvance(state: GameState, distance: Int): Boolean { - if (distance <= 0) return false - - val player: Hare = state.currentPlayer - if (mustEatSalad(state)) return false - - val requiredCarrots = calculateCarrots(distance) - val hasEnoughCarrots = requiredCarrots <= player.carrots - - val newPosition: Int = player.position + distance - val isNotOnOtherPlayer = state.otherPlayer.position != newPosition - - return when (state.board.getField(newPosition)) { - Field.SALAD -> player.salads > 0 - Field.HARE -> { - val advanceMove = Advance(distance) - val nextState: GameState = state.performMove(advanceMove) as GameState - canPlayAnyCard(nextState) - } - - Field.GOAL -> player.carrots - requiredCarrots <= 10 && player.salads == 0 - Field.HEDGEHOG -> false - else -> true - } && hasEnoughCarrots && isNotOnOtherPlayer - } - - /** - * Überprüft, ob ein Spieler aussetzen darf. Er darf dies, wenn kein anderer Zug möglich ist. - * @param state GameState - * @return true, falls der derzeitige Spieler keine andere Aktion machen kann. - */ - fun isValidToSkip(state: GameState): Boolean = - !canDoAnything(state) - - /** - * Überprüft, ob ein Spieler einen Zug (keine Aussetzung) - * @param state GameState - * @return true, falls ein Zug möglich ist. - */ - private fun canDoAnything(state: GameState): Boolean = - (canPlayAnyCard(state) || state.mayFallBack() || isValidToExchangeCarrots(state, 10) - || isValidToExchangeCarrots(state, -10) || state.mayEatSalad() || canAdvanceToAnyField(state)) - - /** - * Überprüft, ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. - * @param state GameState - * @return true, falls der Spieler irgendeinen Vorwärtszug machen kann - */ - private fun canAdvanceToAnyField(state: GameState): Boolean = - (0..calculateMoveableFields(state.currentPlayer.carrots)) - .any { isValidToAdvance(state, it) } - - /** - * Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss. - * @param state GameState - * @return true, falls der derzeitige Spieler einen Vorwärtszug gemacht werden muss - */ - fun playerMustAdvance(state: GameState?): Boolean { - val player: Hare = state!!.currentPlayer - val type: Field = state.board.getField(player.position) - - if (type == Field.HEDGEHOG || type == Field.START) return true - - val lastAction: HuIMove? = player.lastAction - - if (lastAction is EatSalad) return true - else if (lastAction is CardAction) { - // the player has to leave a hare field in next turn - if (lastAction.card === Card.EAT_SALAD) return true - // the player has to leave the hare field - else if (lastAction.card === Card.TAKE_OR_DROP_CARROTS) return true - } - return false - } - - - /** - * Überprüft, ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. - * @param state GameState - * @param n 10 oder -10 je nach Fragestellung - * @return true, falls die durch n spezifizierte Aktion möglich ist. - */ - fun isValidToExchangeCarrots(state: GameState, n: Int) = with(state) { - val player = currentPlayer - val valid = board.getField(player.position) == Field.CARROTS - n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) - } - - - /** - * Überprüft, ob der derzeitige Spieler die `FALL_BACK` Karte spielen darf. - * @param state GameState - * @return true, falls die `FALL_BACK` Karte gespielt werden darf - */ - fun isValidToPlayFallBack(state: GameState): Boolean { - val player: Hare = state.currentPlayer - - val mustNotAdvance = !playerMustAdvance(state) - val isOnHare = state.board.getField(player.position) == Field.HARE - // TODO() val isFirst = state.players.firstOrNull() == player // das ist denke falsch, aber ich war mir nicht sicher was `isFirst` war - val hasFallback = player.getCards().any { it == Card.FALL_BACK } - - val nextPos: Int = state.otherPlayer.position - 1 - if (nextPos == 0) return false - - return when (state.board.getField(nextPos)) { - Field.SALAD -> player.salads > 0 - Field.HARE -> { - val fallBack = CardAction.PlayCard(Card.FALL_BACK) - val nextState: GameState = state.performMove(fallBack) as GameState - canPlayAnyCard(nextState) - } - - Field.HEDGEHOG -> false - else -> true - } && mustNotAdvance && isOnHare && hasFallback - } - - /** - * Überprüft, ob der derzeitige Spieler die `HURRY_AHEAD` Karte spielen darf. - * @param state GameState - * @return true, falls die `HURRY_AHEAD` Karte gespielt werden darf - */ - fun isValidToPlayHurryAhead(state: GameState): Boolean { - val player: Hare = state.currentPlayer - - val mustNotAdvance = !playerMustAdvance(state) - val isOnHare = state.board.getField(player.position) == Field.HARE - // TODO() val isFirst = state.players.firstOrNull() == player // das ist denke falsch, aber ich war mir nicht sicher was `isFirst` war - val hasHurry = player.getCards().any { it == Card.HURRY_AHEAD } - - val nextPos: Int = state.otherPlayer.position + 1 - if (nextPos == 0) return false - - return when (state.board.getField(nextPos)) { - Field.SALAD -> player.salads > 0 - Field.HARE -> { - val fallBack = CardAction.PlayCard(Card.HURRY_AHEAD) - val nextState: GameState = state.performMove(fallBack) as GameState - canPlayAnyCard(nextState) - } - - Field.GOAL -> player.carrots - calculateCarrots(nextPos - player.position) <= 10 && player.salads == 0 - Field.HEDGEHOG -> false - else -> true - } && mustNotAdvance && isOnHare && hasHurry - } - - /** - * Überprüft, ob der derzeitige Spieler die `TAKE_OR_DROP_CARROTS` Karte spielen darf. - * @param state GameState - * @param n 20 für Nehmen, -20 für Abgeben, 0 für nichts tun - * @return true, falls die `TAKE_OR_DROP_CARROTS` Karte gespielt werden darf - */ - fun isValidToPlayTakeOrDropCarrots(state: GameState, n: Int): Boolean { - val player: Hare = state.currentPlayer - - val mustNotAdvance = !playerMustAdvance(state) - val isOnHare = state.board.getField(player.position) == Field.HARE - val hasCarrots = player.getCards().any { it == Card.TAKE_OR_DROP_CARROTS } - - return mustNotAdvance && isOnHare && hasCarrots && - (n == 20 || n == -20 || n == 0) && - if (n < 0) (player.carrots + n) >= 0 else true - } - - /** - * Überprüft, ob der derzeitige Spieler die `EAT_SALAD` Karte spielen darf. - * @param state GameState - * @return true, falls die `EAT_SALAD` Karte gespielt werden darf - */ - fun isValidToPlayEatSalad(state: GameState, player: Hare = state.currentPlayer): Boolean = - state.board.getField(player.position) == Field.HARE && - player.getCards().any { it == Card.EAT_SALAD } && - player.salads > 0 } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.java b/plugin/src/main/kotlin/sc/plugin2025/GameState.java deleted file mode 100644 index 325a5e69f..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.java +++ /dev/null @@ -1,707 +0,0 @@ -package sc.plugin2018; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import com.thoughtworks.xstream.annotations.XStreamOmitField; - -import sc.plugin2025.GameRuleLogic; -import sc.shared.InvalidGameStateException; -import sc.shared.InvalidMoveException; -import sc.shared.PlayerColor; -import sc.plugin2018.util.Constants; - -import java.util.ArrayList; - -/** - * Ein {@code GameState} beinhaltet alle Informationen, die den Spielstand zu - * einem gegebenen Zeitpunkt, das heisst zwischen zwei Spielzuegen, beschreiben. - * Dies umfasst eine fortlaufende Zugnummer ({@link #getTurn() getRound()}), die - * der Spielserver als Antwort von einem der beiden Spieler ( - * {@link #getCurrentPlayer() getCurrentPlayer()}) erwartet. Weiterhin gehoeren - * die Informationen ueber die beiden Spieler und das Spielfeld zum Zustand. - * Zuseatzlich wird ueber den zuletzt getaetigeten Spielzung und ggf. ueber das - * Spielende informiert. - * - * - * Der {@code GameState} ist damit das zentrale Objekt ueber das auf alle - * wesentlichen Informationen des aktuellen Spiels zugegriffen werden kann. - * - * - * Der Spielserver sendet an beide teilnehmenden Spieler nach jedem getaetigten - * Zug eine neue Kopie des {@code GameState}, in dem der dann aktuelle Zustand - * beschrieben wird. Informationen ueber den Spielverlauf sind nur bedingt ueber - * den {@code GameState} erfragbar und muessen von einem Spielclient daher bei - * Bedarf selbst mitgeschrieben werden. - * - * - * Zusaetzlich zu den eigentlichen Informationen koennen bestimmte - * Teilinformationen abgefragt werden. - * - * @author Niklas, Sören - */ -@XStreamAlias(value = "state") -public class GameState implements Cloneable { - - @XStreamOmitField - private static final Logger logger = LoggerFactory - .getLogger(GameState.class); - /** - * momentane Rundenzahl - */ - @XStreamAsAttribute - private int turn; - - /** - * Farbe des Startspielers - */ - @XStreamAsAttribute - private PlayerColor startPlayer; - - /** - * Farbe des aktuellen Spielers - */ - @XStreamAsAttribute - private PlayerColor currentPlayer; - - /** - * der rote Spieler - */ - private Player red; - /** - * der blaue Spieler - */ - private Player blue; - - /** - * Das Spielbrett - */ - private Board board; - - /** - * letzter getaetigter Zug - */ - private HuIMove lastMove; - - /** - * Erzeugt einen neuen {@code GameState}, in dem alle Informationen so gesetzt - * sind, wie sie zu Beginn eines Spiels, bevor die Spieler beigetreten sind, - * gueltig sind. - * - * - * Dieser Konstruktor ist nur fuer den Spielserver relevant und sollte vom - * Spielclient i.A. nicht aufgerufen werden! - * - * Das Spielfeld wird zufällig aufgebaut. - */ - public GameState() { - this.turn = 0; - this.currentPlayer = PlayerColor.RED; - this.startPlayer = PlayerColor.RED; - this.board = new Board(); - this.red = new Player(PlayerColor.RED); - this.blue = new Player(PlayerColor.BLUE); - } - - /** - * Erzeugt einen neuen {@code GameState} mit denselben Eigenschaften von - * stateToClone. Fuer eigene Implementierungen. - */ - protected GameState(GameState stateToClone) throws CloneNotSupportedException { - GameState clone = stateToClone.clone(); - setRedPlayer(clone.getPlayer(PlayerColor.RED)); - setBluePlayer(clone.getPlayer(PlayerColor.BLUE)); - setLastMove(clone.getLastMove()); - setBoard(clone.getBoard()); - setCurrentPlayer(clone.getCurrentPlayerColor()); - } - - /** - * erzeugt eine Deepcopy dieses Objekts - * - * @return ein neues Objekt mit gleichen Eigenschaften - * @throws CloneNotSupportedException falls das Klonen fehlschlaegt - */ - @Override - public GameState clone() throws CloneNotSupportedException { - GameState clone = (GameState) super.clone(); - if (red != null) - clone.red = this.red.clone(); - if (blue != null) - clone.blue = this.blue.clone(); - if (lastMove != null) - clone.lastMove = (HuIMove) this.lastMove.clone(); - if (board != null) - clone.board = this.board.clone(); - if (currentPlayer != null) - clone.currentPlayer = this.currentPlayer; - clone.startPlayer = this.startPlayer; - clone.turn = this.turn; - return clone; - } - - /** - * Fuegt einem Spiel einen weiteren Spieler hinzu. - * - * - * Diese Methode ist nur fuer den Spielserver relevant und sollte vom - * Spielclient i.A. nicht aufgerufen werden! - * - * @param player - * Der hinzuzufuegende Spieler. - */ - public void addPlayer(Player player) { - if (player.getPlayerColor() == PlayerColor.RED) { - red = player; - } else if (player.getPlayerColor() == PlayerColor.BLUE) { - blue = player; - } - } - - /** - * Gibt das Spielfeld zurueck - * - * @return das Spielfeld - */ - public Board getBoard() { - return this.board; - } - - /** - * Liefert den Spieler als {@code Player}-Objekt, der als die entsprechende Farbe spielt - * @param color die Farbe des gefragten Spielers - */ - public Player getPlayer(PlayerColor color) { - return color == PlayerColor.RED ? red : blue; - } - - /** - * Liefert den Spieler, also ein {@code Player}-Objekt, der momentan am Zug - * ist. - * - * @return Der Spieler, der momentan am Zug ist. - */ - public Player getCurrentPlayer() { - return getPlayer(currentPlayer); - } - - /** - * Liefert die {@code PlayerColor}-Farbe des Spielers, der momentan am Zug - * ist. Dies ist aequivalent zum Aufruf - * {@code getCurrentPlayer().getPlayerColor()}, aber schneller. - * - * @return Die Farbe des Spielers, der momentan am Zug ist. - */ - public PlayerColor getCurrentPlayerColor() { - return currentPlayer; - } - - /** - * Nur für den Server relevant - * @param playerColor PlayerColor of new currentPlayer - */ - protected void setCurrentPlayer(PlayerColor playerColor) { - this.currentPlayer = playerColor; - } - - /** - * Liefert den Spieler, also ein {@code Player}-Objekt, der momentan nicht am - * Zug ist. - * - * @return Der Spieler, der momentan nicht am Zug ist. - */ - public Player getOtherPlayer() { - return getPlayer(getOtherPlayerColor()); - } - - /** - * Liefert die {@code PlayerColor}-Farbe des Spielers, der momentan nicht am - * Zug ist. Dies ist aequivalent zum Aufruf @ - * {@code getCurrentPlayerColor.opponent()} oder - * {@code getOtherPlayer().getPlayerColor()}, aber etwas effizienter. - * - * @return Die Farbe des Spielers, der momentan nicht am Zug ist. - */ - public PlayerColor getOtherPlayerColor() { - return currentPlayer.opponent(); - } - - /** - * @deprecated ersetzt durch {@link #getPlayer(PlayerColor)} - */ - public Player getRedPlayer() { - return red; - } - - /** - * @deprecated ersetzt durch {@link #getPlayer(PlayerColor)} - */ - public Player getBluePlayer() { - return blue; - } - - /** - * Nur für den Server relevant - * @param red roter Spieler - */ - protected void setRedPlayer(Player red) { - this.red = red; - } - - /** - * Nur für den Server relevant - * @param blue blauer Spieler - */ - protected void setBluePlayer(Player blue) { - this.blue = blue; - } - - /** - * Liefert den Spieler, also eine {@code Player}-Objekt, der das Spiel - * begonnen hat. - * - * @return Der Spieler, der momentan Startspieler ist. - */ - public Player getStartPlayer() { - return startPlayer == PlayerColor.RED ? red : blue; - } - - /** - * Liefert die {@code PlayerColor}-Farbe des Spielers, der den aktuellen - * Abschnitt begonnen hat. Dies ist aequivalent zum Aufruf - * {@code getStartPlayer().getPlayerColor()}, aber etwas effizienter. - * - * @return Die Farbe des Spielers, der den aktuellen Abschnitt begonnen - * hat. - */ - public PlayerColor getStartPlayerColor() { - return startPlayer; - } - - /** - * wechselt den Spieler, der aktuell an der Reihe ist anhand der Anzahl der Züge turn - */ - public void switchCurrentPlayer() { - if (turn % 2 == 0) { - currentPlayer = PlayerColor.RED; - } else { - currentPlayer = PlayerColor.BLUE; - } - } - - /** - * Überprüft ob ein Feld durch einen Spieler belegt ist, sodass niemand darauf ziehen kann. - * (da Zielfeld von mehreren betretbar, bei Zielfeld immer false) - * - * @param index - * der Index auf der Rennstrecke - * @return Gibt true zurück, falls sich ein Spieler auf dem Feld befindet und es nicht das Zielfeld ist - */ - public final boolean isOccupied(final int index) - { - return (red.getFieldIndex() == index || blue.getFieldIndex() == index) - && (index != Constants.NUM_FIELDS - 1); - } - - /** - * Überprüft ob der angegebene Spieler an erster Stelle ist. Wenn sich beide - * Spieler im Ziel befinden wird zusätzlich überprüft, ob player - * weniger Karotten besitzt als der Gegenspieler. - * - * @param player überprüfter Spieler - * @return true, falls Spieler an erster Stelle - */ - public final boolean isFirst(final Player player) - { - Player o = this.getOpponent(player); - boolean isFirst = o.getFieldIndex() <= player.getFieldIndex(); - if (player.inGoal() && o.getFieldIndex() == player.getFieldIndex()) - isFirst = isFirst - && player.getCarrots() < o.getCarrots(); - return isFirst; - } - - /** - * Gibt den Feldtypen an einem bestimmten Index zurück. Liegt der - * gewählte Index vor dem Startpunkt oder hinter dem Ziel, so wird - * INVALID zurückgegeben. - * - * @param index die Index auf der Rennstrecke - * @return Feldtyp an Index - */ - public final sc.plugin2018.Field getTypeAt(final int index) - { - return board.getTypeAt(index); - } - - /** - * Findet das nächste Spielfeld vom Typ type beginnend an - * Index index auf diesem Spielbrett. - * - * @param type Feldtyp - * @param index Index - * @return Index des nächsten Feldes genannten Typs - */ - public final int getNextFieldByType(sc.plugin2018.Field type, int index) - { - return this.board.getNextFieldByType(type, index); - } - - /** - * Findet das vorherige Spielfeld vom Typ type beginnend an Index - * index auf diesem Spielbrett. - * @param type Feldtyp - * @param index Index - * @return Index des vorherigen Feldes genannten Typs - */ - public final int getPreviousFieldByType(sc.plugin2018.Field type, int index) - { - return this.board.getPreviousFieldByType(type, index); - } - - /** - * liefert die aktuelle Zugzahl - * @return Nummer des aktuellen Zuges (Zaehlung beginnt mit 0) - */ - public int getTurn() { - return turn; - } - - /** - * Setzt die aktuelle Zugzahl. Nur für den Server relevant - * @param turn neue Zugzahl - */ - public void setTurn(int turn) throws InvalidGameStateException { - int turnLimit = Constants.ROUND_LIMIT * 2; - if (turn > turnLimit) { - throw new InvalidGameStateException("Turn " + turn + " exceeded maxTurn " + turnLimit); - } - this.turn = turn; - } - - /** - * liefert die aktuelle Rundenzahl - * - * @return aktuelle Rundenzahl - */ - public int getRound() { - return turn / 2; - } - - /** - * Liefert Statusinformationen zu einem Spieler als Array mit folgenden - * Einträgen: - *
    - *
  • [0] - Punktekonto des Spielers (Flussfortschritt und Passagiere) - *
  • [1] - Anzahl eingesammelter Passagiere - *
- * - * @param player - * Spieler - * @return Array mit Statistiken - */ - public int[] getPlayerStats(Player player) { - assert player != null; - return getPlayerStats(player.getPlayerColor()); - } - - /** - * Liefert Statusinformationen zu einem Spieler als Array mit folgenden - * Einträgen: - *
    - *
  • [0] - Punktekonto des Spielers (Flussfortschritt und Passagiere) - *
  • [1] - Anzahl eingesammelter Passagiere - *
- * - * @param playerColor - * Farbe des Spielers - * @return Array mit Statistiken - */ - public int[] getPlayerStats(PlayerColor playerColor) { - assert playerColor != null; - - if (playerColor == PlayerColor.RED) { - return getGameStats()[Constants.GAME_STATS_RED_INDEX]; - } else { - return getGameStats()[Constants.GAME_STATS_BLUE_INDEX]; - } - } - - /** - * Liefert Statusinformationen zum Spiel. Diese sind ein Array der - * {@link #getPlayerStats(PlayerColor) Spielerstats}, wobei getGameStats()[0], - * einem Aufruf von getPlayerStats(PlayerColor.RED) entspricht. - * - * @see #getPlayerStats(PlayerColor) - * @return Statusinformationen beider Spieler - */ - public int[][] getGameStats() { - - int[][] stats = new int[2][2]; - - stats[Constants.GAME_STATS_RED_INDEX][Constants.GAME_STATS_FIELD_INDEX] = this.red.getFieldIndex(); - stats[Constants.GAME_STATS_RED_INDEX][Constants.GAME_STATS_CARROTS] = this.red.getCarrots(); - stats[Constants.GAME_STATS_BLUE_INDEX][Constants.GAME_STATS_FIELD_INDEX] = this.blue.getFieldIndex(); - stats[Constants.GAME_STATS_BLUE_INDEX][Constants.GAME_STATS_CARROTS] = this.blue.getCarrots(); - return stats; - - } - - /** - * liefert die Namen den beiden Spieler - * @return Namen der Spieler - */ - public String[] getPlayerNames() { - return new String[] { red.getDisplayName(), blue.getDisplayName() }; - - } - - /** - * Gibt die angezeigte Punktzahl des Spielers zurueck. - * @param playerColor Farbe des Spielers - * @return Punktzahl des Spielers - */ - public int getPointsForPlayer(PlayerColor playerColor) { - return getPlayer(playerColor).getFieldIndex(); - } - - /** - * Ueberschreibt das aktuelle Spielbrett. Fuer eigene Implementierungen. - */ - protected void setBoard(Board newValue) { - board = newValue; - } - - public Player getOpponent(Player player) { - return getPlayer(player.getPlayerColor().opponent()); - } - - /** - * Setzt letzten Zug. Nur für den Server relevant. - * @param lastMove letzter Zug - */ - protected void setLastMove(HuIMove lastMove) { - this.lastMove = lastMove; - } - - /** - * Setzt letzte Aktion eines Spielers. Für den Server in der Zugvalidierung relevant. - * @param action letzte Aktion - */ - public void setLastAction(Action action) { - if (action instanceof sc.plugin2018.Skip) { - return; - } - getCurrentPlayer().setLastNonSkipAction(action); - } - - /** - * Gibt den letzten Zugzurück - * @return letzter Zug - */ - public HuIMove getLastMove() { - return this.lastMove; - } - - - /** - * Gibt die letzte Aktion des Spielers zurück. Nötig für das erkennen von ungültigen Zügen. - * @param player Spieler - * @return letzte Aktion die nicht Skip war - */ - public Action getLastNonSkipAction(Player player) { - return getLastNonSkipAction(player.getPlayerColor()); - } - - /** - * Gibt die letzte Aktion des Spielers der entsprechenden Farbe zurück. Nötig für das erkennen von ungültigen Zügen. - * @param playerColor Spielerfarbe - * @return letzte Aktion die nicht Skip war - */ - public Action getLastNonSkipAction(PlayerColor playerColor) { - return getPlayer(playerColor).getLastNonSkipAction(); - } - - /** - * Git das Feld des derzeitigen Spielers zurück - * @return Feldtyp - */ - public sc.plugin2018.Field fieldOfCurrentPlayer() { - return this.getTypeAt(this.getCurrentPlayer().getFieldIndex()); - } - - /** - * Überprüft ob sich der derzeitige Spieler auf einem Hasenfeld befindet. - * @return true, falls auf Hasenfeld - */ - public boolean isOnHareField() - { - return fieldOfCurrentPlayer().equals(sc.plugin2018.Field.HARE); - } - - public ArrayList getPossibleMoves() { - ArrayList possibleMove = new ArrayList<>(); - ArrayList actions = new ArrayList<>(); - if (GameRuleLogic.isValidToEat(this)) { - // Wenn ein Salat gegessen werden kann, muss auch ein Salat gegessen werden - actions.add(new sc.plugin2018.EatSalad()); - HuIMove move = new HuIMove(actions); - possibleMove.add(move); - return possibleMove; - } - if (GameRuleLogic.isValidToExchangeCarrots(this, 10)) { - actions.add(new sc.plugin2018.ExchangeCarrots(10)); - possibleMove.add(new HuIMove(actions)); - actions.clear(); - } - if (GameRuleLogic.isValidToExchangeCarrots(this, -10)) { - actions.add(new sc.plugin2018.ExchangeCarrots(-10)); - possibleMove.add(new HuIMove(actions)); - actions.clear(); - } - if (GameRuleLogic.isValidToFallBack(this)) { - actions.add(new sc.plugin2018.FallBack()); - possibleMove.add(new HuIMove(actions)); - actions.clear(); - } - // Generiere mögliche Vorwärtszüge - for (int i = 1; i <= GameRuleLogic.calculateMoveableFields(this.getCurrentPlayer().getCarrots()); i++) { - GameState clone = null; - try { - clone = this.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - } - // Überrüfe ob Vorwärtszug möglich ist - if (GameRuleLogic.isValidToAdvance(clone, i)) { - Advance tryAdvance = new Advance(i); - try { - tryAdvance.perform(clone); - } catch (InvalidMoveException e) { - // Sollte nicht passieren, da Zug valide ist - e.printStackTrace(); - break; - } - actions.add(tryAdvance); - // überprüfe, ob eine Karte gespielt werden muss/kann - if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { - possibleMove.addAll(clone.checkForPlayableCards(actions)); - } else { - // Füge möglichen Vorwärtszug hinzu - possibleMove.add(new HuIMove(actions)); - } - } - actions.clear(); - } - if (possibleMove.isEmpty()) { - HuIMove move; - logger.warn("Muss aussetzen"); - actions.add(new sc.plugin2018.Skip()); - move = new HuIMove(actions); - possibleMove.add(move); - } - return possibleMove; - } - - /** - * Überprüft für übergebenen GameState und bisher getätigte Züge, - * ob das Ausspielen einer Karte nötig/möglich ist - * @param actions bisherige Aktionenliste - * @return mögliche Züge - */ - private ArrayList checkForPlayableCards(ArrayList actions) { - ArrayList possibleMove = new ArrayList<>(); - if (this.getCurrentPlayer().mustPlayCard()) { // überprüfe, ob eine Karte gespielt werden muss - if (GameRuleLogic.isValidToPlayEatSalad(this)) { - actions.add(new Card(sc.plugin2018.CardType.EAT_SALAD, actions.size())); - possibleMove.add(new HuIMove(actions)); - - actions.remove(new Card(sc.plugin2018.CardType.EAT_SALAD, 1)); - } - if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 20)) { - actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); - possibleMove.add(new HuIMove(actions)); - - actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 20, actions.size())); - } - if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, -20)) { - actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); - possibleMove.add(new HuIMove(actions)); - - actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, -20, actions.size())); - } - if (GameRuleLogic.isValidToPlayTakeOrDropCarrots(this, 0)) { - actions.add(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); - possibleMove.add(new HuIMove(actions)); - - actions.remove(new Card(sc.plugin2018.CardType.TAKE_OR_DROP_CARROTS, 0, actions.size())); - } - if (GameRuleLogic.isValidToPlayHurryAhead(this)) { - Card card = new Card(sc.plugin2018.CardType.HURRY_AHEAD, actions.size()); - actions.add(card); - // Überprüfe ob wieder auf Hasenfeld gelandet: - GameState clone = null; - try { - clone = this.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - } - try { - card.perform(clone); - } catch (InvalidMoveException e) { - // Sollte nie passieren - e.printStackTrace(); - } - if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { - ArrayList moves = clone.checkForPlayableCards(actions); - if (!moves.isEmpty()) { - possibleMove.addAll(moves); - } - } else { - possibleMove.add(new HuIMove(actions)); - } - - actions.remove(new Card(sc.plugin2018.CardType.HURRY_AHEAD, actions.size())); - } - if (GameRuleLogic.isValidToPlayFallBack(this)) { - Card card = new Card(sc.plugin2018.CardType.FALL_BACK, actions.size()); - actions.add(card); - // Überprüfe ob wieder auf Hasenfeld gelandet: - GameState clone = null; - try { - clone = this.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - } - try { - card.perform(clone); - } catch (InvalidMoveException e) { - // Sollte nie passieren - e.printStackTrace(); - } - if (clone != null && clone.getCurrentPlayer().mustPlayCard()) { - ArrayList moves = clone.checkForPlayableCards(actions); - if (!moves.isEmpty()) { - possibleMove.addAll(moves); - } - } else { - possibleMove.add(new HuIMove(actions)); - } - actions.remove(new Card(sc.plugin2018.CardType.FALL_BACK, actions.size())); - } - } - return possibleMove; - } - - @Override - public String toString() { - return "GameState:\n" - + "turn=" + this.getTurn() + this.getCurrentPlayer() - + this.red + this.blue - + this.board - + this.getLastMove(); - - } -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index efa20da9e..ea958203f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -5,6 +5,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* import sc.plugin2025.GameRuleLogic.calculateCarrots +import sc.plugin2025.GameRuleLogic.calculateMoveableFields import sc.shared.InvalidMoveException /** @@ -21,11 +22,9 @@ import sc.shared.InvalidMoveException data class GameState @JvmOverloads constructor( /** Das aktuelle Spielfeld. */ override val board: Board = Board(), - /** Die Anzahl an bereits getätigten Zügen. - * Modifikation nur via [advanceTurn]. */ + /** Die Anzahl an bereits getätigten Zügen. */ @XStreamAsAttribute override var turn: Int = 0, - @XStreamImplicit - val players: List = Team.values().map { Hare(it) }, + @XStreamImplicit val players: List = Team.values().map { Hare(it) }, /** Der zuletzt gespielte Zug. */ override var lastMove: HuIMove? = null, ): TwoPlayerGameState(players.first().team) { @@ -42,14 +41,14 @@ data class GameState @JvmOverloads constructor( fun isAhead(player: Hare = currentPlayer) = player.position > player.opponent.position - val currentField: Field + val currentField: Field? get() = currentPlayer.field - val Hare.field: Field + val Hare.field: Field? get() = board.getField(position) val Hare.opponent: Hare - get() = getHare(team) + get() = getHare(team.opponent()) fun getHare(team: ITeam) = players.find { it.team == team }!! @@ -72,19 +71,25 @@ data class GameState @JvmOverloads constructor( fun getSensibleMoves(player: Hare): List { if(mustEatSalad()) return listOf(EatSalad) - return (1..GameRuleLogic.calculateMoveableFields(player.carrots)).flatMap { distance -> + return (1..calculateMoveableFields(player.carrots)).flatMap { distance -> val newField = player.position + distance - if(!mayEnterField(newField)) - return emptyList() + if(validateTargetField(newField) != null) + return@flatMap emptyList() when(board.getField(newField)) { - //Field.HARE -> Card.values().map { Advance(distance, it) } - //Field.MARKET -> Card.values().map { Advance(distance, it) } + Field.HARE -> { + val newState = copy(players = players.map { if(it.team == currentTeam) it.clone().apply { advanceBy(distance) } else it }) + Card.values().mapNotNull { card -> + Advance(distance, card).takeIf { card.playable(newState) == null } + // TODO verify card chain playing + } + } + Field.MARKET -> Card.values().map { Advance(distance, it) } else -> listOf(Advance(distance)) } } + listOfNotNull( FallBack.takeIf { mayFallBack() }, - ExchangeCarrots(10).takeIf { GameRuleLogic.isValidToExchangeCarrots(this, 10) }, - ExchangeCarrots(-10).takeIf { GameRuleLogic.isValidToExchangeCarrots(this, -10) }, + ExchangeCarrots(10).takeIf { mayExchangeCarrots(10) }, + ExchangeCarrots(-10).takeIf { mayExchangeCarrots(-10) }, ) } @@ -98,42 +103,41 @@ data class GameState @JvmOverloads constructor( if(mist != null) throw InvalidMoveException(mist, move) turn++ - if(GameRuleLogic.isValidToSkip(this)) { + if(!moveIterator().hasNext()) { turn++ } } fun moveToField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? = - if(mayEnterField(newPosition, player)) { + validateTargetField(newPosition, player) ?: run { player.position = newPosition null - } else { - MoveMistake.CANNOT_ENTER_FIELD } /** Basic validation whether a player may move forward by that distance. * Does not validate whether a card can be played on hare field. */ - fun checkAdvance(distance: Int, player: Hare = currentPlayer) = - when { - !mayEnterField(player.position + distance, player) -> MoveMistake.CANNOT_ENTER_FIELD - player.carrots < calculateCarrots(distance) -> MoveMistake.MISSING_CARROTS - else -> null - } + fun checkAdvance(distance: Int, player: Hare = currentPlayer): MoveMistake? { + if(player.carrots < calculateCarrots(distance)) + return MoveMistake.MISSING_CARROTS + return validateTargetField(player.position + distance, player) + } /** Basic validation whether a field may be entered via a jump that is not backward. * Does not validate whether a card can be played on hare field. */ - fun mayEnterField(newPosition: Int, player: Hare = currentPlayer): Boolean { + fun validateTargetField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? { val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) - return false - return when(field) { - Field.SALAD -> player.salads > 0 - Field.MARKET -> player.carrots >= 10 - Field.HARE -> player.getCards().isNotEmpty() - Field.GOAL -> player.carrots - calculateCarrots(newPosition - player.position) <= 10 && player.salads == 0 - Field.HEDGEHOG -> false - else -> true + return MoveMistake.FIELD_OCCUPIED + when(field) { + Field.SALAD -> player.salads > 0 || return MoveMistake.NO_SALAD + Field.MARKET -> player.carrots >= 10 || return MoveMistake.MISSING_CARROTS + Field.HARE -> player.getCards().isNotEmpty() || return MoveMistake.CARD_NOT_OWNED + Field.GOAL -> player.carrots - calculateCarrots(newPosition - player.position) <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS + Field.HEDGEHOG -> return MoveMistake.HEDGEHOG_ONLY_BACKWARDS + null -> return MoveMistake.FIELD_NONEXISTENT + else -> return null } + return null } /** @@ -161,9 +165,21 @@ data class GameState @JvmOverloads constructor( fun mayEatSalad(player: Hare = currentPlayer) = player.salads > 0 && mustEatSalad(player) + /** + * Überprüft, ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. + * @param state GameState + * @param n 10 oder -10 je nach Fragestellung + * @return true, falls die durch n spezifizierte Aktion möglich ist. + */ + fun mayExchangeCarrots(n: Int): Boolean { + val player = currentPlayer + val valid = board.getField(player.position) == Field.CARROTS + return n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) + } + /** Gibt zurück, ob der Spieler eine Karte spielen kann. */ - fun canPlayCard(player: Hare = currentPlayer): Boolean = - board.getField(player.position) === Field.HARE && player.getCards().any { it.playable(this) } + fun canPlayAnyCard(player: Hare = currentPlayer): Boolean = + board.getField(player.position) === Field.HARE && player.getCards().any { it.playable(this) == null } fun mustEatSalad(player: Hare = currentPlayer) = player.field == Field.SALAD && player.lastAction != EatSalad diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 07a7bf869..cf7073810 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -3,6 +3,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAsAttribute import sc.api.plugins.Team import sc.framework.PublicCloneable +import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.plugin2025.util.HuIConstants data class Hare( @@ -11,7 +12,7 @@ data class Hare( @XStreamAsAttribute var salads: Int = HuIConstants.INITIAL_SALADS, @XStreamAsAttribute var carrots: Int = HuIConstants.INITIAL_CARROTS, @XStreamAsAttribute var lastAction: HuIMove? = null, - private val cards: ArrayList = arrayListOf(*Card.values()), + private val cards: ArrayList = arrayListOf(), ): PublicCloneable { fun getCards(): List = cards fun addCard(card: Card) = cards.add(card) @@ -26,6 +27,11 @@ data class Hare( fun eatSalad() = salads-- + fun advanceBy(distance: Int) { + calculateCarrots(distance) + position += distance + } + fun consumeCarrots(count: Int): MoveMistake? = if(carrots < count) { MoveMistake.MISSING_CARROTS diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt index 0afc06fd6..35de1df3f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt @@ -3,12 +3,17 @@ package sc.plugin2025 import sc.shared.IMoveMistake enum class MoveMistake(override val message: String) : IMoveMistake { + GOAL_CONDITIONS("Voraussetzungen für Zielfeld nicht erfüllt."), + NO_SALAD("Kein Salat verfügbar."), MISSING_CARROTS("Nicht genügend Karotten"), MUST_EAT_SALAD("Auf einem Salatfeld muss ein Salat gegessen werden"), CANNOT_ENTER_FIELD("Feld kann nicht betreten werden."), + FIELD_OCCUPIED("Das Feld ist besetzt."), + FIELD_NONEXISTENT("Das Feld existiert nicht."), + HEDGEHOG_ONLY_BACKWARDS("Ein Igelfeld kann nur mit einem Rückwärtzug betreten werden"), CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), - CANNOT_EAT_SALAD("Es kann gerade kein Salat (mehr) gegessen werden."), + CANNOT_EAT_SALAD("Es kann gerade kein Salat gegessen werden."), CANNOT_EXCHANGE_CARROTS("Karottentauschen kann nicht mit dieser Karottenanzahl gespielt werden."), MUST_BUY_ONE_CARD("Auf einem Marktfeld muss genau eine Karte gekauft werden."), diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt new file mode 100644 index 000000000..7a6597f2b --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -0,0 +1,32 @@ +package sc.plugin2025 + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.* + +class MoveTest: WordSpec({ + "Advance" should { + "allow cards and buy" { + val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) + + state.checkAdvance(2) shouldBe null + state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT + + state.getSensibleMoves().size shouldBe Card.values().size + 1 + + state.performMoveDirectly(Advance(2)) + state.turn shouldBe 1 + + state.checkAdvance(2) shouldBe MoveMistake.FIELD_OCCUPIED + state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT + + state.currentPlayer.addCard(Card.FALL_BACK) + state.checkAdvance(3) shouldBe null + state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) + state.currentPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) + } + } +}) \ No newline at end of file From b873dc6722a45e42f6334c39e271fddc4779cb46 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sat, 11 May 2024 08:18:58 +0300 Subject: [PATCH 21/38] fix(plugin25): GameState cloning --- plugin/src/main/kotlin/sc/plugin2025/Board.kt | 2 +- .../src/main/kotlin/sc/plugin2025/GameState.kt | 2 +- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- .../test/kotlin/sc/plugin2025/GameStateTest.kt | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 plugin/src/test/kotlin/sc/plugin2025/GameStateTest.kt diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt index 0e6cf9832..595dea3d9 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -28,7 +28,7 @@ data class Board( override fun toString() = track.joinToString(prefix = "Board[", postfix = "]") { it.unicode } - override fun clone(): Board = Board(Array(track.size) { track[it] }) + override fun clone(): Board = this companion object { /** diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index ea958203f..8e90834b1 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -61,7 +61,7 @@ data class GameState @JvmOverloads constructor( get() = players.any { it.inGoal } override fun clone(): GameState = - copy(board = board.clone(), players = players.clone()) + copy(players = players.clone()) override fun getPointsForTeam(team: ITeam): IntArray = getHare(team).let { intArrayOf(it.position, it.salads) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index cf7073810..068dabe01 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -40,5 +40,5 @@ data class Hare( null } - override fun clone(): Hare = copy() + override fun clone(): Hare = copy(cards = ArrayList(cards)) } \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2025/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2025/GameStateTest.kt new file mode 100644 index 000000000..5844f38df --- /dev/null +++ b/plugin/src/test/kotlin/sc/plugin2025/GameStateTest.kt @@ -0,0 +1,16 @@ +package sc.plugin2025 + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.* + +class GameStateTest: WordSpec({ + "GameState" should { + "clone correctly" { + val state = GameState() + val clone = state.clone() + state.currentPlayer.getCards().size shouldBe 0 + clone.currentPlayer.addCard(Card.EAT_SALAD) + state.currentPlayer.getCards().size shouldBe 0 + } + } +}) \ No newline at end of file From f68759a04c96cf8406dac8c6ad5ecedaff198648 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sun, 12 May 2024 00:29:52 +0300 Subject: [PATCH 22/38] fix(plugin25): further testing and fixes --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 12 ++++ plugin/src/main/kotlin/sc/plugin2025/Board.kt | 2 + .../main/kotlin/sc/plugin2025/GameState.kt | 58 +++++++++++-------- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 37 +++++++++--- 5 files changed, 80 insertions(+), 31 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 39083a7c7..f0ec60b04 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -2,6 +2,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import sc.plugin2025.util.HuIConstants import sc.shared.IMoveMistake /** @@ -50,4 +51,15 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu return "Vorwärts um $distance${cards.joinToString(prefix = " mit Karten [", postfix = "]")}" } + override fun equals(other: Any?): Boolean { + if(this === other) return true + if(other !is Advance) return false + if(distance != other.distance) return false + if(!cards.contentEquals(other.cards)) return false + return true + } + + override fun hashCode(): Int = + distance + cards.contentHashCode() * HuIConstants.NUM_FIELDS + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt index 595dea3d9..4195b4202 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -10,6 +10,8 @@ data class Board( @XStreamImplicit(itemFieldName = "fields") private val track: Array = generateTrack().toTypedArray(), ): IBoard { + val size = track.size + val fields get() = track.iterator() diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 8e90834b1..d57653d14 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -63,36 +63,49 @@ data class GameState @JvmOverloads constructor( override fun clone(): GameState = copy(players = players.clone()) + fun cloneCurrentPlayer(transform: (Hare) -> Unit) = + copy(players = players.map { if(it.team == currentTeam) it.clone().apply(transform) else it }) + override fun getPointsForTeam(team: ITeam): IntArray = getHare(team).let { intArrayOf(it.position, it.salads) } override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) fun getSensibleMoves(player: Hare): List { - if(mustEatSalad()) + if(mustEatSalad(player)) return listOf(EatSalad) - return (1..calculateMoveableFields(player.carrots)).flatMap { distance -> + return (1..calculateMoveableFields(player.carrots).coerceAtMost(board.size - player.position)).flatMap { distance -> val newField = player.position + distance - if(validateTargetField(newField) != null) + if(validateTargetField(newField, player) != null) return@flatMap emptyList() - when(board.getField(newField)) { - Field.HARE -> { - val newState = copy(players = players.map { if(it.team == currentTeam) it.clone().apply { advanceBy(distance) } else it }) - Card.values().mapNotNull { card -> - Advance(distance, card).takeIf { card.playable(newState) == null } - // TODO verify card chain playing - } - } - Field.MARKET -> Card.values().map { Advance(distance, it) } - else -> listOf(Advance(distance)) - } + val newState = copy(players = players.map { if(it.team == player.team) it.clone().apply { advanceBy(distance) } else it }) + return@flatMap newState.nextCards()?.map { cards -> + Advance(distance, *cards) + } ?: listOf(Advance(distance)) } + listOfNotNull( - FallBack.takeIf { mayFallBack() }, - ExchangeCarrots(10).takeIf { mayExchangeCarrots(10) }, - ExchangeCarrots(-10).takeIf { mayExchangeCarrots(-10) }, + FallBack.takeIf { mayFallBack(player) }, + ExchangeCarrots(10).takeIf { mayExchangeCarrots(10, player) }, + ExchangeCarrots(-10).takeIf { mayExchangeCarrots(-10, player) }, ) } + fun nextCards(player: Hare = currentPlayer): Collection>? = + when(player.field) { + Field.HARE -> { + player.getCards().flatMap { card -> + if(card.playable(this) == null) { + val newState = clone() + card.play(newState) + newState.nextCards(player)?.map { arrayOf(card, *it) } ?: listOf(arrayOf(card)) + } else { + listOf() + } + } + } + Field.MARKET -> Card.values().map { arrayOf(it) } + else -> null + } + override fun moveIterator(): Iterator = getSensibleMoves().iterator() override fun performMoveDirectly(move: HuIMove) { @@ -146,10 +159,10 @@ data class GameState @JvmOverloads constructor( * @param state GameState * @return true, falls der currentPlayer einen Rückzug machen darf */ - fun mayFallBack(): Boolean { - if(mustEatSalad()) return false - val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, currentPlayer.position) - return lastHedgehog != null && otherPlayer.position != lastHedgehog + fun mayFallBack(player: Hare = currentPlayer): Boolean { + if(mustEatSalad(player)) return false + val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, player.position) + return lastHedgehog != null && player.opponent.position != lastHedgehog } /** @@ -171,8 +184,7 @@ data class GameState @JvmOverloads constructor( * @param n 10 oder -10 je nach Fragestellung * @return true, falls die durch n spezifizierte Aktion möglich ist. */ - fun mayExchangeCarrots(n: Int): Boolean { - val player = currentPlayer + fun mayExchangeCarrots(n: Int, player: Hare = currentPlayer): Boolean { val valid = board.getField(player.position) == Field.CARROTS return n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 068dabe01..2f995567f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -28,7 +28,7 @@ data class Hare( fun eatSalad() = salads-- fun advanceBy(distance: Int) { - calculateCarrots(distance) + carrots -= calculateCarrots(distance) position += distance } diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 7a6597f2b..1be3f2f5b 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -4,16 +4,20 @@ import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.* class MoveTest: WordSpec({ - "Advance" should { - "allow cards and buy" { + "Advance" When { + "one player advanced" should { val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) + state.checkAdvance(1) shouldBe null state.checkAdvance(2) shouldBe null state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT - state.getSensibleMoves().size shouldBe Card.values().size + 1 + state.getSensibleMoves() shouldBe listOf( + *Card.values().map { Advance(1, it) }.toTypedArray(), + Advance(2), + ) state.performMoveDirectly(Advance(2)) state.turn shouldBe 1 @@ -23,10 +27,29 @@ class MoveTest: WordSpec({ state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT - state.currentPlayer.addCard(Card.FALL_BACK) - state.checkAdvance(3) shouldBe null - state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) - state.currentPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) + "allow eat salad" { + state.currentPlayer.addCard(Card.EAT_SALAD) + state.performMoveDirectly(Advance(3, Card.EAT_SALAD)) + } + + "allow buy and eat salad" { + state.currentPlayer.position shouldBe 0 + state.performMoveDirectly(Advance(1, Card.EAT_SALAD)) + state.turn shouldBe 2 + state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) + state.getSensibleMoves(state.otherPlayer) shouldBe listOf(Advance(2, Card.EAT_SALAD)) + state.performMoveDirectly(ExchangeCarrots(10)) + state.turn shouldBe 3 + state.performMoveDirectly(Advance(2, Card.EAT_SALAD)) + state.turn shouldBe 4 + } + + "allow fallback and buy" { + state.currentPlayer.addCard(Card.FALL_BACK) + state.checkAdvance(3) shouldBe null + state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) + state.currentPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) + } } } }) \ No newline at end of file From 541074bb50fb4b0d31276c411f4df3335ff80b05 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sun, 12 May 2024 22:23:39 +0300 Subject: [PATCH 23/38] fix(plugin25): some card move checks --- plugin/src/main/kotlin/sc/plugin2025/GameState.kt | 8 +++++--- plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index d57653d14..273aac91c 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -79,7 +79,7 @@ data class GameState @JvmOverloads constructor( if(validateTargetField(newField, player) != null) return@flatMap emptyList() val newState = copy(players = players.map { if(it.team == player.team) it.clone().apply { advanceBy(distance) } else it }) - return@flatMap newState.nextCards()?.map { cards -> + return@flatMap newState.nextCards(newState.getHare(player.team))?.map { cards -> Advance(distance, *cards) } ?: listOf(Advance(distance)) } + listOfNotNull( @@ -96,7 +96,9 @@ data class GameState @JvmOverloads constructor( if(card.playable(this) == null) { val newState = clone() card.play(newState) - newState.nextCards(player)?.map { arrayOf(card, *it) } ?: listOf(arrayOf(card)) + if(card.moves) + return@flatMap newState.nextCards(player)?.map { arrayOf(card, *it) } ?: listOf(arrayOf(card)) + listOf(arrayOf(card)) } else { listOf() } @@ -139,7 +141,7 @@ data class GameState @JvmOverloads constructor( * Does not validate whether a card can be played on hare field. */ fun validateTargetField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? { val field = board.getField(newPosition) - if(field != Field.GOAL && newPosition == currentPlayer.opponent.position) + if(field != Field.GOAL && newPosition == player.opponent.position) return MoveMistake.FIELD_OCCUPIED when(field) { Field.SALAD -> player.salads > 0 || return MoveMistake.NO_SALAD diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 1be3f2f5b..84a361860 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -36,6 +36,8 @@ class MoveTest: WordSpec({ state.currentPlayer.position shouldBe 0 state.performMoveDirectly(Advance(1, Card.EAT_SALAD)) state.turn shouldBe 2 + state.currentPlayer.position shouldBe 2 + state.otherPlayer.position shouldBe 1 state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) state.getSensibleMoves(state.otherPlayer) shouldBe listOf(Advance(2, Card.EAT_SALAD)) state.performMoveDirectly(ExchangeCarrots(10)) From 6aa0a5b2ef136e78b5a7f5f136396f05a607cf3b Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 13 May 2024 00:29:17 +0300 Subject: [PATCH 24/38] fix(plugin25): fallback card check --- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 32 +++++++++++-------- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 16 ++++++++-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 03b959c96..a632b02ca 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -7,26 +7,32 @@ import sc.shared.IMoveMistake @XStreamAlias(value = "card") enum class Card(val moves: Boolean, val playable: (GameState) -> MoveMistake?, val play: (GameState) -> Unit): HuIMove { /** Falle hinter den Gegenspieler. */ - FALL_BACK(true, { state -> - MoveMistake.CANNOT_PLAY_FALL_BACK.takeUnless { state.isAhead() } - ?: state.validateTargetField(state.otherPlayer.position + 1) - }, { it.moveToField(it.otherPlayer.position - 1) }), + FALL_BACK(true, + { state -> + MoveMistake.CANNOT_PLAY_FALL_BACK.takeUnless { state.isAhead() } + ?: state.validateTargetField(state.otherPlayer.position - 1) + }, + { it.moveToField(it.otherPlayer.position - 1) }), /** Rücke vor den Gegenspieler. */ - HURRY_AHEAD(true, { state -> - MoveMistake.CANNOT_PLAY_HURRY_AHEAD.takeIf { state.isAhead() } - ?: state.validateTargetField(state.otherPlayer.position + 1) - }, { it.moveToField(it.otherPlayer.position + 1) }), + HURRY_AHEAD(true, + { state -> + MoveMistake.CANNOT_PLAY_HURRY_AHEAD.takeIf { state.isAhead() } + ?: state.validateTargetField(state.otherPlayer.position + 1) + }, + { it.moveToField(it.otherPlayer.position + 1) }), /** Friss sofort einen Salat. */ EAT_SALAD( false, { state -> MoveMistake.NO_SALAD.takeUnless { state.currentPlayer.salads > 0 } }, { it.eatSalad() }), /** Karottenvorrat mit dem Gegner tauschen. */ - SWAP_CARROTS(false, { null }, { - val car = it.currentPlayer.carrots - it.currentPlayer.carrots = it.otherPlayer.carrots - it.otherPlayer.carrots = car - }); + SWAP_CARROTS(false, + { null }, + { + val car = it.currentPlayer.carrots + it.currentPlayer.carrots = it.otherPlayer.carrots + it.otherPlayer.carrots = car + }); override fun perform(state: GameState): IMoveMistake? { if(state.currentField != Field.HARE) diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 84a361860..2d8e6b54d 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -1,9 +1,11 @@ package sc.plugin2025 +import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.* class MoveTest: WordSpec({ + isolationMode = IsolationMode.InstancePerTest "Advance" When { "one player advanced" should { val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) @@ -44,13 +46,23 @@ class MoveTest: WordSpec({ state.turn shouldBe 3 state.performMoveDirectly(Advance(2, Card.EAT_SALAD)) state.turn shouldBe 4 + state.currentPlayer.position shouldBe 2 + } + + state.currentPlayer.position shouldBe 0 + state.currentPlayer.addCard(Card.FALL_BACK) + state.checkAdvance(3) shouldBe null + "allow fallback card" { + state.currentPlayer.position += 3 + Card.FALL_BACK.perform(state) } "allow fallback and buy" { - state.currentPlayer.addCard(Card.FALL_BACK) state.checkAdvance(3) shouldBe null + Advance(3, Card.FALL_BACK).perform(state.clone()) shouldBe MoveMistake.MUST_BUY_ONE_CARD state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) - state.currentPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) + state.turn shouldBe 2 + state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) } } } From 1537683efd9e3a96602468c5bbaa56757e32a867 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Mon, 20 May 2024 22:48:38 +0300 Subject: [PATCH 25/38] fix(plugin25): move oversights all over the place --- .../main/kotlin/sc/plugin2023/GameState.kt | 4 +- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 3 +- .../src/main/kotlin/sc/plugin2025/FallBack.kt | 9 ++- plugin/src/main/kotlin/sc/plugin2025/Field.kt | 4 +- .../main/kotlin/sc/plugin2025/GameState.kt | 56 ++++++++++++++----- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 2 +- plugin/src/test/kotlin/sc/GamePlayTest.kt | 5 +- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 1 + .../server-api/sc/api/plugins/IGameState.kt | 2 +- 9 files changed, 57 insertions(+), 29 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2023/GameState.kt b/plugin/src/main/kotlin/sc/plugin2023/GameState.kt index 2143bd95f..c09c491d5 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/GameState.kt @@ -6,8 +6,8 @@ import sc.api.plugins.Coordinates import sc.api.plugins.ITeam import sc.api.plugins.Team import sc.api.plugins.TwoPlayerGameState -import sc.plugin2023.util.PenguinMoveMistake import sc.plugin2023.util.PenguinConstants +import sc.plugin2023.util.PenguinMoveMistake import sc.shared.InvalidMoveException import sc.shared.MoveMistake @@ -87,6 +87,8 @@ data class GameState @JvmOverloads constructor( override fun getPointsForTeam(team: ITeam): IntArray = intArrayOf(fishes[team.index]) + override fun teamStats(team: ITeam): List> = listOf() + override fun clone() = GameState(this) override fun toString(): String = diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index abcbab02e..af3fc7cf3 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -13,7 +13,8 @@ import sc.shared.IMoveMistake @XStreamAlias(value = "EatSalad") object EatSalad: HuIMove { override fun perform(state: GameState): IMoveMistake? { - if(state.mayEatSalad()) { + if(state.mustEatSalad()) { + state.currentPlayer.saladEaten = true state.eatSalad() return null } else { diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt index 73b44a89f..88e1ac09b 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -11,11 +11,10 @@ import com.thoughtworks.xstream.annotations.XStreamAlias @XStreamAlias(value = "fallBack") object FallBack: HuIMove { override fun perform(state: GameState): MoveMistake? { - if(state.mayFallBack()) { - val previousFieldIndex: Int = state.currentPlayer.position - state.currentPlayer.position = - state.board.getPreviousField(Field.HEDGEHOG, previousFieldIndex) ?: return MoveMistake.CANNOT_FALL_BACK - state.currentPlayer.carrots += 10 * (previousFieldIndex - state.currentPlayer.position) + val previousFieldIndex = state.nextFallBack() + if(previousFieldIndex != null) { + state.currentPlayer.carrots += 10 * (state.currentPlayer.position - previousFieldIndex) + state.currentPlayer.position = previousFieldIndex return null } else { return MoveMistake.CANNOT_FALL_BACK diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt index 834804eec..238e65973 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Field.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -19,9 +19,9 @@ enum class Field(val short: String, val unicode: String = short) { /** Karottenfeld: Hier dürfen Karotten getauscht werden. */ CARROTS("K", "\uD83E\uDD55"), /** Hasenfeld: Hier wird sofort eine Hasenkarte gespielt. */ - HARE("H"), + HARE("H", "\uD83D\uDC07"), /** Marktfeld: Hier wird eine Hasenkarte gekauft (Variation). */ - MARKET("M"), + MARKET("M", "\uD83E\uDDFA"), /** Das Zielfeld */ GOAL("Z", "🏁"), /** Das Startfeld */ diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 273aac91c..10f1649d5 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -83,11 +83,16 @@ data class GameState @JvmOverloads constructor( Advance(distance, *cards) } ?: listOf(Advance(distance)) } + listOfNotNull( - FallBack.takeIf { mayFallBack(player) }, + FallBack.takeIf { nextFallBack(player) != null }, + *possibleExchangeCarrotMoves(player).toTypedArray() + ) + } + + fun possibleExchangeCarrotMoves(player: Hare = currentPlayer) = + listOfNotNull( ExchangeCarrots(10).takeIf { mayExchangeCarrots(10, player) }, ExchangeCarrots(-10).takeIf { mayExchangeCarrots(-10, player) }, ) - } fun nextCards(player: Hare = currentPlayer): Collection>? = when(player.field) { @@ -114,12 +119,30 @@ data class GameState @JvmOverloads constructor( val mist = MoveMistake.MUST_EAT_SALAD.takeIf { mustEatSalad() && move != EatSalad - } ?: move.perform(this) + }.also { currentPlayer.saladEaten = false } ?: move.perform(this) if(mist != null) throw InvalidMoveException(mist, move) turn++ + awardPositionFields() if(!moveIterator().hasNext()) { turn++ + awardPositionFields() + } + } + + fun awardPositionFields() { + when(currentField) { + Field.POSITION_1 -> { + if(isAhead()) { + currentPlayer.carrots += 10 + } + } + Field.POSITION_2 -> { + if(!isAhead()) { + currentPlayer.carrots += 30 + } + } + else -> {} } } @@ -159,13 +182,10 @@ data class GameState @JvmOverloads constructor( * Überprüft `FallBack` Züge auf Korrektheit * * @param state GameState - * @return true, falls der currentPlayer einen Rückzug machen darf + * @return Igelfeldposition, falls der currentPlayer einen Rückzug machen darf */ - fun mayFallBack(player: Hare = currentPlayer): Boolean { - if(mustEatSalad(player)) return false - val lastHedgehog: Int? = this.board.getPreviousField(Field.HEDGEHOG, player.position) - return lastHedgehog != null && player.opponent.position != lastHedgehog - } + fun nextFallBack(player: Hare = currentPlayer): Int? = + this.board.getPreviousField(Field.HEDGEHOG, player.position)?.takeUnless { player.opponent.position == it } /** * Überprüft `EatSalad` Zug auf Korrektheit. @@ -186,17 +206,15 @@ data class GameState @JvmOverloads constructor( * @param n 10 oder -10 je nach Fragestellung * @return true, falls die durch n spezifizierte Aktion möglich ist. */ - fun mayExchangeCarrots(n: Int, player: Hare = currentPlayer): Boolean { - val valid = board.getField(player.position) == Field.CARROTS - return n == 10 && valid || (n == -10 && player.carrots >= 10 && valid) - } + fun mayExchangeCarrots(n: Int, player: Hare = currentPlayer): Boolean = + player.field == Field.CARROTS && (n == 10 || (n == -10 && player.carrots >= 10)) /** Gibt zurück, ob der Spieler eine Karte spielen kann. */ fun canPlayAnyCard(player: Hare = currentPlayer): Boolean = board.getField(player.position) === Field.HARE && player.getCards().any { it.playable(this) == null } fun mustEatSalad(player: Hare = currentPlayer) = - player.field == Field.SALAD && player.lastAction != EatSalad + player.field == Field.SALAD && !player.saladEaten /** Isst einen Salat, keine Überprüfung der Regelkonformität. */ fun eatSalad(player: Hare = currentPlayer) { @@ -208,4 +226,14 @@ data class GameState @JvmOverloads constructor( } } + override fun teamStats(team: ITeam): List> = + getHare(team).run { + listOf( + "▶ Position" to this.position, + "\uD83E\uDD55 Karotten" to this.carrots, + "\uD83E\uDD57 \uD83E\uDD57 Salate" to this.salads, + "Karten" to this.getCards().count(), + ) + } + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 2f995567f..508f2888f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -11,7 +11,7 @@ data class Hare( @XStreamAsAttribute var position: Int = 0, @XStreamAsAttribute var salads: Int = HuIConstants.INITIAL_SALADS, @XStreamAsAttribute var carrots: Int = HuIConstants.INITIAL_CARROTS, - @XStreamAsAttribute var lastAction: HuIMove? = null, + @XStreamAsAttribute var saladEaten: Boolean = false, private val cards: ArrayList = arrayListOf(), ): PublicCloneable { fun getCards(): List = cards diff --git a/plugin/src/test/kotlin/sc/GamePlayTest.kt b/plugin/src/test/kotlin/sc/GamePlayTest.kt index 9959af79b..f03c1b605 100644 --- a/plugin/src/test/kotlin/sc/GamePlayTest.kt +++ b/plugin/src/test/kotlin/sc/GamePlayTest.kt @@ -15,10 +15,7 @@ import sc.api.plugins.exceptions.TooManyPlayersException import sc.api.plugins.host.IGameListener import sc.framework.plugins.AbstractGame import sc.framework.plugins.Constants -import sc.framework.plugins.Player import sc.shared.GameResult -import sc.shared.PlayerScore -import sc.shared.Violation import kotlin.time.Duration.Companion.milliseconds /** This test verifies that the Game implementation can be used to play a game. @@ -94,7 +91,7 @@ class GamePlayTest: WordSpec({ } } withClue(game.currentState) { - // Note that this fails if the game ends incorrectly + // Note that this fails if the game ends irregularly game.currentState.isOver.shouldBeTrue() } } diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 2d8e6b54d..1ab4489af 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -6,6 +6,7 @@ import io.kotest.matchers.* class MoveTest: WordSpec({ isolationMode = IsolationMode.InstancePerTest + // TODO other move types and card chains "Advance" When { "one player advanced" should { val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) diff --git a/sdk/src/main/server-api/sc/api/plugins/IGameState.kt b/sdk/src/main/server-api/sc/api/plugins/IGameState.kt index 59a4bf469..c2fc438b0 100644 --- a/sdk/src/main/server-api/sc/api/plugins/IGameState.kt +++ b/sdk/src/main/server-api/sc/api/plugins/IGameState.kt @@ -63,5 +63,5 @@ interface IGameState: RoomMessage, PublicCloneable { fun moveIterator(): Iterator /** Spielspezifische Informationen, für die GUI. */ - fun teamStats(team: ITeam): List> = listOf() + fun teamStats(team: ITeam): List> } \ No newline at end of file From eb3a738a2063b0b06e45c17110d2ded197537246 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 12:47:54 +0300 Subject: [PATCH 26/38] feat(plugin): improve scores and stats --- plugin/src/main/kotlin/sc/plugin2024/GameState.kt | 2 +- plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt | 3 --- plugin/src/main/kotlin/sc/plugin2025/GameState.kt | 8 ++++---- plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt | 1 + sdk/src/main/server-api/sc/api/plugins/IGameState.kt | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt index b261f7f71..ba1119df7 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt @@ -435,7 +435,7 @@ data class GameState @JvmOverloads constructor( override fun getPointsForTeamExtended(team: ITeam): IntArray = ships[team.index].let { ship -> - intArrayOf(*getPointsForTeam(team), ship.coal * 2, if(inGoal(ship)) MQConstants.FINISH_POINTS else 0) + intArrayOf(*getPointsForTeam(team), ship.coal * 2, if(inGoal(ship)) 6 else 0) } override fun teamStats(team: ITeam): List> = diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt b/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt index 6bd0b9f85..f485009a9 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt @@ -10,9 +10,6 @@ object MQConstants { const val MAX_SPEED = 6 const val FREE_ACC = 1 - // Points - const val FINISH_POINTS = 6 - const val POINTS_PER_PASSENGER = 5 const val POINTS_PER_SEGMENT = 5 diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 10f1649d5..d06517b77 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -67,7 +67,7 @@ data class GameState @JvmOverloads constructor( copy(players = players.map { if(it.team == currentTeam) it.clone().apply(transform) else it }) override fun getPointsForTeam(team: ITeam): IntArray = - getHare(team).let { intArrayOf(it.position, it.salads) } + getHare(team).let { intArrayOf(if(it.inGoal) 1 else 0, it.position, it.salads) } override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) @@ -229,9 +229,9 @@ data class GameState @JvmOverloads constructor( override fun teamStats(team: ITeam): List> = getHare(team).run { listOf( - "▶ Position" to this.position, - "\uD83E\uDD55 Karotten" to this.carrots, - "\uD83E\uDD57 \uD83E\uDD57 Salate" to this.salads, + " ⃞ Position" to this.position, + "▾ Karotten" to this.carrots, + " ⃝ Salate" to this.salads, "Karten" to this.getCards().count(), ) } diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt index 919ae57c8..5de1ff134 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt @@ -22,6 +22,7 @@ class GamePlugin: IGamePlugin { val scoreDefinition: ScoreDefinition = ScoreDefinition(arrayOf( ScoreFragment("Siegpunkte", WinReason("%s hat gewonnen"), ScoreAggregation.SUM), + ScoreFragment("Ziel", HuIWinReason.GOAL, ScoreAggregation.AVERAGE), ScoreFragment("Feld", HuIWinReason.DIFFERING_SCORES, ScoreAggregation.AVERAGE), ScoreFragment("Karotten", HuIWinReason.DIFFERING_CARROTS, ScoreAggregation.AVERAGE), )) diff --git a/sdk/src/main/server-api/sc/api/plugins/IGameState.kt b/sdk/src/main/server-api/sc/api/plugins/IGameState.kt index c2fc438b0..3bbdfe1f9 100644 --- a/sdk/src/main/server-api/sc/api/plugins/IGameState.kt +++ b/sdk/src/main/server-api/sc/api/plugins/IGameState.kt @@ -54,7 +54,7 @@ interface IGameState: RoomMessage, PublicCloneable { /** Gibt Punktzahlen des Teams passend zur ScoreDefinition des aktuellen Spielplugins zurück. */ fun getPointsForTeam(team: ITeam): IntArray - /* Erweiterte Punktzahlen für eine grobe Evaluierung eines Zuges. */ + /** Erweiterte Punktzahlen für eine grobe Evaluierung eines Zuges. */ fun getPointsForTeamExtended(team: ITeam): IntArray = getPointsForTeam(team) From 49d7d4bae09ff8a2258cd1a740a70d3bdaa4f4da Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 13:42:14 +0300 Subject: [PATCH 27/38] feat(plugin25): generate Board with Markets --- plugin/src/main/kotlin/sc/plugin2025/Board.kt | 130 +++++++++--------- plugin/src/main/kotlin/sc/plugin2025/Field.kt | 7 +- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Board.kt b/plugin/src/main/kotlin/sc/plugin2025/Board.kt index 4195b4202..b0cec7409 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Board.kt @@ -33,6 +33,9 @@ data class Board( override fun clone(): Board = this companion object { + + private fun shuffledFields(vararg fields: Field) = fields.asList().shuffled() + /** * Erstellt eine zufällige Rennstrecke. * Die Indizes der Salat- und Igelfelder bleiben unverändert - @@ -42,131 +45,122 @@ data class Board( */ private fun generateTrack(): List { val track = ArrayList() - val segment = ArrayList() track.add(Field.START) - segment.addAll( - listOf( + track.addAll( + shuffledFields( + Field.HARE, + Field.CARROTS, + Field.HARE, + Field.POSITION_3, + Field.CARROTS, Field.HARE, - Field.CARROTS, Field.HARE, Field.CARROTS, - Field.CARROTS, Field.HARE, Field.POSITION_1, - Field.POSITION_2, Field.CARROTS + Field.POSITION_1, + Field.POSITION_2, + Field.POSITION_4 ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.SALAD) track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( + Field.POSITION_3, Field.CARROTS, - Field.CARROTS, Field.HARE + Field.HARE ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( Field.POSITION_1, - Field.POSITION_2, Field.CARROTS + Field.POSITION_2, + Field.POSITION_4 ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( + Field.POSITION_3, Field.CARROTS, - Field.CARROTS, Field.POSITION_2 + Field.SALAD, + Field.POSITION_2, ) ) - segment.shuffle() - track.add(segment.removeAt(0)) - track.add(segment.removeAt(0)) - track.add(Field.SALAD) - track.add(segment.removeAt(0)) track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( Field.HARE, - Field.CARROTS, Field.CARROTS, Field.CARROTS, + Field.CARROTS, + Field.POSITION_4, + Field.POSITION_3, Field.POSITION_2 ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( + Field.HARE, + Field.POSITION_1, + Field.CARROTS, Field.HARE, - Field.POSITION_1, Field.CARROTS, Field.HARE, - Field.POSITION_2, Field.CARROTS + Field.POSITION_2, + Field.POSITION_3 ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( + Field.CARROTS, + Field.HARE, Field.CARROTS, - Field.HARE, Field.CARROTS, Field.POSITION_2 + Field.POSITION_2 ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.SALAD) track.add(Field.HEDGEHOG) - segment.addAll( - listOf( - Field.CARROTS, - Field.CARROTS, Field.HARE, Field.POSITION_2, - Field.POSITION_1, Field.CARROTS + track.addAll( + shuffledFields( + Field.POSITION_3, + Field.POSITION_4, + Field.HARE, + Field.POSITION_2, + Field.POSITION_1, + Field.CARROTS ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.HEDGEHOG) - segment.addAll( - listOf( + track.addAll( + shuffledFields( Field.HARE, - Field.CARROTS, Field.POSITION_2, Field.CARROTS, + Field.POSITION_3, + Field.POSITION_2, + Field.POSITION_4, Field.CARROTS ) ) - segment.shuffle() - track.addAll(segment) track.add(Field.HEDGEHOG) track.add(Field.SALAD) - segment.clear() - segment.addAll( + track.addAll( listOf( Field.HARE, - Field.CARROTS, Field.POSITION_1, Field.CARROTS, - Field.HARE, Field.CARROTS + Field.CARROTS, + Field.POSITION_1, + Field.CARROTS, + Field.HARE, + Field.CARROTS ) ) - segment.shuffle() - track.addAll(segment) - segment.clear() track.add(Field.GOAL) return track } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Field.kt b/plugin/src/main/kotlin/sc/plugin2025/Field.kt index 238e65973..4215b5659 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Field.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Field.kt @@ -25,5 +25,10 @@ enum class Field(val short: String, val unicode: String = short) { /** Das Zielfeld */ GOAL("Z", "🏁"), /** Das Startfeld */ - START("0", "▶"), + START("0", "▶"); + + companion object { + val POSITION_3 = CARROTS + val POSITION_4 = MARKET + } } From 62978439e49010c0d159939c5350d08f4c2567be Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 13:43:46 +0300 Subject: [PATCH 28/38] fix(plugin25): some issues with processing moves involving cards --- .../main/kotlin/sc/plugin2025/GameState.kt | 34 ++++++++++++++----- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 17 ++++++++-- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index d06517b77..2b2ad0921 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -78,16 +78,23 @@ data class GameState @JvmOverloads constructor( val newField = player.position + distance if(validateTargetField(newField, player) != null) return@flatMap emptyList() - val newState = copy(players = players.map { if(it.team == player.team) it.clone().apply { advanceBy(distance) } else it }) - return@flatMap newState.nextCards(newState.getHare(player.team))?.map { cards -> - Advance(distance, *cards) - } ?: listOf(Advance(distance)) + return@flatMap possibleCardMoves(distance, player) ?: listOf(Advance(distance)) } + listOfNotNull( FallBack.takeIf { nextFallBack(player) != null }, *possibleExchangeCarrotMoves(player).toTypedArray() ) } + /** Possible Advances including buying/playing of cards. + * @return null if target field is neither market nor hare, empty list if no possibilities, otherwise possible Moves */ + fun possibleCardMoves(distance: Int, player: Hare = currentPlayer): List? { + val state = + copy(players = players.map { if(it.team == player.team) it.clone().apply { advanceBy(distance) } else it }) + return state.nextCards(state.getHare(player.team))?.map { cards -> + Advance(distance, *cards) + } + } + fun possibleExchangeCarrotMoves(player: Hare = currentPlayer) = listOfNotNull( ExchangeCarrots(10).takeIf { mayExchangeCarrots(10, player) }, @@ -100,16 +107,23 @@ data class GameState @JvmOverloads constructor( player.getCards().flatMap { card -> if(card.playable(this) == null) { val newState = clone() + newState.currentPlayer.removeCard(card) card.play(newState) if(card.moves) - return@flatMap newState.nextCards(player)?.map { arrayOf(card, *it) } ?: listOf(arrayOf(card)) + return@flatMap newState.nextCards(player)?.map { arrayOf(card, *it) } + ?: listOf(arrayOf(card)) listOf(arrayOf(card)) } else { listOf() } } } - Field.MARKET -> Card.values().map { arrayOf(it) } + Field.MARKET -> { + if(player.carrots >= 10) + Card.values().map { arrayOf(it) } + else + listOf() + } else -> null } @@ -155,7 +169,8 @@ data class GameState @JvmOverloads constructor( /** Basic validation whether a player may move forward by that distance. * Does not validate whether a card can be played on hare field. */ fun checkAdvance(distance: Int, player: Hare = currentPlayer): MoveMistake? { - if(player.carrots < calculateCarrots(distance)) + val cost = calculateCarrots(distance) + if(player.carrots < cost) return MoveMistake.MISSING_CARROTS return validateTargetField(player.position + distance, player) } @@ -166,11 +181,12 @@ data class GameState @JvmOverloads constructor( val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == player.opponent.position) return MoveMistake.FIELD_OCCUPIED + val playerCarrots = { player.carrots - calculateCarrots(newPosition - player.position) } when(field) { Field.SALAD -> player.salads > 0 || return MoveMistake.NO_SALAD - Field.MARKET -> player.carrots >= 10 || return MoveMistake.MISSING_CARROTS + Field.MARKET -> playerCarrots() >= 10 || return MoveMistake.MISSING_CARROTS Field.HARE -> player.getCards().isNotEmpty() || return MoveMistake.CARD_NOT_OWNED - Field.GOAL -> player.carrots - calculateCarrots(newPosition - player.position) <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS + Field.GOAL -> playerCarrots() <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS Field.HEDGEHOG -> return MoveMistake.HEDGEHOG_ONLY_BACKWARDS null -> return MoveMistake.FIELD_NONEXISTENT else -> return null diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 1ab4489af..275ecd0a0 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -3,6 +3,7 @@ package sc.plugin2025 import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.* +import io.kotest.matchers.collections.* class MoveTest: WordSpec({ isolationMode = IsolationMode.InstancePerTest @@ -32,7 +33,9 @@ class MoveTest: WordSpec({ "allow eat salad" { state.currentPlayer.addCard(Card.EAT_SALAD) - state.performMoveDirectly(Advance(3, Card.EAT_SALAD)) + val adv = Advance(3, Card.EAT_SALAD) + state.possibleCardMoves(3) shouldContainExactly listOf(adv) + state.performMoveDirectly(adv) } "allow buy and eat salad" { @@ -53,15 +56,23 @@ class MoveTest: WordSpec({ state.currentPlayer.position shouldBe 0 state.currentPlayer.addCard(Card.FALL_BACK) state.checkAdvance(3) shouldBe null - "allow fallback card" { + "allow isolated fallback card" { state.currentPlayer.position += 3 Card.FALL_BACK.perform(state) } + "not allow fallback to start" { + state.otherPlayer.position = 1 + state.possibleCardMoves(3).shouldBeEmpty() + } + "allow fallback and buy" { state.checkAdvance(3) shouldBe null Advance(3, Card.FALL_BACK).perform(state.clone()) shouldBe MoveMistake.MUST_BUY_ONE_CARD - state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) + val adv = Advance(3, Card.FALL_BACK, Card.EAT_SALAD) + state.possibleCardMoves(3) shouldContainExactly listOf(adv) + // TODO Debug + state.performMoveDirectly(adv) state.turn shouldBe 2 state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) } From c0c3b04be6eb81f76c38dd967063ee812ac3d467 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 18:44:25 +0300 Subject: [PATCH 29/38] fix(plugin25): card move advance validation --- .../main/kotlin/sc/plugin2025/GameState.kt | 25 ++++---- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 59 ++++++++++--------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 2b2ad0921..9784b08cc 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -75,8 +75,7 @@ data class GameState @JvmOverloads constructor( if(mustEatSalad(player)) return listOf(EatSalad) return (1..calculateMoveableFields(player.carrots).coerceAtMost(board.size - player.position)).flatMap { distance -> - val newField = player.position + distance - if(validateTargetField(newField, player) != null) + if(checkAdvance(distance, player) != null) return@flatMap emptyList() return@flatMap possibleCardMoves(distance, player) ?: listOf(Advance(distance)) } + listOfNotNull( @@ -104,13 +103,13 @@ data class GameState @JvmOverloads constructor( fun nextCards(player: Hare = currentPlayer): Collection>? = when(player.field) { Field.HARE -> { - player.getCards().flatMap { card -> + player.getCards().toSet().flatMap { card -> if(card.playable(this) == null) { val newState = clone() newState.currentPlayer.removeCard(card) card.play(newState) if(card.moves) - return@flatMap newState.nextCards(player)?.map { arrayOf(card, *it) } + return@flatMap newState.nextCards(newState.getHare(player.team))?.map { arrayOf(card, *it) } ?: listOf(arrayOf(card)) listOf(arrayOf(card)) } else { @@ -169,24 +168,26 @@ data class GameState @JvmOverloads constructor( /** Basic validation whether a player may move forward by that distance. * Does not validate whether a card can be played on hare field. */ fun checkAdvance(distance: Int, player: Hare = currentPlayer): MoveMistake? { - val cost = calculateCarrots(distance) - if(player.carrots < cost) - return MoveMistake.MISSING_CARROTS - return validateTargetField(player.position + distance, player) + return validateTargetField( + player.position + distance, + player, + (player.carrots - calculateCarrots(distance)).takeIf { it >= 0 } ?: return MoveMistake.MISSING_CARROTS + ) } /** Basic validation whether a field may be entered via a jump that is not backward. * Does not validate whether a card can be played on hare field. */ - fun validateTargetField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? { + fun validateTargetField(newPosition: Int, player: Hare = currentPlayer, carrots: Int = player.carrots): MoveMistake? { + if(newPosition == 0) + return MoveMistake.CANNOT_ENTER_FIELD val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == player.opponent.position) return MoveMistake.FIELD_OCCUPIED - val playerCarrots = { player.carrots - calculateCarrots(newPosition - player.position) } when(field) { Field.SALAD -> player.salads > 0 || return MoveMistake.NO_SALAD - Field.MARKET -> playerCarrots() >= 10 || return MoveMistake.MISSING_CARROTS + Field.MARKET -> carrots >= 10 || return MoveMistake.MISSING_CARROTS Field.HARE -> player.getCards().isNotEmpty() || return MoveMistake.CARD_NOT_OWNED - Field.GOAL -> playerCarrots() <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS + Field.GOAL -> carrots <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS Field.HEDGEHOG -> return MoveMistake.HEDGEHOG_ONLY_BACKWARDS null -> return MoveMistake.FIELD_NONEXISTENT else -> return null diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 275ecd0a0..dc9b34d3d 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -9,27 +9,27 @@ class MoveTest: WordSpec({ isolationMode = IsolationMode.InstancePerTest // TODO other move types and card chains "Advance" When { + val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) + + state.checkAdvance(1) shouldBe null + state.checkAdvance(2) shouldBe null + state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT + + state.getSensibleMoves() shouldBe listOf( + *Card.values().map { Advance(1, it) }.toTypedArray(), + Advance(2), + ) + + state.performMoveDirectly(Advance(2)) + state.turn shouldBe 1 + + state.checkAdvance(2) shouldBe MoveMistake.FIELD_OCCUPIED + state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT "one player advanced" should { - val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) - - state.checkAdvance(1) shouldBe null - state.checkAdvance(2) shouldBe null - state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED - state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS - state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT - - state.getSensibleMoves() shouldBe listOf( - *Card.values().map { Advance(1, it) }.toTypedArray(), - Advance(2), - ) - - state.performMoveDirectly(Advance(2)) - state.turn shouldBe 1 - - state.checkAdvance(2) shouldBe MoveMistake.FIELD_OCCUPIED - state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED - state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS - state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT "allow eat salad" { state.currentPlayer.addCard(Card.EAT_SALAD) @@ -53,15 +53,18 @@ class MoveTest: WordSpec({ state.currentPlayer.position shouldBe 2 } - state.currentPlayer.position shouldBe 0 - state.currentPlayer.addCard(Card.FALL_BACK) - state.checkAdvance(3) shouldBe null + } + "has a card" should { "allow isolated fallback card" { state.currentPlayer.position += 3 + state.currentPlayer.position shouldBe 3 Card.FALL_BACK.perform(state) } - "not allow fallback to start" { + state.currentPlayer.addCard(Card.FALL_BACK) + state.checkAdvance(3) shouldBe null + + "not allow fallback to startfield" { state.otherPlayer.position = 1 state.possibleCardMoves(3).shouldBeEmpty() } @@ -69,10 +72,10 @@ class MoveTest: WordSpec({ "allow fallback and buy" { state.checkAdvance(3) shouldBe null Advance(3, Card.FALL_BACK).perform(state.clone()) shouldBe MoveMistake.MUST_BUY_ONE_CARD - val adv = Advance(3, Card.FALL_BACK, Card.EAT_SALAD) - state.possibleCardMoves(3) shouldContainExactly listOf(adv) - // TODO Debug - state.performMoveDirectly(adv) + state.cloneCurrentPlayer { it.position = 1 }.nextCards() shouldBe Card.values().map { listOf(it) } + state.cloneCurrentPlayer { it.position = 3 }.nextCards() shouldBe Card.values().map { listOf(Card.FALL_BACK, it) } + + state.performMoveDirectly(Advance(3, Card.FALL_BACK, Card.EAT_SALAD)) state.turn shouldBe 2 state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) } From 31f6dc69173c2ccad88ff19e4e9fa59212bf16c8 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 20:43:52 +0300 Subject: [PATCH 30/38] test(plugin25): swap carrots --- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index dc9b34d3d..35848d428 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -30,7 +30,6 @@ class MoveTest: WordSpec({ state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT "one player advanced" should { - "allow eat salad" { state.currentPlayer.addCard(Card.EAT_SALAD) val adv = Advance(3, Card.EAT_SALAD) @@ -53,6 +52,24 @@ class MoveTest: WordSpec({ state.currentPlayer.position shouldBe 2 } + "allow buy and swap carrots" { + state.currentPlayer.position shouldBe 0 + state.performMoveDirectly(Advance(1, Card.SWAP_CARROTS)) + state.turn shouldBe 2 + state.currentPlayer.position shouldBe 2 + state.otherPlayer.position shouldBe 1 + state.otherPlayer.getCards() shouldBe listOf(Card.SWAP_CARROTS) + state.getSensibleMoves(state.otherPlayer) shouldBe listOf(Advance(2, Card.SWAP_CARROTS)) + state.performMoveDirectly(ExchangeCarrots(10)) + state.turn shouldBe 3 + state.currentPlayer.carrots shouldBe 57 + state.otherPlayer.carrots shouldBe 75 + state.performMoveDirectly(Advance(2, Card.SWAP_CARROTS)) + state.turn shouldBe 4 + state.currentPlayer.carrots shouldBe 54 + state.otherPlayer.carrots shouldBe 75 + } + } "has a card" should { "allow isolated fallback card" { From 0af97cac6eecc065ecb6c9fb12bf453d6328d846 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 22:48:15 +0300 Subject: [PATCH 31/38] fix(plugin24): respect round limit --- plugin/src/main/kotlin/sc/plugin2025/GameState.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 9784b08cc..e92797408 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -6,6 +6,7 @@ import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* import sc.plugin2025.GameRuleLogic.calculateCarrots import sc.plugin2025.GameRuleLogic.calculateMoveableFields +import sc.plugin2025.util.HuIConstants import sc.shared.InvalidMoveException /** @@ -58,7 +59,7 @@ data class GameState @JvmOverloads constructor( get() = currentTeamFromTurn() override val isOver: Boolean - get() = players.any { it.inGoal } + get() = players.any { it.inGoal } || round >= HuIConstants.ROUND_LIMIT override fun clone(): GameState = copy(players = players.clone()) From d266d6ce4a765b165772dd90f6ddcb0a130375bc Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 22:48:49 +0300 Subject: [PATCH 32/38] chore(gradle): add alpha version suffix --- gradle.properties | 3 ++- gradle/build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 91db40fbd..2c9b9feb1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ socha.gameName=hui socha.version.year=25 socha.version.minor=00 -socha.version.patch=00 \ No newline at end of file +socha.version.patch=00 +socha.version.suffix=alpha1 \ No newline at end of file diff --git a/gradle/build.gradle.kts b/gradle/build.gradle.kts index 522258b96..3cb4255c4 100644 --- a/gradle/build.gradle.kts +++ b/gradle/build.gradle.kts @@ -18,7 +18,7 @@ plugins { val gameName by extra { property("socha.gameName") as String } val versions = arrayOf("year", "minor", "patch").map { property("socha.version.$it").toString().toInt() } val versionObject = KotlinVersion(versions[0], versions[1], versions[2]) -version = versionObject.toString() +version = versionObject.toString() + property("socha.version.suffix").toString().takeUnless { it.isBlank() }?.let { "-$it" }.orEmpty() val year by extra { "20${versionObject.major}" } val game by extra { "${gameName}_$year" } From fffe59cd685f6978164db1239cf75309da78734e Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 22:59:12 +0300 Subject: [PATCH 33/38] refactor(plugin): plugin-specific MoveMistake class naming without dots in strings --- .../main/kotlin/sc/plugin2024/GameState.kt | 14 +++++----- .../sc/plugin2024/mistake/MQMoveMistake.kt | 12 +++++++++ .../sc/plugin2024/mistake/MoveMistake.kt | 12 --------- .../src/main/kotlin/sc/plugin2025/Advance.kt | 8 +++--- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 12 ++++----- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 2 +- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 2 +- .../src/main/kotlin/sc/plugin2025/FallBack.kt | 4 +-- .../main/kotlin/sc/plugin2025/GameState.kt | 26 +++++++++---------- plugin/src/main/kotlin/sc/plugin2025/Hare.kt | 4 +-- .../kotlin/sc/plugin2025/HuIMoveMistake.kt | 25 ++++++++++++++++++ .../main/kotlin/sc/plugin2025/MoveMistake.kt | 25 ------------------ .../kotlin/sc/plugin2025/util/Constants.kt | 2 ++ .../kotlin/sc/plugin2024/GameResultTest.kt | 4 +-- .../kotlin/sc/plugin2024/GameStateTest.kt | 4 +-- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 16 ++++++------ .../java/sc/server/plugins/TestGameState.kt | 3 +++ 17 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt delete mode 100644 plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt create mode 100644 plugin/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt delete mode 100644 plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt index ba1119df7..272a9b944 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2024/GameState.kt @@ -9,7 +9,7 @@ import sc.plugin2024.actions.Advance import sc.plugin2024.actions.Push import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.AdvanceProblem -import sc.plugin2024.mistake.MoveMistake +import sc.plugin2024.mistake.MQMoveMistake import sc.plugin2024.util.MQConstants import sc.plugin2024.util.MQConstants.POINTS_PER_SEGMENT import sc.plugin2024.util.MQWinReason @@ -93,7 +93,7 @@ data class GameState @JvmOverloads constructor( * @throws InvalidMoveException wenn der Zug ungültig ist */ override fun performMoveDirectly(move: Move) { - if(move.actions.isEmpty()) throw InvalidMoveException(MoveMistake.NO_ACTIONS) + if(move.actions.isEmpty()) throw InvalidMoveException(MQMoveMistake.NO_ACTIONS) val actions = move.actions.fold(ArrayList()) { acc, act -> val last = acc.lastOrNull() @@ -106,16 +106,16 @@ data class GameState @JvmOverloads constructor( } actions.forEachIndexed { index, action -> when { - board[currentShip.position] == Field.SANDBANK && index != 0 -> throw InvalidMoveException(MoveMistake.SAND_BANK_END, move) - mustPush && action !is Push -> throw InvalidMoveException(MoveMistake.PUSH_ACTION_REQUIRED, move) - action is Accelerate && index != 0 -> throw InvalidMoveException(MoveMistake.FIRST_ACTION_ACCELERATE, move) + board[currentShip.position] == Field.SANDBANK && index != 0 -> throw InvalidMoveException(MQMoveMistake.SAND_BANK_END, move) + mustPush && action !is Push -> throw InvalidMoveException(MQMoveMistake.PUSH_ACTION_REQUIRED, move) + action is Accelerate && index != 0 -> throw InvalidMoveException(MQMoveMistake.FIRST_ACTION_ACCELERATE, move) else -> action.perform(this)?.let { throw InvalidMoveException(it, move) } } } when { - currentShip.movement > 0 -> throw InvalidMoveException(MoveMistake.MOVEMENT_POINTS_LEFT, move) - currentShip.movement < 0 -> throw InvalidMoveException(MoveMistake.MOVEMENT_POINTS_MISSING, move) + currentShip.movement > 0 -> throw InvalidMoveException(MQMoveMistake.MOVEMENT_POINTS_LEFT, move) + currentShip.movement < 0 -> throw InvalidMoveException(MQMoveMistake.MOVEMENT_POINTS_MISSING, move) } board.pickupPassenger(currentShip) diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt new file mode 100644 index 000000000..f12161eff --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt @@ -0,0 +1,12 @@ +package sc.plugin2024.mistake + +import sc.shared.IMoveMistake + +enum class MQMoveMistake(override val message: String) : IMoveMistake { + NO_ACTIONS("Der Zug enthält keine Aktionen"), + PUSH_ACTION_REQUIRED("Wenn du auf einem gegnerischen Schiff landest, muss darauf eine Abdrängaktion folgen"), + SAND_BANK_END("Zug auf eine Sandbank muss letzte Aktion sein"), + FIRST_ACTION_ACCELERATE("Du kannst nur in der ersten Aktion beschleunigen"), + MOVEMENT_POINTS_LEFT("Es sind noch Bewegungspunkte übrig"), + MOVEMENT_POINTS_MISSING("Nicht genug Bewegungspunkte"); +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt deleted file mode 100644 index c6a0df35c..000000000 --- a/plugin/src/main/kotlin/sc/plugin2024/mistake/MoveMistake.kt +++ /dev/null @@ -1,12 +0,0 @@ -package sc.plugin2024.mistake - -import sc.shared.IMoveMistake - -enum class MoveMistake(override val message: String) : IMoveMistake { - NO_ACTIONS("Der Zug enthält keine Aktionen."), - PUSH_ACTION_REQUIRED("Wenn du auf einem gegnerischen Schiff landest, muss darauf eine Abdrängaktion folgen."), - SAND_BANK_END("Zug auf eine Sandbank muss letzte Aktion sein."), - FIRST_ACTION_ACCELERATE("Du kannst nur in der ersten Aktion beschleunigen."), - MOVEMENT_POINTS_LEFT("Es sind noch Bewegungspunkte übrig."), - MOVEMENT_POINTS_MISSING("Nicht genug Bewegungspunkte."); -} \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index f0ec60b04..07ef3a1b6 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -26,7 +26,7 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu var bought = false return cards.firstNotNullOfOrNull { if(bought) - return MoveMistake.MUST_BUY_ONE_CARD + return HuIMoveMistake.MUST_BUY_ONE_CARD if(state.currentField == Field.MARKET) { return@firstNotNullOfOrNull player.consumeCarrots(10) ?: run { bought = true @@ -35,13 +35,13 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Hu } } if(state.currentField != Field.HARE || lastCard?.moves == false) - return MoveMistake.CANNOT_PLAY_CARD + return HuIMoveMistake.CANNOT_PLAY_CARD lastCard = it it.perform(state) - } ?: MoveMistake.MUST_BUY_ONE_CARD.takeIf { + } ?: HuIMoveMistake.MUST_BUY_ONE_CARD.takeIf { // On Market field and no card bought or just moved there through card state.currentField == Field.MARKET && !bought - } ?: MoveMistake.MUST_PLAY_CARD.takeIf { + } ?: HuIMoveMistake.MUST_PLAY_CARD.takeIf { // On Hare field and no card played or just moved there through card state.currentField == Field.HARE && lastCard?.moves != false } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index a632b02ca..6217e8847 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -5,25 +5,25 @@ import sc.shared.IMoveMistake /** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ @XStreamAlias(value = "card") -enum class Card(val moves: Boolean, val playable: (GameState) -> MoveMistake?, val play: (GameState) -> Unit): HuIMove { +enum class Card(val moves: Boolean, val playable: (GameState) -> HuIMoveMistake?, val play: (GameState) -> Unit): HuIMove { /** Falle hinter den Gegenspieler. */ FALL_BACK(true, { state -> - MoveMistake.CANNOT_PLAY_FALL_BACK.takeUnless { state.isAhead() } + HuIMoveMistake.CANNOT_PLAY_FALL_BACK.takeUnless { state.isAhead() } ?: state.validateTargetField(state.otherPlayer.position - 1) }, { it.moveToField(it.otherPlayer.position - 1) }), /** Rücke vor den Gegenspieler. */ HURRY_AHEAD(true, { state -> - MoveMistake.CANNOT_PLAY_HURRY_AHEAD.takeIf { state.isAhead() } + HuIMoveMistake.CANNOT_PLAY_HURRY_AHEAD.takeIf { state.isAhead() } ?: state.validateTargetField(state.otherPlayer.position + 1) }, { it.moveToField(it.otherPlayer.position + 1) }), /** Friss sofort einen Salat. */ EAT_SALAD( false, - { state -> MoveMistake.NO_SALAD.takeUnless { state.currentPlayer.salads > 0 } }, + { state -> HuIMoveMistake.NO_SALAD.takeUnless { state.currentPlayer.salads > 0 } }, { it.eatSalad() }), /** Karottenvorrat mit dem Gegner tauschen. */ SWAP_CARROTS(false, @@ -36,9 +36,9 @@ enum class Card(val moves: Boolean, val playable: (GameState) -> MoveMistake?, v override fun perform(state: GameState): IMoveMistake? { if(state.currentField != Field.HARE) - return MoveMistake.CANNOT_PLAY_CARD + return HuIMoveMistake.CANNOT_PLAY_CARD if(!state.currentPlayer.removeCard(this)) - return MoveMistake.CARD_NOT_OWNED + return HuIMoveMistake.CARD_NOT_OWNED return playable(state) ?: run { play(state) null diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index af3fc7cf3..8d1887326 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -18,7 +18,7 @@ object EatSalad: HuIMove { state.eatSalad() return null } else { - return MoveMistake.CANNOT_EAT_SALAD + return HuIMoveMistake.CANNOT_EAT_SALAD } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index 011808ad2..acab4ef24 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -15,7 +15,7 @@ data class ExchangeCarrots(val value: Int): HuIMove { state.currentPlayer.carrots += value return null } else { - return MoveMistake.CANNOT_EXCHANGE_CARROTS + return HuIMoveMistake.CANNOT_EXCHANGE_CARROTS } } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt index 88e1ac09b..beafaaef0 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -10,14 +10,14 @@ import com.thoughtworks.xstream.annotations.XStreamAlias */ @XStreamAlias(value = "fallBack") object FallBack: HuIMove { - override fun perform(state: GameState): MoveMistake? { + override fun perform(state: GameState): HuIMoveMistake? { val previousFieldIndex = state.nextFallBack() if(previousFieldIndex != null) { state.currentPlayer.carrots += 10 * (state.currentPlayer.position - previousFieldIndex) state.currentPlayer.position = previousFieldIndex return null } else { - return MoveMistake.CANNOT_FALL_BACK + return HuIMoveMistake.CANNOT_FALL_BACK } } diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index e92797408..2a0eca525 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -131,7 +131,7 @@ data class GameState @JvmOverloads constructor( override fun performMoveDirectly(move: HuIMove) { val mist = - MoveMistake.MUST_EAT_SALAD.takeIf { + HuIMoveMistake.MUST_EAT_SALAD.takeIf { mustEatSalad() && move != EatSalad }.also { currentPlayer.saladEaten = false } ?: move.perform(this) if(mist != null) @@ -160,7 +160,7 @@ data class GameState @JvmOverloads constructor( } } - fun moveToField(newPosition: Int, player: Hare = currentPlayer): MoveMistake? = + fun moveToField(newPosition: Int, player: Hare = currentPlayer): HuIMoveMistake? = validateTargetField(newPosition, player) ?: run { player.position = newPosition null @@ -168,29 +168,29 @@ data class GameState @JvmOverloads constructor( /** Basic validation whether a player may move forward by that distance. * Does not validate whether a card can be played on hare field. */ - fun checkAdvance(distance: Int, player: Hare = currentPlayer): MoveMistake? { + fun checkAdvance(distance: Int, player: Hare = currentPlayer): HuIMoveMistake? { return validateTargetField( player.position + distance, player, - (player.carrots - calculateCarrots(distance)).takeIf { it >= 0 } ?: return MoveMistake.MISSING_CARROTS + (player.carrots - calculateCarrots(distance)).takeIf { it >= 0 } ?: return HuIMoveMistake.MISSING_CARROTS ) } /** Basic validation whether a field may be entered via a jump that is not backward. * Does not validate whether a card can be played on hare field. */ - fun validateTargetField(newPosition: Int, player: Hare = currentPlayer, carrots: Int = player.carrots): MoveMistake? { + fun validateTargetField(newPosition: Int, player: Hare = currentPlayer, carrots: Int = player.carrots): HuIMoveMistake? { if(newPosition == 0) - return MoveMistake.CANNOT_ENTER_FIELD + return HuIMoveMistake.CANNOT_ENTER_FIELD val field = board.getField(newPosition) if(field != Field.GOAL && newPosition == player.opponent.position) - return MoveMistake.FIELD_OCCUPIED + return HuIMoveMistake.FIELD_OCCUPIED when(field) { - Field.SALAD -> player.salads > 0 || return MoveMistake.NO_SALAD - Field.MARKET -> carrots >= 10 || return MoveMistake.MISSING_CARROTS - Field.HARE -> player.getCards().isNotEmpty() || return MoveMistake.CARD_NOT_OWNED - Field.GOAL -> carrots <= 10 && player.salads == 0 || return MoveMistake.GOAL_CONDITIONS - Field.HEDGEHOG -> return MoveMistake.HEDGEHOG_ONLY_BACKWARDS - null -> return MoveMistake.FIELD_NONEXISTENT + Field.SALAD -> player.salads > 0 || return HuIMoveMistake.NO_SALAD + Field.MARKET -> carrots >= 10 || return HuIMoveMistake.MISSING_CARROTS + Field.HARE -> player.getCards().isNotEmpty() || return HuIMoveMistake.CARD_NOT_OWNED + Field.GOAL -> carrots <= 10 && player.salads == 0 || return HuIMoveMistake.GOAL_CONDITIONS + Field.HEDGEHOG -> return HuIMoveMistake.HEDGEHOG_ONLY_BACKWARDS + null -> return HuIMoveMistake.FIELD_NONEXISTENT else -> return null } return null diff --git a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt index 508f2888f..7c751aa23 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Hare.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Hare.kt @@ -32,9 +32,9 @@ data class Hare( position += distance } - fun consumeCarrots(count: Int): MoveMistake? = + fun consumeCarrots(count: Int): HuIMoveMistake? = if(carrots < count) { - MoveMistake.MISSING_CARROTS + HuIMoveMistake.MISSING_CARROTS } else { carrots -= count null diff --git a/plugin/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt new file mode 100644 index 000000000..0ce3b9c5e --- /dev/null +++ b/plugin/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt @@ -0,0 +1,25 @@ +package sc.plugin2025 + +import sc.shared.IMoveMistake + +enum class HuIMoveMistake(override val message: String) : IMoveMistake { + GOAL_CONDITIONS("Voraussetzungen für Zielfeld nicht erfüllt"), + NO_SALAD("Kein Salat verfügbar"), + MISSING_CARROTS("Nicht genügend Karotten"), + MUST_EAT_SALAD("Auf einem Salatfeld muss ein Salat gegessen werden"), + CANNOT_ENTER_FIELD("Feld kann nicht betreten werden"), + FIELD_OCCUPIED("Das Feld ist besetzt"), + FIELD_NONEXISTENT("Das Feld existiert nicht"), + HEDGEHOG_ONLY_BACKWARDS("Ein Igelfeld kann nur mit einem Rückwärtzug betreten werden"), + CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich"), + CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich"), + CANNOT_EAT_SALAD("Es kann gerade kein Salat gegessen werden"), + CANNOT_EXCHANGE_CARROTS("Karottentauschen kann nicht mit dieser Karottenanzahl gespielt werden"), + + MUST_BUY_ONE_CARD("Auf einem Marktfeld muss genau eine Karte gekauft werden"), + MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden"), + CARD_NOT_OWNED("Karte kann nicht gespielt werden, da nicht im Besitz"), + CANNOT_PLAY_CARD("Karte kann nicht gespielt werden"), + CANNOT_PLAY_FALL_BACK("Rückzugskarte nicht spielbar"), + CANNOT_PLAY_HURRY_AHEAD("Vorwärtssprungkarte nicht spielbar"), +} diff --git a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt b/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt deleted file mode 100644 index 35de1df3f..000000000 --- a/plugin/src/main/kotlin/sc/plugin2025/MoveMistake.kt +++ /dev/null @@ -1,25 +0,0 @@ -package sc.plugin2025 - -import sc.shared.IMoveMistake - -enum class MoveMistake(override val message: String) : IMoveMistake { - GOAL_CONDITIONS("Voraussetzungen für Zielfeld nicht erfüllt."), - NO_SALAD("Kein Salat verfügbar."), - MISSING_CARROTS("Nicht genügend Karotten"), - MUST_EAT_SALAD("Auf einem Salatfeld muss ein Salat gegessen werden"), - CANNOT_ENTER_FIELD("Feld kann nicht betreten werden."), - FIELD_OCCUPIED("Das Feld ist besetzt."), - FIELD_NONEXISTENT("Das Feld existiert nicht."), - HEDGEHOG_ONLY_BACKWARDS("Ein Igelfeld kann nur mit einem Rückwärtzug betreten werden"), - CANNOT_MOVE_FORWARD("Vorwärtszug ist nicht möglich."), - CANNOT_FALL_BACK("Rückwärtszug ist nicht möglich."), - CANNOT_EAT_SALAD("Es kann gerade kein Salat gegessen werden."), - CANNOT_EXCHANGE_CARROTS("Karottentauschen kann nicht mit dieser Karottenanzahl gespielt werden."), - - MUST_BUY_ONE_CARD("Auf einem Marktfeld muss genau eine Karte gekauft werden."), - MUST_PLAY_CARD("Beim Betreten eines Hasenfeldes muss eine Hasenkarte gespielt werden."), - CARD_NOT_OWNED("Karte kann nicht gespielt werden, da nicht im Besitz."), - CANNOT_PLAY_CARD("Karte kann nicht gespielt werden."), - CANNOT_PLAY_FALL_BACK("Rückzugskarte nicht spielbar."), - CANNOT_PLAY_HURRY_AHEAD("Vorwärtssprungkarte nicht spielbar."), -} diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt b/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt index a245e39dd..7e278947f 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/Constants.kt @@ -3,7 +3,9 @@ package sc.plugin2025.util /** Eine Sammlung an verschiedenen Konstanten, die im Spiel verwendet werden. */ object HuIConstants { const val NUM_FIELDS: Int = 65 + const val INITIAL_SALADS: Int = 5 const val INITIAL_CARROTS: Int = 68 + const val ROUND_LIMIT: Int = 30 } \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt index c41938006..e42ece83c 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt @@ -11,7 +11,7 @@ import sc.helpers.testXStream import sc.plugin2024.actions.Accelerate import sc.plugin2024.actions.Advance import sc.plugin2024.actions.Turn -import sc.plugin2024.mistake.MoveMistake +import sc.plugin2024.mistake.MQMoveMistake import sc.plugin2024.util.GamePlugin import sc.shared.InvalidMoveException import sc.shared.Violation @@ -89,7 +89,7 @@ class GameResultTest: WordSpec({ game.currentState.performMoveDirectly(Move(Accelerate(2), Advance(3))) game.currentState.performMoveDirectly(Move(Accelerate(1), Advance(1), Turn(CubeDirection.DOWN_RIGHT), Advance(1))) "work with violation result" { - game.players.first().violation = Violation.RULE_VIOLATION(InvalidMoveException(MoveMistake.NO_ACTIONS, Move())) + game.players.first().violation = Violation.RULE_VIOLATION(InvalidMoveException(MQMoveMistake.NO_ACTIONS, Move())) game.getResult() shouldSerializeTo """ diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt index f17714e56..2c2ca0554 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt @@ -19,7 +19,7 @@ import sc.plugin2024.actions.Advance import sc.plugin2024.actions.Push import sc.plugin2024.actions.Turn import sc.plugin2024.mistake.AdvanceProblem -import sc.plugin2024.mistake.MoveMistake +import sc.plugin2024.mistake.MQMoveMistake import sc.plugin2024.util.MQConstants import sc.shared.InvalidMoveException @@ -264,7 +264,7 @@ class GameStateTest: FunSpec({ state.performMove(Move(Accelerate(3), Advance(4))) state.performMove(Move(Accelerate(4), Advance(5))) } - shouldThrow { state.performMove(Move(Accelerate(2), Advance(3))) }.mistake shouldBe MoveMistake.MOVEMENT_POINTS_LEFT + shouldThrow { state.performMove(Move(Accelerate(2), Advance(3))) }.mistake shouldBe MQMoveMistake.MOVEMENT_POINTS_LEFT shouldThrow { state.performMove(Move(Accelerate(2), Advance(4))) }.mistake shouldBe AdvanceProblem.MOVEMENT_POINTS_MISSING } } diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 35848d428..526c71c79 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -13,9 +13,9 @@ class MoveTest: WordSpec({ state.checkAdvance(1) shouldBe null state.checkAdvance(2) shouldBe null - state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED - state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS - state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT + state.checkAdvance(3) shouldBe HuIMoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe HuIMoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe HuIMoveMistake.FIELD_NONEXISTENT state.getSensibleMoves() shouldBe listOf( *Card.values().map { Advance(1, it) }.toTypedArray(), @@ -25,10 +25,10 @@ class MoveTest: WordSpec({ state.performMoveDirectly(Advance(2)) state.turn shouldBe 1 - state.checkAdvance(2) shouldBe MoveMistake.FIELD_OCCUPIED - state.checkAdvance(3) shouldBe MoveMistake.CARD_NOT_OWNED - state.checkAdvance(4) shouldBe MoveMistake.GOAL_CONDITIONS - state.checkAdvance(5) shouldBe MoveMistake.FIELD_NONEXISTENT + state.checkAdvance(2) shouldBe HuIMoveMistake.FIELD_OCCUPIED + state.checkAdvance(3) shouldBe HuIMoveMistake.CARD_NOT_OWNED + state.checkAdvance(4) shouldBe HuIMoveMistake.GOAL_CONDITIONS + state.checkAdvance(5) shouldBe HuIMoveMistake.FIELD_NONEXISTENT "one player advanced" should { "allow eat salad" { state.currentPlayer.addCard(Card.EAT_SALAD) @@ -88,7 +88,7 @@ class MoveTest: WordSpec({ "allow fallback and buy" { state.checkAdvance(3) shouldBe null - Advance(3, Card.FALL_BACK).perform(state.clone()) shouldBe MoveMistake.MUST_BUY_ONE_CARD + Advance(3, Card.FALL_BACK).perform(state.clone()) shouldBe HuIMoveMistake.MUST_BUY_ONE_CARD state.cloneCurrentPlayer { it.position = 1 }.nextCards() shouldBe Card.values().map { listOf(it) } state.cloneCurrentPlayer { it.position = 3 }.nextCards() shouldBe Card.values().map { listOf(Card.FALL_BACK, it) } diff --git a/server/src/test/java/sc/server/plugins/TestGameState.kt b/server/src/test/java/sc/server/plugins/TestGameState.kt index bd134f550..eed3f17c9 100644 --- a/server/src/test/java/sc/server/plugins/TestGameState.kt +++ b/server/src/test/java/sc/server/plugins/TestGameState.kt @@ -23,6 +23,9 @@ data class TestGameState( override fun moveIterator(): Iterator = throw NotImplementedError("TestGame has no Moves") + override fun teamStats(team: ITeam): List> = + throw NotImplementedError("TestGame has no teamStats") + override val round get() = turn / 2 val red = Player(Team.ONE) From 2f64466c9c85a9f94b38c3545020bc600ea916d4 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 22:59:39 +0300 Subject: [PATCH 34/38] refactor(plugin25): rename HuIMove to Move to fix player template --- plugin/src/main/kotlin/sc/plugin2025/Advance.kt | 2 +- plugin/src/main/kotlin/sc/plugin2025/Card.kt | 2 +- plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt | 2 +- .../src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt | 2 +- plugin/src/main/kotlin/sc/plugin2025/FallBack.kt | 2 +- plugin/src/main/kotlin/sc/plugin2025/GameState.kt | 12 ++++++------ .../kotlin/sc/plugin2025/{HuIMove.kt => Move.kt} | 2 +- .../src/main/kotlin/sc/plugin2025/util/GamePlugin.kt | 6 +++--- .../main/kotlin/sc/plugin2025/util/XStreamClasses.kt | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) rename plugin/src/main/kotlin/sc/plugin2025/{HuIMove.kt => Move.kt} (89%) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index 07ef3a1b6..af65f07ba 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -13,7 +13,7 @@ import sc.shared.IMoveMistake * Der Wert der Karottentauschkarte spielt dann keine Rolle. */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): HuIMove { +class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Move { override fun perform(state: GameState): IMoveMistake? { val player = state.currentPlayer diff --git a/plugin/src/main/kotlin/sc/plugin2025/Card.kt b/plugin/src/main/kotlin/sc/plugin2025/Card.kt index 6217e8847..784af9206 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Card.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Card.kt @@ -5,7 +5,7 @@ import sc.shared.IMoveMistake /** Mögliche Aktionen, die durch das Ausspielen einer Karte ausgelöst werden können. */ @XStreamAlias(value = "card") -enum class Card(val moves: Boolean, val playable: (GameState) -> HuIMoveMistake?, val play: (GameState) -> Unit): HuIMove { +enum class Card(val moves: Boolean, val playable: (GameState) -> HuIMoveMistake?, val play: (GameState) -> Unit): Move { /** Falle hinter den Gegenspieler. */ FALL_BACK(true, { state -> diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index 8d1887326..a12dde4b7 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -11,7 +11,7 @@ import sc.shared.IMoveMistake * und es werden je nachdem ob der Spieler führt oder nicht 10 oder 30 Karotten aufgenommen. */ @XStreamAlias(value = "EatSalad") -object EatSalad: HuIMove { +object EatSalad: Move { override fun perform(state: GameState): IMoveMistake? { if(state.mustEatSalad()) { state.currentPlayer.saladEaten = true diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index acab4ef24..3a896ed9a 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -9,7 +9,7 @@ import sc.shared.IMoveMistake * Dies kann beliebig oft hintereinander ausgeführt werden. */ @XStreamAlias(value = "ExchangeCarrots") -data class ExchangeCarrots(val value: Int): HuIMove { +data class ExchangeCarrots(val value: Int): Move { override fun perform(state: GameState): IMoveMistake? { if(state.mayExchangeCarrots(this.value)) { state.currentPlayer.carrots += value diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt index beafaaef0..434e25f1a 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -9,7 +9,7 @@ import com.thoughtworks.xstream.annotations.XStreamAlias * Dabei werden die zurückgezogene Distanz * 10 Karotten aufgenommen. */ @XStreamAlias(value = "fallBack") -object FallBack: HuIMove { +object FallBack: Move { override fun perform(state: GameState): HuIMoveMistake? { val previousFieldIndex = state.nextFallBack() if(previousFieldIndex != null) { diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index 2a0eca525..e2b93dd47 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -27,8 +27,8 @@ data class GameState @JvmOverloads constructor( @XStreamAsAttribute override var turn: Int = 0, @XStreamImplicit val players: List = Team.values().map { Hare(it) }, /** Der zuletzt gespielte Zug. */ - override var lastMove: HuIMove? = null, -): TwoPlayerGameState(players.first().team) { + override var lastMove: Move? = null, +): TwoPlayerGameState(players.first().team) { val currentPlayer get() = getHare(currentTeam) @@ -70,9 +70,9 @@ data class GameState @JvmOverloads constructor( override fun getPointsForTeam(team: ITeam): IntArray = getHare(team).let { intArrayOf(if(it.inGoal) 1 else 0, it.position, it.salads) } - override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) + override fun getSensibleMoves(): List = getSensibleMoves(currentPlayer) - fun getSensibleMoves(player: Hare): List { + fun getSensibleMoves(player: Hare): List { if(mustEatSalad(player)) return listOf(EatSalad) return (1..calculateMoveableFields(player.carrots).coerceAtMost(board.size - player.position)).flatMap { distance -> @@ -127,9 +127,9 @@ data class GameState @JvmOverloads constructor( else -> null } - override fun moveIterator(): Iterator = getSensibleMoves().iterator() + override fun moveIterator(): Iterator = getSensibleMoves().iterator() - override fun performMoveDirectly(move: HuIMove) { + override fun performMoveDirectly(move: Move) { val mist = HuIMoveMistake.MUST_EAT_SALAD.takeIf { mustEatSalad() && move != EatSalad diff --git a/plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt b/plugin/src/main/kotlin/sc/plugin2025/Move.kt similarity index 89% rename from plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt rename to plugin/src/main/kotlin/sc/plugin2025/Move.kt index ffc2ae960..ec2a24a6d 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/HuIMove.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Move.kt @@ -5,7 +5,7 @@ import sc.api.plugins.IMove import sc.shared.IMoveMistake @XStreamAlias(value = "action") -interface HuIMove: IMove { +interface Move: IMove { fun perform(state: GameState): IMoveMistake? } diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt index 5de1ff134..8e21c95b1 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt @@ -6,7 +6,7 @@ import sc.api.plugins.IGamePlugin import sc.api.plugins.IGameState import sc.framework.plugins.TwoPlayerGame import sc.plugin2025.GameState -import sc.plugin2025.HuIMove +import sc.plugin2025.Move import sc.shared.* @XStreamAlias(value = "winreason") @@ -16,7 +16,7 @@ enum class HuIWinReason(override val message: String, override val isRegular: Bo GOAL("%s hat das Ziel zuerst erreicht."), } -class GamePlugin: IGamePlugin { +class GamePlugin: IGamePlugin { companion object { const val PLUGIN_ID = "swc_2025_hase_und_igel" val scoreDefinition: ScoreDefinition = @@ -36,7 +36,7 @@ class GamePlugin: IGamePlugin { override val turnLimit: Int = HuIConstants.ROUND_LIMIT * 2 - override val moveClass = HuIMove::class.java + override val moveClass = Move::class.java override fun createGame(): IGameInstance = TwoPlayerGame(this, GameState()) diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt index ab28a7355..d973179b9 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt @@ -1,13 +1,13 @@ package sc.plugin2025.util import sc.networking.XStreamProvider -import sc.plugin2025.* +import sc.plugin2025.Move class XStreamClasses: XStreamProvider { override val classesToRegister = listOf( - HuIMove::class.java + Move::class.java ) } \ No newline at end of file From ab93522b19501692a9d9eac761770a44e2e8a409 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 21 May 2024 23:03:21 +0300 Subject: [PATCH 35/38] release: v25.0.0-alpha1 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3cf6a3f..f3a45b72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ The version should always be in sync with the [GUI](https://github.com/software- and likely contains breaking changes between patches. -### 25.0.0 - 2024 +### 25.0.0 - 2024-06 + +## 2025 Game Mississippi Queen - 2024-06 - Allow other player to move on when one is disqualified ### 24 Post-Finale From bf1d78f11d63ea611de9d47e6711d0120e867910 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 4 Jun 2024 09:37:47 +0300 Subject: [PATCH 36/38] fix(plugin25/GameState): disallow double EatSalad --- plugin/src/main/kotlin/sc/plugin2025/GameState.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt index e2b93dd47..91709c9da 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/GameState.kt @@ -133,9 +133,11 @@ data class GameState @JvmOverloads constructor( val mist = HuIMoveMistake.MUST_EAT_SALAD.takeIf { mustEatSalad() && move != EatSalad - }.also { currentPlayer.saladEaten = false } ?: move.perform(this) + } ?: move.perform(this) if(mist != null) throw InvalidMoveException(mist, move) + if(move != EatSalad) + currentPlayer.saladEaten = false turn++ awardPositionFields() if(!moveIterator().hasNext()) { From f0cb80f7cd36eeb22f4fc8520b2209824f8bfab3 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 4 Jun 2024 09:38:56 +0300 Subject: [PATCH 37/38] fix(plugin25): move XML and stringification with tests --- .../src/main/kotlin/sc/plugin2025/Advance.kt | 19 +-- .../src/main/kotlin/sc/plugin2025/EatSalad.kt | 6 +- .../kotlin/sc/plugin2025/ExchangeCarrots.kt | 11 +- .../src/main/kotlin/sc/plugin2025/FallBack.kt | 6 +- plugin/src/main/kotlin/sc/plugin2025/Move.kt | 2 - .../sc/plugin2025/util/XStreamClasses.kt | 12 +- .../src/test/kotlin/sc/plugin2025/MoveTest.kt | 114 +++++++++++++++++- 7 files changed, 148 insertions(+), 22 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt index af65f07ba..1fa22269e 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Advance.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Advance.kt @@ -2,6 +2,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.plugin2025.util.HuIConstants import sc.shared.IMoveMistake @@ -13,7 +14,10 @@ import sc.shared.IMoveMistake * Der Wert der Karottentauschkarte spielt dann keine Rolle. */ @XStreamAlias(value = "advance") -class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Move { +class Advance(@XStreamAsAttribute val distance: Int, @XStreamImplicit private vararg val cards: Card): Move { + + @Suppress("USELESS_ELVIS") + fun getCards() = cards ?: arrayOf() override fun perform(state: GameState): IMoveMistake? { val player = state.currentPlayer @@ -24,7 +28,7 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Mo var lastCard: Card? = null var bought = false - return cards.firstNotNullOfOrNull { + return getCards().firstNotNullOfOrNull { if(bought) return HuIMoveMistake.MUST_BUY_ONE_CARD if(state.currentField == Field.MARKET) { @@ -47,19 +51,18 @@ class Advance(@XStreamAsAttribute val distance: Int, vararg val cards: Card): Mo } } - override fun toString(): String { - return "Vorwärts um $distance${cards.joinToString(prefix = " mit Karten [", postfix = "]")}" - } + @Suppress("SAFE_CALL_WILL_CHANGE_NULLABILITY", "UNNECESSARY_SAFE_CALL") + override fun toString(): String = + "Vorwärts um $distance${cards?.takeUnless { it.isEmpty() }?.joinToString(prefix = " mit Karten [", postfix = "]") ?: ""}" override fun equals(other: Any?): Boolean { if(this === other) return true if(other !is Advance) return false if(distance != other.distance) return false - if(!cards.contentEquals(other.cards)) return false - return true + return getCards().contentEquals(other.getCards()) } override fun hashCode(): Int = - distance + cards.contentHashCode() * HuIConstants.NUM_FIELDS + distance + getCards().contentHashCode() * HuIConstants.NUM_FIELDS } \ No newline at end of file diff --git a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt index a12dde4b7..567ebbcb4 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/EatSalad.kt @@ -10,7 +10,7 @@ import sc.shared.IMoveMistake * Durch eine Salatessen-Aktion wird ein Salat verbraucht * und es werden je nachdem ob der Spieler führt oder nicht 10 oder 30 Karotten aufgenommen. */ -@XStreamAlias(value = "EatSalad") +@XStreamAlias(value = "eatsalad") object EatSalad: Move { override fun perform(state: GameState): IMoveMistake? { if(state.mustEatSalad()) { @@ -22,5 +22,9 @@ object EatSalad: Move { } } + override fun toString(): String = "Salat fressen" + override fun equals(other: Any?): Boolean = other is EatSalad + + override fun hashCode(): Int = javaClass.name.hashCode() } diff --git a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt index 3a896ed9a..64561f326 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt @@ -1,6 +1,7 @@ package sc.plugin2025 import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamAsAttribute import sc.shared.IMoveMistake /** @@ -8,14 +9,16 @@ import sc.shared.IMoveMistake * Auf einem Karottenfeld können 10 Karotten abgegeben oder aufgenommen werden. * Dies kann beliebig oft hintereinander ausgeführt werden. */ -@XStreamAlias(value = "ExchangeCarrots") -data class ExchangeCarrots(val value: Int): Move { +@XStreamAlias(value = "exchangecarrots") +data class ExchangeCarrots(@XStreamAsAttribute val amount: Int): Move { override fun perform(state: GameState): IMoveMistake? { - if(state.mayExchangeCarrots(this.value)) { - state.currentPlayer.carrots += value + if(state.mayExchangeCarrots(this.amount)) { + state.currentPlayer.carrots += amount return null } else { return HuIMoveMistake.CANNOT_EXCHANGE_CARROTS } } + + override fun toString(): String = "Karottenvorrat um $amount aendern" } diff --git a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt index 434e25f1a..33befe337 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/FallBack.kt @@ -8,7 +8,7 @@ import com.thoughtworks.xstream.annotations.XStreamAlias * darf anstatt nach vorne zu ziehen ein Rückzug gemacht werden. * Dabei werden die zurückgezogene Distanz * 10 Karotten aufgenommen. */ -@XStreamAlias(value = "fallBack") +@XStreamAlias(value = "fallback") object FallBack: Move { override fun perform(state: GameState): HuIMoveMistake? { val previousFieldIndex = state.nextFallBack() @@ -21,5 +21,9 @@ object FallBack: Move { } } + override fun toString(): String = "Zurückfallen" + override fun equals(other: Any?): Boolean = other is FallBack + + override fun hashCode(): Int = javaClass.name.hashCode() } diff --git a/plugin/src/main/kotlin/sc/plugin2025/Move.kt b/plugin/src/main/kotlin/sc/plugin2025/Move.kt index ec2a24a6d..d19057cc3 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/Move.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/Move.kt @@ -1,10 +1,8 @@ package sc.plugin2025 -import com.thoughtworks.xstream.annotations.XStreamAlias import sc.api.plugins.IMove import sc.shared.IMoveMistake -@XStreamAlias(value = "action") interface Move: IMove { fun perform(state: GameState): IMoveMistake? } diff --git a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt index d973179b9..a0b664bf5 100644 --- a/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt +++ b/plugin/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt @@ -1,13 +1,17 @@ package sc.plugin2025.util import sc.networking.XStreamProvider -import sc.plugin2025.Move +import sc.plugin2025.* class XStreamClasses: XStreamProvider { override val classesToRegister = - listOf( - Move::class.java - ) + listOf( + Advance::class.java, + Card::class.java, + FallBack::class.java, + EatSalad::class.java, + ExchangeCarrots::class.java, + ) } \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 526c71c79..1eb4ae551 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -1,13 +1,16 @@ package sc.plugin2025 +import io.kotest.assertions.forEachAsClue +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.* import io.kotest.matchers.collections.* +import sc.helpers.shouldSerializeTo +import sc.shared.InvalidMoveException class MoveTest: WordSpec({ isolationMode = IsolationMode.InstancePerTest - // TODO other move types and card chains "Advance" When { val state = GameState(Board(arrayOf(Field.START, Field.MARKET, Field.CARROTS, Field.HARE, Field.GOAL))) @@ -81,7 +84,7 @@ class MoveTest: WordSpec({ state.currentPlayer.addCard(Card.FALL_BACK) state.checkAdvance(3) shouldBe null - "not allow fallback to startfield" { + "not allow fallback to start" { state.otherPlayer.position = 1 state.possibleCardMoves(3).shouldBeEmpty() } @@ -97,5 +100,112 @@ class MoveTest: WordSpec({ state.otherPlayer.getCards() shouldBe listOf(Card.EAT_SALAD) } } + + "converted to XML" should { + "be concise" { + Advance(1) shouldSerializeTo "" + } + "include cards" { + Advance(3, Card.FALL_BACK, Card.EAT_SALAD) shouldSerializeTo """ + + FALL_BACK + EAT_SALAD + + """.trimIndent() + } + } + } + + "Fallback" should { + val state = GameState(Board(arrayOf(Field.START, Field.CARROTS, Field.HEDGEHOG, Field.CARROTS, Field.HEDGEHOG, Field.HARE))) + "be invalid on start" { + state.nextFallBack(state.currentPlayer) shouldBe null + state.nextFallBack(state.otherPlayer) shouldBe null + } + "be invalid before first hedgehog" { + state.currentPlayer.position = 1 + state.nextFallBack(state.currentPlayer) shouldBe null + } + "be invalid on first hedgehog" { + state.currentPlayer.position = 2 + state.nextFallBack(state.currentPlayer) shouldBe null + } + "be valid beyond first hedgehog" { + state.currentPlayer.position = 3 + state.nextFallBack(state.currentPlayer) shouldBe 2 + state.nextFallBack(state.otherPlayer) shouldBe null + state.currentPlayer.position = 4 + state.nextFallBack(state.currentPlayer) shouldBe 2 + } + "be invalid if hedgehog is blocked" { + state.otherPlayer.position = 4 + state.currentPlayer.position = 5 + state.nextFallBack(state.currentPlayer) shouldBe null + state.nextFallBack(state.otherPlayer) shouldBe 2 + } + + "produce concise XML" { + FallBack shouldSerializeTo "" + } + } + + "EatSalad" should { + val state = GameState(Board(arrayOf(Field.START, Field.CARROTS, Field.HEDGEHOG, Field.HARE, Field.SALAD))) + "not be valid outside Salad field" { + (0..3).forEachAsClue { + state.currentPlayer.position = it + state.mayEatSalad() shouldBe false + } + } + state.currentPlayer.position = 4 + "be valid on salad field" { + state.mayEatSalad() shouldBe true + state.mustEatSalad() shouldBe true + } + "be invalid without salad" { + state.currentPlayer.salads = 0 + state.mayEatSalad() shouldBe false + state.mustEatSalad() shouldBe true + } + "only be accepted once" { + shouldThrow { + state.performMoveDirectly(ExchangeCarrots(10)) + }.mistake shouldBe HuIMoveMistake.MUST_EAT_SALAD + state.performMoveDirectly(EatSalad) + state.turn++ + state.mayEatSalad() shouldBe false + state.mustEatSalad() shouldBe false + shouldThrow { + state.performMoveDirectly(EatSalad) + }.mistake shouldBe HuIMoveMistake.CANNOT_EAT_SALAD + } + + "produce concise XML" { + EatSalad shouldSerializeTo "" + } + } + + "ExchangeCarrots" should { + val state = GameState(Board(arrayOf(Field.START, Field.HEDGEHOG, Field.HARE, Field.SALAD, Field.CARROTS))) + "not be valid outside Carrot field" { + (0..3).forEachAsClue { + state.currentPlayer.position = it + state.possibleExchangeCarrotMoves().shouldBeEmpty() + } + } + "be valid on carrot field" { + state.currentPlayer.position = 4 + state.possibleExchangeCarrotMoves() shouldBe listOf(ExchangeCarrots(10), ExchangeCarrots(-10)) + } + "not produce negative carrots" { + state.currentPlayer.carrots = 12 + state.performMoveDirectly(Advance(4)) + state.turn++ + state.getSensibleMoves() shouldBe listOf(FallBack, ExchangeCarrots(10)) + } + + "produce concicse XML" { + ExchangeCarrots(-10) shouldSerializeTo "" + } } }) \ No newline at end of file From edd242cd929e949bfdb78b5b852d98be8cb0eb3d Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 4 Jun 2024 09:43:43 +0300 Subject: [PATCH 38/38] test(plugin25): room packet XML --- plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt index 1eb4ae551..9e0f70f30 100644 --- a/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/plugin/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -7,6 +7,7 @@ import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.* import io.kotest.matchers.collections.* import sc.helpers.shouldSerializeTo +import sc.protocol.room.RoomPacket import sc.shared.InvalidMoveException class MoveTest: WordSpec({ @@ -208,4 +209,14 @@ class MoveTest: WordSpec({ ExchangeCarrots(-10) shouldSerializeTo "" } } + + "Move" should { + "produce a nice room message" { + RoomPacket("abcd", Advance(3)) shouldSerializeTo """ + + + + """.trimIndent() + } + } }) \ No newline at end of file