数独游戏

某一天,看到微博@屠龙的胭脂介绍的几何数独游戏视频介绍,一看挺不错,很好玩!

要不要买一个给我儿子玩呢?回头想了一下,觉得以我儿子的智慧,可能不会玩。不由得感叹,像我这样才华横溢的程序员,怎么儿子是个大笨蛋呢?还不如我来编一个放在平板上,看看我儿子到底会不会玩。

因为我儿子是蛇蛇爱好者(前几年是奥特曼爱好者,6岁就开始不喜欢咸蛋超人),就来一个拯救蛇蛇大作战:蛇蛇数独游戏源代码@gitcode.net。

我不费吹灰之力,实现如下图的精美游戏SnakeSudoku,三条蛇蛇图片网上下载的,左上角的Logo我亲自书写的!结果我儿子一局都没玩通,我自己倒是玩了几十局……就刚才还在玩。

解压运行版下载地址

玩法也很简单:双击左上角白色区域,重开一局。按照第一行,第一列的要求,每种蛇蛇3条,放置于3x3区域。

如何做一个游戏或者如何用Java FX+Kotlin做一个游戏

做一个游戏,我自己感觉可以分为几个步骤:

  1. 游戏的核心概念;
  2. 游戏的玩家(单人/多人/分组);
  3. 游戏形式和胜负判定。

数独,是一个填数字的游戏。

数独,起源于18世纪初,瑞士数学家Leonhard Euler的拉丁方阵。到了1880年,建筑师Howard Garns 又在拉丁方阵的基础上创造了一种有意思的填数游戏,这就是数独的原型。

到了1970年,美国一本叫做《Math Puzzles and Logic Problems》的益智书上也出现了数独,但是,在那时,数独是被叫做“填数字”。

到了1984年,一位日本学者将数独带到了日本,并将其刊登在一本游戏图书上,起初叫做“数字は独身に限る”(单身数字),后又改名为“数独”(すうどく),其中“数”(すう)指的是“数字”,“独”(どく)指的是“唯一”。

这里我们设计是一个对玩家没有限定的填图片游戏,共有9张蛇蛇照片(每种3张),填入3x3方格,要求

  1. 每行的约束条件;
  2. 每列的约束条件;

