今年开始学习Scala语言,对它的强大和精妙叹为观止,同时也深深感到,要熟练掌握这门语言,还必须克服很多艰难险阻。

这时,我就在想,如果能有一种方式,通过实际的应用实例,以寓教于乐的方式,引导我们逐步掌握这门语言的精妙之处,那该

有多好呀!网上搜索,偶然发现了一个网友引路蜂移动软件的计算24点的Scala游戏代码:

http://blog.csdn.net/mapdigit/article/details/37498653

下载下来研究过之后,对Scala语言的很多特性,都很有启发和帮助。遗憾的是,这个游戏代码是命令行方式的,不够直观,

也不利于激发学习的兴趣。于是,我便在此基础之上,给游戏增加了GUI界面,在编程实践中领悟Scala语言的精妙,提升自己

Scala编程的实际能力,在此分享给各位学习Scala的网友。

首先是游戏运行的界面。选择四张扑克后,单击计算:

你也可以输入自己的答案,让系统检查:

整个游戏的工程代码都已上传到CSDN我的资源中:

http://download.csdn.net/detail/yangdanbo1975/9632225

工程代码架构如下图所示:

总共两个Scala类:

Calculate24主要是copy引路蜂移动软件网友的代码,实现了计算24点的算法逻辑;

CalculateGUI实现了游戏的GUI界面,用户可以通过点击扑克牌的方式选择不同数字进行计算,也可以输入自己的答案进行检查。

其余五个package中,common中放的是共用的比如扑克背面图片,其余四个package分别存放了四种花色的扑克图片, 从A到K,对应数字1到13.

在IT领域,解决问题的模式一般都是分而治之 ,先分层,再分块。在这个计算24点的应用中,Calculate24和CalculateGUI以及资源文件的package

划分,可以看做是分层,然后到了具体的实现类里面,再去分模块。

在Calculate24中实现了计算24点的算法。首先是定义了各种可能的加减乘除计算组合的模板:

(个人感觉这个似乎可以用代码逻辑生成的方式来改进,但是眼高手低,能力有限,所以就没有改进了,也没去验证是否穷尽了所有可能的计算组合)。

接下来定义的eval方法,是整个算法的核心,采用Scala语言强大的模式匹配功能,对通过前面模板加入数字组成的表达式进行递归求值:

要理解整个eval函数的递归算法,首先要了解这个递归方法的输入输出。输入很简单,就是一个表达式字符串,而输出则是一个自定义的Rational类:

这个Rational类通过整数型的分子分母来构造,这里利用Scala语言的操作符重载的方式,定义了加减乘除四种计算。

因为我给按加减乘除四种方式计算不出24点的情况,增加了一个平方根的运算符√,而Scala语言的平方根运算,参数及返回值都是Double类型,、

所以我给Rational类增加了toDouble和从Double构造的方法,同时,为了GUI界面调用方便,还增加了判断Rational是否有解的方法isResolved():

计算24点算法的核心,是在eval中对加减乘除组成的计算表达式进行模式匹配,而要进行Scala语言的模式匹配,就要定义代表不同操作运算的Case Class。

由于这些Case Class的共同之处都是计算,都有运算符和操作数,所以,抽象定义了一个二元操作的scala trait:

//二元操作
trait BinaryOp{
  val op:String
  def apply(expr1:String,expr2:String) = expr1 + op + expr2
  def unapply(str:String) :Option[(String,String)] ={
    val index=str indexOf (op)
    if(index>0)
      Some(str substring(0,index),str substring(index+1))
    else None
  }
}

这个trait并不复杂,它有一个操作符op,和操作符分开的前后表达式。在这里,我们可以清楚地看到,scala语言是如何精妙和强大。

首先,unapply方法接收到一段字符串,解析出其中的操作符位置,然后以操作符前后的两个表达式,调用apply方法,加上操作符构成具体的运算对象。

如果操作符不存在,直接返回None,没有运算对象产生。

我仿照这个二元操作的trait,依葫芦画瓢,写了一个单目运算的trait,实现了平方根运算:

