diff --git a/game/tictactoe-ktree/.gitignore b/game/tictactoe-ktree/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/game/tictactoe-ktree/.gitignore @@ -0,0 +1 @@ +/build diff --git a/game/tictactoe-ktree/build.gradle b/game/tictactoe-ktree/build.gradle new file mode 100644 index 0000000..205d93a --- /dev/null +++ b/game/tictactoe-ktree/build.gradle @@ -0,0 +1,6 @@ +apply plugin: com.soywiz.korge.gradle.KorgeGradlePlugin + +korge { + id = "org.korge.samples.game.tictactoe" + targetDefault() +} diff --git a/game/tictactoe-ktree/src/commonMain/kotlin/main.kt b/game/tictactoe-ktree/src/commonMain/kotlin/main.kt new file mode 100644 index 0000000..d9a9a9c --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/kotlin/main.kt @@ -0,0 +1,19 @@ +import com.soywiz.korge.* +import com.soywiz.korge.scene.* +import com.soywiz.korim.color.* +import com.soywiz.korinject.* +import com.soywiz.korma.geom.* +import scene.* + +suspend fun main() = Korge(Korge.Config(module = TicTacToeModule)) + +object TicTacToeModule : Module() { + override val mainScene = InGameScene::class + override val bgcolor = Colors["#2b2b2b"] + override val size = SizeInt(640, 480) + + override suspend fun AsyncInjector.configure() { + mapPrototype { InGameScene() } + } +} + diff --git a/game/tictactoe-ktree/src/commonMain/kotlin/model/model.kt b/game/tictactoe-ktree/src/commonMain/kotlin/model/model.kt new file mode 100644 index 0000000..1c509ac --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/kotlin/model/model.kt @@ -0,0 +1,88 @@ +package model + +import com.soywiz.kds.* +import com.soywiz.korma.geom.* + +data class TicTacToeModel( + val board: Array2 = Array2(3, 3) { CellKind.EMPTY } +) { + companion object { + operator fun invoke(str: String) = TicTacToeModel(Array2(str) { char, x, y -> + when (char) { + 'X' -> CellKind.CROSS + 'O' -> CellKind.CIRCLE + else -> CellKind.EMPTY + } + }) + } + + override fun toString(): String = board.toString(mapOf( + CellKind.EMPTY to '.', + CellKind.CROSS to 'X', + CellKind.CIRCLE to 'O', + )) +} + +enum class CellKind { CIRCLE, CROSS, EMPTY } + +sealed class GameResult { + data class Winner(val player: CellKind, val winnerCells: List) : GameResult() + object Tie : GameResult() + object InProgress : GameResult() +} + +sealed class Command { + data class PlaceChip(val x: Int, val y: Int, val kind: CellKind) : Command() +} + +data class TicTacToeTransition( + val oldModel: TicTacToeModel, + val newModel: TicTacToeModel, + val commands: List +) + +fun TicTacToeModel.checkValidCell(x: Int, y: Int): Boolean = board[x, y] == CellKind.EMPTY + +fun TicTacToeModel.place(x: Int, y: Int, kind: CellKind): TicTacToeTransition { + return TicTacToeTransition( + oldModel = this, + newModel = TicTacToeModel(this.board.map2 { cx, cy, cv -> if (cx == x && cy == y) kind else cv }), + commands = listOf( + Command.PlaceChip(x, y, kind) + ) + ) +} + +fun TicTacToeModel.checkLine(x: Int, y: Int, dx: Int, dy: Int): GameResult.Winner? { + val startCellValue = board[x, y] + val points = arrayListOf() + val lineWithAllSameChipsNonEmpty = (0 until 3).all { n -> + val px = x + (dx * n) + val py = y + (dy * n) + val value = board[px, py] + points.add(PointInt(px, py)) + value == startCellValue && value != CellKind.EMPTY + } + return if (lineWithAllSameChipsNonEmpty) GameResult.Winner(startCellValue, points) else null +} + +fun TicTacToeModel.checkResult(): GameResult { + // . | . | . + // --------- + // . | . | . + // --------- + // . | . | . + + for (n in 0 until 3) { + checkLine(n, 0, 0, +1)?.let { return it } // Verticals + checkLine(0, n, +1, 0)?.let { return it } // Horizontals + } + checkLine(0, 0, +1, +1)?.let { return it } // Diagonal1 + checkLine(2, 0, -1, +1)?.let { return it } // Diagonal2 + + if (board.all { it != CellKind.EMPTY }) { + return GameResult.Tie + } + + return GameResult.InProgress +} diff --git a/game/tictactoe-ktree/src/commonMain/kotlin/scene/InGameScene.kt b/game/tictactoe-ktree/src/commonMain/kotlin/scene/InGameScene.kt new file mode 100644 index 0000000..7a760e2 --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/kotlin/scene/InGameScene.kt @@ -0,0 +1,105 @@ +package scene + +import com.soywiz.korge.input.* +import com.soywiz.korge.scene.* +import com.soywiz.korge.view.* +import com.soywiz.korge.view.ktree.* +import com.soywiz.korio.file.std.* +import model.* + +class InGameScene( + +) : Scene() { + override suspend fun Container.sceneInit() { + addChild(resourcesVfs["board.ktree"].readKTree(views)) + } + + override suspend fun Container.sceneMain() { + var model = TicTacToeModel() + + setResultText("") + + var turn = CellKind.CROSS + + // Register the input events + setModel(model) + for (y in 0 until 3) { + for (x in 0 until 3) { + getCell(x, y).first.mouse { + onOver { if (model.checkResult() is GameResult.InProgress) cellSetHighlight(x, y, true) } + onOut { if (model.checkResult() is GameResult.InProgress) cellSetHighlight(x, y, false) } + onUp { + val result = model.checkResult() + + if (result is GameResult.InProgress) { + if (model.checkValidCell(x, y)) { + val transition = model.place(x, y, turn) + model = transition.newModel + executeTransition(transition) + turn = if (turn == CellKind.CROSS) CellKind.CIRCLE else CellKind.CROSS + //cellSetKind(x, y, model.CellKind.CIRCLE) + + val result = model.checkResult() + when (result) { + is GameResult.Winner -> { + setResultText("${result.player} wins") + for (cell in result.winnerCells) { + cellSetHighlight(cell.x, cell.y, true) + } + } + GameResult.Tie -> { + setResultText("TIE!") + } + GameResult.InProgress -> Unit + } + } + } else { + sceneContainer.changeTo() + //model = model.TicTacToeModel() + //setModel(model) + //setResultText("") + } + } + } + } + } + } + + + fun Container.setResultText(text: String) { + val gameResultView = stage["gameresult"] + (gameResultView.firstOrNull as? Text?)?.text = text + } + + fun Container.executeTransition(transition: TicTacToeTransition) { + for (command in transition.commands) { + when (command) { + is Command.PlaceChip -> { + cellSetKind(command.x, command.y, command.kind) + } + } + } + } + + fun Container.setModel(model: TicTacToeModel) { + for (y in 0 until 3) { + for (x in 0 until 3) { + cellSetKind(x, y, model.board[x, y]) + cellSetHighlight(x, y, false) + } + } + } + + fun Container.getCell(row: Int, column: Int): QView = this["row$row"]["cell$column"] + + fun Container.cellSetKind(row: Int, column: Int, kind: CellKind) { + val cell = getCell(row, column) + cell["cross"].alpha(if (kind == CellKind.CROSS) 1.0 else 0.0) + cell["circle"].alpha(if (kind == CellKind.CIRCLE) 1.0 else 0.0) + } + + fun Container.cellSetHighlight(row: Int, column: Int, highlight: Boolean) { + val cell = getCell(row, column) + cell["highlight"].alpha(if (highlight) 0.2 else 0.0) + } +} diff --git a/game/tictactoe-ktree/src/commonMain/resources/board.ktree b/game/tictactoe-ktree/src/commonMain/resources/board.ktree new file mode 100644 index 0000000..d2568b0 --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/resources/board.ktree @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/game/tictactoe-ktree/src/commonMain/resources/cell.ktree b/game/tictactoe-ktree/src/commonMain/resources/cell.ktree new file mode 100644 index 0000000..38b0fc8 --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/resources/cell.ktree @@ -0,0 +1,5 @@ + + + + + diff --git a/game/tictactoe-ktree/src/commonMain/resources/circle.png b/game/tictactoe-ktree/src/commonMain/resources/circle.png new file mode 100644 index 0000000..bca63b7 Binary files /dev/null and b/game/tictactoe-ktree/src/commonMain/resources/circle.png differ diff --git a/game/tictactoe-ktree/src/commonMain/resources/cross.png b/game/tictactoe-ktree/src/commonMain/resources/cross.png new file mode 100644 index 0000000..b6d535e Binary files /dev/null and b/game/tictactoe-ktree/src/commonMain/resources/cross.png differ diff --git a/game/tictactoe-ktree/src/commonMain/resources/korge.png b/game/tictactoe-ktree/src/commonMain/resources/korge.png new file mode 100644 index 0000000..0526e1d Binary files /dev/null and b/game/tictactoe-ktree/src/commonMain/resources/korge.png differ diff --git a/game/tictactoe-ktree/src/commonMain/resources/row.ktree b/game/tictactoe-ktree/src/commonMain/resources/row.ktree new file mode 100644 index 0000000..7fde7ff --- /dev/null +++ b/game/tictactoe-ktree/src/commonMain/resources/row.ktree @@ -0,0 +1,5 @@ + + + + + diff --git a/game/tictactoe-ktree/src/commonTest/kotlin/TicTacToeModelTest.kt b/game/tictactoe-ktree/src/commonTest/kotlin/TicTacToeModelTest.kt new file mode 100644 index 0000000..297bfd4 --- /dev/null +++ b/game/tictactoe-ktree/src/commonTest/kotlin/TicTacToeModelTest.kt @@ -0,0 +1,41 @@ +import com.soywiz.korma.geom.* +import model.* +import kotlin.test.* + +class TicTacToeModelTest { + @Test + fun test() { + assertEquals( + """ + ... + .X. + ... + """.trimIndent(), + TicTacToeModel().place(1, 1, CellKind.CROSS).newModel.toString() + ) + } + + @Test + fun testResult() { + assertEquals(GameResult.InProgress, TicTacToeModel().checkResult()) + assertEquals(GameResult.InProgress, TicTacToeModel().place(1, 1, CellKind.CROSS).newModel.checkResult()) + + assertEquals(GameResult.Winner(CellKind.CROSS, listOf(PointInt(1, 0), PointInt(1, 1), PointInt(1, 2))), TicTacToeModel(""" + .X. + .X. + .X. + """.trimIndent()).checkResult()) + + assertEquals(GameResult.Tie, TicTacToeModel(""" + OXX + XOO + OXX + """.trimIndent()).checkResult()) + + assertEquals(GameResult.InProgress, TicTacToeModel(""" + OXX + XO. + OXX + """.trimIndent()).checkResult()) + } +} diff --git a/game/tictactoe-ktree/src/commonTest/kotlin/test.kt b/game/tictactoe-ktree/src/commonTest/kotlin/test.kt new file mode 100644 index 0000000..666f779 --- /dev/null +++ b/game/tictactoe-ktree/src/commonTest/kotlin/test.kt @@ -0,0 +1,26 @@ +import com.soywiz.klock.* +import com.soywiz.korge.input.* +import com.soywiz.korge.tests.* +import com.soywiz.korge.tween.* +import com.soywiz.korge.view.* +import com.soywiz.korim.color.* +import com.soywiz.korma.geom.* +import kotlin.test.* + +class MyTest : ViewsForTesting() { + @Test + fun test() = viewsTest { + val log = arrayListOf() + val rect = solidRect(100, 100, Colors.RED) + rect.onClick { + log += "clicked" + } + assertEquals(1, views.stage.numChildren) + rect.simulateClick() + assertEquals(true, rect.isVisibleToUser()) + tween(rect::x[-102], time = 10.seconds) + assertEquals(Rectangle(x=-102, y=0, width=100, height=100), rect.globalBounds) + assertEquals(false, rect.isVisibleToUser()) + assertEquals(listOf("clicked"), log) + } +} \ No newline at end of file diff --git a/game/tictactoe-ktree/src/jvmMain/kotlin/GenerateResources.kt b/game/tictactoe-ktree/src/jvmMain/kotlin/GenerateResources.kt new file mode 100644 index 0000000..1a0ce2d --- /dev/null +++ b/game/tictactoe-ktree/src/jvmMain/kotlin/GenerateResources.kt @@ -0,0 +1,26 @@ +import com.soywiz.korim.bitmap.* +import com.soywiz.korim.color.* +import com.soywiz.korim.format.* +import com.soywiz.korio.file.std.* +import com.soywiz.korma.geom.vector.* +import kotlinx.coroutines.* + +object GenerateResources { + @JvmStatic + fun main(args: Array) { + runBlocking { + Bitmap32(64, 64).context2d { + stroke(Colors.RED, lineWidth = 6.0) { + line(0 + 4, 0 + 4, 64 - 4, 64 - 4) + line(64 - 4, 0 + 4, 0 + 4, 64 - 4) + } + }.writeTo(localCurrentDirVfs["src/commonMain/resources/cross.png"], PNG) + + Bitmap32(64, 64).context2d { + stroke(Colors.BLUE, lineWidth = 6.0) { + circle(32, 32, 32 - 4) + } + }.writeTo(localCurrentDirVfs["src/commonMain/resources/circle.png"], PNG) + } + } +} \ No newline at end of file