Added tictactoe game sample using ktree

This commit is contained in:
soywiz
2020-12-02 22:29:01 +01:00
parent 9e3b8c0b1b
commit ca097b9970
14 changed files with 332 additions and 0 deletions

1
game/tictactoe-ktree/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,6 @@
apply plugin: com.soywiz.korge.gradle.KorgeGradlePlugin
korge {
id = "org.korge.samples.game.tictactoe"
targetDefault()
}

View File

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

View File

@@ -0,0 +1,88 @@
package model
import com.soywiz.kds.*
import com.soywiz.korma.geom.*
data class TicTacToeModel(
val board: Array2<CellKind> = 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<PointInt>) : 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<Command>
)
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<PointInt>()
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
}

View File

@@ -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<InGameScene>()
//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)
}
}

View File

@@ -0,0 +1,10 @@
<ktree width="640.0" height="480.0" gridWidth="20" gridHeight="20">
<treeviewref name="row0" x="220.0" y="140.0" sourceFile="row.ktree"/>
<treeviewref name="row1" x="220.0" y="220.0" sourceFile="row.ktree"/>
<treeviewref name="row2" x="220.0" y="300.0" sourceFile="row.ktree"/>
<solidrect x="290.0" y="134.99999999999997" width="5.0" height="235.0"/>
<solidrect x="370.0" y="134.99999999999997" width="5.0" height="235.0"/>
<solidrect x="214.99999999999991" y="210.0" width="235.0" height="5.0"/>
<solidrect x="214.99999999999994" y="290.0" width="235.0" height="5.0"/>
<text name="gameresult" x="340.0" y="100.0" text="CROSS wins" fontSize="64.0" verticalAlign="MIDDLE" horizontalAlign="CENTER"/>
</ktree>

View File

@@ -0,0 +1,5 @@
<ktree width="1280.0" height="720.0" gridWidth="20" gridHeight="20">
<image name="circle" width="64.0" height="64.0" sourceFile="circle.png"/>
<image name="cross" width="64.0" height="64.0" sourceFile="cross.png"/>
<solidrect name="highlight" colorMul="#ffe39543" alpha="0.2" width="64.0" height="64.0"/>
</ktree>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,5 @@
<ktree width="640.0" height="480.0" gridWidth="20" gridHeight="20">
<treeviewref name="cell0" sourceFile="cell.ktree"/>
<treeviewref name="cell1" x="80.0" sourceFile="cell.ktree"/>
<treeviewref name="cell2" x="160.0" sourceFile="cell.ktree"/>
</ktree>

View File

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

View File

@@ -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<String>()
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)
}
}

View File

@@ -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<String>) {
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)
}
}
}