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

       今年开始学习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编程事件响应机制掌握不够 ,所以,在用户输入计算表达式完成之后,还需要

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

 



      

       



猜你喜欢

转载自blog.csdn.net/yangdanbo1975/article/details/52565616
今日推荐