//单目操作
trait UnitaryOp{
  val op:String
  def apply(expr:String) = op + expr
  def unapply(str:String) :Option[(String)] ={
    val index=str indexOf (op)
    if(index==0)
      Some(str substring(index+1))
    else None
  }
}

Scala中trait特质(相当于java的接口)定义好之后,就要在具体的实现类中引用了。这里定义了加减乘除四种Case Class分别实现了上面定义的二元操

作trait,代码相当简洁:(Rational类分子分母的表达方式也满足二元操作的trait要求)

object Multiply  extends {val op="*"} with BinaryOp
object Divide  extends {val op="/"} with BinaryOp
object Add  extends {val op="+"} with BinaryOp
object Subtract  extends {val op="-"} with BinaryOp
object Rational  extends {val op="\\"} with BinaryOp

我仿照定义了平方根运算的对象来实现单目运算的trait:

//added by Dumbbell Yang at 2016-09-17
object SquareRoot extends {val op="√"} with UnitaryOp

比较复杂的是Bracket(括号)对象的定义,牵涉到左右括号的压栈弹出及匹配:

同样,也是利用了Scala语言强大的unapply和apply方法进行表达式字符串到括号对象的解析和定义。

所有的Case Class对象都定义好了,就是在eval方法中对传入的计算表达式字符串进行模式匹配了,其实就是一个简单的递归:

def eval(str:String):Rational = {
    str match {
      //括号
      case Bracket(part1, expr, part2) => eval(part1 + eval(expr) + part2)
      //加
      case Add(expr1, expr2) => eval(expr1) + eval(expr2)
      //减
      case Subtract(expr1, expr2) => eval(expr1) - eval(expr2)
      //乘
      case Multiply(expr1, expr2) => eval(expr1) * eval(expr2)
      //除
      case Divide(expr1, expr2) =>  eval(expr1) / eval(expr2)
      //空
      case "" => new Rational(0, 1)
      //Rational表达式
      case Rational(expr1, expr2) => new Rational(expr1.trim toInt, expr2.trim toInt)
      //其他,case到这里,应该只剩下立即数了
      case _ => {
        str match{
          //方根 √N
          case SquareRoot(expr1) => new Rational(Math.sqrt(eval(expr1).toDouble))
          //纯数字
          case _ => new Rational(str.trim toInt, 1)
        }
      }

}
  }

最先是括号,具有最高优先级;然后是四种运算,最后是空及Rational及其他情况的处理。处理逻辑基本雷同,根据运算对象的运算符对运算符分开的两个表达式

的结果进行运算。而要获得两个表达式的结果 ,就要对者两个表达式递归调用这个eval方法。

最初的时候,我把我添加的方根的运算对象匹配也放在和加减乘除相同的层级上,结果导致无法计算出结果。后来仔细研读代码逻辑,发现单目运算的处理,其实

只涉及运算符和紧随其后的操作数(至少按照现在代码的trait定义,是这样的),和二元操作,一个运算符,两个表达式根本不同,所以把对象的模式匹配移到了其他立

即数的相同层级,顺利实现了预期的功能。

以上的功能完成后,计算24,就是简单的用一组数据,去根据预定好的模板,生成计算表达式,利用eval函数去求值。如果最终返回的Rational值等于24,则说明

有解,输出模板及解表达式;否则无解。代码如下,通过cal24或cal24Once传入一组数值,然后循环遍历模板,调用calculate方法,进而调用到eval方法去计算:

def calculate(template:String,numbers:List[Int])={
    val values=template.split('N')
    var expression=""
    for(i <- 0 to 3)  expression=expression+values(i) + numbers(i)
    if (values.length==5) expression=expression+values(4)
    //println(expression)
    (expression,template,eval(expression))
  }

def cal24(input:List[Int])={
    var found = false
    for (template <- templates; list <- input.permutations ) {
      try {
        val (expression, tp, result) = calculate(template, list)
        if (result.numer == 24 && result.denom == 1) {
          println(input + ":" + tp + ":" + expression)
          found = true
        }
      } catch {
        case e:Throwable=>
      }
    }
    if (!found) {
      println(input+":"+"no result")
    }
  }

