diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index b3cc2e0..84e4dbc 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -7,7 +7,7 @@ jobs:
matrix:
# https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
os: [ubuntu-latest, windows-latest, macos-latest]
- jdk: [11]
+ jdk: [11, 17, 21]
steps:
- uses: actions/checkout@v4
with:
@@ -55,7 +55,7 @@ jobs:
- name: Upload jar as artifact
uses: actions/upload-artifact@v4
with:
- name: software-challenge-gui-${{ github.sha }}-${{ matrix.os }}
+ name: software-challenge-gui-${{ github.sha }}-j${{ matrix.jdk }}-${{ matrix.os }}
path: build/*.jar
release:
needs: [build, build-arm]
@@ -64,7 +64,7 @@ jobs:
steps:
- uses: actions/download-artifact@v4 # https://github.com/actions/download-artifact
with:
- pattern: software-challenge-gui-${{ github.sha }}-*
+ pattern: software-challenge-gui-${{ github.sha }}-j11-*
path: build
merge-multiple: true
- name: Release ${{ github.ref }}
diff --git a/.gitmodules b/.gitmodules
index 9bc842f..ae0d755 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,6 +2,7 @@
path = backend
url = https://github.com/software-challenge/backend
shallow = true
+ branch = plugin/4-gewinnt
[submodule ".idea"]
path = .idea
url = https://github.com/software-challenge/idea-config
diff --git a/.kotlin/sessions/kotlin-compiler-13424373081474350713.salive b/.kotlin/sessions/kotlin-compiler-13424373081474350713.salive
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index b141e81..bbd46d0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
##
Grafischer Spieleserver der Software-Challenge Germany 
Dies ist die Grafische Oberfläche für die Software-Challenge Germany,
-seit Saison 2020/21 in Kotlin TornadoFX aufbauend auf JavaFX.
+seit Saison 2020/21 in Kotlin TornadoFX, aufbauend auf JavaFX.
Nutzerdokumentation: https://docs.software-challenge.de/server.html
@@ -23,7 +23,7 @@ wird der Server auf diesem Port Verbindungen von Spielern erwarten.
### Kollaboration
Unsere Commit-Messages folgen dem Muster `type(scope): summary`
-(siehe [Karma Runner Konvention](http://karma-runner.github.io/6.2/dev/git-commit-msg.html)),
+(siehe [Karma Runner Konvention](http://karma-runner.github.io/6.4/dev/git-commit-msg.html)),
wobei die gängigen Scopes in [.dev/scopes.txt](.dev/scopes.txt) definiert werden.
Nach dem Klonen mit git sollte dazu der hook aktiviert werden:
@@ -31,7 +31,7 @@ Nach dem Klonen mit git sollte dazu der hook aktiviert werden:
Um bei den Branches die Übersicht zu behalten,
sollten diese ebenfalls nach der Konvention benannt werden,
-z. B. könnte ein Branch mit einem Release-Fix für Gradle `chore/gradle/release-fix` heißen
+z. B. könnte ein Branch mit einem Release-Fix für Gradle `build/gradle/release-fix` heißen
und ein Branch, der ein neues Login-Feature zur GUI hinzufügt, `feat/gui-login`.
Wenn die einzelnen Commits eines Pull Requests eigenständig funktionieren,
@@ -40,15 +40,4 @@ ansonsten (gerade bei experimentier-Branches) ein squash merge,
wobei der Titel des Pull Requests der Commit-Message entsprechen sollte.
Detaillierte Informationen zu unserem Kollaborations-Stil
-findet ihr in der [Kull Konvention](https://kull.jfischer.org).
-
-### Java-Versionen und Abhängigkeiten
-
-Aktuell können die Backend-docs nur mit JDK 8 gebaut werden,
-dieses Projekt braucht jedoch für [tornadofx](https://github.com/edvin/tornadofx2)
-mindestens Java 11.
-Daher müssen die Releases separat gebaut werden.
-
-Tornadofx wird leider seit einigen Jahren nicht mehr entwickelt.
-Wir schauen gerade wie es da weitergeht.
-Eventuell ein eigener Fork.
+findet ihr in der [Kull Konvention](https://kull.jfischer.org).
\ No newline at end of file
diff --git a/backend b/backend
index e2a37e1..7b67e3a 160000
--- a/backend
+++ b/backend
@@ -1 +1 @@
-Subproject commit e2a37e18b3ca4929003ed2d56e43d492ee9caff9
+Subproject commit 7b67e3a100c55ad7ea550217f242d52f805994c6
diff --git a/build.gradle.kts b/build.gradle.kts
index c249214..b3a800a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,10 @@
import org.gradle.internal.os.OperatingSystem
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Properties
val minJavaVersion = JavaVersion.VERSION_11
+val targetJavaVersion = JavaVersion.current() // minJavaVersion can be set for compatibility
plugins {
val minJavaVersion = JavaVersion.VERSION_11 // Declared twice because plugins block has its own scope
require(JavaVersion.current() >= minJavaVersion) {
@@ -10,13 +12,13 @@ plugins {
}
application
- kotlin("jvm") version "1.9.25"
+ kotlin("jvm") version "2.3.0"
id("idea")
id("org.openjfx.javafxplugin") version "0.1.0"
- id("com.github.johnrengelman.shadow") version "6.1.0"
+ id("com.gradleup.shadow") version "9.1.0"
- id("com.github.ben-manes.versions") version "0.47.0"
- id("se.patrikerdes.use-latest-versions") version "0.2.18"
+ id("com.github.ben-manes.versions") version "0.53.0"
+ id("se.patrikerdes.use-latest-versions") version "0.2.19"
}
idea {
@@ -34,7 +36,7 @@ val versionFromBackend by lazy {
arrayOf("year", "minor", "patch").map { versions["socha.version.$it"].toString().toInt() }.joinToString(".") + suffix
}
-group = "sc.gui"
+group = "software-challenge"
version = try {
Runtime.getRuntime().exec(arrayOf("git", "describe", "--tags"))
.inputStream.reader().readText().trim().ifEmpty { null }
@@ -44,7 +46,7 @@ version = try {
println("Current version: $version (Java version: ${JavaVersion.current()})")
application {
- mainClassName = "sc.gui.GuiAppKt" // needs shadow-update which needs gradle update to 7.0
+ mainClass.set("sc.gui.GuiAppKt")
}
repositories {
@@ -55,34 +57,39 @@ repositories {
maven("https://jitpack.io")
}
+// ./gradlew run -Pdebug for debug tools and logging
val debug = project.hasProperty("debug")
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
-
+
+ implementation(files("./gradle/tornadofx2-21e933fd41.jar"))
// implementation("no.tornado", "tornadofx", "2.0.0-SNAPSHOT") { exclude("org.jetbrains.kotlin", "kotlin-reflect") }
// implementation("com.github.software-challenge.tornadofx2", "tornadofx2", "2.0.0")
// implementation("com.github.edvin", "tornadofx2", "master-SNAPSHOT")
// implementation("com.github.edvin", "tornadofx2", "21e933fd41")
- implementation(files("./gradle/tornadofx2-21e933fd41.jar"))
- implementation("ch.qos.logback", "logback-classic", "1.5.18")
- implementation("io.github.oshai", "kotlin-logging-jvm", "6.0.9") // TODO version 7 with kotlin 2
+ implementation("ch.qos.logback", "logback-classic", "1.5.32")
+ implementation("io.github.oshai", "kotlin-logging-jvm", "8.0.01")
implementation("software-challenge", "server")
+ implementation("software-challenge", "plugin2023")
implementation("software-challenge", "plugin2024")
implementation("software-challenge", "plugin2025")
- implementation("software-challenge", "plugin")
+ implementation("software-challenge", "plugin2026")
+ implementation("software-challenge", "plugin2098")
- if(debug)
+ if(debug) {
+ // hold Ctrl to view component hierarchy and bounds
implementation("com.tangorabox", "component-inspector-fx", "1.1.0")
+ }
}
tasks {
compileJava {
- options.release.set(minJavaVersion.majorVersion.toInt())
+ options.release.set(targetJavaVersion.majorVersion.toInt())
}
processResources {
if(!debug)
@@ -92,14 +99,14 @@ tasks {
}
}
withType {
- kotlinOptions {
- jvmTarget = minJavaVersion.toString()
- freeCompilerArgs = listOf("-Xjvm-default=all")
+ compilerOptions {
+ jvmTarget.set(JvmTarget.fromTarget(targetJavaVersion.toString()))
+ //freeCompilerArgs.addAll("-jvm-default=all")
}
}
withType {
- manifest.attributes["Main-Class"] = application.mainClassName
+ manifest.attributes["Main-Class"] = application.mainClass.get()
}
javafx {
@@ -107,13 +114,14 @@ tasks {
val mods = mutableListOf(
"javafx.base", "javafx.controls", "javafx.fxml",
"javafx.web", "javafx.media", "javafx.swing"
- ) // included because of tornadofx already
+ )
+ // included because of tornadofx already
// if(debug) mods.addAll(listOf("javafx.swing"))
modules = mods
}
shadowJar {
- destinationDirectory.set(buildDir)
+ destinationDirectory.set(layout.buildDirectory.asFile.get())
archiveClassifier.set(
"${
OperatingSystem.current().familyName.replace(
@@ -140,24 +148,25 @@ tasks {
run.configure {
dependsOn(backend.task(":server:makeRunnable"))
- workingDir(buildDir.resolve("run"))
+ workingDir(layout.buildDirectory.asFile.get().resolve("run"))
doFirst {
workingDir.mkdirs()
}
args = System.getProperty("args", "").split(" ")
}
- val release by creating {
+ val release by registering {
dependsOn(clean, check)
group = "distribution"
description = "Create and push a tagged commit matching the backend version"
doLast {
val desc = project.properties["m"]?.toString()
?: throw InvalidUserDataException("Das Argument -Pm=\"Beschreibung dieser Version\" wird benötigt")
- exec { commandLine("git", "add", "CHANGELOG.md") }
- exec { commandLine("git", "commit", "-m", "release: v$versionFromBackend") }
- exec { commandLine("git", "tag", versionFromBackend, "-m", desc) }
- exec { commandLine("git", "push", "--follow-tags", "--recurse-submodules=on-demand") }
+
+ providers.exec { commandLine("git", "add", "CHANGELOG.md") }
+ providers.exec { commandLine("git", "commit", "-m", "release: v$versionFromBackend") }
+ providers.exec { commandLine("git", "tag", versionFromBackend, "-m", desc) }
+ providers.exec { commandLine("git", "push", "--follow-tags", "--recurse-submodules=on-demand") }
}
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c51cbf1..e69d040 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index bebc179..5a5913a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,16 +1,2 @@
-rootProject.name = "software-challenge-gui"
-
includeBuild("backend/gradle/custom-tasks")
-includeBuild("backend") {
- // https://publicobject.com/2021/03/11/includebuild
- dependencySubstitution {
- substitute(module("software-challenge:plugin2024"))
- .with(project(":plugin"))
- substitute(module("software-challenge:plugin2025"))
- .with(project(":plugin2025"))
- substitute(module("software-challenge:plugin"))
- .with(project(":plugin2026"))
- substitute(module("software-challenge:server"))
- .with(project(":server"))
- }
-}
\ No newline at end of file
+includeBuild("backend")
diff --git a/src/main/kotlin/sc/gui/AppStyle.kt b/src/main/kotlin/sc/gui/AppStyle.kt
index 01acab4..612aa8c 100644
--- a/src/main/kotlin/sc/gui/AppStyle.kt
+++ b/src/main/kotlin/sc/gui/AppStyle.kt
@@ -11,6 +11,7 @@ import javafx.scene.text.FontWeight
import javafx.scene.text.TextAlignment
import sc.api.plugins.Team
import sc.gui.model.AppModel
+import sc.plugin2098.util.Connect4Constants
import tornadofx.*
class AppStyle: Stylesheet() {
@@ -22,7 +23,7 @@ class AppStyle: Stylesheet() {
const val pieceOpacity = 1.0
- val fontSizeUnscaled = Font.getDefault().also { logger.debug("System Font: $it") }.size.pt
+ val fontSizeUnscaled = Font.getDefault().also { logger.debug { "System Font: $it" } }.size.pt
val fontSizeRegular = fontSizeUnscaled * AppModel.scaling.value
val fontSizeSmall = fontSizeRegular * 0.6
val fontSizeBig = fontSizeRegular * 1.5
@@ -157,7 +158,35 @@ class AppStyle: Stylesheet() {
prefWidth = 100.percent
}
- piranhasStyles()
+ connect4Styles()
+ }
+
+ fun connect4Styles() {
+ background {
+ opacity = 0.7
+ backgroundColor += c("#88DAF7")
+ backgroundImage += resources.url("/piranhas/water_b.png").toURI()
+ backgroundRepeat += BackgroundRepeat.REPEAT to BackgroundRepeat.REPEAT
+ }
+
+ (1..3).forEach { size ->
+ Team.entries.forEach { team ->
+ ".${team}_${size}" {
+ image = resources.url("/piranhas/${team.color}_${(96 + size).toChar()}.png")
+ .toURI()
+ }
+ }
+ }
+
+ ".one-chip" { image = resources.url("/connect4/chip-rot.png").toURI() }
+ ".two-chip" { image = resources.url("/connect4/chip-gelb.png").toURI() }
+ ".one-chip-winning" { image = resources.url("/connect4/chip-rot-winning.png").toURI() }
+ ".two-chip-winning" { image = resources.url("/connect4/chip-gelb-winning.png").toURI() }
+ ".cell" { image = resources.url("/connect4/cell-debug.png").toURI() }
+ ".grid" {
+ backgroundImage += resources.url("/connect4/board.png").toURI()
+ backgroundSize += BackgroundSize(1.0, 1.0, true, true, false, false)
+ }
}
fun piranhasStyles() {
diff --git a/src/main/kotlin/sc/gui/controller/ServerController.kt b/src/main/kotlin/sc/gui/controller/ServerController.kt
index 0b8c741..f67f09c 100644
--- a/src/main/kotlin/sc/gui/controller/ServerController.kt
+++ b/src/main/kotlin/sc/gui/controller/ServerController.kt
@@ -1,7 +1,7 @@
package sc.gui.controller
import ch.qos.logback.classic.LoggerContext
-import ch.qos.logback.core.util.StatusPrinter
+import ch.qos.logback.core.util.StatusPrinter2
import org.slf4j.LoggerFactory
import sc.server.Configuration
import sc.server.Lobby
@@ -13,7 +13,7 @@ class ServerController : Controller() {
fun startServer() {
// output logback diagnostics to see if a logback.xml config was found
val lc = LoggerFactory.getILoggerFactory() as LoggerContext
- StatusPrinter.print(lc)
+ StatusPrinter2().print(lc)
Configuration.loadServerProperties()
Configuration.set(Configuration.SAVE_REPLAY, true)
diff --git a/src/main/kotlin/sc/gui/view/AppView.kt b/src/main/kotlin/sc/gui/view/AppView.kt
index 2098dda..396a469 100644
--- a/src/main/kotlin/sc/gui/view/AppView.kt
+++ b/src/main/kotlin/sc/gui/view/AppView.kt
@@ -29,13 +29,13 @@ class AppView: View("Software-Challenge Germany") {
// TODO help menus keep disappearing and is offset
menu(graphic = sochaIcon) {
item("Beenden", "Shortcut+Q").action {
- logger.debug("Quitting!")
+ logger.debug { "Quitting!" }
Platform.exit()
}
item("Neues Spiel", "Shortcut+N") {
enableWhen(controller.model.currentView.isNotEqualTo(ViewType.GAME_CREATION))
action {
- logger.debug("New Game!")
+ logger.debug { "New Game!" }
if(controller.model.currentView.get() == ViewType.GAME) {
confirm(
header = "Neues Spiel anfangen",
diff --git a/src/main/kotlin/sc/gui/view/ControlView.kt b/src/main/kotlin/sc/gui/view/ControlView.kt
index d60c9b8..e05e2c3 100644
--- a/src/main/kotlin/sc/gui/view/ControlView.kt
+++ b/src/main/kotlin/sc/gui/view/ControlView.kt
@@ -55,7 +55,7 @@ class ControlView: View() {
}
}
val prev = button {
- if(logger.isTraceEnabled)
+ if(logger.isTraceEnabled())
hoverProperty().listenImmediately {
logger.trace { "$this: $padding on hover $it" }
}
@@ -86,7 +86,7 @@ class ControlView: View() {
)
}
button {
- if(logger.isTraceEnabled)
+ if(logger.isTraceEnabled())
hoverProperty().listenImmediately {
logger.trace { "$this: $padding on hover $it" }
}
diff --git a/src/main/kotlin/sc/gui/view/PieceImage.kt b/src/main/kotlin/sc/gui/view/PieceImage.kt
index 3cc18a3..37720f0 100644
--- a/src/main/kotlin/sc/gui/view/PieceImage.kt
+++ b/src/main/kotlin/sc/gui/view/PieceImage.kt
@@ -84,7 +84,6 @@ class PieceImage(private val sizeProperty: ObservableDoubleValue, val content: S
}
fun addChild(graphic: String, index: Int? = null) {
- //logger.trace { "$this: Adding $graphic" }
children.add(index ?: children.size, ResizableImageView(sizeProperty).apply {
addClass(graphic)
if(graphic == "penguin")
diff --git a/src/main/kotlin/sc/gui/view/game/Connect4Board.kt b/src/main/kotlin/sc/gui/view/game/Connect4Board.kt
new file mode 100644
index 0000000..dfb1b8f
--- /dev/null
+++ b/src/main/kotlin/sc/gui/view/game/Connect4Board.kt
@@ -0,0 +1,114 @@
+package sc.gui.view.game
+
+import javafx.geometry.Pos
+import javafx.scene.Node
+import javafx.scene.effect.Glow
+import javafx.scene.input.KeyEvent
+import javafx.scene.layout.GridPane
+import sc.api.plugins.Coordinates
+import sc.gui.view.GameBoard
+import sc.gui.view.PieceImage
+import sc.plugin2098.FieldState
+import sc.plugin2098.GameState
+import sc.plugin2098.util.Connect4Constants
+import sc.plugin2098.util.GameRuleLogic
+import tornadofx.*
+
+class Connect4Board: GameBoard() {
+
+ private val gridSize
+ get() = squareSize.div(Connect4Constants.BOARD_WIDTH) // "Length of the smaller side of the window."
+
+ val grid: GridPane = GridPane().addClass("grid")
+
+ override val root = hbox {
+ this.alignment = Pos.BOTTOM_CENTER
+ vbox {
+ this.alignment = Pos.CENTER
+ add(grid)
+ }
+ }
+
+ var selected: Node? = null
+ val hovers = ArrayList()
+
+ fun addToGrid(child: Node, coordinates: Coordinates) {
+ grid.add(child, coordinates.x, Connect4Constants.BOARD_HEIGHT - 1 - coordinates.y)
+ }
+
+ override fun onNewState(oldState: GameState?, state: GameState?) {
+ selected = grid
+ logger.debug { "New State: $state" }
+ grid.children.clear()
+ hovers.clear()
+ selected = null
+
+ // this ensures proper sizing of the board
+ (0 until Connect4Constants.BOARD_WIDTH).forEach { y ->
+ grid.add(PieceImage(gridSize, "cell").apply { opacity = 0.0 }, y, 0)
+ }
+
+ (0 until Connect4Constants.BOARD_HEIGHT).forEach { y ->
+ grid.add(PieceImage(gridSize, "cell").apply { opacity = 0.0 }, 0, y)
+ }
+
+ state?.let { state ->
+
+ var winningCoords: List = ArrayList()
+
+ state.isOver?.let { isOver ->
+ if(isOver && state.lastMove != null) {
+ winningCoords = GameRuleLogic.get4Connected(state.board, state.otherTeam, state.lastMove!!.position)
+ println(winningCoords.size)
+ }
+ }
+
+ state.board.forEach { (pos: Coordinates, field: FieldState) ->
+ if(field.team == null) {
+ return@forEach
+ }
+ val piece: PieceImage
+
+ if(winningCoords.contains(pos)) {
+ piece = PieceImage(gridSize, field.team.let { team -> "${team}-chip-winning".lowercase() })
+ piece.effect = Glow(1.0)
+ } else {
+ piece = PieceImage(gridSize, field.team.let { team -> "${team}-chip".lowercase() })
+ }
+
+ addToGrid(piece, pos)
+ }
+ }
+ }
+
+ override fun handleKeyPress(state: GameState, keyEvent: KeyEvent): Boolean {
+
+ var x = keyEvent.text.toIntOrNull() ?: return false
+ x -= 1
+
+ state.getSensibleMoves().forEach { move ->
+ if(move.position.x == x) {
+ sendHumanMove(move)
+ return true
+ }
+ }
+
+ return false
+ }
+
+ override fun renderHumanControls(state: GameState) {
+ state.getSensibleMoves().forEach { move ->
+
+ val piece = PieceImage(gridSize, "${state.currentTeam}-chip".lowercase())
+
+ piece.opacity = 0.5
+ //piece.effect = Glow(1.0)
+
+ piece.onLeftClick {
+ sendHumanMove(move)
+ }
+
+ addToGrid(piece, move.position)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/sc/gui/view/game/HuIBoard.kt b/src/main/kotlin/sc/gui/view/game/HuIBoard.kt
index ba21f91..e1a5224 100644
--- a/src/main/kotlin/sc/gui/view/game/HuIBoard.kt
+++ b/src/main/kotlin/sc/gui/view/game/HuIBoard.kt
@@ -286,7 +286,7 @@ class HuIBoard: GameBoard() {
putOnPosition(
Button(carrotCostString(car.amount)).apply {
fixHoverInsets()
- if(logger.isTraceEnabled)
+ if(logger.isTraceEnabled())
hoverProperty().listenImmediately {
logger.trace { "$this: $padding on hover $it" }
}
diff --git a/src/main/kotlin/sc/gui/view/game/PenguinsBoard.kt b/src/main/kotlin/sc/gui/view/game/PenguinsBoard.kt
index 50571cd..ef117e8 100644
--- a/src/main/kotlin/sc/gui/view/game/PenguinsBoard.kt
+++ b/src/main/kotlin/sc/gui/view/game/PenguinsBoard.kt
@@ -53,7 +53,7 @@ class PenguinBoard: View() {
size.bind(Bindings.min(widthProperty(), heightProperty().multiply(1.6)))
anchorpane {
this.paddingAll = AppStyle.spacing
- val stateListener = ChangeListener { _, oldState, state ->
+ val stateListener = ChangeListener { _, oldState: GameState?, state ->
clearTargetHighlights()
if(state == null) {
//children.remove(BOARD_SIZE.toDouble().pow(2).toInt(), children.size)
diff --git a/src/main/resources/META-INF/services/sc.gui.view.GameBoard b/src/main/resources/META-INF/services/sc.gui.view.GameBoard
index aaec2a4..bc28cf4 100644
--- a/src/main/resources/META-INF/services/sc.gui.view.GameBoard
+++ b/src/main/resources/META-INF/services/sc.gui.view.GameBoard
@@ -1 +1 @@
-sc.gui.view.game.PiranhasBoard
+sc.gui.view.game.Connect4Board
diff --git a/src/main/resources/connect4/board.png b/src/main/resources/connect4/board.png
new file mode 100644
index 0000000..58247b5
Binary files /dev/null and b/src/main/resources/connect4/board.png differ
diff --git a/src/main/resources/connect4/cell-debug.png b/src/main/resources/connect4/cell-debug.png
new file mode 100644
index 0000000..5415b4e
Binary files /dev/null and b/src/main/resources/connect4/cell-debug.png differ
diff --git a/src/main/resources/connect4/cell.png b/src/main/resources/connect4/cell.png
new file mode 100644
index 0000000..e374f6d
Binary files /dev/null and b/src/main/resources/connect4/cell.png differ
diff --git a/src/main/resources/connect4/chip-gelb-winning.png b/src/main/resources/connect4/chip-gelb-winning.png
new file mode 100644
index 0000000..3af3c44
Binary files /dev/null and b/src/main/resources/connect4/chip-gelb-winning.png differ
diff --git a/src/main/resources/connect4/chip-gelb.png b/src/main/resources/connect4/chip-gelb.png
new file mode 100644
index 0000000..59421bc
Binary files /dev/null and b/src/main/resources/connect4/chip-gelb.png differ
diff --git a/src/main/resources/connect4/chip-rot-winning.png b/src/main/resources/connect4/chip-rot-winning.png
new file mode 100644
index 0000000..1355777
Binary files /dev/null and b/src/main/resources/connect4/chip-rot-winning.png differ
diff --git a/src/main/resources/connect4/chip-rot.png b/src/main/resources/connect4/chip-rot.png
new file mode 100644
index 0000000..d1d59fa
Binary files /dev/null and b/src/main/resources/connect4/chip-rot.png differ