【问题标题】:Functional Alternative to Game Loop游戏循环的功能替代品
【发布时间】:2011-11-19 16:14:20
【问题描述】:

我刚开始使用 Scala 并正在尝试一个小玩具程序 - 在本例中是基于文本的井字游戏。我根据我对 scala 的了解编写了一个工作版本,但注意到它主要是命令式的,而且我的类是可变的。

我正在尝试实现一些功能性惯用语,并设法至少使代表游戏状态的类不可变。但是,我剩下一个类负责执行依赖于可变状态和命令式循环的游戏循环,如下所示:

  var board: TicTacToeBoard = new TicTacToeBoard

  def start() {
    var gameState: GameState = new XMovesNext
    outputState(gameState)
    while (!gameState.isGameFinished) {
      val position: Int = getSelectionFromUser
      board = board.updated(position, gameState.nextTurn)
      gameState = getGameState(board)
      outputState(gameState)      
    }
  }

在这个循环中对我正在做的事情进行编程的更惯用的方法是什么?

完整的源代码在这里https://github.com/whaley/TicTacToe-in-Scala/tree/master/src/main/scala/com/jasonwhaley/tictactoe

【问题讨论】:

  • 您可以从 Vasil Remeniuk 的 Tic-Tac-Toe API with phantom types 文章中获得一些灵感。
  • @4e6 +1'ed,我稍后会保留该链接。在那篇文章中讨论了我完全不熟悉的 scala 功能(特征,以及 scala 如何处理类型参数)。我会从头开始,准备好后可能会使用这篇文章作为跳板。谢谢!
  • 堆栈溢出的一个问题是您不能接受多个答案。感谢大家的帮助!
  • 虽然很高兴您担心可变状态等副作用,但值得记住的是,除了将电能转化为热能之外,每个有用的程序都必须改变“世界状态”。因此,可以有移动输出、移动输入和棋盘状态。只要确保其他一切都不会干扰。

标签: scala functional-programming game-loop


【解决方案1】:

恕我直言,对于 Scala,命令式循环很好。你总是可以编写一个递归函数来表现得像一个循环。我还加入了一些模式匹配。

def start() {
    def loop(board: TicTacToeBoard) = board.state match {
        case Finished => Unit
        case Unfinished(gameState) => {
             gameState.output()
             val position: Int = getSelectionFromUser()
             loop(board.updated(position))
        }
    }

    loop(new TicTacToeBoard)
}

假设我们有一个函数whileSome : (a -> Option[a]) a -> (),它运行输入函数直到它的结果为None。这会去掉一些样板。

def start() {
    def step(board: TicTacToeBoard) = {
        board.gameState.output()
        val position: Int = getSelectionFromUser()
        board.updated(position) // returns either Some(nextBoard) or None
    }

    whileSome(step, new TicTacToeBoard)
}

whileSome 写起来应该很简单;它只是前一种模式的抽象。我不确定它是否在任何常见的 Scala 库中,但在 Haskell 中,您可以从 monad-loops 获取 whileJust_

【讨论】:

    【解决方案2】:

    您可以将其实现为递归方法。这是一个不相关的例子:

    object Guesser extends App {
      val MIN = 1
      val MAX = 100
    
      readLine("Think of a number between 1 and 100. Press enter when ready")
    
      def guess(max: Int, min: Int) {
        val cur = (max + min) / 2
        readLine("Is the number "+cur+"? (y/n) ") match {
          case "y" => println("I thought so")
          case "n" => {
            def smallerGreater() { 
              readLine("Is it smaller or greater? (s/g) ") match {
                case "s" => guess(cur - 1, min)
                case "g" => guess(max, cur + 1)
                case _   => smallerGreater()
              }
            }
            smallerGreater()
          }
          case _   => {
            println("Huh?")
            guess(max, min)
          } 
        }
      }
    
      guess(MAX, MIN)
    }
    

    【讨论】:

    • 我将它重新实现为递归函数,效果很好。我想我应该相信 scalac 会检测到它并执行 TCO。
    • @whaley,相信但verify ;)
    • @whaley 我不明白为什么井字游戏的实现需要进行尾调用优化
    • @LuigiPlinge 它根本不会,但如果这是一款无限期运行“游戏循环”的游戏,那么它会
    【解决方案3】:

    比如:

    Stream.continually(processMove).takeWhile(!_.isGameFinished)
    

    其中processMove 是一个从用户那里获取选择、更新板并返回新状态的函数。

    【讨论】:

    • 也许我错过了 Stream.continually 和 Stream.takeWhile 的工作原理。使用这种技术不允许我在我的 isGameFinished 为假时继续重复 processMove。事实上,无论我传递给 takeWhile 的谓词返回 true 还是 false,它都只执行一次 processMove。代码已在 github 上更新以反映尝试。
    • takeWhile 会建立一个非常大的列表吗?或者 Scala 编译器是否识别出未使用的流元素?
    • @whaley 我看到你正在创建不断的新板!也许你应该声明一个val board = new TicTacToeBoard 并将它传递给processMove(或者更好的是,使processMove 成为TicTacToeBoard 的方法)。考虑到continually 推迟了参数评估(call by name),因为它的参数类型是=> A
    • @DanBurton Stream 是一个惰性列表,因此它可以是无限的。我认为要使该解决方案正常运行,您需要对 Stream 进行递归定义,以便上一个动作的状态是下一个动作的输入。如果您使用continually,则必须依赖外部vars。
    • Stream.iterate 会更充分地表达问题。
    【解决方案4】:

    我会选择递归版本,但这是Stream 版本的正确实现:

    var board: TicTacToeBoard = new TicTacToeBoard

    def start() {
      def initialBoard: TicTacToeBoard = new TicTacToeBoard
      def initialGameState: GameState = new XMovesNext
      def gameIterator = Stream.iterate(initialBoard -> initialGameState) _
      def game: Stream[GameState] = {
        val (moves, end) = gameIterator {
          case (board, gameState) =>
            val position: Int = getSelectionFromUser
            val updatedBoard = board.updated(position, gameState.nextTurn)
            (updatedBoard, getGameState(board))
        }.span { case (_, gameState) => !gameState.isGameFinished }
        (moves ::: end.take(1)) map { case (_, gameState) => gameState }
      }
      game foreach outputState
    }
    

    这看起来比它应该的更奇怪。理想情况下,我会使用takeWhile,然后使用map,但它不会起作用,因为 last 的情况会被忽略!

    如果游戏的移动可以被丢弃,那么dropWhile 后跟head 就可以了。如果我有副作用(outputState)而不是Stream,我可以走那条路,但是在Stream 中产生副作用比带有while 循环的var 更糟糕。

    所以,我改为使用span,它给了我takeWhiledropWhile,但迫使我保存中间结果——如果内存是一个问题,这可能真的很糟糕,因为整个游戏将是保存在内存中,因为moves 指向Stream 的头部。所以我不得不将所有这些封装在另一个方法中,game。这样,当我foreach通过game的结果时,不会有任何指向Streamhead的东西。

    另一种选择是摆脱您拥有的其他副作用:getSelectionFromUser。您可以使用 Iteratee 摆脱它,然后您可以保存最后一步并重新应用它。

    或者...您可以为自己编写一个 takeTo 方法并使用它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-11
      • 2021-06-16
      • 1970-01-01
      • 2014-01-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多