【问题标题】:Why is my minimax algorithm not undoing every move?为什么我的极小极大算法没有撤消每一步?
【发布时间】:2017-10-20 13:26:27
【问题描述】:

我正在为自己制作的原创 4 人棋盘游戏创建 AI。

有关棋盘游戏的详细信息:

4 名玩家轮流在四个主要方向之一同时移动他们的彩色棋子。棋子可以移出棋盘。开始时每个玩家都有 5 条生命。每移出一块棋子,玩家就会失去 1 条生命。新棋子将在整个游戏中确定地生成。

我查找了如何执行极小极大算法并找到了this。我通读了一遍,认为我理解了所有内容,因此我尝试将第 1.5 节中的 Java 代码翻译成 Swift。

这是我的思考过程:

  • 由于我的游戏有 4 个玩家,我会将其他所有人视为最小化玩家。
  • 在 Java 代码中,有一行撤消了移动。由于我的游戏的游戏状态在每次移动时都会发生巨大变化,因此我只需将所有游戏状态存储在一个数组中,当需要撤消某些操作时,我可以在数组上调用dropLast
  • 由于我的游戏中的移动表示为 Direction 枚举,如果像 Java 代码这样的 int 数组,我将返回一个 (Int, Direction) 元组。
  • game 是一个计算属性,它只返回 gameStates.last!
  • 每次我在game 上调用moveUp/Down/Left/Right 方法之一时,game.currentPlayer 都会发生变化,因此我不需要编写额外的代码来决定谁是下一个玩家。
  • 在最后一行,我需要返回(bestScore, bestDirection),但我意识到有时bestDirection 没有被赋值。因此,我将bestDirection 设为可选。如果没有在return语句中赋值,我就返回任意方向。

这是我的尝试:

private func minimax(depth: Int, color: Color) -> (score: Int, direction: Direction) {
    var bestScore = color == myColor ? Int.min : Int.max
    var currentScore: Int
    var bestDirection: Direction?
    if game.players.filter({$0.lives > 0}).count < 2 || depth == 0 {
        // This is a call to my heuristic evaluation function
        bestScore = evaluateHeuristics()
    } else {
        // if the player has no pieces on the board, just move up since moving in any direction won't change anything
        for move in (game.board.indicesOf(color: color).count == 0 ? [Direction.up] : [Direction.up, .down, .left, .right]) {
            let gameCopy = game.createCopy()
            switch move {
            case .up: gameCopy.moveUp()
            case .down: gameCopy.moveDown()
            case .left: gameCopy.moveLeft()
            case .right: gameCopy.moveRight()
            }
            gameStates.append(gameCopy)
            // myColor is like mySeed in the original Java code
            if color == myColor {
                currentScore = minimax(depth: depth - 1, color: game.currentPlayer.color).score
                if currentScore > bestScore {
                    bestScore = currentScore
                    bestDirection = move
                }
            } else {
                currentScore = minimax(depth: depth - 1, color: game.currentPlayer.color).score
                if currentScore < bestScore {
                    bestScore = currentScore
                    bestDirection = move
                }
            }
            _ = gameStates.dropLast()
        }
    }
    return (bestScore, bestDirection ?? .left)
}

当我用 4 的 depth 测试这个 AI 时,它似乎要么做出愚蠢的动作,比如将他的棋子移出棋盘,要么只将他的棋子朝一个方向移动。

我还注意到,当递归调用返回时,gameStates 的长度约为 90。通常它应该是1对吗?因为在递归调用返回时,AI 尝试的所有动作都应该已经撤消,而gameStates 将只包含初始状态。

我做错了什么?

【问题讨论】:

  • 我不相信这段代码会编译。 minimax 被声明为返回一个元组 (score: Int, direction: Direction),但您将结果分配给 Int
  • 另外,除非我误解了游戏规则,否则移动应该包含一个方向列表,玩家仍然拥有的每个棋子对应一个方向,但您只是返回一个方向。
  • @JeremyP 不,我在该行的最后访问了 .score 元素。
  • 啊,好吧,代码框在错误的地方截断了线路!
  • 我说的是“同时”,所以所有的棋子都朝一个方向移动,可以是左上下也可以是右上。

标签: swift artificial-intelligence minimax


【解决方案1】:

dropLast() 返回一个数组切片,其中包含除数组最后一个元素之外的所有元素。它不会修改原始数组。使用removeLast()

编辑

您真正想要的是堆栈数据结构。这是一个。

public struct Stack<Element>
{
    fileprivate var elements: [Element] = []

    public init() {}

    ///    Push an element onto the top of the stack
    ///
    /// - parameter newElement: The element to push
    public mutating func push(_ newElement: Element)
    {
        elements.append(newElement)
    }

    ///    Pops the top element off the stack
    ///
    ///    - returns: The top element or nil if the stack is empty.
    public mutating func pop() -> Element?
    {
        let ret = elements.last
        if ret != nil
        {
            elements.removeLast()
        }
        return ret
    }

    ///  The top element of the stack. Will be nil if the stack is empty
    public var top: Element?
    {
        return elements.last
    }

    /// Number of items in the stack
    public var count: Int
    {
        return elements.count
    }

    /// True if the stack is empty
    public var isEmpty: Bool
    {
        return elements.isEmpty
    }
}

【讨论】:

  • 我认为 dropLast 会像弹出堆栈并返回弹出的元素一样工作!可惜 swift 没有栈数据结构。
  • @Sweeper 你有我的同情。一两年前,在 Swift-Evolution 上有一个关于命名约定的冗长乏味的帖子。 dropLast 似乎违反了约定。真的应该叫lastDropped
  • @Sweeper 栈类型很容易写。我会在答案中添加一个。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-24
  • 2019-05-09
  • 1970-01-01
相关资源
最近更新 更多