Skip to content

Commit

Permalink
rework: revamp the GameResult structure
Browse files Browse the repository at this point in the history
And along the way simplify the Player
and expand reasons in the protocol.
Still some tests are failing,
especially violations ending up as ties,
will investigate in the following.
  • Loading branch information
xeruf committed Mar 10, 2024
1 parent f7d1052 commit 64ce836
Show file tree
Hide file tree
Showing 35 changed files with 327 additions and 359 deletions.
2 changes: 1 addition & 1 deletion helpers/test-client/src/sc/TestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ protected void onObject(@NotNull ProtocolPacket message) {
GameResult result = (GameResult) packet.getData();
if (!result.isRegular())
irregularGames++;
logger.warn("Game {} ended {} Winner: {}", finishedTests, result.isRegular() ? "regularly -" : "irregularly!", result.getWinner());
logger.warn("Game {} ended {} Winner: {}", finishedTests, result.isRegular() ? "regularly -" : "irregularly!", result.getWin());

finishedTests++;
ScoreDefinition scoreDefinition = result.getDefinition();
Expand Down
28 changes: 1 addition & 27 deletions plugin/src/main/kotlin/sc/plugin2023/Game.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
package sc.plugin2023

import org.slf4j.LoggerFactory
import sc.api.plugins.IMove
import sc.api.plugins.Team
import sc.framework.plugins.AbstractGame
import sc.shared.MoveMistake
import sc.plugin2023.util.WinReason
import sc.plugin2023.util.GamePlugin
import sc.shared.InvalidMoveException
import sc.shared.WinCondition

fun <T> Collection<T>.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.PLUGIN_ID) {
class Game(override val currentState: GameState = GameState()): AbstractGame(GamePlugin()) {
val isGameOver: Boolean
get() = currentState.isOver

Expand All @@ -33,18 +19,6 @@ class Game(override val currentState: GameState = GameState()): AbstractGame(Gam
logger.debug("Current State: {}", currentState.longString())
}

/**
* Checks whether and why the game is over.
*
* @return null if any player can still move, otherwise a WinCondition with the winner and reason.
*/
override fun checkWinCondition(): WinCondition? {
if (!isGameOver) return null
return Team.values().toList().maxByNoEqual { currentState.getPointsForTeam(it).first() }?.let {
WinCondition(it, WinReason.DIFFERING_SCORES)
} ?: WinCondition(null, WinReason.EQUAL_SCORE)
}

override fun toString(): String =
"Game(${
when {
Expand Down
5 changes: 3 additions & 2 deletions plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import sc.plugin2023.GameState
import sc.shared.ScoreAggregation
import sc.shared.ScoreDefinition
import sc.shared.ScoreFragment
import sc.shared.WinReason

class GamePlugin: IGamePlugin {
companion object {
const val PLUGIN_ID = "swc_2023_penguins"
val scoreDefinition: ScoreDefinition =
ScoreDefinition(arrayOf(
ScoreFragment("Siegpunkte", ScoreAggregation.SUM),
ScoreFragment("Fische", ScoreAggregation.AVERAGE),
ScoreFragment("Siegpunkte", WinReason("%s hat gewonnen"), ScoreAggregation.SUM),
ScoreFragment("Fische", WinReason("%s hat mehr Fische gesammelt"), ScoreAggregation.AVERAGE),
))
}

Expand Down
12 changes: 0 additions & 12 deletions plugin/src/main/kotlin/sc/plugin2023/util/WinReason.kt

This file was deleted.

33 changes: 3 additions & 30 deletions plugin/src/main/kotlin/sc/plugin2024/Game.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package sc.plugin2024

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import sc.api.plugins.IMove
import sc.framework.plugins.AbstractGame
import sc.shared.MoveMistake
import sc.plugin2024.util.WinReason
import sc.plugin2024.util.GamePlugin
import sc.shared.InvalidMoveException
import sc.shared.WinCondition

fun <T> Collection<T>.maxByNoEqual(selector: (T) -> Int): T? =
fold(Int.MIN_VALUE to (null as T?)) { acc, pos ->
Expand All @@ -20,9 +16,7 @@ fun <T> Collection<T>.maxByNoEqual(selector: (T) -> Int): T? =
}
}.second

class Game(override val currentState: GameState = GameState()): AbstractGame(GamePlugin.PLUGIN_ID) {
val isGameOver: Boolean
get() = currentState.isOver
class Game(override val currentState: GameState = GameState()): AbstractGame(GamePlugin()) {

override fun onRoundBasedAction(move: IMove) {
if(move !is Move)
Expand All @@ -33,34 +27,13 @@ class Game(override val currentState: GameState = GameState()): AbstractGame(Gam
logger.debug("Current State: ${currentState.longString()}")
}

/**
* Checks whether and why the game is over.
*
* @return null if any player can still move, otherwise a WinCondition with the winner and reason.
*/
override fun checkWinCondition(): WinCondition? {
if(!isGameOver) return null
val currentShip: Ship = currentState.currentShip
val otherShip: Ship = currentState.otherShip

return when {
// victory by points
currentShip.points > otherShip.points -> WinCondition(currentState.currentTeam, WinReason.DIFFERING_SCORES)
currentShip.points < otherShip.points -> WinCondition(currentState.otherTeam, WinReason.DIFFERING_SCORES)
// victory by passengers
currentShip.passengers > otherShip.passengers -> WinCondition(currentState.currentTeam, WinReason.DIFFERING_PASSENGERS)
currentShip.passengers < otherShip.passengers -> WinCondition(currentState.otherTeam, WinReason.DIFFERING_PASSENGERS)
else -> WinCondition(null, WinReason.EQUAL_PASSENGERS)
}
}

override fun toString(): String =
"Game(${
when {
isGameOver -> "OVER, "
currentState.isOver -> "OVER, "
isPaused -> "PAUSED, "
else -> ""
}
}players=$players, gameState=$currentState)"

}
}
16 changes: 11 additions & 5 deletions plugin/src/main/kotlin/sc/plugin2024/GameState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,15 @@ 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
// 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() = moveIterator().hasNext()

override val isOver: Boolean
get() = when {
// TODO return a WinCondition here indicating why somebody won
// Bedingung 1: ein Dampfer mit 2 Passagieren erreicht ein Zielfeld mit Geschwindigkeit 1
turn % 2 == 0 && ships.any { isWinner(it) } -> true
turn % 2 == 0 && ships.any { inGoal(it) } -> true
// Bedingung 2: ein Spieler macht einen ungültigen Zug.
// Das wird durch eine InvalidMoveException während des Spiels behandelt.
// Bedingung 3: am Ende einer Runde liegt ein Dampfer mehr als 3 Spielsegmente zurück
Expand All @@ -406,12 +407,17 @@ data class GameState @JvmOverloads constructor(
else -> false
}

fun isWinner(ship: Ship) =
fun inGoal(ship: Ship) =
ship.passengers > 1 && board.effectiveSpeed(ship) < 2 && board[ship.position] == Field.GOAL

override fun getPointsForTeam(team: ITeam): IntArray =
ships[team.index].let { ship ->
intArrayOf(ship.points, ship.coal * 2, if(isWinner(ship)) PluginConstants.FINISH_POINTS else 0)
intArrayOf(ship.points, ship.passengers)
}

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)
}

override fun teamStats(team: ITeam): List<Pair<String, Int>> =
Expand Down
19 changes: 12 additions & 7 deletions plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package sc.plugin2024.util

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.plugin2024.GameState
import sc.shared.ScoreAggregation
import sc.shared.ScoreDefinition
import sc.shared.ScoreFragment
import sc.shared.*

@XStreamAlias(value = "winreason")
enum class MQWinReason(override val message: String): IWinReason {
DIFFERING_SCORES("%s hat mehr Punkte."),
DIFFERING_PASSENGERS("%S hat mehr Passagiere befördert.");
override val isRegular = true
}

class GamePlugin: IGamePlugin {
companion object {
const val PLUGIN_ID = "swc_2024_mississippi_queen"
val scoreDefinition: ScoreDefinition =
ScoreDefinition(arrayOf(
ScoreFragment("Siegpunkte", ScoreAggregation.SUM),
ScoreFragment("Punkte", ScoreAggregation.AVERAGE),
ScoreFragment("Kohle", ScoreAggregation.AVERAGE),
ScoreFragment("Gewonnen", ScoreAggregation.AVERAGE),
ScoreFragment("Siegpunkte", WinReason("%s hat gewonnen"), ScoreAggregation.SUM),
ScoreFragment("Punkte", MQWinReason.DIFFERING_SCORES, ScoreAggregation.AVERAGE),
ScoreFragment("Passagiere", MQWinReason.DIFFERING_PASSENGERS, ScoreAggregation.AVERAGE),
))
}

Expand Down
13 changes: 0 additions & 13 deletions plugin/src/main/kotlin/sc/plugin2024/util/WinReason.kt

This file was deleted.

19 changes: 8 additions & 11 deletions plugin/src/test/kotlin/sc/GamePlayTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ 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.ScoreCause
import sc.shared.Violation
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime

/** This test verifies that the Game implementation can be used to play a game.
* It is the only plugin-test independent of the season. */
Expand Down Expand Up @@ -58,8 +58,8 @@ class GamePlayTest: WordSpec({

var finalState: Int? = null
game.addGameListener(object: IGameListener {
override fun onGameOver(results: Map<Player, PlayerScore>) {
logger.info("Game over: $results")
override fun onGameOver(result: GameResult) {
logger.info("Game over: $result")
}

override fun onStateChanged(data: IGameState, observersOnly: Boolean) {
Expand Down Expand Up @@ -102,13 +102,10 @@ class GamePlayTest: WordSpec({
finalState shouldBe game.currentState.hashCode()
}
"return regular scores" {
val scores = game.playerScores
val score1 = game.getScoreFor(game.players.first())
val score2 = game.getScoreFor(game.players.last())
scores shouldBe listOf(score1, score2)
scores.forEach { it.cause shouldBe ScoreCause.REGULAR }

score2.parts.first().intValueExact() shouldBe when(score1.parts.first().intValueExact()) {
val result = game.getResult()
result.isRegular shouldBe true
val scores = result.scores.values
scores.first().parts.first().intValueExact() shouldBe when(scores.last().parts.first().intValueExact()) {
Constants.LOSE_SCORE -> Constants.WIN_SCORE
Constants.WIN_SCORE -> Constants.LOSE_SCORE
Constants.DRAW_SCORE -> Constants.DRAW_SCORE
Expand Down
27 changes: 22 additions & 5 deletions sdk/src/main/framework/sc/shared/GameResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sc.shared

import com.thoughtworks.xstream.annotations.XStreamAlias
import com.thoughtworks.xstream.annotations.XStreamConverter
import sc.api.plugins.ITeam
import sc.framework.plugins.Player
import sc.protocol.room.ObservableRoomMessage
import sc.protocol.room.RoomOrchestrationMessage
Expand All @@ -17,16 +16,34 @@ import sc.util.GameResultConverter
data class GameResult(
val definition: ScoreDefinition,
val scores: Map<Player, PlayerScore>,
val winner: ITeam?,
val win: WinCondition?,
): RoomOrchestrationMessage, ObservableRoomMessage {

val isRegular: Boolean
get() = scores.all { it.value.cause == ScoreCause.REGULAR }
get() = win?.reason?.isRegular ?: true

override fun toString() =
"GameResult(winner=$winner, scores=[${
"GameResult(winner=$win, scores=[${
scores.entries.joinToString {
"${it.key.displayName}${it.value.toString(definition).removePrefix("PlayerScore")}"
"${it.key.displayName}${it.value.toString(definition).removePrefix(PlayerScore::class.simpleName.toString())}"
}
}])"

override fun equals(other: Any?): Boolean {
if(this === other) return true
if(other !is GameResult) return false

if(definition != other.definition) return false
if(scores != other.scores) return false
if(win?.winner != other.win?.winner) return false

return true
}

override fun hashCode(): Int {
var result = definition.hashCode()
result = 31 * result + scores.hashCode()
result = 31 * result + (win?.winner?.hashCode() ?: 0)
return result
}
}
7 changes: 7 additions & 0 deletions sdk/src/main/framework/sc/shared/IWinReason.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package sc.shared

interface IWinReason {
val isRegular: Boolean
val message: String

fun getMessage(playerName: String?): String =
message.format(playerName)
}

open class WinReason(override val message: String, override val isRegular: Boolean = true): IWinReason {
override fun equals(other: Any?) = other is IWinReason && other.message == this.message
}

object WinReasonTie: WinReason("Beide Teams sind gleichauf")
23 changes: 5 additions & 18 deletions sdk/src/main/framework/sc/shared/PlayerScore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,12 @@ import java.math.BigDecimal

@XStreamAlias(value = "score")
data class PlayerScore(
@XStreamAsAttribute
val cause: ScoreCause?,
@XStreamAsAttribute
val reason: String,
@XStreamImplicit(itemFieldName = "part")
val parts: Array<BigDecimal>
) {

constructor(winner: Boolean, reason: String):
this(ScoreCause.REGULAR, reason, if (winner) Constants.WIN_SCORE else Constants.LOSE_SCORE)
constructor(cause: ScoreCause?, reason: String, vararg scores: Int):
this(cause, reason, scores.map { BigDecimal(it) }.toTypedArray())
constructor(vararg scores: Int):
this(scores.map { BigDecimal(it) }.toTypedArray())

fun size(): Int = parts.size

Expand All @@ -28,22 +22,15 @@ data class PlayerScore(

override fun equals(other: Any?): Boolean =
other is PlayerScore &&
cause == other.cause &&
reason == other.reason &&
parts.contentEquals(other.parts)

override fun hashCode(): Int {
var result = parts.contentHashCode()
result = 31 * result + cause.hashCode()
result = 31 * result + reason.hashCode()
return result
}
override fun hashCode(): Int = parts.contentHashCode()

fun toString(definition: ScoreDefinition): String {
if(!matches(definition))
throw IllegalArgumentException("$definition does not match $this")
return "PlayerScore(cause=$cause, reason='$reason', parts=[${parts.withIndex().joinToString { "${definition[it.index].name}=${it.value}" }}])"
return "PlayerScore[${parts.withIndex().joinToString { "${definition[it.index].name}=${it.value}" }}]"
}

override fun toString(): String = "PlayerScore(cause=$cause, reason='$reason', parts=${parts.contentToString()})"
override fun toString(): String = "PlayerScore(${parts.contentToString()})"
}
Loading

0 comments on commit 64ce836

Please sign in to comment.