Skip to content

Commit c78035c

Browse files
committed
feat(plugin26): basic structure
1 parent 92d19f7 commit c78035c

File tree

11 files changed

+241
-2
lines changed

11 files changed

+241
-2
lines changed

.idea

plugin2026/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
val game: String by project
2+
3+
dependencies {
4+
api(project(":sdk"))
5+
}
6+
7+
tasks {
8+
jar {
9+
archiveBaseName.set(game)
10+
}
11+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package sc.plugin2026
2+
3+
import com.thoughtworks.xstream.annotations.XStreamAlias
4+
import sc.api.plugins.Coordinates
5+
import sc.api.plugins.IMove
6+
import sc.api.plugins.Vector
7+
8+
@XStreamAlias("move")
9+
/** Ein Spielzug. */
10+
data class Move(
11+
/** Ursprungsposition des Zugs. */
12+
val from: Coordinates,
13+
/** Zielposition des Zugs. */
14+
val to: Coordinates,
15+
): IMove, Comparable<Move> {
16+
val delta: Vector?
17+
get() = from?.let { to - it }
18+
19+
fun reversed(): Move? =
20+
from?.let { Move(to, from) }
21+
22+
override fun compareTo(other: Move): Int =
23+
other.delta?.let { delta?.compareTo(it) } ?: 0
24+
25+
override fun toString(): String =
26+
"Schwimmen $from zu $to"
27+
28+
companion object {
29+
@JvmStatic
30+
fun run(start: Coordinates, delta: Vector): Move =
31+
Move(start, (start + delta))
32+
}
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package sc.plugin2026
2+
3+
import sc.shared.IMoveMistake
4+
5+
enum class PiranhaMoveMistake(override val message: String) : IMoveMistake {
6+
// TODO
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package sc.plugin2026.util
2+
3+
/** Eine Sammlung an verschiedenen Konstanten, die im Spiel verwendet werden. */
4+
object PiranhaConstants {
5+
const val BOARD_LENGTH: Int = 10
6+
7+
const val ROUND_LIMIT: Int = 30
8+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package sc.plugin2026.util
2+
3+
import com.thoughtworks.xstream.annotations.XStreamAlias
4+
import sc.api.plugins.IGameInstance
5+
import sc.api.plugins.IGamePlugin
6+
import sc.api.plugins.IGameState
7+
import sc.framework.plugins.TwoPlayerGame
8+
import sc.plugin2026.GameState
9+
import sc.plugin2026.Move
10+
import sc.shared.*
11+
12+
// TODO
13+
14+
@XStreamAlias(value = "winreason")
15+
enum class HuIWinReason(override val message: String, override val isRegular: Boolean = true): IWinReason {
16+
DIFFERING_SCORES("%s ist weiter vorne."),
17+
DIFFERING_CARROTS("%s hat weniger Karotten uebrig."),
18+
GOAL("%s hat das Ziel zuerst erreicht."),
19+
}
20+
21+
class GamePlugin: IGamePlugin<Move> {
22+
companion object {
23+
const val PLUGIN_ID = "swc_2026_piranhas"
24+
val scoreDefinition: ScoreDefinition =
25+
ScoreDefinition(arrayOf(
26+
ScoreFragment("Siegpunkte", WinReason("%s hat gewonnen."), ScoreAggregation.SUM),
27+
ScoreFragment("Feldnummer", HuIWinReason.DIFFERING_SCORES, ScoreAggregation.AVERAGE),
28+
ScoreFragment("Karotten", HuIWinReason.DIFFERING_CARROTS, ScoreAggregation.AVERAGE, true),
29+
))
30+
}
31+
32+
override val id = PLUGIN_ID
33+
34+
override val scoreDefinition =
35+
Companion.scoreDefinition
36+
37+
override val turnLimit: Int =
38+
PiranhaConstants.ROUND_LIMIT * 2
39+
40+
override val moveClass = Move::class.java
41+
42+
override fun createGame(): IGameInstance =
43+
TwoPlayerGame(this, GameState())
44+
45+
override fun createGameFromState(state: IGameState): IGameInstance =
46+
TwoPlayerGame(this, state as GameState)
47+
48+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package sc.plugin2026.util
2+
3+
import sc.networking.XStreamProvider
4+
import sc.plugin2026.*
5+
6+
class XStreamClasses: XStreamProvider {
7+
8+
override val classesToRegister: List<Class<*>> =
9+
listOf(
10+
)
11+
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sc.plugin2026.util.GamePlugin
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sc.plugin2026.util.XStreamClasses
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package sc
2+
3+
import io.kotest.assertions.throwables.shouldThrow
4+
import io.kotest.assertions.withClue
5+
import io.kotest.core.spec.IsolationMode
6+
import io.kotest.core.spec.style.WordSpec
7+
import io.kotest.matchers.*
8+
import io.kotest.matchers.booleans.*
9+
import io.kotest.matchers.iterator.*
10+
import io.kotest.matchers.nulls.*
11+
import org.slf4j.LoggerFactory
12+
import sc.api.plugins.IGamePlugin
13+
import sc.api.plugins.IGameState
14+
import sc.api.plugins.Team
15+
import sc.api.plugins.TwoPlayerGameState
16+
import sc.api.plugins.exceptions.TooManyPlayersException
17+
import sc.api.plugins.host.IGameListener
18+
import sc.framework.plugins.AbstractGame
19+
import sc.framework.plugins.Constants
20+
import sc.shared.GameResult
21+
import kotlin.time.Duration.Companion.milliseconds
22+
23+
/** This test verifies that the Game implementation can be used to play a game.
24+
* It is the only plugin-test independent of the season. */
25+
class GamePlayTest: WordSpec({
26+
val logger = LoggerFactory.getLogger(GamePlayTest::class.java)
27+
isolationMode = IsolationMode.SingleInstance
28+
val plugin = IGamePlugin.loadPlugin()
29+
fun createGame() = plugin.createGame() as AbstractGame<*>
30+
"A Game" should {
31+
val game = createGame()
32+
"let players join" {
33+
game.onPlayerJoined()
34+
game.onPlayerJoined()
35+
}
36+
"throw on third player join" {
37+
shouldThrow<TooManyPlayersException> {
38+
game.onPlayerJoined()
39+
}
40+
}
41+
"set activePlayer on start" {
42+
game.start()
43+
game.activePlayer shouldNotBe null
44+
}
45+
"stay paused after move" {
46+
game.isPaused = true
47+
game.onRoundBasedAction(game.currentState.moveIterator().next())
48+
game.isPaused shouldBe true
49+
}
50+
}
51+
"A Game started with two players" When {
52+
"played normally" should {
53+
val game = createGame()
54+
game.onPlayerJoined().team shouldBe Team.ONE
55+
game.onPlayerJoined().team shouldBe Team.TWO
56+
game.start()
57+
58+
var finalState: Int? = null
59+
game.addGameListener(object: IGameListener {
60+
override fun onGameOver(result: GameResult) {
61+
logger.info("Game over: $result")
62+
}
63+
64+
override fun onStateChanged(data: IGameState, observersOnly: Boolean) {
65+
val state = data as? TwoPlayerGameState<*>
66+
state?.lastMove.shouldNotBeNull()
67+
data.hashCode() shouldNotBe finalState
68+
// hashing it to avoid cloning, since we get the original object which might be mutable
69+
finalState = data.hashCode()
70+
logger.debug("Updating state hash to $finalState")
71+
}
72+
})
73+
74+
"finish without issues".config(invocationTimeout = plugin.gameTimeout.milliseconds) {
75+
while(true) {
76+
try {
77+
val condition = game.checkWinCondition()
78+
if(condition != null) {
79+
logger.info("Game ended with $condition")
80+
break
81+
}
82+
83+
val state = game.currentState
84+
if(finalState != null)
85+
finalState shouldBe state.hashCode()
86+
87+
val moves = state.moveIterator()
88+
withClue(state) {
89+
moves.shouldHaveNext()
90+
game.onAction(game.players[state.currentTeam.index], moves.next())
91+
}
92+
} catch(e: Exception) {
93+
logger.warn(e.message)
94+
break
95+
}
96+
}
97+
withClue(game.currentState) {
98+
// Note that this fails if the game ends irregularly
99+
game.currentState.isOver.shouldBeTrue()
100+
}
101+
}
102+
"send the final state to listeners" {
103+
finalState shouldBe game.currentState.hashCode()
104+
}
105+
"return regular scores" {
106+
val result = game.getResult()
107+
result.isRegular shouldBe true
108+
val scores = result.scores.values
109+
scores.first().parts.first().intValueExact() shouldBe when(scores.last().parts.first().intValueExact()) {
110+
Constants.LOSE_SCORE -> Constants.WIN_SCORE
111+
Constants.WIN_SCORE -> Constants.LOSE_SCORE
112+
Constants.DRAW_SCORE -> Constants.DRAW_SCORE
113+
else -> throw NoWhenBranchMatchedException()
114+
}
115+
}
116+
}
117+
}
118+
})

0 commit comments

Comments
 (0)