在我添加了平方根的运算符之后,如果调用加减乘除四则运算无法获得结果,就会增加尝试加入平方根的运算符:

要加入平方根的运算尝试,有两种做法,简单做法就是修改操作数,如果操作数可以取方根,直接传入方根去尝试:

//取平方根改变数据,暂时只考虑改变一个运算数
  def changeListWithSQR(list:List[Int])={
    list(0) match {
      case 9 => List(3,list(1),list(2),list(3))
      case 4 => List(2,list(1),list(2),list(3))
      case _ => list
    }
   
    list(1) match {
      case 9 => List(list(0),3,list(2),list(3))
      case 4 => List(list(0),2,list(2),list(3))
      case _ => list
    }
   
    list(2) match {
      case 9 => List(list(0),list(1),3,list(3))
      case 4 => List(list(0),list(1),2,list(3))
      case _ => list
    }
   
    list(3) match {
      case 9 => List(list(0),list(1),list(2),3)
      case 4 => List(list(0),list(1),list(2),2)
      case _ => list
    }
  }

这里用到了Scala语言的模式匹配语法,代码变得简洁而优雅。

另一种做法或者标准的做法就是修改模板,加入平方根的运算符,再次调用原有的eval机制去计算。同样利用了scala语言的模式匹配语法:

//根据数据选择模板
  def selectTemplates(list:List[Int])={
    list(0) match {
      case 9 => templates1
      case 4 => templates1
      case _ => templates
    }
   
    list(1) match {
      case 9 => templates2
      case 4 => templates2
      case _ => templates
    }
   
    list(2) match {
      case 9 => templates3
      case 4 => templates3
      case _ => templates
    }
   
    list(3) match {
      case 9 => templates4
      case 4 => templates4
      case _ => templates
    }
  }

而模板的生成,则采用Scala语言的map语法,变得相当简洁高效:

//added by Dumbbell Yang at 2016-09-17,加入了平方根运算符的表达式模板
  //第一运算数取平方根
  val templates1 = templates.map { _.replaceFirst("N", "√N") }
  //第二运算数取平方根
  val templates2 = templates.map { t => {
      var index = t.indexOf("N")
      t.substring(0,index + 1) + t.substring(index + 1).replaceFirst("N", "√N")
    }
  }
  //第三运算数取平方根
  val templates3 = templates.map { t => {
      var index = t.indexOf("N")
      index = t.substring(index + 1).indexOf("N")
      t.substring(0,index + 1) + t.substring(index + 1).replaceFirst("N", "√N")
    }
  }
  //第四运算数取平方根
  val templates4 = templates.map { t => {
      var index = t.lastIndexOf("N")
      t.substring(0,index) + "√N" + t.substring(index + 1)
    }
  }

最后,增加了一个方法,用于向GUI调用界面返回结果:

