diff --git a/gradle/build.gradle.kts b/gradle/build.gradle.kts index a6fbe0829..ffe9c8585 100644 --- a/gradle/build.gradle.kts +++ b/gradle/build.gradle.kts @@ -285,43 +285,6 @@ allprojects { } } -project("sdk") { - sourceSets { - main.get().java.srcDirs("src/framework", "src/server-api") - test.get().java.srcDir("src/test") - } - - dependencies { - api(kotlin("stdlib")) - api("com.thoughtworks.xstream", "xstream", "1.4.12") - api("jargs", "jargs", "1.0") - api("ch.qos.logback", "logback-classic", "1.2.3") - - implementation("org.hamcrest", "hamcrest-core", "2.2") - implementation("net.sf.kxml", "kxml2", "2.3.0") - implementation("xmlpull", "xmlpull", "1.1.3.1") - - testImplementation("junit", "junit", "4.13") - testImplementation("io.kotlintest", "kotlintest-runner-junit5", "3.4.2") - } -} - -project("plugin") { - sourceSets { - main.get().java.srcDirs("src/client", "src/server", "src/shared") - test.get().java.srcDir("src/test") - } - - dependencies { - api(project(":sdk")) - - testImplementation("junit", "junit", "4.13") - testImplementation("io.kotlintest", "kotlintest-runner-junit5", "3.4.2") - } - - tasks.jar.get().archiveBaseName.set(game) -} - // == Utilities == fun InputStream.dump(name: String? = null) { diff --git a/helpers/test-client/src/sc/TestClient.java b/helpers/test-client/src/sc/TestClient.java index 26325c1c6..5bb6c4356 100644 --- a/helpers/test-client/src/sc/TestClient.java +++ b/helpers/test-client/src/sc/TestClient.java @@ -196,8 +196,9 @@ protected void onObject(ProtocolMessage message) { irregularGames++; StringBuilder log = new StringBuilder("Game {} ended " + (result.isRegular() ? "regularly -" : "abnormally!") + " Winner: "); - for (Player winner : result.getWinners()) - log.append(winner.getDisplayName()).append(", "); + if (result.getWinners() != null) + for (Player winner : result.getWinners()) + log.append(winner.getDisplayName()).append(", "); logger.warn(log.substring(0, log.length() - 2), finishedTests); finishedTests++; diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 3c037a252..2115cabcf 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,6 +1,24 @@ -tasks.withType { - useJUnitPlatform() +val game: String by project + +sourceSets { + main.get().java.srcDirs("src/client", "src/server", "src/shared") + test.get().java.srcDir("src/test") } + dependencies { + api(project(":sdk")) + + testImplementation("junit", "junit", "4.13") + testImplementation("io.kotest", "kotest-runner-junit5-jvm", "4.0.5") + testImplementation("io.kotest", "kotest-assertions-core", "4.0.5") testImplementation(kotlin("script-runtime")) -} \ No newline at end of file +} + +tasks{ + jar { + archiveBaseName.set(game) + } + test { + useJUnitPlatform() + } +} diff --git a/plugin/src/server/sc/plugin2020/GamePlugin.java b/plugin/src/server/sc/plugin2020/GamePlugin.java index 23a31a8af..e273c4e85 100644 --- a/plugin/src/server/sc/plugin2020/GamePlugin.java +++ b/plugin/src/server/sc/plugin2020/GamePlugin.java @@ -18,15 +18,11 @@ public class GamePlugin implements IGamePlugin { public static final String PLUGIN_AUTHOR = ""; public static final String PLUGIN_UUID = "swc_2020_hive"; - public static final ScoreDefinition SCORE_DEFINITION; - - static { - SCORE_DEFINITION = new ScoreDefinition(); - SCORE_DEFINITION.add("Gewinner"); - // NOTE: Always write the XML representation of unicode characters, not the character directly, as it confuses the - // parsers which consume the server messages! - SCORE_DEFINITION.add(new ScoreFragment("\u2205 freie Felder", ScoreAggregation.AVERAGE)); - } + // NOTE: Always write the XML representation of unicode characters, not the character directly, as it confuses the + // parsers which consume the server messages! + public static final ScoreDefinition SCORE_DEFINITION = new ScoreDefinition(new ScoreFragment[]{ + new ScoreFragment("Gewinner"), + new ScoreFragment("\u2205 freie Felder", ScoreAggregation.AVERAGE)}); @Override public IGameInstance createGame() { diff --git a/plugin/src/server/sc/plugin2021/Game.kt b/plugin/src/server/sc/plugin2021/Game.kt index d69fbc563..23e107bc7 100644 --- a/plugin/src/server/sc/plugin2021/Game.kt +++ b/plugin/src/server/sc/plugin2021/Game.kt @@ -42,12 +42,12 @@ class Game(UUID: String = GamePlugin.PLUGIN_UUID): RoundBasedGameInstance { - if (players.first().violated) { - if (players.last().violated) + if (players.first().hasViolated()) { + if (players.last().hasViolated()) return mutableListOf() return players.subList(1, 2) } - if (players.last().violated) + if (players.last().hasViolated()) return players.subList(0, 1) val first = gameState.getPointsForPlayer(players.first().color) diff --git a/plugin/src/server/sc/plugin2021/GamePlugin.kt b/plugin/src/server/sc/plugin2021/GamePlugin.kt index 27d6b04c1..9ac6b7cbf 100644 --- a/plugin/src/server/sc/plugin2021/GamePlugin.kt +++ b/plugin/src/server/sc/plugin2021/GamePlugin.kt @@ -16,10 +16,10 @@ class GamePlugin: IGamePlugin { val PLUGIN_AUTHOR = "" val PLUGIN_UUID = "swc_2021_blokus" - val SCORE_DEFINITION = ScoreDefinition().apply { - add("Gewinner") - add(ScoreFragment("\u2205 Punkte", ScoreAggregation.AVERAGE)) - } + val SCORE_DEFINITION = ScoreDefinition(arrayOf( + ScoreFragment("Gewinner"), + ScoreFragment("\u2205 Punkte", ScoreAggregation.AVERAGE) + )) } override fun createGame(): IGameInstance { diff --git a/plugin/src/shared/sc/plugin2020/Team.kt b/plugin/src/shared/sc/plugin2020/Team.kt index 6ef4b39fb..90fd5bc43 100644 --- a/plugin/src/shared/sc/plugin2020/Team.kt +++ b/plugin/src/shared/sc/plugin2020/Team.kt @@ -4,15 +4,10 @@ import sc.api.plugins.ITeam enum class Team(override val index: Int, val displayName: String): ITeam { RED(0, "Rot") { - val letter = name.first() - override fun opponent(): Team = BLUE - override fun toString(): String = displayName }, BLUE(1, "Blau") { - val letter = name.first() - override fun opponent(): Team = RED - override fun toString(): String = displayName }; + override fun toString(): String = displayName } \ No newline at end of file diff --git a/plugin/src/test/sc/plugin2020/CloneTest.kt b/plugin/src/test/sc/plugin2020/CloneTest.kt index 5ea41ac6d..eb372b609 100644 --- a/plugin/src/test/sc/plugin2020/CloneTest.kt +++ b/plugin/src/test/sc/plugin2020/CloneTest.kt @@ -1,8 +1,8 @@ package sc.plugin2020 -import io.kotlintest.matchers.types.shouldNotBeSameInstanceAs -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.core.spec.style.StringSpec import sc.framework.plugins.Player class CloneTest: StringSpec({ diff --git a/plugin/src/test/sc/plugin2020/GamePlayTest.kt b/plugin/src/test/sc/plugin2020/GamePlayTest.kt index c7ad4e8ad..be06c0bda 100644 --- a/plugin/src/test/sc/plugin2020/GamePlayTest.kt +++ b/plugin/src/test/sc/plugin2020/GamePlayTest.kt @@ -1,9 +1,10 @@ package sc.plugin2020 -import io.kotlintest.matchers.collections.shouldContain -import io.kotlintest.matchers.collections.shouldNotContain -import io.kotlintest.shouldThrow -import io.kotlintest.specs.AnnotationSpec +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec import org.junit.Assert.* import sc.plugin2020.util.Constants import sc.plugin2020.util.CubeCoordinates diff --git a/plugin/src/test/sc/plugin2020/GameRuleTest.kt b/plugin/src/test/sc/plugin2020/GameRuleTest.kt index 19d537a94..209164f5b 100644 --- a/plugin/src/test/sc/plugin2020/GameRuleTest.kt +++ b/plugin/src/test/sc/plugin2020/GameRuleTest.kt @@ -1,9 +1,9 @@ package sc.plugin2020 -import io.kotlintest.matchers.collections.shouldHaveSize -import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec import sc.plugin2020.util.CubeCoordinates import sc.plugin2020.util.GameRuleLogic import sc.api.plugins.ITeam diff --git a/plugin/src/test/sc/plugin2020/XMLTest.kt b/plugin/src/test/sc/plugin2020/XMLTest.kt index 024200f7f..ea4151cf6 100644 --- a/plugin/src/test/sc/plugin2020/XMLTest.kt +++ b/plugin/src/test/sc/plugin2020/XMLTest.kt @@ -283,14 +283,12 @@ class XMLTest { val xstream = Configuration.xStream val xml = xstream.toXML(RoomPacket(roomId, move)) val expect = """ - | - | - | - | RED - | - | - | - |""".trimMargin() + + + + + + """.trimIndent() Assert.assertEquals(expect, xml) } diff --git a/plugin/src/test/sc/plugin2021/BoardTest.kt b/plugin/src/test/sc/plugin2021/BoardTest.kt index 7c0c71280..7a6451f03 100644 --- a/plugin/src/test/sc/plugin2021/BoardTest.kt +++ b/plugin/src/test/sc/plugin2021/BoardTest.kt @@ -1,9 +1,8 @@ package sc.plugin2021 -import io.kotlintest.matchers.types.shouldNotBeSameInstanceAs - -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldNotBeSameInstanceAs import sc.plugin2021.util.Constants class BoardTest : StringSpec({ diff --git a/plugin/src/test/sc/plugin2021/ComparisonTest.kt b/plugin/src/test/sc/plugin2021/ComparisonTest.kt index 4130d6fea..9e1ce7edd 100644 --- a/plugin/src/test/sc/plugin2021/ComparisonTest.kt +++ b/plugin/src/test/sc/plugin2021/ComparisonTest.kt @@ -1,15 +1,13 @@ package sc.plugin2021 -import io.kotlintest.matchers.types.shouldNotBeSameInstanceAs -import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec import sc.plugin2021.util.Constants class ComparisonTest: StringSpec({ "Coordinate comparison" { Coordinates(3, 2) shouldBe Coordinates(3, 2) - Coordinates(3, 2) shouldNotBeSameInstanceAs Coordinates(3, 2) Coordinates(3, 2) shouldNotBe Coordinates(2, 3) Coordinates(3, 2) shouldNotBe Board() } diff --git a/plugin/src/test/sc/plugin2021/GameRuleLogicTest.kt b/plugin/src/test/sc/plugin2021/GameRuleLogicTest.kt index 362f73dad..cb407b4c1 100644 --- a/plugin/src/test/sc/plugin2021/GameRuleLogicTest.kt +++ b/plugin/src/test/sc/plugin2021/GameRuleLogicTest.kt @@ -1,9 +1,9 @@ package sc.plugin2021 -import io.kotlintest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import sc.plugin2021.util.Constants diff --git a/plugin/src/test/sc/plugin2021/GameStateTest.kt b/plugin/src/test/sc/plugin2021/GameStateTest.kt index 2c7975b2e..7566eee30 100644 --- a/plugin/src/test/sc/plugin2021/GameStateTest.kt +++ b/plugin/src/test/sc/plugin2021/GameStateTest.kt @@ -1,7 +1,7 @@ package sc.plugin2021 -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import sc.plugin2021.util.Configuration diff --git a/plugin/src/test/sc/plugin2021/GameTest.kt b/plugin/src/test/sc/plugin2021/GameTest.kt index 602c62bec..55fec1a3a 100644 --- a/plugin/src/test/sc/plugin2021/GameTest.kt +++ b/plugin/src/test/sc/plugin2021/GameTest.kt @@ -1,13 +1,11 @@ package sc.plugin2021 -import io.kotlintest.matchers.collections.shouldContainExactly -import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe -import io.kotlintest.specs.StringSpec -import org.junit.jupiter.api.assertThrows +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactly import sc.plugin2020.util.Constants import sc.plugin2021.util.GameRuleLogic -import sc.shared.InvalidMoveException import sc.shared.PlayerScore import sc.shared.ScoreCause diff --git a/plugin/src/test/sc/plugin2021/PieceTest.kt b/plugin/src/test/sc/plugin2021/PieceTest.kt index d62c8a744..6de9dcf83 100644 --- a/plugin/src/test/sc/plugin2021/PieceTest.kt +++ b/plugin/src/test/sc/plugin2021/PieceTest.kt @@ -1,11 +1,11 @@ package sc.plugin2021 -import io.kotlintest.data.forall -import io.kotlintest.matchers.maps.shouldContain -import io.kotlintest.matchers.maps.shouldContainExactly -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec -import io.kotlintest.tables.row +import io.kotest.core.spec.style.StringSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.maps.shouldContain +import io.kotest.matchers.maps.shouldContainExactly +import io.kotest.matchers.shouldBe import org.opentest4j.AssertionFailedError import sc.plugin2021.util.* @@ -164,7 +164,7 @@ class PieceTest: StringSpec({ piece.coordinates shouldBe coordinates } "XML conversion" { - forall( + forAll( row(Piece(Color.YELLOW, PieceShape.TETRO_O, Rotation.RIGHT, false), """ diff --git a/plugin/src/test/sc/plugin2021/RotationTest.kt b/plugin/src/test/sc/plugin2021/RotationTest.kt index f5d017f1c..fed26ae13 100644 --- a/plugin/src/test/sc/plugin2021/RotationTest.kt +++ b/plugin/src/test/sc/plugin2021/RotationTest.kt @@ -1,7 +1,7 @@ package sc.plugin2021 -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec class RotationTest: StringSpec ({ "Rotations can get rotated" { diff --git a/plugin/src/test/sc/plugin2021/helper/MoveParser.kt b/plugin/src/test/sc/plugin2021/helper/MoveParser.kt index 1600f9d15..b8876f8ce 100644 --- a/plugin/src/test/sc/plugin2021/helper/MoveParser.kt +++ b/plugin/src/test/sc/plugin2021/helper/MoveParser.kt @@ -1,6 +1,6 @@ package sc.plugin2021.helper -import io.kotlintest.shouldBe +import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import sc.plugin2021.* diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts new file mode 100644 index 000000000..4766128d0 --- /dev/null +++ b/sdk/build.gradle.kts @@ -0,0 +1,25 @@ +sourceSets { + main.get().java.srcDirs("src/framework", "src/server-api") + test.get().java.srcDir("src/test") +} + +dependencies { + api(kotlin("stdlib")) + api("com.thoughtworks.xstream", "xstream", "1.4.11.1") + api("jargs", "jargs", "1.0") + api("ch.qos.logback", "logback-classic", "1.2.3") + + implementation("org.hamcrest", "hamcrest-core", "2.2") + implementation("net.sf.kxml", "kxml2", "2.3.0") + implementation("xmlpull", "xmlpull", "1.1.3.1") + + testImplementation("junit", "junit", "4.13") + testImplementation("io.kotest", "kotest-runner-junit5-jvm", "4.0.5") + testImplementation("io.kotest", "kotest-assertions-core", "4.0.5") +} + +tasks{ + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/sdk/src/framework/sc/helpers/HelperMethods.java b/sdk/src/framework/sc/helpers/HelperMethods.java index 7746243c0..a86850b71 100644 --- a/sdk/src/framework/sc/helpers/HelperMethods.java +++ b/sdk/src/framework/sc/helpers/HelperMethods.java @@ -37,7 +37,7 @@ public static String getCurrentDateTime() { public static String generateReplayFilename(String pluginUuid, List descriptors) { StringBuilder replayFileName = new StringBuilder("./replays/replay"); replayFileName.append("_"); - replayFileName.append(pluginUuid); // something like hui_2018 + replayFileName.append(pluginUuid); for (SlotDescriptor descriptor : descriptors) { replayFileName.append("_"); replayFileName.append(descriptor.getDisplayName().replace(' ', '_')); diff --git a/sdk/src/framework/sc/shared/GameResult.java b/sdk/src/framework/sc/shared/GameResult.java index ae70f1dbd..e69de29bb 100644 --- a/sdk/src/framework/sc/shared/GameResult.java +++ b/sdk/src/framework/sc/shared/GameResult.java @@ -1,75 +0,0 @@ -package sc.shared; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamImplicit; -import sc.framework.plugins.Player; -import sc.protocol.responses.ProtocolMessage; - -import java.util.ArrayList; -import java.util.List; - -@XStreamAlias(value = "result") -public class GameResult implements ProtocolMessage { - private final ScoreDefinition definition; - - @XStreamImplicit(itemFieldName = "score") - private final List scores; - - @XStreamImplicit(itemFieldName = "winner") - private List winners; - - /** might be needed by XStream */ - public GameResult() { - definition = null; - scores = null; - winners = null; - } - - public GameResult(ScoreDefinition definition, List scores, List winners) { - this.definition = definition; - this.scores = scores; - this.winners = winners; - } - - public ScoreDefinition getDefinition() { - return this.definition; - } - - public List getScores() { - return this.scores; - } - - public List getWinners() { - if (this.winners == null) - this.winners = new ArrayList<>(2); - return this.winners; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("Winner: ").append(winners); - int playerIndex = 0; - for (PlayerScore score : this.scores) { - builder.append("\n").append("Player ").append(playerIndex).append(": "); - String[] scoreParts = score.toStrings(); - for (int i = 0; i < scoreParts.length; i++) { - builder.append(this.definition.get(i).getName()).append("=").append(scoreParts[i]); - if (i + 1 < scoreParts.length) - builder.append("; "); - } - playerIndex++; - } - - return builder.toString(); - } - - public boolean isRegular() { - for (PlayerScore score : this.scores) { - if (score.getCause() != ScoreCause.REGULAR) { - return false; - } - } - return true; - } - -} diff --git a/sdk/src/framework/sc/shared/GameResult.kt b/sdk/src/framework/sc/shared/GameResult.kt new file mode 100644 index 000000000..bdf39c231 --- /dev/null +++ b/sdk/src/framework/sc/shared/GameResult.kt @@ -0,0 +1,37 @@ +package sc.shared + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamImplicit +import sc.framework.plugins.Player +import sc.protocol.responses.ProtocolMessage + +@XStreamAlias(value = "result") +data class GameResult( + val definition: ScoreDefinition, + @XStreamImplicit(itemFieldName = "score") + val scores: List, + @XStreamImplicit(itemFieldName = "winner") + val winners: List? +): ProtocolMessage { + + val isRegular: Boolean + get() = scores.all { it.cause == ScoreCause.REGULAR } + + override fun toString() = + "GameResult(winner=$winners, scores=[${scores.withIndex().joinToString { "Player${it.index + 1}${it.value.toString(definition).removePrefix("PlayerScore")}" }}])" + + override fun equals(other: Any?) = + other is GameResult && + definition == other.definition && + scores == other.scores && + (winners == other.winners || + (winners.isNullOrEmpty() && other.winners.isNullOrEmpty())) + + override fun hashCode(): Int { + var result = definition.hashCode() + result = 31 * result + scores.hashCode() + result = 31 * result + (winners ?: emptyList()).hashCode() + return result + } + +} \ No newline at end of file diff --git a/sdk/src/framework/sc/shared/PlayerScore.java b/sdk/src/framework/sc/shared/PlayerScore.java index cea7672db..e69de29bb 100644 --- a/sdk/src/framework/sc/shared/PlayerScore.java +++ b/sdk/src/framework/sc/shared/PlayerScore.java @@ -1,117 +0,0 @@ -package sc.shared; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import com.thoughtworks.xstream.annotations.XStreamImplicit; -import sc.helpers.CollectionHelper; - -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -@XStreamAlias(value = "score") -public final class PlayerScore { - @XStreamImplicit(itemFieldName = "part") - private final List parts; - - @XStreamAsAttribute - private ScoreCause cause; - - @XStreamAsAttribute - private String reason; - - /** might be needed by XStream */ - public PlayerScore() { - parts = null; - } - - public PlayerScore(boolean winner, String reason) { - this(ScoreCause.REGULAR, reason, 1); - } - - public PlayerScore(ScoreCause cause, String reason, Integer... scores) { - this(cause, reason, CollectionHelper.iterableToCollection( - CollectionHelper.intArrayToBigDecimalArray(scores)).toArray( - new BigDecimal[scores.length])); - } - - public PlayerScore(ScoreCause cause, String reason, BigDecimal... parts) { - if (parts == null) { - throw new IllegalArgumentException("scores must not be null"); - } - - this.parts = Arrays.asList(parts); - this.cause = cause; - this.reason = reason; - } - - public int size() { - return this.parts.size(); - } - - public ScoreCause getCause() { - return this.cause; - } - - public String getReason() { - return this.reason; - } - - public String[] toStrings() { - return CollectionHelper.iterableToCollection(CollectionHelper.map(parts, val -> val.toString())).toArray(new String[parts.size()]); - } - - public String toString() { - StringBuilder result = new StringBuilder(); - String[] strings = this.toStrings(); - for (int i = 0; i < strings.length; i++) { - if (i > 0) result.append("; "); - result.append(strings[i]); - } - return result.toString(); - } - - public void setCause(ScoreCause cause) { - this.cause = cause; - } - - public void setReason(String reason) { - this.reason = reason; - } - - public List getValues() { - return Collections.unmodifiableList(parts); - } - - public void setValueAt(int index, BigDecimal v) { - parts.set(index, v); - } - - public boolean matches(ScoreDefinition definition) { - return size() == definition.size(); - } - - @Override - public boolean equals(Object eq) { - if (eq instanceof PlayerScore) { - PlayerScore score = (PlayerScore) eq; - if (!this.getCause().equals(score.getCause()) || - !(this.getValues().size() == score.getValues().size())) { - return false; - } - if (!this.getReason().equals(score.getReason())) { // may be null - return false; - } - for (int i = 0; i < this.parts.size(); i++) { - if (!this.getValues().get(i).equals(score.getValues().get(i))) { - return false; - } - } - return true; - } else { - return false; - } - } - -} diff --git a/sdk/src/framework/sc/shared/PlayerScore.kt b/sdk/src/framework/sc/shared/PlayerScore.kt new file mode 100644 index 000000000..e9d797d17 --- /dev/null +++ b/sdk/src/framework/sc/shared/PlayerScore.kt @@ -0,0 +1,50 @@ +package sc.shared + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamAsAttribute +import com.thoughtworks.xstream.annotations.XStreamImplicit +import java.lang.IllegalArgumentException +import java.math.BigDecimal + +@XStreamAlias(value = "score") +data class PlayerScore( + @XStreamAsAttribute + val cause: ScoreCause?, + @XStreamAsAttribute + val reason: String, + @XStreamImplicit(itemFieldName = "part") + val parts: Array +) { + + constructor(winner: Boolean, reason: String): this(ScoreCause.REGULAR, reason, if (winner) 2 else 0) + constructor(cause: ScoreCause?, reason: String, vararg scores: Int): this(cause, reason, scores.map { BigDecimal(it) }.toTypedArray()) + + fun size(): Int = parts.size + + val values: List + get() = parts.asList() + + fun matches(definition: ScoreDefinition): Boolean = + size() == definition.size + + 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 + } + + 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}" }}])" + } + + override fun toString(): String = "PlayerScore(cause=$cause, reason='$reason', parts=${parts.contentToString()})" +} \ No newline at end of file diff --git a/sdk/src/framework/sc/shared/Score.java b/sdk/src/framework/sc/shared/Score.java index d8d0bfcba..d89123c9d 100644 --- a/sdk/src/framework/sc/shared/Score.java +++ b/sdk/src/framework/sc/shared/Score.java @@ -43,11 +43,7 @@ public int getNumberOfTests() { } public ScoreDefinition getScoreDefinition() { - ScoreDefinition scoreDefinition = new ScoreDefinition(); - for (ScoreValue scoreValue : this) { - scoreDefinition.add(scoreValue.getFragment()); - } - return scoreDefinition; + return new ScoreDefinition((ScoreFragment[]) scoreValues.stream().map(ScoreValue::getFragment).toArray()); } public List getScoreValues() { diff --git a/sdk/src/framework/sc/shared/ScoreDefinition.java b/sdk/src/framework/sc/shared/ScoreDefinition.java deleted file mode 100644 index a7954d488..000000000 --- a/sdk/src/framework/sc/shared/ScoreDefinition.java +++ /dev/null @@ -1,55 +0,0 @@ -package sc.shared; - -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamImplicit; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -@XStreamAlias(value = "scoreDefinition") -public class ScoreDefinition implements Iterable { - @XStreamImplicit(itemFieldName = "fragment") - private List fragments = new ArrayList<>(); - - public void add(String name) { - this.fragments.add(new ScoreFragment(name)); - } - - public void add(ScoreFragment fragment) { - this.fragments.add(fragment); - } - - public int size() { - return this.fragments.size(); - } - - public boolean isValid() { - return size() > 0; - } - - public ScoreFragment get(int i) { - return this.fragments.get(i); - } - - @Override - public Iterator iterator() { - return this.fragments.iterator(); - } - - @Override - public boolean equals(Object o) { - if (o instanceof ScoreDefinition) { - int i = 0; - for (ScoreFragment fragment : (ScoreDefinition) o) { - if (!this.fragments.get(i).equals(fragment)) { - return false; - } - } - } else { - return false; - } - return true; - } - -} diff --git a/sdk/src/framework/sc/shared/ScoreDefinition.kt b/sdk/src/framework/sc/shared/ScoreDefinition.kt new file mode 100644 index 000000000..d1c6b14e7 --- /dev/null +++ b/sdk/src/framework/sc/shared/ScoreDefinition.kt @@ -0,0 +1,35 @@ +package sc.shared + +import com.thoughtworks.xstream.annotations.XStreamAlias +import com.thoughtworks.xstream.annotations.XStreamImplicit + +@XStreamAlias(value = "scoreDefinition") +class ScoreDefinition( + @XStreamImplicit(itemFieldName = "fragment") + private val fragments: Array +): Iterable, RandomAccess { + + constructor(vararg fragments: String): this(Array(fragments.size) { ScoreFragment(fragments[it]) }) + + val size + get() = fragments.size + + val isValid + get() = size > 0 + + operator fun get(index: Int) = + fragments[index] + + override fun iterator() = + fragments.iterator() + + override fun toString() = + "ScoreDefinition[${fragments.joinToString()}]" + + override fun equals(other: Any?): Boolean = + other is ScoreDefinition && fragments.contentEquals(other.fragments) + + override fun hashCode() = + fragments.contentHashCode() + +} \ No newline at end of file diff --git a/sdk/src/server-api/sc/framework/plugins/Player.kt b/sdk/src/server-api/sc/framework/plugins/Player.kt index de816a412..f8b265bea 100644 --- a/sdk/src/server-api/sc/framework/plugins/Player.kt +++ b/sdk/src/server-api/sc/framework/plugins/Player.kt @@ -12,28 +12,30 @@ import java.util.* private val logger = LoggerFactory.getLogger(Player::class.java) +/** + * Keeps information about a player: + * - basic info: name and color + * - state info: if they can time out, whether their game is paused + * - game result info: left & timeouts, to determine the winner and potential violation information + * - listeners: subscribers that get notified about new messages concerning this player, notably Welcome and Errors + * + * Note: the toString/equals/hashCode/clone methods only take [color] and [displayName] into account! + */ +// TODO split this beast up! @XStreamAlias(value = "player") open class Player @JvmOverloads constructor( @XStreamAsAttribute var color: ITeam, - @XStreamAsAttribute var displayName: String = "") : Cloneable { + @XStreamAsAttribute var displayName: String = "" +): Cloneable { - public override fun clone() = Player(color, displayName) - - override fun equals(other: Any?) = other is Player && other.color == color && other.displayName == displayName - @XStreamOmitField protected var listeners: MutableList = ArrayList() @XStreamOmitField - var isCanTimeout: Boolean = false + var canTimeout: Boolean = false @XStreamOmitField - var isShouldBePaused: Boolean = false - - @XStreamOmitField - var violated = false - - fun hasViolated() = violated + var shouldBePaused: Boolean = false @XStreamOmitField var left = false @@ -49,7 +51,9 @@ open class Player @JvmOverloads constructor( var hardTimeout = false fun hasHardTimeout() = hardTimeout - + + fun hasViolated() = violationReason != null + @XStreamOmitField var violationReason: String? = null @@ -74,5 +78,15 @@ open class Player @JvmOverloads constructor( } override fun toString(): String = "Player %s(%s)".format(color, displayName) - + + public override fun clone() = Player(color, displayName) + + override fun equals(other: Any?) = other is Player && other.color == color && other.displayName == displayName + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + displayName.hashCode() + return result + } + } diff --git a/sdk/src/server-api/sc/framework/plugins/RoundBasedGameInstance.java b/sdk/src/server-api/sc/framework/plugins/RoundBasedGameInstance.java index 035b6b048..877a96582 100644 --- a/sdk/src/server-api/sc/framework/plugins/RoundBasedGameInstance.java +++ b/sdk/src/server-api/sc/framework/plugins/RoundBasedGameInstance.java @@ -129,14 +129,14 @@ public void onPlayerLeft(Player player) { * * @param player the player that left. * @param cause the cause for the leave. If none is provided, then it will either be {@link ScoreCause#RULE_VIOLATION} - * or {@link ScoreCause#LEFT}, depending on whether the player has {@link Player#getViolated()} + * or {@link ScoreCause#LEFT}, depending on whether the player has {@link Player#hasViolated()} */ public void onPlayerLeft(Player player, ScoreCause cause) { if(cause == ScoreCause.REGULAR) return; if (cause == null) { - if (!player.getViolated()) { + if (!player.hasViolated()) { player.setLeft(true); cause = ScoreCause.LEFT; } else { @@ -149,7 +149,7 @@ public void onPlayerLeft(Player player, ScoreCause cause) { for (Entry entry : scores.entrySet()) { if (entry.getKey() == player) { PlayerScore score = entry.getValue(); - score.setCause(cause); + entry.setValue(new PlayerScore(cause, score.getReason(), score.getParts())); } } @@ -199,7 +199,7 @@ protected final void notifyActivePlayer() { * @param player player to make a move */ protected synchronized final void requestMove(P player) { - final ActionTimeout timeout = player.isCanTimeout() ? getTimeoutFor(player) + final ActionTimeout timeout = player.getCanTimeout() ? getTimeoutFor(player) : new ActionTimeout(false); final Logger logger = RoundBasedGameInstance.logger; @@ -311,10 +311,9 @@ protected void notifyOnNewState(IGameState mementoState, boolean observersOnly) * @throws InvalidMoveException Always thrown */ public void catchInvalidMove(InvalidMoveException e, Player author) throws InvalidMoveException { - author.setViolated(true); String err = "Ungueltiger Zug von '" + author.getDisplayName() + "'.\n" + e.getMessage(); - author.setViolationReason(e.getMessage()); logger.error(err, e); + author.setViolationReason(e.getMessage()); author.notifyListeners(new ProtocolErrorMessage(e.move, err)); throw e; } diff --git a/sdk/src/server-api/sc/networking/clients/ControllingClient.java b/sdk/src/server-api/sc/networking/clients/ControllingClient.java index 201d7b6d7..4498e4835 100644 --- a/sdk/src/server-api/sc/networking/clients/ControllingClient.java +++ b/sdk/src/server-api/sc/networking/clients/ControllingClient.java @@ -17,9 +17,8 @@ public class ControllingClient extends ObservingClient implements IAdministrativ private boolean pauseHitReceived; public ControllingClient(LobbyClient client, String roomId) { - super(client, roomId); + super(roomId); this.client = client; - client.addListener((IAdministrativeListener) this); } @Override diff --git a/sdk/src/server-api/sc/networking/clients/LobbyClient.java b/sdk/src/server-api/sc/networking/clients/LobbyClient.java index 2c3e30a6e..8b683acbb 100644 --- a/sdk/src/server-api/sc/networking/clients/LobbyClient.java +++ b/sdk/src/server-api/sc/networking/clients/LobbyClient.java @@ -319,23 +319,25 @@ public void removeListener(ILobbyClientListener listener) { public IControllableGame observeAndControl(PrepareGameProtocolMessage handle) { String roomId = handle.getRoomId(); - IControllableGame result = new ControllingClient(this, roomId); + ControllingClient controller = new ControllingClient(this, roomId); + addListener((IAdministrativeListener) controller); start(); logger.debug("Sending observation request for roomId: {}", roomId); send(new ObservationRequest(roomId)); - result.pause(); - return result; + controller.pause(); + return controller; } public IControllableGame observe(PrepareGameProtocolMessage handle) { return observe(handle.getRoomId()); } - public IControllableGame observe(String roomId) { - IControllableGame result = new ObservingClient(this, roomId); + public ObservingClient observe(String roomId) { + ObservingClient observer = new ObservingClient(roomId); + addListener(observer); start(); send(new ObservationRequest(roomId)); - return result; + return observer; } @Override diff --git a/sdk/src/server-api/sc/networking/clients/ObservingClient.java b/sdk/src/server-api/sc/networking/clients/ObservingClient.java index 8a4588407..71fe33252 100644 --- a/sdk/src/server-api/sc/networking/clients/ObservingClient.java +++ b/sdk/src/server-api/sc/networking/clients/ObservingClient.java @@ -13,15 +13,11 @@ public class ObservingClient implements IControllableGame, IHistoryListener { - public ObservingClient(IPollsHistory client, String roomId) { - this.poller = client; + public ObservingClient(String roomId) { this.roomId = roomId; - this.poller.addListener(this); this.replay = false; } - protected final IPollsHistory poller; - public final String roomId; private static final Logger logger = LoggerFactory.getLogger(ObservingClient.class); diff --git a/sdk/src/server-api/sc/networking/clients/XStreamClient.java b/sdk/src/server-api/sc/networking/clients/XStreamClient.java index f0feddea7..f0241d82b 100644 --- a/sdk/src/server-api/sc/networking/clients/XStreamClient.java +++ b/sdk/src/server-api/sc/networking/clients/XStreamClient.java @@ -50,7 +50,7 @@ public boolean isReady() { return ready; } - /** Signals that client can receive and send */ + /** Signals that client can receive and send. */ public void start() { synchronized(readyLock) { if (!ready) { diff --git a/sdk/src/server-api/sc/protocol/requests/PrepareGameRequest.kt b/sdk/src/server-api/sc/protocol/requests/PrepareGameRequest.kt index 119fbc3f4..a1d3f0fd2 100644 --- a/sdk/src/server-api/sc/protocol/requests/PrepareGameRequest.kt +++ b/sdk/src/server-api/sc/protocol/requests/PrepareGameRequest.kt @@ -4,21 +4,21 @@ import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.shared.SlotDescriptor -import java.util.* +/** Request to prepare a game of [gameType] with two reserved slots according to [slotDescriptors]. */ @XStreamAlias("prepare") class PrepareGameRequest( @XStreamAsAttribute val gameType: String, @XStreamImplicit(itemFieldName = "slot") val slotDescriptors: Array -): ILobbyRequest { +): AdminLobbyRequest { /** * Create a prepared game with descriptors for each player. * The player descriptors default to "Player1" and "Player2". * - * @param gameType name of the Game as String + * @param gameType type of the game (plugin id) * @param descriptor1 descriptor for Player 1 * @param descriptor2 descriptor for Player 2 */ diff --git a/sdk/src/server-api/sc/protocol/responses/GameRoomMessage.kt b/sdk/src/server-api/sc/protocol/responses/GameRoomMessage.kt index ef1076025..0cc8a48bf 100644 --- a/sdk/src/server-api/sc/protocol/responses/GameRoomMessage.kt +++ b/sdk/src/server-api/sc/protocol/responses/GameRoomMessage.kt @@ -7,7 +7,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute @XStreamAlias(value = "joinedGameRoom") data class GameRoomMessage( @XStreamAsAttribute - private val roomId: String, + val roomId: String, @XStreamAsAttribute - private val existing: Boolean + val existing: Boolean ): ProtocolMessage diff --git a/sdk/src/server-api/sc/protocol/responses/PrepareGameProtocolMessage.kt b/sdk/src/server-api/sc/protocol/responses/PrepareGameProtocolMessage.kt index 3e9fbeb74..1b99ee4fc 100644 --- a/sdk/src/server-api/sc/protocol/responses/PrepareGameProtocolMessage.kt +++ b/sdk/src/server-api/sc/protocol/responses/PrepareGameProtocolMessage.kt @@ -4,7 +4,8 @@ import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamAsAttribute import com.thoughtworks.xstream.annotations.XStreamImplicit -/** Response to PrepareGameRequest. */ +/** Response to [sc.protocol.requests.PrepareGameRequest]. + * @param reservations the reservations for the reserved slots */ @XStreamAlias(value = "prepared") data class PrepareGameProtocolMessage( @XStreamAsAttribute diff --git a/sdk/src/test/sc/api/plugins/TestTeam.kt b/sdk/src/test/sc/api/plugins/TestTeam.kt new file mode 100644 index 000000000..04a592d7f --- /dev/null +++ b/sdk/src/test/sc/api/plugins/TestTeam.kt @@ -0,0 +1,7 @@ +package sc.api.plugins + +enum class TestTeam : ITeam { + BLUE; + override val index = 0 + override fun opponent() = this +} \ No newline at end of file diff --git a/sdk/src/test/sc/shared/GameResultTest.kt b/sdk/src/test/sc/shared/GameResultTest.kt new file mode 100644 index 000000000..b6c4e25d4 --- /dev/null +++ b/sdk/src/test/sc/shared/GameResultTest.kt @@ -0,0 +1,80 @@ +package sc.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import sc.api.plugins.TestTeam +import sc.framework.plugins.Player + +class GameResultTest: StringSpec({ + val definition = ScoreDefinition("winner") + val scoreRegular = PlayerScore(ScoreCause.REGULAR, "", 1) + val scores = listOf(scoreRegular, PlayerScore(ScoreCause.LEFT, "Player left", 0)) + val winners = listOf(Player(TestTeam.BLUE, "bluez")) + + "PlayerScore toString with ScoreDefinition" { + scoreRegular.toString(definition) shouldContain "winner=1" + val definition2 = ScoreDefinition("winner", "test") + shouldThrow { scoreRegular.toString(definition2) } + } + + val gameResultWinners = GameResult(definition, scores, winners) + val gameResultWinnersEmpty = GameResult(definition, scores, emptyList()) + val gameResultWinnersNull = GameResult(definition, scores, null) + "equality" { + gameResultWinners shouldNotBe gameResultWinnersEmpty + gameResultWinnersEmpty shouldBe gameResultWinnersNull + gameResultWinnersEmpty.hashCode() shouldBe gameResultWinnersNull.hashCode() + } + "GameResult XML".config(enabled = false) { + // FIXME needs https://github.com/CAU-Kiel-Tech-Inf/backend/issues/295 + val xstream = getXStream() + + val gameResultXMLWinner = """ + + + + SUM + true + + + + 1 + + + 0 + + + """.trimIndent() + val gameResultXMLNoWinner = """ + + + + SUM + true + + + + 1 + + + 0 + + """.trimIndent() + forAll( + row(gameResultWinners, gameResultXMLWinner), + row(gameResultWinnersEmpty, gameResultXMLNoWinner), + row(gameResultWinnersNull, gameResultXMLNoWinner) + ) + { result, xml -> + val toXML = xstream.toXML(result) + toXML shouldBe xml + xstream.fromXML(xml) shouldBe result + xstream.fromXML(toXML) shouldBe result + } + } +}) diff --git a/sdk/src/test/sc/shared/PlayerScoreTest.kt b/sdk/src/test/sc/shared/PlayerScoreTest.kt index ed2d8684f..58c0c2204 100644 --- a/sdk/src/test/sc/shared/PlayerScoreTest.kt +++ b/sdk/src/test/sc/shared/PlayerScoreTest.kt @@ -1,8 +1,9 @@ package sc.shared -import io.kotlintest.shouldBe -import io.kotlintest.shouldNotBe -import io.kotlintest.specs.StringSpec +import com.thoughtworks.xstream.XStream +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec class PlayerScoreTest: StringSpec({ "check equality" { @@ -16,9 +17,12 @@ class PlayerScoreTest: StringSpec({ playerScoreUnknown1 shouldNotBe playerScoreScores playerScoreScores shouldBe playerScoreScores } - "XML Serialization" { + "convert XML" { val playerScore = PlayerScore(ScoreCause.REGULAR, "Game ended regularly", 0, 1, 2) - val xstream = getXStream() + val xstream = XStream().apply { + setMode(XStream.NO_REFERENCES) + autodetectAnnotations(true) + } val playerScoreXML = """ 0 diff --git a/sdk/src/test/sc/shared/SlotDescriptorTest.kt b/sdk/src/test/sc/shared/SlotDescriptorTest.kt index eca5d1132..6a7dbca25 100644 --- a/sdk/src/test/sc/shared/SlotDescriptorTest.kt +++ b/sdk/src/test/sc/shared/SlotDescriptorTest.kt @@ -1,11 +1,10 @@ package sc.shared import com.thoughtworks.xstream.XStream -import io.kotlintest.data.forall -import io.kotlintest.specs.StringSpec -import io.kotlintest.inspectors.forAll -import io.kotlintest.shouldBe -import io.kotlintest.tables.row +import io.kotest.core.spec.style.StringSpec +import io.kotest.data.row +import io.kotest.matchers.shouldBe +import io.kotest.data.forAll class SlotDescriptorTest : StringSpec({ "convert XML" { @@ -13,7 +12,7 @@ class SlotDescriptorTest : StringSpec({ setMode(XStream.NO_REFERENCES) autodetectAnnotations(true) } - forall( + forAll( row(SlotDescriptor(), """"""), diff --git a/sdk/src/test/sc/shared/WelcomeMessageTest.kt b/sdk/src/test/sc/shared/WelcomeMessageTest.kt index a436ff4d3..057f9a74f 100644 --- a/sdk/src/test/sc/shared/WelcomeMessageTest.kt +++ b/sdk/src/test/sc/shared/WelcomeMessageTest.kt @@ -1,7 +1,7 @@ package sc.shared -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe import sc.api.plugins.ITeam class WelcomeMessageTest: StringSpec({ diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 5025d07d2..8a48823e5 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,4 +1,3 @@ -import java.util.Scanner import sc.gradle.ScriptsTask plugins { @@ -20,7 +19,9 @@ dependencies { implementation(project(":sdk")) implementation(project(":plugin")) - testImplementation("junit", "junit", "4.12") + testImplementation("junit", "junit", "4.13") + testImplementation("io.kotest", "kotest-runner-junit5-jvm", "4.0.5") + testImplementation("io.kotest", "kotest-assertions-core", "4.0.5") } val deployDir: File by project diff --git a/server/src/sc/server/Lobby.kt b/server/src/sc/server/Lobby.kt index 16f68abd8..7669c00af 100644 --- a/server/src/sc/server/Lobby.kt +++ b/server/src/sc/server/Lobby.kt @@ -22,22 +22,19 @@ import java.io.IOException * The lobby will help clients find an open game or create new games to play with * another client. */ -class Lobby : IClientListener { +class Lobby: GameRoomManager(), IClientListener { private val logger = LoggerFactory.getLogger(Lobby::class.java) - - val gameManager: GameRoomManager = GameRoomManager() - val clientManager: ClientManager = ClientManager(this) - - /** - * Starts the ClientManager in it's own daemon thread. This method should be used only once. - * ClientManager starts clientListener. - * clientListener starts SocketListener on defined port to watch for new connecting clients. - */ + + val clientManager = ClientManager().also { + it.setOnClientConnected(this::onClientConnected) + } + + /** @see ClientManager.start */ @Throws(IOException::class) fun start() { this.clientManager.start() } - + /** * Add lobby as listener to client. * Prepare client for send and receive. @@ -48,13 +45,13 @@ class Lobby : IClientListener { client.addClientListener(this) client.start() } - + override fun onClientDisconnected(source: Client) { logger.info("{} disconnected.", source) source.removeClientListener(this) } - - /** handle requests or moves of clients */ + + /** Handle requests or moves of clients. */ @Throws(RescuableClientException::class, InvalidGameStateException::class) override fun onRequest(source: Client, callback: PacketCallback) { val packet = callback.packet @@ -62,90 +59,89 @@ class Lobby : IClientListener { when (packet) { is JoinPreparedRoomRequest -> ReservationManager.redeemReservationCode(source, packet.reservationCode) is JoinRoomRequest -> { - val gameRoomMessage = this.gameManager.joinOrCreateGame(source, packet.gameType) + val gameRoomMessage = this.joinOrCreateGame(source, packet.gameType) // null is returned if join was unsuccessful - if (gameRoomMessage != null) { - for (admin in clientManager.clients) { - if (admin.isAdministrator) { - admin.send(gameRoomMessage) - } - } + if(gameRoomMessage != null) { + clientManager.clients + .filter { it.isAdministrator } + .forEach { it.send(gameRoomMessage) } } } - is AuthenticateRequest -> source.authenticate(packet.password) - is PrepareGameRequest -> if (source.isAdministrator) { - source.send(this.gameManager.prepareGame(packet)) - } - is FreeReservationRequest -> if (source.isAdministrator) { - ReservationManager.freeReservation(packet.reservation) - } is RoomPacket -> { // i.e. new move - val room = this.gameManager.findRoom(packet.roomId) + val room = this.findRoom(packet.roomId) room.onEvent(source, packet.data) } - is ObservationRequest -> if (source.isAdministrator) { - val room = this.gameManager.findRoom(packet.roomId) - room.addObserver(source) - } - is PauseGameRequest -> if (source.isAdministrator) { - try { - val room = this.gameManager.findRoom(packet.roomId) - room.pause(packet.pause) - } catch (e: RescuableClientException) { - this.logger.error("Got exception on pause: {}", e) + is AuthenticateRequest -> source.authenticate(packet.password) + + is AdminLobbyRequest -> if (source.isAdministrator) when (packet) { + is PrepareGameRequest -> { + source.send(this.prepareGame(packet)) } - - } - is ControlTimeoutRequest -> if (source.isAdministrator) { - val room = this.gameManager.findRoom(packet.roomId) - val slot = room.slots[packet.slot] - slot.role.player.isCanTimeout = packet.activate - - } - is StepRequest -> // It is not checked whether there is a prior pending StepRequest - if (source.isAdministrator) { - val room = this.gameManager.findRoom(packet.roomId) + is FreeReservationRequest -> { + ReservationManager.freeReservation(packet.reservation) + } + is ControlTimeoutRequest -> { + val room = this.findRoom(packet.roomId) + val slot = room.slots[packet.slot] + slot.role.player.canTimeout = packet.activate + } + is ObservationRequest -> { + val room = this.findRoom(packet.roomId) + room.addObserver(source) + } + is PauseGameRequest -> { + try { + val room = this.findRoom(packet.roomId) + room.pause(packet.pause) + } catch (e: RescuableClientException) { + this.logger.error("Got exception on pause: {}", e) + } + } + is StepRequest -> { + // TODO check for a prior pending StepRequest + val room = this.findRoom(packet.roomId) room.step(packet.forced) } - is CancelRequest -> if (source.isAdministrator) { - val room = this.gameManager.findRoom(packet.roomId) - room.cancel() - // TODO check whether all clients receive game over message - this.gameManager.games.remove(room) - } - is TestModeRequest -> if (source.isAdministrator) { - val testMode = packet.testMode - logger.info("Test mode is set to {}", testMode) - Configuration.set(Configuration.TEST_MODE, java.lang.Boolean.toString(testMode)) - source.send(TestModeMessage(testMode)) - } - is GetScoreForPlayerRequest -> if (source.isAdministrator) { - val displayName = packet.displayName - val score = getScoreOfPlayer(displayName) - ?: throw IllegalArgumentException("Score for \"$displayName\" could not be found!") - logger.debug("Sending score of player \"{}\"", displayName) - source.send(PlayerScorePacket(score)) + is CancelRequest -> { + val room = this.findRoom(packet.roomId) + room.cancel() + // TODO check whether all clients receive game over message + this.games.remove(room) + } + is GetScoreForPlayerRequest -> { + val displayName = packet.displayName + val score = getScoreOfPlayer(displayName) + ?: throw IllegalArgumentException("Score for \"$displayName\" could not be found!") + logger.debug("Sending score of player \"{}\"", displayName) + source.send(PlayerScorePacket(score)) + } + is TestModeRequest -> { + val testMode = packet.testMode + logger.info("Setting Test mode to {}", testMode) + Configuration.set(Configuration.TEST_MODE, testMode.toString()) + source.send(TestModeMessage(testMode)) + } } else -> throw RescuableClientException("Unhandled Packet of type: " + packet.javaClass) } callback.setProcessed() } } - + private fun getScoreOfPlayer(displayName: String): Score? { - for (score in this.gameManager.scores) { + for (score in this.scores) { if (score.displayName == displayName) { return score } } return null } - + fun close() { this.clientManager.close() } - + override fun onError(source: Client, errorPacket: ProtocolErrorMessage) { for (role in source.roles) { if (role.javaClass == PlayerRole::class.java) { diff --git a/server/src/sc/server/gaming/GameRoom.java b/server/src/sc/server/gaming/GameRoom.java index f1eca3a81..112760567 100644 --- a/server/src/sc/server/gaming/GameRoom.java +++ b/server/src/sc/server/gaming/GameRoom.java @@ -12,7 +12,6 @@ import sc.framework.plugins.RoundBasedGameInstance; import sc.helpers.HelperMethods; import sc.networking.InvalidScoreDefinitionException; -import sc.networking.clients.IControllableGame; import sc.networking.clients.LobbyClient; import sc.networking.clients.ObservingClient; import sc.networking.clients.XStreamClient; @@ -21,7 +20,6 @@ import sc.server.network.Client; import sc.server.network.DummyClient; import sc.server.network.IClient; -import sc.server.plugins.GamePluginInstance; import sc.shared.*; import java.io.BufferedWriter; @@ -39,68 +37,46 @@ public class GameRoom implements IGameListener { private final String id; private final GameRoomManager gameRoomManager; - private final GamePluginInstance provider; - private final IGameInstance game; - private List observers = new ArrayList<>(); - private List playerSlots = new ArrayList<>(2); + private final ScoreDefinition scoreDefinition; + private final List playerSlots = new ArrayList<>(getMaximumPlayerCount()); private final boolean prepared; private GameStatus status = GameStatus.CREATED; private GameResult result = null; private boolean pauseRequested = false; private ObservingClient replayObserver; - /** currently unused */ - private IControllableGame replay; - - public List getObservers() { - return observers; - } + public final IGameInstance game; + public final List observers = new ArrayList<>(); public enum GameStatus { CREATED, ACTIVE, OVER } - public GameRoom(String id, GameRoomManager gameRoomManager, - GamePluginInstance provider, IGameInstance game, boolean prepared) { - - if (provider == null) - throw new IllegalArgumentException("Provider must not be null"); - + public GameRoom(String id, GameRoomManager gameRoomManager, ScoreDefinition scoreDefinition, IGameInstance game, boolean prepared) { this.id = id; - this.provider = provider; + // TODO the GameRoom shouldn't need to know its manager + this.gameRoomManager = gameRoomManager; + this.scoreDefinition = scoreDefinition; this.game = game; this.prepared = prepared; - this.gameRoomManager = gameRoomManager; game.addGameListener(this); - // if option is set, add observer to save replay + if (Boolean.parseBoolean(Configuration.get(Configuration.SAVE_REPLAY))) { try { logger.debug("Save replay is active and try to save it to file"); LobbyClient lobbyClient = new LobbyClient(Configuration.getXStream(), null, "127.0.0.1", Configuration.getPort()); lobbyClient.start(); - replayObserver = new ObservingClient(lobbyClient, this.getId()); lobbyClient.authenticate(Configuration.getAdministrativePassword()); - replay = lobbyClient.observe(this.getId()); + replayObserver = lobbyClient.observe(this.getId()); } catch (IOException e) { + logger.warn("Failed to start replay recording"); e.printStackTrace(); } } } - public GamePluginInstance getProvider() { - return this.provider; - } - - public IGameInstance getGame() { - return this.game; - } - - /** - * Generate Game Result, set status to OVER and remove from gameRoomManager - * - * @param results result of game - */ + /** Generate Game Result, set status to OVER and remove from manager. */ @Override public synchronized void onGameOver(Map results) throws InvalidScoreDefinitionException { if (isOver()) { @@ -112,61 +88,28 @@ public synchronized void onGameOver(Map results) throws Inv this.result = generateGameResult(results); logger.info("The game {} is over. (regular={})", getId(), this.result.isRegular()); broadcast(this.result); - // save replay after game over - if (Boolean.parseBoolean(Configuration.get(Configuration.SAVE_REPLAY))) { - List slotDescriptors = new ArrayList<>(); - for (PlayerSlot slot : this.getSlots()) { - slotDescriptors.add(slot.getDescriptor()); - } - String fileName = HelperMethods.generateReplayFilename(this.getGame().getPluginUUID(), slotDescriptors); - try { - File f = new File(fileName); - f.getParentFile().mkdirs(); - f.createNewFile(); - - List replayHistory = replayObserver.getHistory(); - BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); - writer.write("\n"); - for (ProtocolMessage element : replayHistory) { - if(!(element instanceof IGameState)) - continue; - IGameState state = (IGameState) element; - MementoPacket data = new MementoPacket(state, null); - RoomPacket roomPacket = new RoomPacket(this.getId(), data); - String xmlReplay = Configuration.getXStream().toXML(roomPacket); - writer.write(xmlReplay + "\n"); - writer.flush(); - } - String result = Configuration.getXStream().toXML(new RoomPacket(this.getId(), replayObserver.getResult())); - writer.write(result + "\n"); - writer.write(""); - writer.flush(); - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } + if (Boolean.parseBoolean(Configuration.get(Configuration.SAVE_REPLAY))) { + saveReplay(); } + // save playerScore if test mode enabled if (Boolean.parseBoolean(Configuration.get(Configuration.TEST_MODE))) { List players = game.getPlayers(); - gameRoomManager.addResultToScore(this.getResult(), game.getPlayerScores(), players.get(0).getDisplayName(), players.get(1).getDisplayName()); + gameRoomManager.addResultToScore(this.getResult(), players.get(0).getDisplayName(), players.get(1).getDisplayName()); } + kickAllClients(); - this.game.destroy(); - this.gameRoomManager.remove(this); + cancel(); } /** - * Set ScoreDefinition and create GameResult Object from results parameter - * - * @param results map of Player and PlayerScore + * Generate scores from results parameter and return GameResult. * - * @return GameResult, containing all PlayerScores and + * @return GameResult containing ordered PlayerScores and winners. */ private GameResult generateGameResult(Map results) { - ScoreDefinition definition = getProvider().getPlugin().getScoreDefinition(); List scores = new ArrayList<>(); // restore order @@ -177,40 +120,71 @@ private GameResult generateGameResult(Map results) { throw new RuntimeException("GameScore was not complete!"); // FIXME: remove cause != unknown - if (score.getCause() != ScoreCause.UNKNOWN && !score.matches(definition)) - throw new RuntimeException("ScoreSize did not match Definition"); + if (score.getCause() != ScoreCause.UNKNOWN && !score.matches(scoreDefinition)) + throw new RuntimeException(String.format("Score %1s did not match Definition %2s", score, scoreDefinition)); scores.add(score); } - return new GameResult(definition, scores, this.game.getWinners()); + return new GameResult(scoreDefinition, scores, this.game.getWinners()); } - /** - * Send Object o to all Player in this room - * - * @param o Object containing the message - */ - private void broadcast(ProtocolMessage o) { - broadcast(o, true); + /** Save replay from {@link #replayObserver} to a file. */ + private void saveReplay() { + List slotDescriptors = new ArrayList<>(); + for (PlayerSlot slot : this.getSlots()) { + slotDescriptors.add(slot.getDescriptor()); + } + String fileName = HelperMethods.generateReplayFilename(this.game.getPluginUUID(), slotDescriptors); + try { + File f = new File(fileName); + f.getParentFile().mkdirs(); + f.createNewFile(); + + List replayHistory = replayObserver.getHistory(); + BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); + writer.write("\n"); + for (ProtocolMessage element : replayHistory) { + if (!(element instanceof IGameState)) + continue; + IGameState state = (IGameState) element; + MementoPacket data = new MementoPacket(state, null); + RoomPacket roomPacket = new RoomPacket(this.getId(), data); + String xmlReplay = Configuration.getXStream().toXML(roomPacket); + writer.write(xmlReplay + "\n"); + writer.flush(); + } + + String result = Configuration.getXStream().toXML(new RoomPacket(this.getId(), replayObserver.getResult())); + writer.write(result + "\n"); + writer.write(""); + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** Send the given message to all Players and Observers in this room. */ + private void broadcast(ProtocolMessage message) { + broadcast(message, true); } /** - * Send ProtocolMessage o to all Players or all Players in this room + * Send ProtocolMessage to all listeners. * - * @param o a ProtocolMessage - * @param roomSpecific only send this room + * @param roomSpecific whether the message is specifically about this room */ - private void broadcast(ProtocolMessage o, boolean roomSpecific) { - ProtocolMessage toSend = o; + private void broadcast(ProtocolMessage message, boolean roomSpecific) { + ProtocolMessage toSend = message; // If message is specific to room, wrap the message in a RoomPacket if (roomSpecific) { - toSend = new RoomPacket(getId(), o); + toSend = createRoomPacket(toSend); } // Send to all Players for (PlayerRole player : getPlayers()) { - logger.debug("Sending {} to {}", o, player); + logger.debug("Sending {} to {}", message, player); player.getClient().send(toSend); } @@ -218,11 +192,7 @@ private void broadcast(ProtocolMessage o, boolean roomSpecific) { observerBroadcast(toSend); } - /** - * Send Message to all registered Observers - * - * @param toSend Message to send - */ + /** Send Message to all registered Observers. */ private void observerBroadcast(ProtocolMessage toSend) { for (ObserverRole observer : Collections.unmodifiableCollection(this.observers)) { logger.debug("Sending {} to observer {}", toSend, observer.getClient().getClass().getSimpleName()); @@ -230,17 +200,13 @@ private void observerBroadcast(ProtocolMessage toSend) { } } - /** Send {@link GameRoom#broadcast(ProtocolMessage, boolean) broadcast} message with {@link LeftGameEvent LeftGameEvent} */ + /** Send {@link GameRoom#broadcast(ProtocolMessage, boolean) broadcast} message with {@link LeftGameEvent LeftGameEvent}. */ private void kickAllClients() { - logger.debug("Kicking clients (and observer)"); + logger.debug("Kicking clients and observers"); broadcast(new LeftGameEvent(getId()), false); } - /** - * send StateObject to all players and observers - * - * @param data State Object that derives Object - */ + /** Send updated GameState to all players and observers. */ @Override public void onStateChanged(IGameState data, boolean observersOnly) { sendStateToObservers(data); @@ -250,16 +216,15 @@ public void onStateChanged(IGameState data, boolean observersOnly) { /** - * {@link GameRoom#broadcast(ProtocolMessage, boolean) Broadcast} the error package to this room + * {@link GameRoom#broadcast(ProtocolMessage, boolean) Broadcast} the error package to this room. * * @param errorPacket ProtocolErrorMessage */ public void onClientError(ProtocolErrorMessage errorPacket) { - // packet = createRoomPacket(errorPacket); broadcast(errorPacket, true); } - /** Sends the given IGameState to all Players */ + /** Sends the given GameState to all Players. */ private void sendStateToPlayers(IGameState data) { for (PlayerRole player : getPlayers()) { RoomPacket packet = createRoomPacket(new MementoPacket(data, player.getPlayer())); @@ -268,7 +233,7 @@ private void sendStateToPlayers(IGameState data) { } - /** Sends the given IGameState to all Observers */ + /** Sends the given GameState to all Observers. */ private void sendStateToObservers(IGameState data) { RoomPacket packet = createRoomPacket(new MementoPacket(data, null)); @@ -278,38 +243,22 @@ private void sendStateToObservers(IGameState data) { } } - /** - * Create {@link RoomPacket RoomPacket} from id and data Object. - * - * @param data to be send - * - * @return prepared RoomPacket - */ + /** Create {@link RoomPacket RoomPacket} from id and data Object. */ public RoomPacket createRoomPacket(ProtocolMessage data) { return new RoomPacket(getId(), data); } - /** - * Getter for Room ID - * - * @return id of Room as String - */ public String getId() { return this.id; } /** - * gameRoom - * Let a client join a GameRoom. Starts a game, if all players joined. - * - * @param client Client to join + * Join a client into this room. + * Starts the game if full. * - * @return true if join successful - * - * @throws RescuableClientException + * @return true if successfully joined */ - public synchronized boolean join(Client client) - throws RescuableClientException { + public synchronized boolean join(Client client) throws RescuableClientException { PlayerSlot openSlot = null; for (PlayerSlot slot : this.playerSlots) { @@ -336,19 +285,15 @@ public synchronized boolean join(Client client) } /** - * If game is not prepared set attributes of PlayerSlot and start game if game is {@link #isReady() ready} + * If game is not prepared set attributes of PlayerSlot and start game if game is {@link #isReady() ready}. * - * @param openSlot PLayerSlot to fill + * @param openSlot PlayerSlot to fill * @param client Client to fill PlayerSlot - * - * @throws RescuableClientException */ - synchronized void fillSlot(PlayerSlot openSlot, Client client) - throws RescuableClientException { - + synchronized void fillSlot(PlayerSlot openSlot, Client client) throws RescuableClientException { openSlot.setClient(client); // set role of Slot as PlayerRole - if (!isPrepared()) // is set when game is game is created or prepared + if (!this.prepared) // is set when game is game is created or prepared { logger.debug("GameRoom was not prepared, syncSlots"); // seems to happen every time a client manually connects to server (JoinRoomRequest) @@ -359,17 +304,15 @@ synchronized void fillSlot(PlayerSlot openSlot, Client client) } /** - * sets player in GameState and sets player specific values (displayName, shoudbePaused, canTimeout). - * Registers player to role in given slot - * sends JoinGameProtocolMessage when successful - * - * @param slot PlayerSlot to sync - * - * @throws RescuableClientException + * Sets player in GameState and sets player specific values (displayName, shouldbePaused, canTimeout). + * Registers player to role in given slot. + * Sends JoinGameProtocolMessage when successful. */ private void syncSlot(PlayerSlot slot) throws RescuableClientException { - Player player = getGame().onPlayerJoined(); // make new player in gameState of game - // set attributes for player XXX check whether this is needed for prepared games + // create new player in gameState of game + Player player = game.onPlayerJoined(); + // set attributes for player + // TODO check whether this is needed for prepared games player.setDisplayName(slot.getDescriptor().getDisplayName()); player.setShouldBePaused(slot.getDescriptor().getShouldBePaused()); player.setCanTimeout(slot.getDescriptor().getCanTimeout()); @@ -384,14 +327,9 @@ private void syncSlot(PlayerSlot slot) throws RescuableClientException { slot.getClient().send(new JoinGameProtocolMessage(getId())); } - /** - * Returns true, if game was prepared and all slots are in use or maxplayercount of game - * (or any new attribute for readiness) is reached - * - * @return true, if two PlayerSlots are filled - */ + /** Returns true if game is full of players. */ private boolean isReady() { - if (isPrepared()) { + if (this.prepared) { for (PlayerSlot slot : this.playerSlots) { if (slot.isEmpty()) { return false; @@ -400,15 +338,11 @@ private boolean isReady() { return true; } else { - return this.playerSlots.size() == 2; + return this.playerSlots.size() == getMaximumPlayerCount(); } } - /** - * Starts game, if gameStatus isn't over or - * - * @throws RescuableClientException - */ + /** Starts game if ready and not over. */ private void startIfReady() throws RescuableClientException { logger.debug("startIfReady called"); if (isOver()) { @@ -425,13 +359,9 @@ private void startIfReady() throws RescuableClientException { start(); } - /** - * If the Game is prepared, sync all slots - * - * @throws RescuableClientException - */ + /** If the Game is prepared, sync all slots. */ private void start() throws RescuableClientException { - if (isPrepared()) // sync slots for prepared game. This was already called for PlayerSlots in a game created by a join + if (this.prepared) // sync slots for prepared game. This was already called for PlayerSlots in a game created by a join { for (PlayerSlot slot : this.playerSlots) { // creates players in gameState and sets their attributes @@ -445,28 +375,20 @@ private void start() throws RescuableClientException { logger.info("Started the game."); } - /** - * Get the number of players allowed in the game - * - * @return number of allowed players - */ + /** Get the number of players allowed in the game. */ private int getMaximumPlayerCount() { return 2; } - /** - * Returns the list of slots (correct ordering). - * - * @return all PlayerSlots as unmodifiable list - */ + /** Returns the list of slots (ordered). */ public List getSlots() { return Collections.unmodifiableList(this.playerSlots); } /** - * Threadsafe method to Reserve Slots for the player + * Threadsafe method to reserve all PlayerSlots. * - * @return a List of unique IDs + * @return list of reservations */ public synchronized List reserveAllSlots() { List result = new ArrayList<>(this.playerSlots.size()); @@ -479,16 +401,14 @@ public synchronized List reserveAllSlots() { } /** - * Received new data from player and execute data in game + * Execute received action. * * @param source Client which caused the event - * @param data ProtocolMessage which caused the event - * - * @throws RescuableClientException + * @param data ProtocolMessage containing the action */ public synchronized void onEvent(Client source, ProtocolMessage data) throws RescuableClientException, InvalidGameStateException { if (isOver()) - throw new RescuableClientException("Game is already over, but got data: " + data.getClass()); + throw new RescuableClientException("Game is already over, but got message: " + data.getClass()); try { this.game.onAction(resolvePlayer(source), data); @@ -499,19 +419,10 @@ public synchronized void onEvent(Client source, ProtocolMessage data) throws Res } } - /** - * Getter for player out of all playerRoles - * - * @param source Client to find corresponding Player to - * - * @return Player instance - * - * @throws RescuableClientException - */ - private Player resolvePlayer(Client source) - throws RescuableClientException { + /** Finds player matching the given client. */ + private Player resolvePlayer(Client client) throws RescuableClientException { for (PlayerRole role : getPlayers()) { - if (role.getClient().equals(source)) { + if (role.getClient().equals(client)) { Player resolvedPlayer = role.getPlayer(); if (resolvedPlayer == null) { @@ -523,15 +434,10 @@ private Player resolvePlayer(Client source) } } - throw new RescuableClientException("Client is not a member of game " - + this.id); + throw new RescuableClientException("Client is not a member of game " + this.id); } - /** - * Get {@link PlayerRole Players} that occupy a slot - * - * @return List of PlayerRole Objects - */ + /** Get {@link PlayerRole Players} that occupy a slot. */ private Collection getPlayers() { ArrayList clients = new ArrayList<>(); for (PlayerSlot slot : this.playerSlots) { @@ -542,11 +448,7 @@ private Collection getPlayers() { return clients; } - /** - * Get Server {@link IClient Clients} of all {@link PlayerRole Players} - * - * @return all Clients of the Players - */ + /** Get Server {@link IClient Clients} of all {@link PlayerRole Players}. */ public Collection getClients() { ArrayList clients = new ArrayList<>(); for (PlayerRole slot : getPlayers()) { @@ -555,11 +457,7 @@ public Collection getClients() { return clients; } - /** - * Add a Server {@link Client Client} in the role of an Observer - * - * @param source Client to be added - */ + /** Add a Server {@link Client Client} in the role of an Observer. */ public void addObserver(Client source) { ObserverRole role = new ObserverRole(source, this); source.addRole(role); @@ -568,7 +466,7 @@ public void addObserver(Client source) { } /** - * Pause or un-pause a game + * Pause or un-pause a game. * * @param pause true if game is to be paused */ @@ -596,14 +494,13 @@ public synchronized void pause(boolean pause) { } /** + * Execute one turn on a paused Game. + * * @param forced If true, game will be started even if there are not enough * players to complete the game. This should result in a * GameOver. - * - * @throws RescuableClientException */ - public synchronized void step(boolean forced) - throws RescuableClientException { + public synchronized void step(boolean forced) throws RescuableClientException { if (this.status == GameStatus.CREATED) { if (forced) { logger.warn("Forcing a game to start."); @@ -622,30 +519,30 @@ public synchronized void step(boolean forced) } } - /** Kick all Player and destroy game afterwards */ + /** Kick all players, destroy the game and remove it from the manager. */ public void cancel() { if (!isOver()) { kickAllClients(); - this.game.destroy(); + setStatus(GameStatus.OVER); } + this.game.destroy(); + this.gameRoomManager.remove(this); } /** - * Broadcast to all observers, that the game is paused + * Broadcast to all observers that the game is paused. * - * @param nextPlayer Player to do the next move + * @param nextPlayer Player who comes next after the pause */ @Override public void onPaused(Player nextPlayer) { observerBroadcast(new RoomPacket(getId(), new GamePausedEvent(nextPlayer))); } - /** - * Set descriptors of PlayerSlots - */ + /** Set descriptors of PlayerSlots. */ public void openSlots(SlotDescriptor[] descriptors) throws TooManyPlayersException { - if (descriptors.length > 2) { + if (descriptors.length > getMaximumPlayerCount()) { throw new TooManyPlayersException(); } this.playerSlots.add(new PlayerSlot(this)); @@ -659,68 +556,37 @@ public void openSlots(SlotDescriptor[] descriptors) } } - /** - * If game is prepared return true - * - * @return true if Game is prepared - */ - public boolean isPrepared() { - return this.prepared; - } - - /** - * Return true if GameStatus is OVER - * - * @return true if Game is over - */ + /** Return true if GameStatus is OVER. */ public boolean isOver() { return getStatus() == GameStatus.OVER; } /** * Return whether or not the game is paused or will be paused in the next turn. - * Refer to {@link RoundBasedGameInstance#isPaused()} for the current value - * - * @return true, if game is paused + * Refer to {@link RoundBasedGameInstance#isPaused()} for the current value. */ public boolean isPauseRequested() { return this.pauseRequested; } - /** - * Get the current status of the Game - * - * @return status of Game e.g. OVER, CREATED, ... - */ + /** @return current status of the Game. */ public GameStatus getStatus() { return this.status; } - /** - * Update the {@link GameStatus status} of current Game - * - * @param status status to be set - */ + /** Update the {@link GameStatus status} of the current Game. */ protected void setStatus(GameStatus status) { logger.info("Updating Status to {} (was: {})", status, getStatus()); this.status = status; } - /** - * Remove specific player by calling {@link IGameInstance#onPlayerLeft(Player) onPlayerLeft(player)} - * - * @param player to be removed - */ + /** Remove a player by calling {@link IGameInstance#onPlayerLeft(Player) onPlayerLeft(player)}. */ public void removePlayer(Player player, XStreamClient.DisconnectCause cause) { logger.info("Removing {} from {}", player, this); this.game.onPlayerLeft(player, cause == XStreamClient.DisconnectCause.DISCONNECTED ? ScoreCause.REGULAR : null); } - /** - * Get the saved {@link GameResult result} - * - * @return GameResult - */ + /** Get the saved {@link GameResult result}. */ public GameResult getResult() { return this.result; } diff --git a/server/src/sc/server/gaming/GameRoomManager.java b/server/src/sc/server/gaming/GameRoomManager.java index d4fbf49a7..0b6310555 100644 --- a/server/src/sc/server/gaming/GameRoomManager.java +++ b/server/src/sc/server/gaming/GameRoomManager.java @@ -24,7 +24,6 @@ * which seem to be dead-locked or have caused a timeout. */ public class GameRoomManager { - /* Private fields */ private Map rooms; private GamePluginApi pluginApi; @@ -86,7 +85,7 @@ public synchronized GameRoom createGame(String gameType, boolean prepared) throw logger.info("Created new game of type " + gameType); String roomId = generateRoomId(); - GameRoom room = new GameRoom(roomId, this, plugin, plugin.createGame(), prepared); + GameRoom room = new GameRoom(roomId, this, plugin.getPlugin().getScoreDefinition(), plugin.createGame(), prepared); // pause room if specified in server.properties on joinRoomRequest if (!prepared) { boolean paused = Boolean.parseBoolean(Configuration.get(Configuration.PAUSED)); @@ -106,10 +105,10 @@ public synchronized GameRoom createGame(String gameType, boolean prepared) throw logger.debug("Turns is to load is: " + turn); if (turn > 0) { logger.debug("Loading from non default turn"); - room.getGame().loadFromFile(gameFile, turn); + room.game.loadFromFile(gameFile, turn); } else { logger.debug("Loading first gameState found"); - room.getGame().loadFromFile(gameFile); + room.game.loadFromFile(gameFile); } } @@ -123,12 +122,9 @@ private static synchronized String generateRoomId() { } /** - * Open new GameRoom and join Client + * Open new GameRoom and join the client. * - * @param client Client to join the game - * @param gameType String of current game - * - * @return GameRoomMessage for new GameRoom, null im unsuccessful + * @return GameRoomMessage with roomId, null if unsuccessful * * @throws RescuableClientException if game could not be created */ @@ -142,12 +138,9 @@ public synchronized GameRoomMessage createAndJoinGame(Client client, String game } /** - * Called after JoinRoomRequest. Client joins already existing GameRoom or opens new one - * - * @param client to join the game - * @param gameType String of current game + * Called on JoinRoomRequest. Client joins an already existing open GameRoom or opens new one and joins. * - * @return GameRoomMessage with roomId an success null if unsuccessful + * @return GameRoomMessage with roomId, null if unsuccessful * * @throws RescuableClientException if client could not join room */ @@ -162,7 +155,7 @@ public synchronized GameRoomMessage joinOrCreateGame(Client client, String gameT return createAndJoinGame(client, gameType); } - /** Create an unmodifiable Collection of the {@link GameRoom GameRooms} */ + /** Create an unmodifiable Collection of the {@link GameRoom GameRooms}. */ public synchronized Collection getGames() { return Collections.unmodifiableCollection(this.rooms.values()); } @@ -176,14 +169,10 @@ public GamePluginApi getPluginApi() { } /** - * Creates a new GameRoom {@link #createGame(String) createGame}, set descriptors of PlayerSlots, - * if exists load state of game from file - * - * @param gameType String of current game - * @param descriptors which are displayName, canTimeout and shouldBePaused - * @param loadGameInfo Object for game information + * Creates a new GameRoom through {@link #createGame(String) createGame} with reserved PlayerSlots according to the + * descriptors and loads a game state from a file if provided. * - * @return new PrepareGameProtocolMessage with roomId and slots + * @return new PrepareGameProtocolMessage with roomId and slot reservations * * @throws RescuableClientException if game could not be created */ @@ -193,18 +182,16 @@ public synchronized PrepareGameProtocolMessage prepareGame(String gameType, Slot room.openSlots(descriptors); if (loadGameInfo != null) { - room.getGame().loadGameInfo(loadGameInfo); + room.game.loadGameInfo(loadGameInfo); } return new PrepareGameProtocolMessage(room.getId(), room.reserveAllSlots()); } /** - * Calls {@link #prepareGame} + * Overload for {@link #prepareGame}. * - * @param prepared PrepareGameRequest with gameType and slotsDescriptors - * - * @return new PrepareGameProtocolMessage with roomId and slots + * @return new PrepareGameProtocolMessage with roomId and slot reservations * * @throws RescuableClientException if game could not be created */ @@ -217,16 +204,13 @@ public synchronized PrepareGameProtocolMessage prepareGame(PrepareGameRequest pr } /** - * Getter for GameRoom - * * @param roomId String Id of room to be found * - * @return returns GameRoom specified by rooId + * @return returns GameRoom specified by roomId * * @throws RescuableClientException if no room could be found */ - public synchronized GameRoom findRoom(String roomId) - throws RescuableClientException { + public synchronized GameRoom findRoom(String roomId) throws RescuableClientException { GameRoom room = this.rooms.get(roomId); if (room == null) { @@ -236,11 +220,7 @@ public synchronized GameRoom findRoom(String roomId) return room; } - /** - * Remove specified room from game - * - * @param gameRoom to be removed - */ + /** Remove specified room from this manager. */ public void remove(GameRoom gameRoom) { this.rooms.remove(gameRoom.getId()); } @@ -250,16 +230,14 @@ public List getScores() { } /** - * Called by gameRoom after game ended and test mode enabled to save results in playerScores + * Called by gameRoom after game ended and test mode enabled to save results in playerScores. * - * @param result GameResult - * @param playerScores List of playerScores * @param name1 displayName of player1 * @param name2 displayName of player2 * * @throws InvalidScoreDefinitionException if scoreDefinitions do not match */ - public void addResultToScore(GameResult result, List playerScores, String name1, String name2) throws InvalidScoreDefinitionException { + public void addResultToScore(GameResult result, String name1, String name2) throws InvalidScoreDefinitionException { if (name1.equals(name2)) { logger.warn("Both player playerScores have the same displayName. Won't save test relevant data"); return; @@ -283,9 +261,10 @@ public void addResultToScore(GameResult result, List playerScores, this.scores.add(secondScore); } + final List playerScores = result.getScores(); firstScore.setNumberOfTests(firstScore.getNumberOfTests() + 1); secondScore.setNumberOfTests(secondScore.getNumberOfTests() + 1); - for (int i = 0; i < scoreDefinition.size(); i++) { + for (int i = 0; i < scoreDefinition.getSize(); i++) { ScoreFragment fragment = scoreDefinition.get(i); ScoreValue firstValue = firstScore.getScoreValues().get(i); ScoreValue secondValue = secondScore.getScoreValues().get(i); @@ -293,22 +272,9 @@ public void addResultToScore(GameResult result, List playerScores, logger.error("Could not add current game result to score. Score definition of player and result do not match."); throw new InvalidScoreDefinitionException("ScoreDefinition of player does not match expected score definition"); } - // average = oldaverage * ((#tests - 1)/ #tests) + newValue / #tests if (fragment.getAggregation().equals(ScoreAggregation.AVERAGE)) { - firstValue.setValue((firstValue.getValue(). - multiply( - (new BigDecimal(firstScore.getNumberOfTests() - 1) - .divide(new BigDecimal(firstScore.getNumberOfTests()), Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP)) - )).add( - playerScores.get(0).getValues().get(i).divide(new BigDecimal(firstScore.getNumberOfTests()), Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP))); - secondValue.setValue((secondValue.getValue(). - multiply( - (new BigDecimal(secondScore.getNumberOfTests() - 1) - .divide(new BigDecimal(secondScore.getNumberOfTests()), Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP)) - )).add( - playerScores.get(1).getValues().get(i).divide(new BigDecimal(secondScore.getNumberOfTests()), Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP))); - firstValue.setValue(firstValue.getValue().round(new MathContext(Configuration.BIG_DECIMAL_SCALE + 2))); - secondValue.setValue(secondValue.getValue().round(new MathContext(Configuration.BIG_DECIMAL_SCALE + 2))); + firstValue.setValue(updateAverage(firstValue.getValue(), firstScore.getNumberOfTests(), playerScores.get(0).getValues().get(i))); + secondValue.setValue(updateAverage(secondValue.getValue(), secondScore.getNumberOfTests(), playerScores.get(1).getValues().get(i))); } else if (fragment.getAggregation().equals(ScoreAggregation.SUM)) { firstValue.setValue(firstValue.getValue().add(playerScores.get(0).getValues().get(i))); secondValue.setValue(secondValue.getValue().add(playerScores.get(1).getValues().get(i))); @@ -316,4 +282,12 @@ public void addResultToScore(GameResult result, List playerScores, } } + /** Calculates a new average value: average = oldAverage * ((#amount - 1)/ #amount) + newValue / #amount */ + private BigDecimal updateAverage(BigDecimal oldAverage, int amount, BigDecimal newValue) { + BigDecimal decAmount = new BigDecimal(amount); + return oldAverage.multiply(decAmount.subtract(BigDecimal.ONE).divide(decAmount, Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP)) + .add(newValue.divide(decAmount, Configuration.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP)) + .round(new MathContext(Configuration.BIG_DECIMAL_SCALE + 2)); + } + } diff --git a/server/src/sc/server/network/Client.java b/server/src/sc/server/network/Client.java index 31486cc42..38afb2857 100644 --- a/server/src/sc/server/network/Client.java +++ b/server/src/sc/server/network/Client.java @@ -20,9 +20,10 @@ import java.util.List; /** - * A generic client. This represents a client in the server. Clients which - * connect to the server (as separate programs or running as threads started by - * the server) are represented by {@link sc.networking.clients.LobbyClient}. + * A generic client. This represents a client in the server. + * + * Clients which connect to the server (as separate programs or running as threads started by the server) + * are represented by {@link sc.networking.clients.LobbyClient}. */ public class Client extends XStreamClient implements IClient { private static final Logger logger = LoggerFactory.getLogger(Client.class); @@ -35,32 +36,18 @@ public Client(INetworkInterface networkInterface, XStream configuredXStream) thr super(configuredXStream, networkInterface); } - /** - * Getter for the roles. Roles can be {@link sc.server.gaming.PlayerRole PlayerRole}, - * {@link sc.server.gaming.ObserverRole ObserverRole} or {@link AdministratorRole AdministratorRole} - * - * @return Collection of roles - */ + /** @return roles of this client. */ public Collection getRoles() { return Collections.unmodifiableCollection(this.roles); } - /** - * Add another role to the client.Roles can be {@link sc.server.gaming.PlayerRole PlayerRole}, - * {@link sc.server.gaming.ObserverRole ObserverRole} or {@link AdministratorRole AdministratorRole} - * - * @param role to be added - */ + /** Add another role to the client. */ @Override public void addRole(IClientRole role) { this.roles.add(role); } - /** - * Send a package to the server - * - * @param packet message to be send - */ + /** Send a package to the server. */ @Override public synchronized void send(ProtocolMessage packet) { if (!isClosed()) { @@ -72,12 +59,8 @@ public synchronized void send(ProtocolMessage packet) { } } - /** - * Call listener that handle new Packages - * - * @param packet which just arrived - */ - private void notifyOnPacket(Object packet) throws UnprocessedPacketException, InvalidGameStateException { + /** Call listener that handle new Packages. */ + private void notifyOnPacket(Object packet) throws UnprocessedPacketException { /* * NOTE that method is called in the receiver thread. Messages should * only be passed to listeners. No callbacks should be invoked directly @@ -121,11 +104,7 @@ private void notifyOnPacket(Object packet) throws UnprocessedPacketException, In } } - /** - * Call listeners upon error - * - * @param packet which rose the error - */ + /** Call listeners upon error. */ private synchronized void notifyOnError(ProtocolErrorMessage packet) { for (IClientListener listener : this.clientListeners) { try { @@ -136,7 +115,7 @@ private synchronized void notifyOnError(ProtocolErrorMessage packet) { } } - /** Call listeners upon disconnect */ + /** Call listeners upon disconnect. */ private synchronized void notifyOnDisconnect() { if (!this.notifiedOnDisconnect) { this.notifiedOnDisconnect = true; @@ -152,7 +131,7 @@ private synchronized void notifyOnDisconnect() { } } - /** Add a {@link IClientListener listener} to the client */ + /** Add a {@link IClientListener listener} to the client. */ public void addClientListener(IClientListener listener) { this.clientListeners.add(listener); } @@ -162,7 +141,7 @@ public void removeClientListener(IClientListener listener) { } /** - * Test if this client is a administrator + * Test if this client is an administrator. * * @return true iff this client has an AdministratorRole */ @@ -216,13 +195,9 @@ protected void onDisconnect(DisconnectCause cause) { notifyOnDisconnect(); } - /** - * Received new package, which is send to all listener - * - * @param o received package - */ + /** Forward received package to listeners. */ @Override - protected void onObject(ProtocolMessage o) throws UnprocessedPacketException, InvalidGameStateException { + protected void onObject(ProtocolMessage o) throws UnprocessedPacketException { /* * NOTE that this method is called in the receiver thread. Messages * should only be passed to listeners. No callbacks should be invoked diff --git a/server/src/sc/server/network/ClientManager.java b/server/src/sc/server/network/ClientManager.java index 693a41553..e69de29bb 100644 --- a/server/src/sc/server/network/ClientManager.java +++ b/server/src/sc/server/network/ClientManager.java @@ -1,160 +0,0 @@ -package sc.server.network; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sc.api.plugins.exceptions.RescuableClientException; -import sc.protocol.responses.ProtocolErrorMessage; -import sc.server.Lobby; -import sc.server.ServiceManager; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** The ClientManager serves as a lookup table for all active connections. */ -public class ClientManager implements Runnable, IClientListener { - private static final Logger logger = LoggerFactory.getLogger(ClientManager.class); - - // Lobby which we are connected to - private Lobby lobby; - private boolean running; - private Thread thread; - - // List of all XStreamClients - protected final List clients; - - // Listener waits for new clients to connect - private final NewClientListener clientListener; - - /** - * Create manager from {@link Lobby lobby} - * - * @param lobby from which the manager is created - */ - public ClientManager(Lobby lobby) { - this.clientListener = new NewClientListener(); - this.lobby = lobby; - this.clients = new ArrayList<>(); - this.running = false; - this.thread = null; - } - - /** - * Adds the given newClient and notifies all listeners by - * invoking onClientConnected.
- * (only used by tests and addAll()) - */ - public void add(Client newClient) { - this.clients.add(newClient); - newClient.addClientListener(this); - this.lobby.onClientConnected(newClient); - } - - /** Used for testing */ - public List getClients() { - return this.clients; - } - - /** Fetch new clients */ - @Override - public void run() { - this.running = true; - - logger.info("ClientManager running."); - - while (this.running && !Thread.interrupted()) { - try { - // Waits blocking for new Client - Client client = this.clientListener.fetchNewSingleClient(); - - logger.info("Delegating new client to ClientManager..."); - this.add(client); - logger.info("Delegation done."); - } catch (InterruptedException e) { - if (this.running) { - logger.error("Interrupted while waiting for a new client.", e); - } else { - logger.error("Client manager is shutting down"); - } - // TODO should it be handled? - } - - } - - this.running = false; - logger.info("ClientManager closed."); - } - - /** - * Starts the ClientManager in it's own daemon thread. This method should be used only once. - * clientListener starts SocketListener on defined port to watch for new connecting clients - */ - public void start() throws IOException { - this.clientListener.start(); - if (this.thread == null) { - this.thread = ServiceManager.createService(this.getClass().getSimpleName(), this); - this.thread.start(); - } - } - - /** - * Set the {@link Lobby lobby}. - * - * @param lobby to be set - */ - public void setLobby(Lobby lobby) { - this.lobby = lobby; - } - - public void close() { - this.running = false; - - if (this.thread != null) { - this.thread.interrupt(); - } - - this.clientListener.close(); - - for (int i = 0; i < this.clients.size(); i++) { - Client client = this.clients.get(i); - client.stop(); - } - } - - /** - * On client disconnect remove it from the list - * - * @param source client which disconnected - */ - @Override - public void onClientDisconnected(Client source) { - logger.info("Removing client {} from client manager", source); - clients.remove(source); - } - - /** - * Do nothing on error - * - * @param source client, which rose the error - * @param packet which contains the error - */ - @Override - public void onError(Client source, ProtocolErrorMessage packet) { - // TODO Error handling needs to happen - } - - /** - * Ignore any request - * - * @param source client, which send the package - * @param packet to be handled - * - * @throws RescuableClientException never - */ - @Override - public void onRequest(Client source, PacketCallback packet) - throws RescuableClientException { - // TODO Handle Request? - } - -} diff --git a/server/src/sc/server/network/ClientManager.kt b/server/src/sc/server/network/ClientManager.kt new file mode 100644 index 000000000..890a6abe1 --- /dev/null +++ b/server/src/sc/server/network/ClientManager.kt @@ -0,0 +1,113 @@ +package sc.server.network + +import org.slf4j.LoggerFactory +import sc.api.plugins.exceptions.RescuableClientException +import sc.protocol.responses.ProtocolErrorMessage +import sc.server.ServiceManager +import java.io.IOException +import java.util.* + +/** The ClientManager serves as a lookup table for all active connections. */ +class ClientManager : Runnable, IClientListener { + + /** List of all XStreamClients. */ + val clients = ArrayList() + + /** Listener waits for new clients to connect. */ + private val clientListener = NewClientListener() + + private var running: Boolean = false + private var serviceThread: Thread? = null + + private var onClientConnected: ((Client) -> Unit)? = null + + init { + running = false + serviceThread = null + } + + /** + * Adds the given `newClient` and notifies all listeners by invoking `onClientConnected`. + * + * *(only used by tests and addAll())* + */ + fun add(newClient: Client) { + clients.add(newClient) + newClient.addClientListener(this) + onClientConnected?.invoke(newClient) + } + + fun setOnClientConnected(consumer: (Client) -> Unit) { + onClientConnected = consumer + } + + /** Fetch new clients. */ + override fun run() { + running = true + + logger.info("ClientManager running") + + while(running && !Thread.interrupted()) { + try { + // Waits blocking for new Client + val client = clientListener.fetchNewSingleClient() + + logger.info("Delegating new client to ClientManager...") + add(client) + logger.info("Delegation done") + } catch(e: InterruptedException) { + if(running) + logger.warn("Interrupted while waiting for a new client", e) + } + } + + running = false + logger.info("ClientManager closed") + } + + /** + * Starts the ClientManager and ClientListener in it's own daemon thread. This method should be used only once. + * + * @see NewClientListener.start + */ + @Throws(IOException::class) + fun start() { + clientListener.start() + if(serviceThread == null) + serviceThread = ServiceManager.createService(javaClass.simpleName, this).apply { start() } + } + + fun close() { + running = false + serviceThread?.interrupt() + clientListener.close() + while (clients.size > 0) { + try { + clients.removeAt(clients.lastIndex).stop() + } catch (ignored: ArrayIndexOutOfBoundsException) { + // Client was removed concurrently + } + } + } + + /** Remove disconnected client. */ + override fun onClientDisconnected(source: Client) { + logger.info("Removing client $source from client manager") + clients.remove(source) + } + + /** Do nothing on error. */ + override fun onError(source: Client, packet: ProtocolErrorMessage) { + // TODO Error handling needs to happen + } + + /** Ignore any request. */ + override fun onRequest(source: Client, packet: PacketCallback) { + // TODO Handle Request? + } + + companion object { + private val logger = LoggerFactory.getLogger(ClientManager::class.java) + } + +} diff --git a/server/src/sc/server/network/NewClientListener.java b/server/src/sc/server/network/NewClientListener.java index 762f881a3..04236a149 100644 --- a/server/src/sc/server/network/NewClientListener.java +++ b/server/src/sc/server/network/NewClientListener.java @@ -17,20 +17,14 @@ /** Listener, which waits for new clients */ public class NewClientListener implements Runnable, Closeable { - - /* private fields */ private ServerSocket serverSocket; private Thread thread; - /* final fields */ private final BlockingQueue queue; - - /* static fields */ protected static final Logger logger = LoggerFactory.getLogger(NewClientListener.class); public static int lastUsedPort = 0; - /* constructor */ NewClientListener() { this.serverSocket = null; this.thread = null; @@ -38,8 +32,8 @@ public class NewClientListener implements Runnable, Closeable { } /** - * Returns a new connected client, if a new one is available. Otherwise this - * method blocks until a new client connects. + * Returns a new connected client, if a new one is available. + * Otherwise this method blocks until a new client connects. * * @throws InterruptedException If interrupted while waiting for a new client. */ @@ -47,7 +41,7 @@ public Client fetchNewSingleClient() throws InterruptedException { return this.queue.take(); } - /** Accept clients in blocking mode */ + /** Wait for a client to connect and add it to the queue. */ private void acceptClient() { try { Socket clientSocket = this.serverSocket.accept(); @@ -72,7 +66,7 @@ private void acceptClient() { } } - /** infinite loop to wait asynchronously for clients */ + /** Infinite loop to wait asynchronously for clients. */ @Override public void run() { while (!this.serverSocket.isClosed() && !Thread.interrupted()) { @@ -82,9 +76,9 @@ public void run() { } /** - * Start the listener and create a daemon thread from this object + * Start the listener and create a daemon thread from this object. * - * @throws IOException + * The SocketListener then watches the {@link Configuration#getPort()} for new connecting clients. */ public void start() throws IOException { startSocketListener(); @@ -95,7 +89,7 @@ public void start() throws IOException { } /** - * Start the listener, whilst opening a port + * Start listening on the configured port. * * @throws IOException if server could not be started on set port */ @@ -119,7 +113,7 @@ private void startSocketListener() throws IOException { } } - /** close the socket */ + /** Close the socket. */ @Override public void close() { try { diff --git a/server/test/sc/protocol/requests/RequestTest.kt b/server/test/sc/protocol/requests/RequestTest.kt index e15862218..1ab20a0df 100644 --- a/server/test/sc/protocol/requests/RequestTest.kt +++ b/server/test/sc/protocol/requests/RequestTest.kt @@ -1,14 +1,11 @@ package sc.protocol.requests -import com.thoughtworks.xstream.XStream import org.junit.Assert.* import org.junit.Before import org.junit.Ignore import org.junit.Test import sc.framework.plugins.RoundBasedGameInstance import sc.networking.clients.LobbyClient -import sc.protocol.helpers.LobbyProtocol -import sc.protocol.responses.ProtocolMessage import sc.server.Configuration import sc.server.client.PlayerListener import sc.server.client.TestLobbyClientListener @@ -22,7 +19,6 @@ import sc.server.plugins.TestMove import sc.server.plugins.TestPlugin import sc.server.plugins.TestTurnRequest import sc.shared.WelcomeMessage -import java.util.* private const val PASSWORD = "TEST_PASSWORD" @@ -50,8 +46,8 @@ class RequestTest: RealServerTest() { fun joinRoomRequest() { player1.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) - TestHelper.assertEqualsWithTimeout(1, { lobby.gameManager.games.size }) - assertEquals(1, lobby.gameManager.games.iterator().next().clients.size.toLong()) + TestHelper.assertEqualsWithTimeout(1, { lobby.games.size }) + assertEquals(1, lobby.games.iterator().next().clients.size.toLong()) } @Test @@ -80,20 +76,15 @@ class RequestTest: RealServerTest() { TestHelper.waitMillis(200) assertNotNull(listener.response) - assertEquals(1, lobby.gameManager.games.size.toLong()) - assertEquals(0, lobby.gameManager.games.iterator().next().clients.size.toLong()) - assertTrue(lobby.gameManager.games.iterator().next().isPauseRequested) + assertEquals(1, lobby.games.size.toLong()) + assertEquals(0, lobby.games.iterator().next().clients.size.toLong()) + assertTrue(lobby.games.iterator().next().isPauseRequested) } @Test fun prepareXmlTest() { - val xStream = XStream() - xStream.setMode(XStream.NO_REFERENCES) - xStream.classLoader = Configuration::class.java.classLoader - LobbyProtocol.registerMessages(xStream) - LobbyProtocol.registerAdditionalMessages(xStream, - Arrays.asList(*arrayOf>(ProtocolMessage::class.java))) + val xStream = Configuration.getXStream() val request = xStream.fromXML("\n" + "\n" + " \n" + @@ -116,11 +107,11 @@ class RequestTest: RealServerTest() { val reservation = response.reservations[0] player1.joinPreparedGame(reservation) TestHelper.waitMillis(200) - assertEquals(1, lobby.gameManager.games.iterator().next().clients.size.toLong()) + assertEquals(1, lobby.games.iterator().next().clients.size.toLong()) player2.joinPreparedGame(response.reservations[1]) TestHelper.waitMillis(200) - assertEquals(2, lobby.gameManager.games.iterator().next().clients.size.toLong()) + assertEquals(2, lobby.games.iterator().next().clients.size.toLong()) player3.joinPreparedGame(response.reservations[1]) TestHelper.waitMillis(200) @@ -134,7 +125,7 @@ class RequestTest: RealServerTest() { TestHelper.waitMillis(200) - val gameRoom = lobby.gameManager.games.iterator().next() + val gameRoom = lobby.games.iterator().next() player3.addListener(TestObserverListener()) player3.authenticate(PASSWORD) player3.observe(gameRoom.id) @@ -168,7 +159,7 @@ class RequestTest: RealServerTest() { TestHelper.waitMillis(500) // Room was created - val room = lobby.gameManager.games.iterator().next() + val room = lobby.games.iterator().next() val sp1 = room.slots[0].role.player sp1.addPlayerListener(p1Listener) admin.send(PauseGameRequest(room.id, true)) @@ -188,8 +179,8 @@ class RequestTest: RealServerTest() { assertTrue(room.isPauseRequested) val pr1 = room.slots[0].role val pr2 = room.slots[1].role - assertTrue(pr1.player.isShouldBePaused) - assertTrue(pr2.player.isShouldBePaused) + assertTrue(pr1.player.shouldBePaused) + assertTrue(pr2.player.shouldBePaused) // Wait for it to register @@ -223,7 +214,7 @@ class RequestTest: RealServerTest() { TestHelper.waitMillis(500) // Room was created - val room = lobby.gameManager.games.iterator().next() + val room = lobby.games.iterator().next() val sp1 = room.slots[0].role.player sp1.addPlayerListener(p1Listener) admin.send(PauseGameRequest(room.id, true)) @@ -242,8 +233,8 @@ class RequestTest: RealServerTest() { val pr1 = room.slots[0].role val pr2 = room.slots[1].role - assertTrue(pr1.player.isShouldBePaused) - assertTrue(pr2.player.isShouldBePaused) + assertTrue(pr1.player.shouldBePaused) + assertTrue(pr2.player.shouldBePaused) // Wait for it to register @@ -303,7 +294,7 @@ class RequestTest: RealServerTest() { listener.newStateReceived = false // Game should be deleted, because player3 send invalid move - assertEquals(0L, lobby.gameManager.games.size.toLong()) + assertEquals(0L, lobby.games.size.toLong()) } @@ -317,11 +308,11 @@ class RequestTest: RealServerTest() { player1.addListener(listener) // Wait for messages to get to server - assertTrue(TestHelper.waitUntilTrue({ lobby.gameManager.games.isNotEmpty() }, 1000)) + assertTrue(TestHelper.waitUntilTrue({ lobby.games.isNotEmpty() }, 1000)) player1.send(CancelRequest(listener.roomId)) - assertTrue(TestHelper.waitUntilTrue({ lobby.gameManager.games.isEmpty() }, 3000)) - assertEquals(0, lobby.gameManager.games.size.toLong()) + assertTrue(TestHelper.waitUntilTrue({ lobby.games.isEmpty() }, 3000)) + assertEquals(0, lobby.games.size.toLong()) } @Test @@ -353,14 +344,14 @@ class RequestTest: RealServerTest() { player1.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) player2.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) - TestHelper.waitUntilEqual(1, { lobby.gameManager.games.size }, 2000) + TestHelper.waitUntilEqual(1, { lobby.games.size }, 2000) var room = gameMgr.games.iterator().next() - assertTrue(room.slots[0].role.player.isCanTimeout) + assertTrue(room.slots[0].role.player.canTimeout) val req = ControlTimeoutRequest(room.id, false, 0) player1.send(req) TestHelper.waitMillis(2000) room = gameMgr.games.iterator().next() - assertFalse(room.slots[0].role.player.isCanTimeout) + assertFalse(room.slots[0].role.player.canTimeout) } @Test @@ -373,7 +364,7 @@ class RequestTest: RealServerTest() { player1.addListener(listener) player1.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) - TestHelper.waitUntilEqual(1, { lobby.gameManager.games.size }, 2000) + TestHelper.waitUntilEqual(1, { lobby.games.size }, 2000) val room = gameMgr.games.iterator().next() room.slots[0].role.player.addPlayerListener(p1Listener) player2.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) diff --git a/server/test/sc/server/LobbyTest.kt b/server/test/sc/server/LobbyTest.kt index 480a939cf..8c911a7f2 100644 --- a/server/test/sc/server/LobbyTest.kt +++ b/server/test/sc/server/LobbyTest.kt @@ -1,6 +1,5 @@ package sc.server -import org.junit.Assert import org.junit.Test import sc.server.helpers.TestHelper import sc.server.network.RealServerTest @@ -10,26 +9,18 @@ class LobbyTest: RealServerTest() { @Test fun shouldConnectAndDisconnect() { - try { - val player1 = connectClient("localhost", serverPort) - val player2 = connectClient("localhost", serverPort) - - player1.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) - player2.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) - - // Was game created? - TestHelper.assertEqualsWithTimeout(1, { lobby.gameManager.games.size }, 2000) - Assert.assertNotNull(lobby.gameManager.games) - Assert.assertNotEquals(0, lobby.gameManager.games.size.toLong()) - Assert.assertNotNull(lobby.clientManager) - - player1.stop() - // FIXME sometimes fails - see Issue #124 - // TestHelper.assertEqualsWithTimeout(0, { lobby.gameManager.games.size }, 5000) - } catch (e: Exception) { - e.printStackTrace() - Assert.fail() - } + val player1 = connectClient("localhost", serverPort) + val player2 = connectClient("localhost", serverPort) + + player1.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) + player2.joinRoomRequest(TestPlugin.TEST_PLUGIN_UUID) + + // Wait for game to be created + TestHelper.assertEqualsWithTimeout(1, { lobby.games.size }, 2000) + + // Game should be stopped when one player dies + player1.stop() + TestHelper.assertEqualsWithTimeout(0, { lobby.games.size }, 5000) } } diff --git a/server/test/sc/server/gaming/GameRoomTest.kt b/server/test/sc/server/gaming/GameRoomTest.kt new file mode 100644 index 000000000..fecec03bd --- /dev/null +++ b/server/test/sc/server/gaming/GameRoomTest.kt @@ -0,0 +1,73 @@ +package sc.server.gaming + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.assertThrows +import sc.protocol.requests.PrepareGameRequest +import sc.server.Configuration +import sc.server.helpers.StringNetworkInterface +import sc.server.network.Client +import sc.server.plugins.TestPlugin +import sc.shared.PlayerScore +import sc.shared.ScoreCause +import sc.shared.SlotDescriptor + +class GameRoomTest: StringSpec({ + val stringInterface = StringNetworkInterface("") + val client = Client(stringInterface, Configuration.getXStream()).apply { start() } + + "create, join, end game" { + val manager = GameRoomManager().apply { pluginManager.loadPlugin(TestPlugin::class.java, pluginApi) } + // TODO Replay observing + // Configuration.set(Configuration.SAVE_REPLAY, "true") + + manager.joinOrCreateGame(client, TestPlugin.TEST_PLUGIN_UUID).existing shouldBe false + manager.games shouldHaveSize 1 + val room = manager.games.single() + room.game.players shouldHaveSize 1 + manager.joinOrCreateGame(client, TestPlugin.TEST_PLUGIN_UUID).existing shouldBe true + + val playersScores = room.game.players.associateWith { PlayerScore(ScoreCause.REGULAR, "Game terminated", 0) } + room.onGameOver(playersScores) + room.result.isRegular shouldBe true + room.result.scores shouldContainExactly playersScores.values + room.isOver shouldBe true + } + + "prepare game & claim reservations" { + val manager = GameRoomManager().apply { pluginManager.loadPlugin(TestPlugin::class.java, pluginApi) } + val player2name = "opponent" + + val reservations = manager.prepareGame(PrepareGameRequest(TestPlugin.TEST_PLUGIN_UUID, descriptor2 = SlotDescriptor(player2name))).reservations + manager.games shouldHaveSize 1 + val room = manager.games.first() + room.clients shouldHaveSize 0 + // reject client with wrong or no reservation + assertThrows { + ReservationManager.redeemReservationCode(client, "nope") + } + room.join(client) shouldBe false + room.clients shouldHaveSize 0 + // join a client + ReservationManager.redeemReservationCode(client, reservations[0]) + room.clients shouldHaveSize 1 + // don't accept a reservation twice + assertThrows { + ReservationManager.redeemReservationCode(client, reservations[0]) + } + room.clients shouldHaveSize 1 + room.game.players shouldHaveSize 0 + // join second client and sync + ReservationManager.redeemReservationCode(client, reservations[1]) + room.clients shouldHaveSize 2 + room.game.players shouldHaveSize 2 + // reject extra client + room.join(client) shouldBe false + room.clients shouldHaveSize 2 + // check game + room.game.players[0].displayName shouldBe "Player1" + room.game.players[1].displayName shouldBe player2name + } +}) \ No newline at end of file diff --git a/server/test/sc/server/network/ClientXmlReadTest.java b/server/test/sc/server/network/ClientXmlReadTest.java index 056f1116b..fbe094b8e 100644 --- a/server/test/sc/server/network/ClientXmlReadTest.java +++ b/server/test/sc/server/network/ClientXmlReadTest.java @@ -55,8 +55,7 @@ public void clientReceivePacketTest() throws IOException, InterruptedException { @Test public void clientSendPacketTest() throws IOException { - StringNetworkInterface stringInterface = new StringNetworkInterface( - EMPTY_OBJECT_STREAM); + StringNetworkInterface stringInterface = new StringNetworkInterface(EMPTY_OBJECT_STREAM); Client client = new Client(stringInterface, Configuration.getXStream()); client.start(); client.send(new ExamplePacket()); diff --git a/server/test/sc/server/network/ConnectionTest.java b/server/test/sc/server/network/ConnectionTest.java index d2ca556c0..3b1cbd30e 100644 --- a/server/test/sc/server/network/ConnectionTest.java +++ b/server/test/sc/server/network/ConnectionTest.java @@ -23,7 +23,7 @@ public void connectionTest() throws IOException, InterruptedException { waitForConnect(1); client.send(new JoinRoomRequest(TestPlugin.TEST_PLUGIN_UUID)); - TestHelper.INSTANCE.assertEqualsWithTimeout(1, () -> ConnectionTest.this.getLobby().getGameManager().getGames().size(), 1, TimeUnit.SECONDS); + TestHelper.INSTANCE.assertEqualsWithTimeout(1, () -> ConnectionTest.this.getLobby().getGames().size(), 1, TimeUnit.SECONDS); } @Ignore diff --git a/server/test/sc/server/network/LobbyTest.kt b/server/test/sc/server/network/LobbyTest.kt index 2543f3f44..8b578ed75 100644 --- a/server/test/sc/server/network/LobbyTest.kt +++ b/server/test/sc/server/network/LobbyTest.kt @@ -35,8 +35,6 @@ class LobbyTest: RealServerTest() { player1.sendCustomData("") TestHelper.assertEqualsWithTimeout(true, { theRoom.isOver }) - TestHelper.assertEqualsWithTimeout(true, { theRoom.result.scores != null }) - TestHelper.assertEqualsWithTimeout(ScoreCause.LEFT, { theRoom.result.scores[0].cause }, 2, TimeUnit.SECONDS) // should cleanup gamelist diff --git a/server/test/sc/server/network/RealServerTest.kt b/server/test/sc/server/network/RealServerTest.kt index b0a1799b1..858c78917 100644 --- a/server/test/sc/server/network/RealServerTest.kt +++ b/server/test/sc/server/network/RealServerTest.kt @@ -36,7 +36,7 @@ abstract class RealServerTest { Configuration.set(Configuration.PASSWORD_KEY, "TEST_PASSWORD") this.lobby = Lobby() this.clientMgr = this.lobby.clientManager - this.gameMgr = this.lobby.gameManager + this.gameMgr = this.lobby this.pluginMgr = this.gameMgr.pluginManager this.pluginMgr.loadPlugin(TestPlugin::class.java, this.gameMgr.pluginApi) diff --git a/server/test/sc/server/plugins/TestGame.java b/server/test/sc/server/plugins/TestGame.java index def9677bb..c53251366 100644 --- a/server/test/sc/server/plugins/TestGame.java +++ b/server/test/sc/server/plugins/TestGame.java @@ -8,10 +8,12 @@ import sc.framework.plugins.Player; import sc.framework.plugins.RoundBasedGameInstance; import sc.protocol.responses.ProtocolMessage; +import sc.server.Configuration; import sc.server.helpers.TestTeam; import sc.server.helpers.WinReason; import sc.shared.*; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -82,8 +84,7 @@ public void onPlayerLeft(Player player, ScoreCause cause) { // this.players.remove(player); logger.debug("Player left {}", player); Map result = generateScoreMap(); - result.put(player, new PlayerScore(false, "Spieler hat das Spiel verlassen.")); - result.get(player).setCause(cause); + result.put(player, new PlayerScore(cause, "Spieler hat das Spiel verlassen.", 0)); notifyOnGameOver(result); } @@ -116,7 +117,7 @@ public List getWinners() { @Override public List getPlayers() { - return null; + return new ArrayList<>(players); } /** Sends welcomeMessage to all listeners and notify player on new gameStates or MoveRequests */ diff --git a/server/test/sc/server/plugins/TestPlugin.java b/server/test/sc/server/plugins/TestPlugin.java index 43825a400..3e46587b6 100644 --- a/server/test/sc/server/plugins/TestPlugin.java +++ b/server/test/sc/server/plugins/TestPlugin.java @@ -11,16 +11,7 @@ public class TestPlugin implements IGamePlugin { public static final String TEST_PLUGIN_UUID = "012345-norris"; - public static final ScoreDefinition SCORE_DEFINITION; - - static { - SCORE_DEFINITION = new ScoreDefinition(); - SCORE_DEFINITION.add("winner"); - } - - public TestPlugin() { - - } + public static final ScoreDefinition SCORE_DEFINITION = new ScoreDefinition("winner"); @Override public IGameInstance createGame() { @@ -37,8 +28,6 @@ public void initialize(IGamePluginHost host) { @Override public void unload() { - // TODO Auto-generated method stub - } @Override diff --git a/server/test/sc/server/roles/AbstractRoleTest.java b/server/test/sc/server/roles/AbstractRoleTest.java index f2fa0fb57..59ee0f449 100644 --- a/server/test/sc/server/roles/AbstractRoleTest.java +++ b/server/test/sc/server/roles/AbstractRoleTest.java @@ -17,6 +17,11 @@ public abstract class AbstractRoleTest { + protected Lobby lobby; + protected ClientManager clientMgr; + protected GameRoomManager gameMgr; + protected GamePluginManager pluginMgr; + @Before public void setup() throws IOException, PluginLoaderException { // Random PortAllocation @@ -24,7 +29,7 @@ public void setup() throws IOException, PluginLoaderException { this.lobby = new Lobby(); this.clientMgr = this.lobby.getClientManager(); - this.gameMgr = this.lobby.getGameManager(); + this.gameMgr = this.lobby; this.pluginMgr = this.gameMgr.getPluginManager(); this.pluginMgr.loadPlugin(TestPlugin.class, this.gameMgr.getPluginApi()); @@ -38,11 +43,6 @@ public void tearDown() { this.lobby.close(); } - protected Lobby lobby; - protected ClientManager clientMgr; - protected GameRoomManager gameMgr; - protected GamePluginManager pluginMgr; - protected MockClient connectClient(boolean administrator) { MockClient client; try {