这里行和列的约束条件都是某种蛇多少条(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.isPresentJava更新版本和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到图片。

然后是一个从ImageImageView的映射,这里写成一个扩展函数的形式,不得不说,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行实现几何数独游戏相关推荐

  1. 《Pygame游戏编程入门》学习——第3章 I/O、数据和字体:Trivia游戏

    <Pygame游戏编程入门>学习--第3章 I/O.数据和字体:Trivia游戏 第3章 挑战[^1] 问题1. 修改Trivia游戏,使用已有的代码来扩展你的背景,加入自己的用户输入和问 ...

  2. 论AI小游戏是怎么练成的——『寻物大作战』原理揭秘

    AI诞生以来,应用在了各个场景来帮助人们提高效率,优化体验.而在娱乐领域,越来越多的电子游戏开始将AI技术与游戏结合.今天我们为大家带来『寻物大作战』小游戏,真是称得上小小的身体,大大的能量!只要在规 ...

  3. windows游戏编程:球球大作战吃鸡版(C语言游戏开发)

    球球大作战: 前言: 本游戏用到了图形界面库graphics.h,图形界面库下载安装:https://blog.csdn.net/alzzw/article/details/100043681 下方有 ...

  4. 300大作战这款国风动漫二次元游戏如何?

    <300大作战>是一款国风动漫的二次元MOBA手游,结合了精致的动漫人物设计,服装的国风搭配,以及简约的画面让玩家身临其境,想要找小姐姐陪你上分吗?那就拿出你的实力参与作战吧. 300大作 ...

  5. pygame做的著名游戏_用Python和Pygame写游戏-从入门到放弃(1)

    Pygame的介绍 Pygame是一组专门为编写游戏设计的Python模块,增加了SDL库功能.可以使你在Python语言中轻松的创建全功能的游戏和多媒体程序. Pygame是免费的,在GPL许可下发 ...

  6. python运行游戏是否需要pygame_用Python和Pygame写游戏-从入门到放弃(1)

    Pygame的介绍 Pygame是一组专门为编写游戏设计的Python模块,增加了SDL库功能.可以使你在Python语言中轻松的创建全功能的游戏和多媒体程序. Pygame是免费的,在GPL许可下发 ...

  7. 小游戏从入门到放弃?

    0.体验QQ轻游戏 需要使用Android手机 登录手Q开启厘米秀 侧滑点击人物形象或者选择任意一好友点击**「+」滑拔一下找到「厘米秀」** 搜索厘米秀 申请体验资格 开启厘米秀 侧滑 好友点「+」 ...

  8. [吐槽]爱国者电源G6 600w翻车 毁硬盘,这就是你以为的一线品牌?——拯救便宜电源大作战!

    Hi,我是潘多拉1号避难所的服主火线兔 这篇文章是2020年11月6日写的吐槽文 写这篇文章的目的是近期有朋友反映我的服务器已经成了"土豆服务器"了,劳资要解释一下发生了什么,当然 ...

  9. 什么是游戏建模 3D游戏建模入门难吗?没有美术基础可以学吗?

    什么是游戏建模 3D游戏建模入门难吗?没有美术基础可以学吗? 许多爱玩游戏的人,都知道什么是3D游戏建模! 目前的3D建模分为3D角色,3D场景,次世代角色,次世代场景. 与3D游戏相比,次世代游戏模 ...

  10. 用Python和Pygame写游戏-从入门到精通(1)

    From: http://eyehere.net/2011/python-pygame-novice-professional-1/ 博客刚开,打算做一个Pygame的系列,翻译自Will McGug ...

最新文章

  1. 想自学stm32不知道怎么买板子?我来告诉你新手该买哪一个!
  2. Kotlin functions
  3. sql2008 附加数据库时 错误5123
  4. ROS系统 创建工作空间与功能包
  5. python聚类分析如何确定分类个数_Python数据挖掘—聚类—KMeans划分法
  6. 杭电acm2015偶数求和
  7. RestTemplate配置使用OkHttpClient示例
  8. 【Zabbix】使用dbforbbix 2.2-beta监控Redhat 7.0上的Oracle、Mysql
  9. python读取有空行的csv_如何在使用python读取CSV文件时跳过空行
  10. Spring注解中@Configuration和@Configurable的区别
  11. 20155207王雪纯 《Java程序设计》实验一报告
  12. 僵尸存在......在.NET中?
  13. Ehcache整合spring配置
  14. python抽奖小程序_python实现简单的抽奖小程序,抽奖的内容从文件里面读取
  15. MacBook在任意文件夹目录打开终端
  16. Android虚拟机多开检测
  17. uniapp省市区三级联动
  18. 关于如何区分Android手机是32位还是64位
  19. TS-修饰符 与 static
  20. 优酷路由宝增加php,优酷路由宝旗舰版YK-L2刷改华硕[N14U N54U]5G 2G的7620老毛子Padavan固件方法...

热门文章

  1. 用Python爬中国银行指定日期九点三十分汇率
  2. vue 在线预览 word ,Excel,pdf,图片 数据流 内网文件流 亲测有效(word 目前支持docx文件以及doc文件(doc需要后端处理))
  3. 【转】学术论文写作方法
  4. Ansible(一) 配置安装
  5. 高效能人士的七个习惯总结
  6. 充分利用微博加快社区发展
  7. 2021微信红包封面免费领取最新攻略 春节免费微信红包封面序列号大全
  8. Battery (Coin Change)
  9. Apache Commons Poo GenericObjectPool 避免泄漏
  10. 旧台式电脑改软路由过程记录