Функциональная альтернатива Game Loop - PullRequest
4 голосов
/ 19 ноября 2011

Я только начинаю со Scala и пробую небольшую игрушечную программу - в данном случае это текстовый TicTacToe. Я написал рабочую версию, основанную на том, что я знаю о 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

Ответы [ 4 ]

7 голосов
/ 19 ноября 2011

imho для 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 вы можете получить whileJust_ из петель монад .

5 голосов
/ 19 ноября 2011

Вы можете реализовать его как рекурсивный метод. Вот несвязанный пример:

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)
}
1 голос
/ 20 ноября 2011

Я бы пошел с рекурсивной версией, но вот правильная реализация 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 намного хуже, чем var с петлей while.

Итак, вместо этого я использую span, что дает мне и takeWhile, и dropWhile, но вынуждает меня сохранять промежуточные результаты - что может быть очень плохо, если память вызывает беспокойство, поскольку вся игра будетхраниться в памяти, потому что moves указывает на голову Stream.Поэтому мне пришлось заключить все это в другой метод, game.Таким образом, когда я foreach через результаты game, ничего не будет указывать на Stream head.

Другой альтернативой будет избавление от другие побочные эффекты у вас есть: getSelectionFromUser.Вы можете избавиться от этого с помощью Iteratee, а затем можете сохранить последний ход и повторно применить его.

ИЛИ ... вы можете написать себе метод takeTo ииспользуйте это.

1 голос
/ 19 ноября 2011

Как насчет чего-то вроде:

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

где processMove - это функция, которая получает выбор от пользователя, обновляет доску и возвращает новое состояние.

...