Skip to content

Commit cb4b31d

Browse files
authored
fix(plugin): improve GameState turn advancing (#391)
- turn can now only be modified by GameState itself - GameState automatically skips invalid colors when advancing - improved corresponding testing
1 parent 3c1c8b5 commit cb4b31d

File tree

6 files changed

+91
-98
lines changed

6 files changed

+91
-98
lines changed

plugin/src/server/sc/plugin2021/Game.kt

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,17 @@ class Game: AbstractGame<Player>(GamePlugin.PLUGIN_UUID) {
5151

5252
override val playerScores: MutableList<PlayerScore>
5353
get() = players.mapTo(ArrayList(players.size)) { getScoreFor(it) }
54+
55+
val isGameOver: Boolean
56+
get() = !currentState.hasValidColors() || currentState.round > Constants.ROUND_LIMIT
5457

5558
/**
5659
* Checks whether and why the game is over.
5760
*
5861
* @return null if any player can still move, otherwise a WinCondition with the winner and reason.
5962
*/
6063
override fun checkWinCondition(): WinCondition? {
61-
if (!checkGameOver()) return null
64+
if (!isGameOver) return null
6265

6366
val scores: Map<Team, Int> = Team.values().map {
6467
it to currentState.getPointsForPlayer(it)
@@ -141,26 +144,14 @@ class Game: AbstractGame<Player>(GamePlugin.PLUGIN_UUID) {
141144
logger.debug("Current State: $currentState")
142145
logger.debug("Performing Move $data")
143146
GameRuleLogic.performMove(currentState, data)
144-
next(if (checkGameOver()) null else currentState.currentPlayer)
147+
GameRuleLogic.removeInvalidColors(currentState)
148+
next(if (isGameOver) null else currentState.currentPlayer)
145149
logger.debug("Current Board:\n${currentState.board}")
146150
} catch(e: InvalidMoveException) {
147151
handleInvalidMove(e, fromPlayer)
148152
}
149153
}
150154

151-
private val isGameOver: Boolean
152-
get() = !currentState.hasValidColors() || currentState.round > Constants.ROUND_LIMIT
153-
154-
fun checkGameOver(): Boolean {
155-
logger.debug("Round: ${currentState.round} > ${Constants.ROUND_LIMIT}")
156-
if (currentState.round > Constants.ROUND_LIMIT) {
157-
currentState.clearValidColors()
158-
} else {
159-
GameRuleLogic.removeInvalidColors(currentState)
160-
}
161-
return isGameOver
162-
}
163-
164155
override fun toString(): String =
165156
"Game(${when {
166157
isGameOver -> "OVER, "

plugin/src/shared/sc/plugin2021/GameState.kt

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,18 @@ class GameState @JvmOverloads constructor(
5757
}
5858

5959
/** @return Liste der noch nicht von [Color] gesetzten Steine. */
60-
fun undeployedPieceShapes(color: Color = currentColor): Collection<PieceShape> = mutableUndeployedPieceShapes(color)
60+
fun undeployedPieceShapes(color: Color = currentColor): Collection<PieceShape> =
61+
mutableUndeployedPieceShapes(color)
6162

6263
fun removeUndeployedPiece(piece: Piece) =
6364
mutableUndeployedPieceShapes(piece.color).remove(piece.kind)
6465

65-
fun roundFromTurn(turn: Int) = 1 + turn / orderedColors.size
66+
fun roundFromTurn(turn: Int) = 1 + turn / Constants.COLORS
6667

6768
/** Die Anzahl an bereits getätigten Zügen. */
6869
@XStreamAsAttribute
6970
override var turn: Int = turn
70-
set(value) {
71+
private set(value) {
7172
if (value < 0) throw IndexOutOfBoundsException("Can't go back in turns (request was $turn to $value)")
7273
field = value
7374
round = roundFromTurn(value)
@@ -97,25 +98,27 @@ class GameState @JvmOverloads constructor(
9798
/** Liste der Farben, die noch im Spiel sind. */
9899
private val validColors = validColors
99100

100-
/** Beendet das Spiel, indem alle Farben entfernt werden. */
101-
internal fun clearValidColors() = validColors.clear()
102-
103101
/** @return true, wenn noch Farben im Spiel sind. */
104-
fun hasValidColors() = validColors.isNotEmpty()
102+
fun hasValidColors() =
103+
validColors.isNotEmpty()
105104

106-
/** Prüfe, ob die gegebene Farbe noch im Spiel ist. */
107-
fun isValid(color: Color = currentColor) = validColors.contains(color)
105+
/** @param color zu prüfende Farbe, standardmäßig die Farbe am Zug
106+
* @return ob die gegebene Farbe noch im Spiel ist. */
107+
@JvmOverloads
108+
fun isValidColor(color: Color = currentColor) =
109+
validColors.contains(color)
108110

109-
/**
110-
* Versuche, zum nächsten Zug überzugehen.
111-
* Schlägt fehl, wenn das Spiel bereits zu ende ist.
112-
* @return Ob es erfolgreich war.
113-
*/
114-
fun tryAdvance(turns: Int = 1): Boolean = try {
111+
/** Geht zum Zug der nächsten noch im Spiel befindlichen Farbe über.
112+
* @param turns wie viele Züge mindestens weiter gerückt werden soll
113+
* @return ob das Spiel vorgerückt oder bereits zu Ende ist. */
114+
@JvmOverloads
115+
fun advance(turns: Int = 1): Boolean {
116+
if(!hasValidColors())
117+
return false
115118
turn += turns
116-
true
117-
} catch (e: Exception) {
118-
false
119+
while(!isValidColor())
120+
turn++
121+
return true
119122
}
120123

121124
fun addPlayer(player: Player) {
@@ -135,16 +138,12 @@ class GameState @JvmOverloads constructor(
135138
return GameRuleLogic.getPointsFromUndeployed(pieces, lastMono)
136139
}
137140

138-
/**
139-
* Entferne die Farbe, die momentan am Zug ist.
140-
* Die resultierende aktive Farbe wird dann die des letzten Zuges sein.
141-
*
142-
* Diese Funktion wird von der [GameRuleLogic] benötigt und sollte nie so aufgerufen werden.
143-
*/
144-
internal fun removeColor(color: Color = currentColor) {
145-
logger.info("Removing $color from the game")
146-
validColors.remove(color)
147-
logger.debug("Remaining Colors: $validColors")
141+
/** Entferne die Farbe, die momentan am Zug ist.
142+
* @return ob noch Farben im Spiel sind */
143+
internal fun removeActiveColor(): Boolean {
144+
validColors.remove(currentColor)
145+
logger.info("Removed ${currentColor.name} from the game - remaining: [${validColors.joinToString { it.name }}]")
146+
return advance()
148147
}
149148

150149
override fun clone() = GameState(this)

plugin/src/shared/sc/plugin2021/util/GameRuleLogic.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ object GameRuleLogic {
5151
is SkipMove -> performSkipMove(gameState)
5252
is SetMove -> performSetMove(gameState, move)
5353
}
54+
gameState.advance()
5455
gameState.lastMove = move
5556
}
5657

@@ -98,10 +99,6 @@ object GameRuleLogic {
9899
// If it was the last piece for this color, remove it from the turn queue
99100
if (gameState.undeployedPieceShapes(move.color).isEmpty())
100101
gameState.lastMoveMono += move.color to (move.piece.kind == PieceShape.MONO)
101-
102-
do {
103-
gameState.tryAdvance()
104-
} while (!gameState.isValid(gameState.currentColor))
105102
}
106103

107104
/**
@@ -184,8 +181,6 @@ object GameRuleLogic {
184181
@JvmStatic
185182
private fun performSkipMove(gameState: GameState) {
186183
validateSkipMove(gameState)
187-
if (!gameState.tryAdvance())
188-
logger.error("Couldn't proceed to next turn!")
189184
}
190185

191186
/** Prüfe, ob das gegebene [Field] bereits an eins mit gleicher Farbe angrenzt. */
@@ -214,7 +209,7 @@ object GameRuleLogic {
214209
fun isFirstMove(gameState: GameState) =
215210
gameState.undeployedPieceShapes().size == Constants.TOTAL_PIECE_SHAPES
216211

217-
/** Return a random Pentomino which is not the `x` one (Used to get a valid starting piece). */
212+
/** @return a random Pentomino which is not the `x` one (Used to get a valid starting piece). */
218213
@JvmStatic
219214
fun getRandomPentomino() =
220215
PieceShape.values()
@@ -226,8 +221,7 @@ object GameRuleLogic {
226221
fun removeInvalidColors(gameState: GameState) {
227222
if (!gameState.hasValidColors()) return
228223
if (streamPossibleMoves(gameState).none { isValidSetMove(gameState, it) }) {
229-
gameState.removeColor()
230-
gameState.turn++
224+
gameState.removeActiveColor()
231225
removeInvalidColors(gameState)
232226
}
233227
}

plugin/src/test/sc/plugin2021/GameStateTest.kt

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,51 @@ class GameStateTest: WordSpec({
2727
state.getPointsForPlayer(Team.TWO) shouldBe 0
2828
}
2929
}
30-
"asked for the current color" should {
31-
"return the correct color" {
32-
state.orderedColors.size shouldBe Constants.COLORS
33-
for (color in Color.values()) {
34-
state.currentColor shouldBe color
35-
state.turn++
30+
"turn number increases" should {
31+
"advance turn, round and currentcolor accordingly" {
32+
GameState().run {
33+
orderedColors.size shouldBe Constants.COLORS
34+
35+
for (color in Color.values()) {
36+
turn shouldBe color.ordinal
37+
round shouldBe 1
38+
currentColor shouldBe color
39+
advance()
40+
}
41+
42+
turn shouldBe 4
43+
round shouldBe 2
44+
currentColor shouldBe Color.BLUE
45+
46+
advance(7)
47+
turn shouldBe 11
48+
round shouldBe 3
49+
currentColor shouldBe Color.GREEN
50+
51+
removeActiveColor()
52+
turn shouldBe 12
53+
round shouldBe 4
54+
currentColor shouldBe Color.BLUE
55+
56+
Color.values().filterNot { it == Color.RED }.forEach { color ->
57+
(undeployedPieceShapes(color) as MutableCollection).clear()
58+
}
59+
60+
GameRuleLogic.removeInvalidColors(this)
61+
turn shouldBe 14
62+
currentColor shouldBe Color.RED
63+
64+
advance()
65+
turn shouldBe 18
66+
round shouldBe 5
67+
currentColor shouldBe Color.RED
68+
69+
advance()
70+
GameRuleLogic.removeInvalidColors(this)
71+
turn shouldBe 22
72+
round shouldBe 6
73+
currentColor shouldBe Color.RED
3674
}
37-
38-
state.currentColor shouldBe Color.BLUE
39-
state.turn++
40-
state.currentColor shouldBe Color.YELLOW
41-
state.turn += 2
42-
state.currentColor shouldBe Color.GREEN
4375
}
4476
}
4577
"a piece is placed a second time" should {
@@ -48,7 +80,7 @@ class GameStateTest: WordSpec({
4880
shouldNotThrow<InvalidMoveException> {
4981
GameRuleLogic.performMove(state, move)
5082
}
51-
state.turn += 4
83+
state.advance(4)
5284
state.undeployedPieceShapes(Color.BLUE).size shouldBe 20
5385
"throw an InvalidMoveException" {
5486
shouldThrow<InvalidMoveException> {
@@ -87,13 +119,13 @@ class GameStateTest: WordSpec({
87119
cloned shouldNotBe state
88120
}
89121
"respect validColors" {
90-
state.removeColor(Color.BLUE)
122+
state.removeActiveColor() shouldBe true
123+
state.currentColor shouldBe Color.YELLOW
91124
val newClone = state.clone()
92125
newClone shouldBe state
93126
cloned shouldNotBe state
94-
newClone.removeColor(Color.BLUE)
95-
newClone shouldBe state
96-
newClone.removeColor(Color.RED)
127+
newClone.removeActiveColor() shouldBe true
128+
newClone.currentColor shouldBe Color.RED
97129
newClone shouldNotBe state
98130
}
99131
val otherState = GameState(lastMove = SetMove(Piece(Color.GREEN, 0)))
@@ -102,29 +134,5 @@ class GameStateTest: WordSpec({
102134
otherState.clone() shouldNotBe state
103135
}
104136
}
105-
"turn number increases" should {
106-
"let turn, round and currentcolor advance accordingly" {
107-
GameState().run {
108-
turn shouldBe 0
109-
round shouldBe 1
110-
currentColor shouldBe Color.BLUE
111-
112-
turn += 10
113-
turn shouldBe 10
114-
round shouldBe 3
115-
currentColor shouldBe Color.RED
116-
117-
turn++
118-
turn shouldBe 11
119-
round shouldBe 3
120-
currentColor shouldBe Color.GREEN
121-
122-
turn++
123-
turn shouldBe 12
124-
round shouldBe 4
125-
currentColor shouldBe Color.BLUE
126-
}
127-
}
128-
}
129137
}
130138
})

plugin/src/test/sc/plugin2021/GameTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class GameTest: WordSpec({
9696
game.onAction(state.currentPlayer, GameRuleLogic.streamPossibleMoves(state).first())
9797

9898
shouldNotThrowAny {
99-
while (!game.checkGameOver()) {
99+
while (!game.isGameOver) {
100100
game.onAction(state.currentPlayer, SkipMove(state.currentColor))
101101
}
102102
}

plugin/src/test/sc/plugin2021/xstream/ConverterTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ class ConverterTest: FunSpec({
219219
states.forEach { (state, xml) ->
220220
test(state.toString()) {
221221
checkSerialization(testXStream, state, xml) { obj, deserialized ->
222-
if(obj != deserialized)
222+
if (obj != deserialized)
223223
throw failure(Expected(Printed(obj.longString())), Actual(Printed(deserialized.longString())))
224224
}
225225
reader.readObject() shouldBe state
@@ -228,12 +228,13 @@ class ConverterTest: FunSpec({
228228
shouldThrow<EOFException> {
229229
reader.readObject()
230230
}
231-
231+
232232
test("update round from turn") {
233233
val state = GameState(turn = 10)
234234
testXStream.toXML(state) shouldContain "round=\"3\""
235-
state.turn = 70
236-
testXStream.toXML(state) shouldContain "round=\"18\""
235+
state.advance(60)
236+
state.turn shouldBe 70
237+
testXStream.toXML(state) shouldContain "round=\"18\""
237238
state.round shouldBe 18
238239
}
239240
}

0 commit comments

Comments
 (0)