From ca097b9970786b2e8e2392ef5736cf15f9e88d6c Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 2 Dec 2020 22:29:01 +0100 Subject: [PATCH] Added tictactoe game sample using ktree --- game/tictactoe-ktree/.gitignore | 1 + game/tictactoe-ktree/build.gradle | 6 + .../src/commonMain/kotlin/main.kt | 19 ++++ .../src/commonMain/kotlin/model/model.kt | 88 +++++++++++++++ .../commonMain/kotlin/scene/InGameScene.kt | 105 ++++++++++++++++++ .../src/commonMain/resources/board.ktree | 10 ++ .../src/commonMain/resources/cell.ktree | 5 + .../src/commonMain/resources/circle.png | Bin 0 -> 1176 bytes .../src/commonMain/resources/cross.png | Bin 0 -> 396 bytes .../src/commonMain/resources/korge.png | Bin 0 -> 14015 bytes .../src/commonMain/resources/row.ktree | 5 + .../commonTest/kotlin/TicTacToeModelTest.kt | 41 +++++++ .../src/commonTest/kotlin/test.kt | 26 +++++ .../src/jvmMain/kotlin/GenerateResources.kt | 26 +++++ 14 files changed, 332 insertions(+) create mode 100644 game/tictactoe-ktree/.gitignore create mode 100644 game/tictactoe-ktree/build.gradle create mode 100644 game/tictactoe-ktree/src/commonMain/kotlin/main.kt create mode 100644 game/tictactoe-ktree/src/commonMain/kotlin/model/model.kt create mode 100644 game/tictactoe-ktree/src/commonMain/kotlin/scene/InGameScene.kt create mode 100644 game/tictactoe-ktree/src/commonMain/resources/board.ktree create mode 100644 game/tictactoe-ktree/src/commonMain/resources/cell.ktree create mode 100644 game/tictactoe-ktree/src/commonMain/resources/circle.png create mode 100644 game/tictactoe-ktree/src/commonMain/resources/cross.png create mode 100644 game/tictactoe-ktree/src/commonMain/resources/korge.png create mode 100644 game/tictactoe-ktree/src/commonMain/resources/row.ktree create mode 100644 game/tictactoe-ktree/src/commonTest/kotlin/TicTacToeModelTest.kt create mode 100644 game/tictactoe-ktree/src/commonTest/kotlin/test.kt create mode 100644 game/tictactoe-ktree/src/jvmMain/kotlin/GenerateResources.kt 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 0000000000000000000000000000000000000000..bca63b7bd038fd4e059ef9eacc2c29be3a505880 GIT binary patch literal 1176 zcmV;J1ZVq+P)4hRQ9-HyCFLK1aAwxd5+# zd`}4)lQzh130H<(;$j)(DktCxB3G8&R_sm877P559D=;!06qlrnGv`t40=aa1_px}bJw;V}fg_NPClSXWFY*R26})!@E#2HY$Yl6@ij*(#dFMfN ziuq_|mK@&-_;v9vW8#N(Y|wHb;VuVHp{#tzf&JB-*#D!4v^^Vo-xo!KTK2X#zZ1Wu zqM8`jd!EwiK7;KbjT6cCL%cP(n#~ibRXG+qIK0vB4|^Y2O6{lI*Z&;3Eejcy^mZ6StSP9;)ZC^T^`I% z3H{&N1{fp^-%ad?%ig5bMks87$U;vuh>J= z#M__eh7>>oX3N^hsS%B;n%qzV2^c0$uFFXeAekK*Y4vN9UMrD+{oan_#PT2IhLM7R zSt61hFgq?I9WS{dq$FUF*al`OFE1<-J8g4A$bf_c1Y6DQgq*Fr3*73bZ{3d$FIQY6 zou;|f&wzjhhqD2?Px>JAwKX)pxiC8+N64qpP!D%)BjBD}^^6D@LF`-We<5Hjlz^wc z1#Zkv4^P+#PC5j6;uk_dUe`=T5`g75kBv^MG^NjzASeNK1lx|;X&33V$*q0{1neW& z8b&AVa-rWcL65-8BEE3y#Jn4Y}VjCQA!wDduiC{UT9;)N3pmYU_ z6b|ZouR=XIjJpkS&JD{E{0I@*UPb>VLlXy}Zdi+~#|tH3kI>)6qmc}DN>ZYV=}s;E z3!j1B6Sm+kaxXS068wBb7IS&qFja?(4JM0^IBIl}rV1*l0?v@S2I(uQ3QgrQOK@CP z@e2jM83#pSgI9=wMNUu^ zDJsc$>p_IjImb=ytBLzY<<4N~s%n*{-yEunEr*qtOu{IUNac`%tJVJ!vF5)TGL@?m zgy!l5*(jC>mFZbw2A)Vn-NRso06)`iFoj_)hKy>wFI|4)W0j^hpd|>q!^oJ@_YQkK qKbl6)7s|M9X%`y$YmevV+I|Bju-`(+v_aYc0000=7D)L0oJs52r*Fk?fEV9taT!HoqWf;)pJfV;dW2#mZ35S+Xv7_7VnAiTULD2==Z zFrB<4IIX;d0KI&VNErElgu=-;M!1%}hm&uN5K~?t0>6AmggEja5grxtmvHjw1EjtM zQa=P!zXnr(2&MiMPW>~~BSIfRLVz}cBsg6J4PcrGnxOCzEC696Sc1Vt@Bo5|;0fX$ z!5zSp0YTo41xenV2~pmR4Ow2DkwIRKl}TQlnNePhomsxyA%eU_guI8QHX=~TS43z! qJ4h+N5~1nzD7E}TggW_slFbwP8YWjJtoA4X0000u+ zq`kJ*&H&)niZ~z$uVqz*YISQ_qm#Xh&04+$;OdAAgfxs=mZ!W7}+ueyJ?b$N;FppArhWO&-epr!1vgU`LFIgE&FumAfS8&)6YR~K@& zHpr{$0v!!FUg3Z2li!h#7f*f;^k4;_sk!7`?R)scIhXfmoJ)Ni-@2L=pW6F!D?m=$ zzt#qEI^dQrxT6p9J4}*n*Ty4w$=Tr`5Z@iOmOhEU$k__O_N=|umM&q?cqIg_5mZsy zSZ^%F{u0)x|7T+69iMZzcfVPwsoG8dU^w5j${MISbA-e$tQ&W_)IZ;!ew&pNn6vLL zS1v6-^wH|EsGNPN4&h~nV+*UFb`Y#?80(*jr!Vz zb^3VQZTMzGc{9%k@_)Js+P$ej0#SQ{&+sNt!pcr~dOE3Y$LYOB&PqXXH7D=Ay^j{~ zRt{~ZUBf#GG97`q(-X7?ngxbJXq^<2F(9mE&~_ko_M1mAe%=L_xEy~6IKvX=gSpM! zU9w$EY$3m0;3lL7w(3o(KV5DvX4z1!(;6P$iEGZAj>ovX8WEXCI>)ZlMoUyv?pAanS{LA4LWfxu!V&P zO*nerj6Ily)GsHbs^Q{;oqLY^h_k*4-h8M|)}RQ8>bwk^#5he7<++B}g|=Y18Vv!Nk)xfCcGb=!-AS>( z?LM<)HUsCfx`C){<>>c@TaaQS6-Lbk&d<@@A6Ui;~fqn z@Ugv6VCZcf0eYSJYURkyO5ALRvSWTsg}3TJ{+aG@+B=)GMc7;7>uJCDFA6J<=H4s0 zB@G(fc{L#8d#DYVAcdtQHfLX9Y0x{WeY;K50${xZ9kG#Da86MZkBbGyHwmD<;;4*F zx5d$s7die(m&DMSfSLRa^q!+P@XVZl+&1@!3l%QpbsLg#Y-2Q#B4}qPVBr$zWM){F%2@g_4>HZ30;QaL$C+_&`p@wB|*S1#O3R&23rRephr?^z;(hWGU zAEwyvk;9=%Aq*$uzrG+FQjta+#>x*9P|f18cQm26XNVX09)?TahMpM}r#+Nu6s(;~ z3k{T#TMq@EG)No>;<2OyRVk8RL207bJthGVkp}}&($USHJ5T%kBQjf-(`$Ae)=5|F zUDM;jexY-lD2=v}ko?v>o?0#)7^z|X~VEZvq;B5`XU8H1)GNZz9ElOy{Zz=F-e!znMA;$IoE>UEp1loZA zpX|5F;t`n17U0agV`rmQ|LgVwEl-GLdhPoSXt_OL8j48F1Fh4kwYlu${VPZO43N)+ zcJi8*7OF@zqhjDmxQe5_N7BzU7yq?;_Z2*d3s`G3^==0u=XXAA2irgPENX+Fb%P;k z5I&StLIqzz@C~?S-?XQq-CqzW5a84l8R16|epv&Amw@78il`nJh1|A0?QI7SqeBgw z?3kqJe<#7Ma3GMqmq6PCWK&a|W61jtfG0Of^}YZb{_F8~^2{nouOz_>G220bTT9S5 z7&%5uT=quxLgr0oH1TpUgx=LzU@9f3$UoRm6G-oDEK zS&kM!=_wa{f&3&C?_U6U9lv{KR~(|E24}46vKw#DJOojMammd?xWXNm?@nxiZMzP4 z$CU`*5oL;JHWU^93&ZJsehT_){tCEXCF$K|X&nGp;Lg3!vc6H~DUg~XXO})P2-xZ+ z{euP2XivfwRRVRQ&I8V3!{P6R_iFDF)Q- zt}9Y^I#4}Bu>6~&s;4Wga}URMUNMnVTuj3u;;?wH4Tw_$pLD>_Z|=olwghPXyS=Tg zBuN@6SlyfCD;nsb0KyHPWJpR#BIhK*Q|$WI`a22OVlUV2m(u;3@g45^e zN}GW{PU4!feF@}HWe2y<#wsyi1q_doNjV+64Uhr05b&%d2u?vIQ2%Z?5$5jNOQ6KA zZv`#^Ml?FGo1f_xKfues0+fM4mJ?ip1_M7ty4@bEKE60bw1+Z`k!*qz z(yGojnQpI39>bMDoI`B_q5}WK(WNaE0cLJODAj~eqRG9R2uu9HmnbW+=U-0NGZh8s zOQ)kZ1NX|tI1+7o{fP-78Y$px2{gOD9fVnDY?{4?8g3d#NVMndz%LMUDqBD06ztw; z1|ynIKu7pN${_xCJHSWQ*AtZ3VI&i&Yoaa1dEzbif%QinEd|iu695bhE+hY(v@ZaM zsSetO&jiibUs6``!nDS{m0v+%4FS!LA%xPdUH>EnjDT?wiCzLVNuEp*1%;M7f#p74 zp(TO2XA4_kbU0Iv`_j|SEe0nsXF^c`lR;om)ajCQ+)8crr*mKSXK#eLS~WLJqeYdN zF=y0ZL_r016RYp)h=!-n9gF*Krh5A0Fg*r(q=byRTWO_2gP!a9cNoW{nF4QSXKk9{?L*mv3!8bs+_07hjGfI!W zTbIfuIM02f>L*|&=Km1&iO}*M`}q9?uy!zonb6AfrUlrS%=ZUIT}fV*oPOO3lt|zM zhuAD+Pe6L6TVZ>rR^k@^U~S4_{$ljYRbZqEBFa-j??os4 z1LJx?R1>^(6w-hZzq~8J?9-V-XcPStWFbmf?9x}`e>;^1(WHp+zhUjCM>^p%a7i;L z422~kx0@&c`?%IJB^!=lKI4}M1A zj2AK|xN1v4_1dt-p3q{255|PyJ5cp*7-6q@{G26wCBe_49X1I0 z{yJfa%>ctC{@@&KJMP+MKh}f&DG=E}0P^7EZoANpnD7@ZdWNQE#P1z~0HOdq0we&A z2L9j5^w|Gd|34c4FBPp1LEhVz3|@!7d@}WBxT*1dN6z8V>mYC8_NQ&_7w-r>nD#C6 zWTRIFnDOUYFkbgqtj@uw-?!ze+7h;T$XrV8$_Wp+=@4q1^0B4E{J~BE*VJ`cDYMu1 z)`*<1f9juA`_97+``l5uHMnvUKZvhD3!8bnqJ31(PBJBW@oCHHt}sr;5ziF*^?%aw zaSVcl_1QUm7WPeD@UZGR-SR1wMdaw#**^rPbN0~poSUozkp%(4;t~6>cfrrU$45Pb zG;d&+H-u5%aVUH#4*agSe6_yTj{FM9`%NxUb+BjxE(&T>`V>Lk)en;l zCbhQ(mu?Jp*IHX{n+aJdSN+8}%BWt5k4l8_TkQ?sNC(_yji!)Cl1w(%_{bv{rEboC zJBUml;|+28&X#Ph(~w6-!Z@+BfwN*O{}}|UY{tZXQ)+&ZXop&t)2E%c&diQ9Upl_0 zfqDh`>8pfDtE--t2cC096y4j@7o%k>u@`R~{{x)Aw064={~_qA;OyHr&ytObst=JU zE`AF$`ZZoy&8qHXV&1-mjy{r(@G}b@TO1xp2CO6VNwu{F)g+y|T%gfP9RlWIm z0>wsnrbkULrmgvrlgb{(9cmmSpai8o_W=*~XN|GGptB#@|4G~|WYug%BZIb`s(S7X z9{fCxrMM~!+`M_8+B@D0lxC@=ciRj=LKdTy{F<>8ZAn!7O>H6}F~KZ}XD{vC>wx{2 zbVe@Y;D+7aYhCUMu}k1vA8tb#4#`m;v0Xp%@z-x>GBQ@f=^-l0y!d0h&*zilA7NS4bZI%N?QR#0Cg5c^la#hl7)ooEI zb2sDMhQaRh`H)qWRqefj0Pr-;9E-s%EOF9rsOy8IOzbURHn!r}w%ZR+zgMD3zx=g_ zsH_JH?6-lFLu3`bB?mqmiSIuTDy8zU##8)gB_AJK_x4$dch7ZQ#<@Gj26-1iV#QAX zABMhgoplmJcH&#faMIr!v%?3Qoq~TAkD9UkkIS97B=bZ2*Dn>P)zQ_G(3uxl$@}SE zIzM*1!}Yp$ptYZWAS&n%BM_a4-U3$RCzH*FKW)6oZze-bJ|g)?5gArn*|QQmG*9$b zlFIq&@%THrhLv8^(>?hdi-M147D3kQE(3PRJlE&jeSUb;LTvmC7O5dpTN&tKr3lB? z#$Y=VUt|%_N<<7fZ6?rzA@U_wTMvoyWa{@eT7nh{l^i6?rw%7o|LRs%0p24^L)2k` zyob<}+s`aoKLq_C&#_E#ISolt^w|NoD1wA}l}+` z48Y}nvMy!TN{G=w3Q>E&dtb{r*yaFfW#8 zg1ugD|M@p<%F^pJNS&PxFlc1OTpiCJVrXNZr6bTF1BAU<6%g$2t(10-F!C5qsv(V3 zd8USf0!_W1Wht)KM!uQKFkilI(aexg4KemyzF+vV?hCMbyiA~|N%7W=jZgX@j&n(> z8Oox_U82_4T40dJEw9(W;=05b8H6Dz3Rq1SDk&UcS$UlV_oe<~J5w0_tDMlU^Lv#4 z5%C?bVWg^$;pTS#*u9%j#?8+boFk(FrQN((4^_e0zRyBzS>8??b%N%GuZ+M|pS1zJ zZt9)iGtvgrbG}gLwK!$B7|iuYL zFwaGn;MBae`L5UWT|Jn2d+c<{v_FVjN9@zbNHiz_UHKby1`b5}gF&9MNb^pkl0+m-y< z1@a=_8TJE&yAPxNRIaNblc&J-2v$J-VsyUd?#nIlmxw zji8#)^m6m{eD1Y*3<16TrU*o;vC|wlyY*uC<%1`&;x6ivSH}$1aq333d&)HmGWiq9 zb@-Ygc8Jdi5K$M!4S%L8v&oKtg9kys6khh6pfBTWuRJ2#wsJ9^C{zMpV&|^{%)S2M zffeGFb?BJdC1^MX6tZ5!b=)ebOnljJ)STZfy=m#%p4W$$r<$QxyHH#U5en^S-|<}* z89(IygZ%N*?o;=Av|-s`yuRgVHy|5vFP$NYvp!3z%I(ntymzy;hFESokEmt z>Lgddp|Bl}UD2q=1#ZTrzdSHRM1I%U*gYX9erDv0(Qj0WP$r5Ne28Zj8A^eOM*JQ@ zCXB5WS^|8(L)R8qQM;i(5sDu`s>MWiFv5{T&qJrAC1^x`58w`|+V8}8J5X=QLNIT^ zHun$uchTUDZK`yvjiawJZ3ufGMo-LVz2R561;mr7Nz~iaq<2^3Geg9|d>>F3b_Z#f zN!S6Y6}PGPI1%a_TX~VIj-xBFxWM<%o{64foqNv@GSN3k%Qax%$#p2~5~A<~DcJ%# z$delwVJQCKGv1D7WJZ>qVVD;t#+<>F@;^@Oo2ipOf0TnQ_w=OH_thoKycxlkM!*s8 zR8&B(sLz9HmD&w10ZUPzlp$b%A*E29_KH;BYM}}?HY3$XEXF7udEiG9c;(5)*~;)Y zaV*VzSo4x`uR}~!jAoiMlSDdcM<8=&^IJw{7}_57RSp%<=H>Q2%z zQ04dr`3oaDIoFfSQ2K!>lzP;fIhy7 z4l`(7_JP~2;P>B7?^EwBJFucb4Eq>S@T@_deycy=E~fbkmP*Sz>76!PuLZ3cN}my&C4(Q`EF4YGqKP@*}NPwrh?>S_ouah zI(u$vnVO8>3NA||+uQKdI4ZQea^S?V3P6}$e;v0UwZR2~T+A6bf#(mSd7aDEBIpCK z+~eVZZNau{(?Y7u&p?kR2q3LLhWAjfg9^0Dv8T^;72OVy(cY106t!VjY-dJAmp-(Ge0KG6jR7P%fuy@-bg&nujwN@eFy+AHwh)f&+Ld4k+3kcfTJ@> z>cAuVyu1+@5TKWM>=34p=E{L3d%mS@RByh%&_OV39k^kx`y(8v!7p`LoBs2zD>3IA z^KUO+p%a}>fR0 zh%IhWRnmaKHd#D9mAwo?gbVujgh$VNu}m3zoA2oFJg`D29SNYqpD z87SO!UKnAV9+92o>z>##&9b3$SXPyW{{;-6-8>kUOn;Z#<-J?6bLfgB`IJLTePwg}PH3`|ie_;aL zhUR@pZ~8Gq(Zk?BBYa%Z1XaK%DirH}1-tYfzHfMKsX+2ylEZc-K#@-cs0I6?_+sZ= z{wKl~&~JR|FeWn2`z#zt#8|<&v(oHCtc;U~ua2$ojROy<^%+955EzFR_9K-AO~5<2 z-O)-0b@$YnAwXMTtXxKeB{qTb(C>>NM;m7nYr zaI!!|e!Cxdzh<(iAl`SuUTiwW!-J&S^-p(hfUh;Dj{Sw%^9PWb4n_f{u|dcMY%373 zt*rRSU+UP5nXasdE`n}gOX7|Q}2^ZcCAxIDb%ZBYL%Cs+M z`pm(rSfIwG5ZgnD2^=s<5f$zeqb|kx|fyRU82H zidX40c> z%mJk1Tkd1cD;{=qvVbNCm7w^YmOk=f=i-heEHmm7E|V#|Zyang82sKt?vV}w_MHA_ zS`mT$_N;^$Es(Wb?P(A(8`B9d1BJbYxIO~Ouv1UGKuI}jvbp*CY4#4Z z@H&P>vif4k+#4k#v9V0#w;TAUo-E4684wZr;bz&%sJYCKMOit6*9M9_krwe zIMAe>V zLp37cjqSu!1nol*SP1>npB88A%!d=shT&|pG_J;~&ULL~woxArhe!wB3pDq@my~bl zJ2r1Quo#`?jT5^3E#I3MxZ(p{s`Anvpq6A^(?BlIX$CsHPkI`}uY#g>L~%YUCyWF% zXd(42D70v7F+-}xZ`ENi|beWY8s<}k(N+F7~09c%A#~3HX z%%66$mHvFA9&6lDi31+R`|ZUK>8)|vO% zuS9Q<9z&f7tT;`*UtNXqcpETt<@MvGJ@qQ^e+03+7nv8C zT)7k*OCh@RI;6VaT>)b>K`kWFJz!{Gu^^j0iDe$&uvK@X4i*#_7b8h~B{_*Xl&Ke_7~JVe|;~)!Ri7m)LuUunBSNQq?+OjD+#rH3ARG&?+K= z#l-5vrD-YAmM7yelv zs30;Zw?Xj?&GVE6$ri}tH3JA+xD02owHf+H0iP#LOQ|D8b+|BJT|_kf)|I8UW6>w6 zLNw58J?$K!rsceh<$%NeAV4}05R{o@evw#U-E&N-{@8P1{}84qG6xw4qz?$LR@mWEZ;ASl&9AZLe z0udE_OA_&;Db9oj0!rzS`~lFhN)_FXP244@Aqqc}_WdP4x`FA4P1gqQh&~JgA}h^6 z^S?X$LH8Q*&sMxw*Bq-_j{-fCAtrx`PWb@J5;sC=H!Ofg{>C-t(~lpA#Q{d&HV z3Z=omlv_$u5ukvxBEzp53%Z?G7!@~Siibh_=&1Htth&94153~*ghPMp9bq)jNt}c0z0wS-lKn@R60KS#44tB z)ot`sICo5}q&Ntg7o;GHg>Cy<9!?a zN>7q$vZ%j?DT?sF9hiDgFqP4L5^)I;SHR%3wO#ZZ<>E`@L^7LPmy11E>P0nB z4vxlY6Q(sYY9Mow1b7|IO#<<+FSy85AKsY#da-8e4O4`$6gI;57GzA~FU37z#r6L;g}|0J6Vd z?9?Z08*f4dvAxFuZO0@)3icxc7e>Oeb8-nd_8fIRsBXhElQ7ez1LY<*mV)%pX@Jca zexD$z>pRzd9%y3u+lgh@sls8QDoB{ziqCY@R3mi<5HJNZ0Uv~r3(m^O#qKmzOK{Ej z&fqmas!%Ar4D1lx{vjJNzJ-&G3H<||c()Q>17+}QxgJuyYU;+v)In2ZvkLoD0{A3vdglM)hQ>|ZRy9iqVhvv?ngq}>Ws@LRR zg9<=vJBTJOMp?oXiX`wz6}z8AOZ$+ys~I?%ELrZgxqT47f~eu{{7pLC@N9C_|rnN|yu|F6EatVhte& z^$b4lv1WVVw(uB=a#y$;o4c+*7)J@n4&~e6RkJA)_n9R0SQ9|@!zAfgJq|e?J0EiqAXv9g- z#zk>l5najv)x<_h!_JQ=6W)I+6&%JrL5b&V9n$Db|e@J9Mq&7M9z(-%P1q{AqTDI+V4xr2Kg? z$=zUgxVO3TR`6P!sXU^r*$0r!%7Xm+tmO_`5 zbVn?#(s!<~5-~aAwep>43E$OuC#YvIiSlQla`FkBXaKerm&KcvL*r}`O)2*xvaadW zN@+IW{;K#8ktMNrO{x)9X2o_8+k>YKEbW{Q!wFKaZ_y{!odY`~aDfG+g5<;Lg2?lA zSZRWvsZvz289W%Ay%`mI;IU?9zY%SwjszrT#C9K@xKOiNuoOc) zNuZD9+k-biTpcAx5Cf3k^DdL}Y>I&rY95GN9@2IBn4oWEI$_*3{$lmdvemJf$kv|W zdi68+1_^-w0NcGBjX!i z=;Zj~(Saa-_s$;+Q>Mu<`!1ChBu;&zYE8^XdCL@1Dd#9u!AaR$3f(A=g(6Yr9%;NlUC1->C5#*2j3-sUE zzF0&&NmLdp0MR*cC2cu*{J(9OVt?*cg6-dU2=Bs;v^(gj>Fnxr?{KIiw zeVUny+x>o4pI5GO!}znt{<4b(tR#3(x!s@*Yy#r$VnARO6Cv zWu4&3SqTa?dU=WJBRqsJ63qeKd{4@^H8*J^=0DcGlb3ys+rlU!X%mCx&9OO?UA$9@$!owT`;mMx zBu)S)|C{w3uPv1O3_GF-=&u!M`U|m~@A$GqjBU>tMQ0Dx-Mh>mn&%MBvLR(xhBAnU zDUvYF4!fdz_K3-(5Xv@`3Am_0dp+++q*?Q-MUdi$*Viw7#AlqqC|=uWm91nRdF>44 zkwp-WZo&GJmjBq97jV;tw++&uL~=SN(@#Ev1^b@8P&d1L_s-z^YF4d=nx4L~W1|Bzdxera}YjO8z*Ez$Rd3>lGfzk(2=$wCs0m{~pL_YpKj zwbw1An1Hwo0Az1SgOpbxiCV@-uF~W$Mz{xNx2~9ibHZYT^zP<PvZ<(5b@@?}xFxh{4AcS&GEALxyeR}op;buX|>ICCho2K)#bWmOJc zS#B68RU)`8c=#UG>h0lBcgozY0T23rQ^lfpX%EctVztbevgjX2SqJ-|GnqhAk+y2PDo!zM&&an1i> z9p68vOwf5$D*WzNySj8y(7q*LP8)y91D}t>DGZ-c%FRh3?JTCgx*;i!_4>L zt|by|+ZdQi@){7Xokul8g6T<1qvR?q=+D3rp@=hFmKbPTeo@v(Z9jTc$XiMTxyIRg zT-ad=TXr`_)?>OJ|G45iEM1kMVbEO*MpHOblAwsu_XRs&9QRuulE-}Pkqrq@HM-2; zxuc7^ji8LP5gb;8gl5_}CT@3P#;~rnuofLkrt$-h*=+s1Y|GP-q~BGt^2*G>wtw2e z^mkl?xq*X>ar|+iEoLG)BH3R6pAQf|D~%jPvu$o=bP67ovnY({#*KAL+467mz78>}j8U04&aE@^!XfJEP%D3OjB$LTdz7rs9_to9y19_XYu=kO z^QgrZYvWmuQ4KfFcb_||-8-EsOX>6c{#}60`a4Qip*6IzbT$>seS3n*sLJktP6kh+ z@Su=^cwlcI$ec2I{Acri3wS;oJmVer0M`6Hg+P1^OEP-n-J1WiF(>Gs{7G&xKLxD$ zk!V*c_?@?y!3P^Ha8I7e?TI2|Mf^FBy6(iEtaooNdYOD{sO>jIM)!k?=h$b+DgQoI zQ?yuRdyw3dZ60JSScpC}I4Xx6Rzej}valh9Ix{hMQX zKjYtG8dplc*2bCCp9eokMqrN^P+S{jL8)caTZ+7W6kI1sM3-jC2a%T?tZ(?1MCWB3 z>Kp|pk6i(3z+&TQ{#)F1xte>{CV3iXkmZBk?gE6Px+9+gzbN2n2OMDVb}AQye8WQe zx#ChtXjQJyQG80^%iY^bx0f^A(Tj4=tBMQtx>K14SXdclEVD19LhPk>l1m7w2*8?ySbfds58f3v=SZb_=L{gN+vufF9oiMIQ4lL+#* zG&M?dx6Bu*7)0lCCIQ$P@8wyJztjoU5Q`i$%Jq7CvCwURi%F1~S zA#d_GCG8Oz)s<%2o=03ms#~{{=sn@jT%#w>gQ*NtPQPltm8el-U@f zTbje4`yz*pkbX^PmB$i`!V(^llX9k$OB*<<(~)cnzMf#~ifo6@dUTqL3L_j1OM9XkNEfLwUIP0Gtx0Gp4&2*p zO;a*Gh5mjS$A{u0vut{dmYTj(FSEsaJACzs~$hT~9Cy-PSTp8~L?-uy$e z1FM7)6S8du-!MuzBa`N5v;dag)un#55s7ew_{n7Wvrfqu1qbMS^jLm7?sWNYpu`Zn z*PcxiGL>w+ftrp8GvRdBx&cS^=7b0Kk6t~Ba@=&1YM6J_TFoW}s~pAr#Ki;wph*B8 jqbt!ax&i#JwB{t(>|?3qNX+;julCwFSU + + + + 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