//added by Dumbbell Yang at 2016-08-31 for GUI
  //返回计算 结果
  def cal24once2(input:List[Int]):String={

上面定义的cal24及cal24Once方法,都是输出控制台,没有返回结果的。

Calculate24类中的主要代码,都是复制自引路蜂移动软件网友的代码,我自己只增加了单目运算的一小部分。而CalculateGUI,实现图形界面,代码

大部分都是参考网上代码,自己try出来的。

CalculateGUI扩展自Scala预定义的Swing应用基类SimpleSwingApplication,先生成四个选择扑克牌的图标按钮对象:

然后是定义计算按钮及输入答案的文本框,其中,输入答案的文本框,对输入字符进行了过滤,只允许数字及运算符及括号输入:

整个游戏GUI界面的主要部分布局是一个FlowPanel:

其他代码都比较简单,唯一值得一提的是,在检查用户输入的计算答案是否正确时,用到了Scala的隐式类型转换这一强大功能。

/*
   *  隐式转换必须有:implicit关键字,需要和参入参数类型一致
   *  方法命名 :str为源方法名 2 目标方法名,例如:string2RichString定义隐式转换的方法名
   */ 
  implicit def string2RichString(str:String) = new RichString(str);  // str -> RichString

//检查用户输入的答案是否使用了所有的数字
  def validInutText(inputText:String):Boolean={
    //用户选择的数字放入集合
    val set = Set(btnNum1.text.replace("spade/", "").toInt,
                  btnNum2.text.replace("heart/", "").toInt,
                  btnNum3.text.replace("club/", "").toInt,
                  btnNum4.text.replace("diamond/", "").toInt)
   
    //把括号及所有运算符都替换成-,然后split成数组
    //如果不采用隐式转换,就必须自己写方法这样嵌套调用,有点难看
    //val newText = replaceAllStr(replaceAllStr(replaceAllStr(replaceAllStr(
    //    replaceAllStr(replaceAllStr(inputText,"(", ""),")", ""),
    //    "√",""),"+","-"),"*","-"),"/", "-");
    //replaceAllStr为隐式类型转换方法,链式调用,比较优雅
    val newText = inputText.replaceAllStr("(", "").replaceAllStr(")", "")
             .replaceAllStr("√","").replaceAllStr("+","-")
             .replaceAllStr("*","-").replaceAllStr("/", "-");
    //println(newText)
    //println(newText.split("-").size)
   
    //判断集合与数组中的数字相同
    val inputArr = newText.split("-")
    //因为集合会去重,而数组未去重,所以集合元素个数必须小于等于数组
    var result = set.size <= inputArr.length
    if (result){
      for(x <- inputArr){
         //println(x)
         if (result && !set.contains(x.toInt)){
           result = false
         }
      }
    }
   
    result
  }

因为Scala语言的字符串预定义的方法中,replaceAll无法对"(",")"及“+“,“*”等字符串进行替换。而我们必须首先验证用户输入的计算表达式中是否包含

了所有四个数字,因而要替换掉计算表达式中的所有运算符及括号,并且split成数组。开始时,自己写了一个replaceAllStr方法,传递三个参数,嵌套了6

层去实现功能,复杂而且难看。后来想到利用Scala语言的隐式类型转换,自定义一个RichString的类,实现了replaceAllStr方法:

class RichString(var source:String){ 
  //隐式转换字符串的查找替换函数,因为Scala字符串本身的replaceAll方法无法替换"(",")","+"等字符串
  def replaceAllStr(target:String,replace:String)={
    var index = source.indexOf(target)
    //println(source +"," + target + "," + replace + "," + index)
    if (index >= 0){
      var newStr:String = source;
      while(index >= 0){
        if (index == 0){
          newStr = replace + newStr.substring(index + 1)
        }
        else{
          newStr = newStr.substring(0,index) + replace + newStr.substring(index + 1)
        }
        index = newStr.indexOf(target)
        //println(index + ":" + newStr)
      }
      //println(newStr)
      newStr
    }
    else{
      source
    }
  }
}

然后,在对象代码中加入隐式类型转换的声明:

implicit def string2RichString(str:String) = new RichString(str);  // str -> RichString

在需要调用到replaceAllStr方法的地方,自动把String类转换成RichString类并调用这个方法,只要两个参数,链式调用风格,完美体现了

Scala语言的简洁与优雅。

最后,关于GUI界面,有一点点小小的遗憾。由于对Scala GUI编程事件响应机制掌握不够 ,所以,在用户输入计算表达式完成之后,还需要

自己点击"检查"按钮系统才会去检查计算表达式是否正确。如果能在编辑框失去焦点或用户完成输入时,自动触发,效果应该会更好一些。



使用Scala语言开发GUI界面的计算24点的游戏应用相关推荐

  1. 可以用来开发GUI界面的主流语言和平台(一)

    可以用来开发GUI界面的主流语言和平台(一) 一.C++(MFC) C++中常见的用来做GUI界面的主要是MFC和Qt creater.本文先介绍如何快速上手MFC(用一个统计字符数量的小例程),下篇 ...

  2. Apache Spark学习:利用Scala语言开发Spark应用程序

    Spark内核是由Scala语言开发的,因此使用Scala语言开发Spark应用程序是自然而然的事情.如果你对Scala语言还不太熟悉,可以阅读网络教程 A Scala Tutorial for Ja ...

  3. android studio scala插件,Scala 语言开发Andorid ,开发环境的搭建(一)

    Scala 语言开发Andorid ,开发环境的搭建 厌倦 Java 繁琐的语法,为了更优雅的开发 Android 程序,Scala 代替 Java 是一个不错的尝试. 开发前可以学习 Scala 的 ...

  4. 安装java和scala语言开发环境

    安装java语言开发环境 第一步 搜索java官网 官网链接 第二步 点击箭头指示的地方 第三步 选择你电脑的系统,再选择适合你电脑的版本进行下载,下载到你所需要下载到的目录 第四步 配置环境变量 此 ...

  5. 开发GUI界面的工具——Qt

    本文主要讲解了GUI界面开发工具的种类,以及最后确定的最优开发工具. 1.GUI界面开发 图形用户界面(简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面. 图形用户界面是一种人 ...

  6. 大数据DTSpark蘑菇云行动之 第一课:Scala语言开发环境搭建

    大数据DTSpark"蘑菇云"行动之 第一课:Scala语言开发环境搭建 第一次听王家林老师的课,感觉很不错,特别是家林老师对技术的那种热情深深的感染了我.希望在以后的日子学有所成 ...

  7. 使用Dart/Flutter语言开发的命令行文字RPG类型小游戏

    使用Dart/Flutter语言开发的命令行文字RPG类型小游戏 项目源码:https://gitee.com/FantasyWind/word_game 介绍 项目背景 本项目为使用Dart/Flu ...

  8. c语言算24点答案,C语言-纸牌计算24点小游戏

    C语言实现纸牌计算24点小游戏 利用系统时间设定随机种子生成4个随机数,并对4个数字之间的运算次序以及运算符号进行枚举,从而计算判断是否能得出24,以达到程序目的.程序主要功能已完成,目前还有部分细节 ...

  9. C语言-纸牌计算24点小游戏

    C语言实现纸牌计算24点小游戏 利用系统时间设定随机种子生成4个随机数,并对4个数字之间的运算次序以及运算符号进行枚举,从而计算判断是否能得出24,以达到程序目的.程序主要功能已完成,目前还有部分细节 ...

最新文章

  1. R 包 optparse 之命令行参数传递
  2. java 正序a~z_Flutter MapString, dynamic 、ListString a-z 排序
  3. wxWidgets:wxSearchCtrl类用法
  4. jsp连接mysql数据库代码_JSP连接MySQL数据库代码
  5. java基础之HashTable和HashMap的区别
  6. BZOJ1901:Zju2112 Dynamic Rankings——题解
  7. 怎么让人爆照_瞬间变上相,让照片颜值大爆棚的跳跃照技巧
  8. 面向对象——三大特性(封装、继承、多态)
  9. python实现动态壁纸_Python 实现macOS Catalina 动态壁纸定时设置
  10. 遗传算法matlab_遗传算法 (GA) 进行多参数拟合 【MATLAB】
  11. CEIWEI CommMonitor 串口监控精灵v11.0 串口过滤 串口驱动
  12. 文档自动同步云服务器,​文件自动同步网盘能实现吗?
  13. c++ append用法
  14. python eml解析_如何在python中读取eml文件?
  15. 夏令时及java中常用方法
  16. sqlserver中返回旬开始日期和结束日期的函数
  17. 关于钉钉投屏功能(通过企业工作台设置投屏)无法通过js取得数据的问题
  18. keras运行时指定显卡及限制GPU用量
  19. 无法启动此程序,因为计算机丢失api-ms-win-crt-process-l1-1-0.dll
  20. Poj P3889 Fractal Streets___规律+dfs+分治

热门文章

  1. BandZip cmd调用参数
  2. Centos rsync + notify 实现数据实时同步
  3. 网吧:连锁网吧是行业快速发展主因
  4. 用PDF阅读器实现PDF合并及PDF拆分技巧
  5. 【Proteus仿真】555振荡电路+CD4017流水灯(频率可调)
  6. 移植uboot-分析uboot启动流程(详解)
  7. Slysoft All-in-One 1.9 (21/03/2007)
  8. .NET CORE JWT
  9. linux定时重启服务
  10. 迅捷CAD编辑器如何将图片转换为CAD