JavaFX+Kotlin游戏从入门到放弃:拯救蛇蛇大作战又名454行实现几何数独游戏
数独游戏
某一天,看到微博@屠龙的胭脂介绍的几何数独游戏视频介绍,一看挺不错,很好玩!
要不要买一个给我儿子玩呢?回头想了一下,觉得以我儿子的智慧,可能不会玩。不由得感叹,像我这样才华横溢的程序员,怎么儿子是个大笨蛋呢?还不如我来编一个放在平板上,看看我儿子到底会不会玩。
因为我儿子是蛇蛇爱好者(前几年是奥特曼爱好者,6岁就开始不喜欢咸蛋超人),就来一个拯救蛇蛇大作战:蛇蛇数独游戏源代码@gitcode.net。
我不费吹灰之力,实现如下图的精美游戏SnakeSudoku,三条蛇蛇图片网上下载的,左上角的Logo我亲自书写的!结果我儿子一局都没玩通,我自己倒是玩了几十局……就刚才还在玩。
解压运行版下载地址
玩法也很简单:双击左上角白色区域,重开一局。按照第一行,第一列的要求,每种蛇蛇3条,放置于3x3区域。
如何做一个游戏或者如何用Java FX+Kotlin做一个游戏
做一个游戏,我自己感觉可以分为几个步骤:
- 游戏的核心概念;
- 游戏的玩家(单人/多人/分组);
- 游戏形式和胜负判定。
数独,是一个填数字的游戏。
数独,起源于18世纪初,瑞士数学家Leonhard Euler的拉丁方阵。到了1880年,建筑师Howard Garns 又在拉丁方阵的基础上创造了一种有意思的填数游戏,这就是数独的原型。
到了1970年,美国一本叫做《Math Puzzles and Logic Problems》的益智书上也出现了数独,但是,在那时,数独是被叫做“填数字”。
到了1984年,一位日本学者将数独带到了日本,并将其刊登在一本游戏图书上,起初叫做“数字は独身に限る”(单身数字),后又改名为“数独”(すうどく),其中“数”(すう)指的是“数字”,“独”(どく)指的是“唯一”。
这里我们设计是一个对玩家没有限定的填图片游戏,共有9张蛇蛇照片(每种3张),填入3x3方格,要求
- 每行的约束条件;
- 每列的约束条件;
这里行和列的约束条件都是某种蛇多少条(0-2)。
我一拍脑袋,就是显示一个4x4的方格,第一行和第一列的后三个方块显示约束,右下角3x3为填空区域,反正只有3钟蛇,就设计为点击更换。
剩下的就是本文所主要想交流的工作:如何利用Kotlin+Java FX把这个功能实现出来。
非常简单,整个454行程序分为四个文件:
- SnakeSudoku:实现数独游戏的逻辑,约束,如何判定解和求解算法;
- SnakeSudokuApp:游戏的入口,
Application
对象(JavaFX) - SnakeSudokuView:游戏界面的实现
- SnakeSudokuViewModel:沟通逻辑和界面的部分
入口程序
不管三七二十一,先从主程序开始。JavaFX应用程序,都有一个入口,也就是Application
的子类,这里的程序与Kotlin编写JavaFX的顺滑 中的例子并没有什么区别。这里不同的是,Stage
的初始样式,设定为UNDECORATED
,也就是没有默认的标题栏,因为我们是游戏,没有标题栏或者自己搞个有个性的伪标题栏显得Bigger更高……
这里有个函数askToExit
,是最VBox中上面的“X”按钮的响应,显示一个对话框确认是否退出。这个函数中的链式调用,包括.filter
,.isPresent
是Java
更新版本和Kotlin
的函数式编程的全新流派……
package org.cardc.snakesudoku
/*** 退出确认*/
fun askToExit() {Alert(Alert.AlertType.CONFIRMATION, "您确认需要退出游戏吗?").showAndWait().filter { response -> response === ButtonType.OK }.ifPresent { Platform.exit() }
}class SnakeSudokuApp : Application() {private val bc = "HONEYDEW"override fun start(stage: Stage) {val scene = Scene(VBox().apply {style = "-fx-background-color: $bc"Color.BLUEalignment = Pos.CENTER_RIGHTpadding = Insets(0.0)children.add(Button("×").apply {style = "-fx-background-color:$bc;-fx-font-size:32;"alignment = Pos.BOTTOM_CENTERsetOnAction { askToExit() }VBox.setVgrow(this, Priority.ALWAYS)padding = Insets(0.0)})children.add(root().apply {padding = Insets(0.0)})}, 4 * size + 5.0, 4 * size + 15.0).apply {setOnKeyPressed {if (it.code == KeyCode.ESCAPE) {askToExit()}}}stage.initStyle(StageStyle.UNDECORATED)stage.fullScreenExitKeyCombination = KeyCombination.keyCombination("Ctrl+F11")stage.isFullScreen = falsestage.icons.add(icon)stage.scene = scenestage.centerOnScreen()stage.show()}
}fun main() {Application.launch(SnakeSudokuApp::class.java)
}
游戏的主要界面,除了最上方的关闭按钮,都由一个叫root
的函数提供。自然而言,接下来我们就是要看root
函数。
游戏界面
游戏界面首先是方格的尺寸,蛇蛇图片,这里导入一个Image,保持长宽比,实际上在编辑图片是已经是正方形。还有一个图片就是Logo图片,同样的。
然后再存一个映射,从Snake
到图片。
然后是一个从Image
到ImageView
的映射,这里写成一个扩展函数的形式,不得不说,Kotlin扩展函数真香。
从root
函数的第一行就能看到,是一个GridPane
。在界面窗格的文章中有介绍和演示。
package org.cardc.snakesudokuconst val size = 200.0val brownSnake = Image(SnakeSudokuApp::class.java.getResourceAsStream("snake-brown.png"), size, size, true, true)
val redSnake = Image(SnakeSudokuApp::class.java.getResourceAsStream("snake-red.png"), size, size, true, true)
val greenSnake = Image(SnakeSudokuApp::class.java.getResourceAsStream("snake-green.png"), size, size, true, true)val icon = Image(SnakeSudokuApp::class.java.getResourceAsStream("ss.png"), size, size, true, true)val snakes = mapOf(Snake.RED to redSnake,Snake.GREEN to greenSnake,Snake.BROWN to brownSnake
)fun Image.view(d: Double = 100.0): ImageView {return ImageView(this).apply {maxWidth(d)maxHeight(d)isPreserveRatio = true}
}const val textStyle = "-fx-font-size: 64; -fx-family:fantasy; -fx-font-weight:bold; -fx-fill: orchid;"
fun root(): Parent = GridPane().apply {add(StackPane().apply {setOnMouseClicked { it ->if (it.clickCount == 2) { //viewModel.newPuzzle()}if (it.clickCount == 1 && it.isAltDown) { // 上上下左左右右AB,通关秘籍viewModel.solve()}if (it.clickCount == 1 && it.isControlDown) { //调试的时候才使用的秘籍viewModel.solve(false) {if (it==null){println("May have no solution.")}else{println(it)}}}}children.add(icon.view())children.add(Text("Double click to start a new game!\nEsc to Exit!").apply {textAlignment = TextAlignment.CENTERstyle = "-fx-family:fantasy; -fx-font-weight:bold; -fx-fill: darkblue;"StackPane.setAlignment(this, Pos.BOTTOM_CENTER)})}, 0, 0)// column 0, row constraintsviewModel.rc.value.forEachIndexed { index, pair ->add(StackPane().apply {children.add(snakes[pair.first]!!.view().apply {imageProperty().bind(Bindings.createObjectBinding({snakes[viewModel.rowSnake(index)]}, viewModel.rc))})children.add(Text("${pair.second}").apply {StackPane.setAlignment(this, Pos.CENTER_RIGHT)textProperty().bind(Bindings.createStringBinding({"${viewModel.rowCount(index)}"}, viewModel.rc))style = textStyle})}, 0, index + 1)}// row 0, column constraintsviewModel.cc.value.forEachIndexed { index, pair ->add(StackPane().apply {children.add(snakes[pair.first]!!.view().apply {imageProperty().bind(Bindings.createObjectBinding({snakes[viewModel.colSnake(index)]}, viewModel.cc))})children.add(Text("${pair.second}").apply {StackPane.setAlignment(this, Pos.BOTTOM_CENTER)textProperty().bind(Bindings.createStringBinding({"${viewModel.colCount(index)}"}, viewModel.cc))style = textStyle})}, index + 1, 0)}(0..2).forEach { i ->(0..2).forEach { j ->add(snakes[viewModel[i, j]]!!.view().apply {// 绑定每个ImageView的图像imageProperty().bind(Bindings.createObjectBinding( // 创建一个对象绑定到VM的解{snakes[viewModel[i, j]]},viewModel.sol))setOnMouseClicked {viewModel.changeAt(i, j) //if (viewModel.isSolved) {// show congratulation message.Alert(Alert.AlertType.INFORMATION,"您救了小蛇蛇们,真棒!").showAndWait()}}}, j + 1, i + 1)}}
}
这里面还有一个比较有意思的就是蛇蛇填空约束的行和列中,是一个图片加一个数字,那么就用StackPane
,同样在界面窗格的文章中有介绍和演示。
这里的图片和数字更新,采用的就是JavaFX
的属性绑定高级应用,下次有时间专门来写一篇Bindings
的高级用法。
以蛇蛇图片为例,ImageView
实例有一个属性,通过imageProperty()
来得到,这里把它绑定到用Bindings.createObjectBinding
返回的一个对象绑定上,这个函数的第一个参数是返回一个Image
的匿名函数,第二个参数是一个Observable
对象。简答一句话,当这个Observable
对象发生变化时,这里的图像就会自动更新为这个匿名函数的返回值。
imageProperty().bind(Bindings.createObjectBinding( // 创建一个对象绑定到VM的解{snakes[viewModel[i, j]]},viewModel.sol)
这里点击的事件,通过setOnAction
设定为一个匿名函数。
要不怎么说函数式编程呢。
这个界面部分,共有若干种操作请求:
- 新建游戏
- 【秘籍】自动求解
- 更换3x3数独中的蛇蛇
- 判断是否完成数独的求解
那么这几个逻辑,就体现在ViewModel
中。这个设计架构,在Kotlin编写JavaFX的顺滑一文中有详细描述。
连接界面与逻辑的视图模型
这里的视图模型,也就是viewModel
,在Kotlin中是一个特殊设计,就是单例模式,object
。
这里唯一值得注意的就是,一共建了三个SimpleObjectProperty
对象,貌似一个就行,但是我懒得弄了。
package org.cardc.snakesudokuobject viewModel {val rc = SimpleObjectProperty(SnakeSudoku().rowCount)val cc = SimpleObjectProperty(SnakeSudoku().columnCount)val sol = SimpleObjectProperty(SnakeSudoku())private fun update(s: SnakeSudoku) {rc.value = s.rowCountcc.value = s.rowCountsol.value = s}/*** UI触发点击3x3中间的一个位置* @param i Int* @param j Int*/fun changeAt(i: Int, j: Int) {sol.value = sol.value.cycleAt(i, j)}/*** UI 触发解开Sudoku* @param isUpdate Boolean :是否更新界面,默认是更新,否则仅在终端打印* @param func Function1<SnakeSudoku?, Unit> :求解后的回调函数,该函数会在Platform.runLater中调用*/fun solve(isUpdate: Boolean = true, func: (SnakeSudoku?) -> Unit = {}) {sol.value.randomSolve {it?.let {Platform.runLater { func(it) }}if (isUpdate)it?.let {Platform.runLater { update(it) }}}}/*** 重开一局*/fun newPuzzle() {update(newSudoku())}fun rowSnake(index: Int) = rc.value[index].firstfun rowCount(index: Int) = rc.value[index].secondfun colSnake(index: Int) = cc.value[index].firstfun colCount(index: Int) = cc.value[index].secondoperator fun get(i: Int, j: Int) = sol.value[i, j]val isSolved: Boolean get() = sol.value.isSolved}
对应着用户界面的四个请求,分别有函数与之对应,此外,这个部分还提供了用户界面中对游戏逻辑部分数据的访问。
最后,当然是这里所调用的游戏逻辑。游戏逻辑的部分呢,就什么都不涉及,只是游戏本身。
游戏逻辑
这个游戏非常简单,因此游戏逻辑的类也很简单,甚至我用的data class
,就是传值调用的类。
package org.cardc.snakesudoku/*** Enum for three tiles*/
enum class Snake {RED, GREEN, BROWN;fun next() = when (this) {RED -> GREENGREEN -> BROWNBROWN -> RED}
}/*** Sudoku Puzzle data class* @property rowCount Array<Pair<Snake, Int>> : constraints on three rows* @property columnCount Array<Pair<Snake, Int>> : constraints on three columns* @property solution Array<Array<Snake>> : 3x3 grid of solution* @constructor: all parameters are optional.*/
data class SnakeSudoku(var rowCount: Array<Pair<Snake, Int>> = arrayOf(Snake.RED to 1,Snake.GREEN to 1,Snake.BROWN to 0,), var columnCount: Array<Pair<Snake, Int>> = arrayOf(Snake.RED to 0,Snake.GREEN to 1,Snake.BROWN to 0,),var solution: Array<Array<Snake>> = arrayOf(arrayOf(Snake.RED, Snake.GREEN, Snake.GREEN), arrayOf(Snake.RED, Snake.GREEN, Snake.BROWN), arrayOf(Snake.RED, Snake.BROWN, Snake.BROWN))) {/*** String representation of the solution and constraints* @return String*/override fun toString(): String {val ss = "%8s %5s(%d) %5s(%d) %5s(%d)\n%5s(%d) %8s %8s %8s\n%5s(%d) %8s %8s %8s\n%5s(%d) %8s %8s %8s"return ss.format("",columnCount[0].first,columnCount[0].second,columnCount[1].first,columnCount[1].second,columnCount[2].first,columnCount[2].second,rowCount[0].first,rowCount[0].second,get(0, 0),get(0, 1),get(0, 2),rowCount[1].first,rowCount[1].second,get(1, 0),get(1, 1),get(1, 2),rowCount[2].first,rowCount[2].second,get(2, 0),get(2, 1),get(2, 2),)}private fun set(ss: Array<Snake>) {(0..2).forEach { i ->(0..2).forEach { j ->solution[i][j] = ss[i * 3 + j]}}}fun randomSolve(notify: (SnakeSudoku?) -> Unit) {val ss = SnakeSudoku(rowCount, columnCount, solution)val sv = arrayOf(Snake.RED, Snake.RED, Snake.RED,Snake.GREEN, Snake.GREEN, Snake.GREEN,Snake.BROWN, Snake.BROWN, Snake.BROWN)var i = 0.0Thread {while (!ss.isSolved) {sv.shuffle(r)ss.set(sv)i += 1.0if (i > 100 * 3.0.pow(9.0)) {notify(null)return@Thread}}notify(ss)}.start()}companion object {private val r = Random(System.nanoTime())fun newSudoku() = SnakeSudoku(Snake.values().apply { shuffle(r) }.map {it to r.nextInt(0, 2)}.toTypedArray(),Snake.values().apply { shuffle(r) }.map {it to r.nextInt(0, 2)}.toTypedArray())}private fun sumSnake(snake: Snake) =row(0).count { it == snake } + row(1).count { it == snake } + row(2).count { it == snake }private fun checkSolution(): Boolean {return arrayOf(*(0..2).map { checkRow(it) }.toTypedArray(),*(0..2).map { checkCol(it) }.toTypedArray(),*(arrayOf(Snake.RED, Snake.BROWN, Snake.GREEN).map {sumSnake(it) == 3}.toTypedArray())).all { it }}private fun checkRow(i: Int) = row(i).count {it == columnCount[i].first}.apply {} == rowCount[i].secondprivate fun checkCol(i: Int) = col(i).count { it == columnCount[i].first }.apply {} == columnCount[i].secondfun cycleAt(index: Int, index2: Int) = SnakeSudoku(rowCount, columnCount, solution).apply {set(index, index2, get(index, index2).next())}private fun col(i: Int): Array<Snake> {return arrayOf(solution[0][i], solution[1][i], solution[2][i])}private fun row(i: Int) = solution[i]operator fun get(i: Int, j: Int): Snake {return solution[i.coerceIn(0, 2)][j.coerceIn(0, 2)]}private operator fun set(i: Int, j: Int, s: Snake) {solution[i.coerceIn(0, 2)][j.coerceIn(0, 2)] = s}private fun toColumVector() = arrayOf(*col(0),*col(1),*col(2))private fun toRowVector() = arrayOf(*row(0), *row(1), *row(2))val isSolved get() = checkSolution()override fun equals(other: Any?): Boolean {if (this === other) return trueif (javaClass != other?.javaClass) return falseother as SnakeSudokuif (!rowCount.contentEquals(other.rowCount)) return falseif (!columnCount.contentEquals(other.columnCount)) return falseif (!solution.contentDeepEquals(other.solution)) return falsereturn true}override fun hashCode(): Int {var result = rowCount.contentHashCode()result = 31 * result + columnCount.contentHashCode()result = 31 * result + solution.contentDeepHashCode()return result}
}
结语
454行程序的确非常简单,虽然我儿子并不在意,在我平板上点了几把搞不定就直接去疯跑去了……可怜的老父亲只好自己多玩了几把……
JavaFX+Kotlin游戏从入门到放弃:拯救蛇蛇大作战又名454行实现几何数独游戏相关推荐
- 《Pygame游戏编程入门》学习——第3章 I/O、数据和字体:Trivia游戏
<Pygame游戏编程入门>学习--第3章 I/O.数据和字体:Trivia游戏 第3章 挑战[^1] 问题1. 修改Trivia游戏,使用已有的代码来扩展你的背景,加入自己的用户输入和问 ...
- 论AI小游戏是怎么练成的——『寻物大作战』原理揭秘
AI诞生以来,应用在了各个场景来帮助人们提高效率,优化体验.而在娱乐领域,越来越多的电子游戏开始将AI技术与游戏结合.今天我们为大家带来『寻物大作战』小游戏,真是称得上小小的身体,大大的能量!只要在规 ...
- windows游戏编程:球球大作战吃鸡版(C语言游戏开发)
球球大作战: 前言: 本游戏用到了图形界面库graphics.h,图形界面库下载安装:https://blog.csdn.net/alzzw/article/details/100043681 下方有 ...
- 300大作战这款国风动漫二次元游戏如何?
<300大作战>是一款国风动漫的二次元MOBA手游,结合了精致的动漫人物设计,服装的国风搭配,以及简约的画面让玩家身临其境,想要找小姐姐陪你上分吗?那就拿出你的实力参与作战吧. 300大作 ...
- pygame做的著名游戏_用Python和Pygame写游戏-从入门到放弃(1)
Pygame的介绍 Pygame是一组专门为编写游戏设计的Python模块,增加了SDL库功能.可以使你在Python语言中轻松的创建全功能的游戏和多媒体程序. Pygame是免费的,在GPL许可下发 ...
- python运行游戏是否需要pygame_用Python和Pygame写游戏-从入门到放弃(1)
Pygame的介绍 Pygame是一组专门为编写游戏设计的Python模块,增加了SDL库功能.可以使你在Python语言中轻松的创建全功能的游戏和多媒体程序. Pygame是免费的,在GPL许可下发 ...
- 小游戏从入门到放弃?
0.体验QQ轻游戏 需要使用Android手机 登录手Q开启厘米秀 侧滑点击人物形象或者选择任意一好友点击**「+」滑拔一下找到「厘米秀」** 搜索厘米秀 申请体验资格 开启厘米秀 侧滑 好友点「+」 ...
- [吐槽]爱国者电源G6 600w翻车 毁硬盘,这就是你以为的一线品牌?——拯救便宜电源大作战!
Hi,我是潘多拉1号避难所的服主火线兔 这篇文章是2020年11月6日写的吐槽文 写这篇文章的目的是近期有朋友反映我的服务器已经成了"土豆服务器"了,劳资要解释一下发生了什么,当然 ...
- 什么是游戏建模 3D游戏建模入门难吗?没有美术基础可以学吗?
什么是游戏建模 3D游戏建模入门难吗?没有美术基础可以学吗? 许多爱玩游戏的人,都知道什么是3D游戏建模! 目前的3D建模分为3D角色,3D场景,次世代角色,次世代场景. 与3D游戏相比,次世代游戏模 ...
- 用Python和Pygame写游戏-从入门到精通(1)
From: http://eyehere.net/2011/python-pygame-novice-professional-1/ 博客刚开,打算做一个Pygame的系列,翻译自Will McGug ...
最新文章
- 想自学stm32不知道怎么买板子?我来告诉你新手该买哪一个!
- Kotlin functions
- sql2008 附加数据库时 错误5123
- ROS系统 创建工作空间与功能包
- python聚类分析如何确定分类个数_Python数据挖掘—聚类—KMeans划分法
- 杭电acm2015偶数求和
- RestTemplate配置使用OkHttpClient示例
- 【Zabbix】使用dbforbbix 2.2-beta监控Redhat 7.0上的Oracle、Mysql
- python读取有空行的csv_如何在使用python读取CSV文件时跳过空行
- Spring注解中@Configuration和@Configurable的区别
- 20155207王雪纯 《Java程序设计》实验一报告
- 僵尸存在......在.NET中?
- Ehcache整合spring配置
- python抽奖小程序_python实现简单的抽奖小程序,抽奖的内容从文件里面读取
- MacBook在任意文件夹目录打开终端
- Android虚拟机多开检测
- uniapp省市区三级联动
- 关于如何区分Android手机是32位还是64位
- TS-修饰符 与 static
- 优酷路由宝增加php,优酷路由宝旗舰版YK-L2刷改华硕[N14U N54U]5G 2G的7620老毛子Padavan固件方法...