You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ConnectX的Minimax算法异常:无法实现基本阻挡操作求助

问题排查:Connect 4 N-step Lookahead Agent 无法正常工作

我正在学习Kaggle Learn的《游戏AI入门》课程,为N-step Lookahead练习编写了agent代码,但无法正常运行——甚至无法完成阻挡随机agent获胜这类简单操作。

我的Agent代码

def my_agent(obs, config):
    
    ###########
    # Imports #
    ###########
    import random
    import numpy as np
    
    ####################
    # Helper functions #
    ####################
    # Gets board at next step if agent drops piece in selected column
    def drop_piece(grid, col, mark, config):
        next_grid = grid.copy()
        for row in range(config.rows-1, -1 ,-1):
            if next_grid[row][col] == 0:
                break
        next_grid[row][col] == mark
        return next_grid
                
    # Helper function for get_heuristic: check if window meets heuristic conditions
    def check_window(window, num_discs, piece, config):
        return (window.count(piece) == num_discs and window.count(0) == config.inarow-num_discs)
    
    # Helper function for get_heuristic: counts number of windows satisfying specified heuristic conditions
    def count_windows(grid, num_discs, piece, config):
        num_windows = 0
        # horizontal
        for row in range(config.rows):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[row, col:col+config.inarow])
                if check_window(window, num_discs, piece, config):
                    num_windows += 1
        # vertical
        for row in range(config.rows-(config.inarow-1)):
            for col in range(config.columns):
                window = list(grid[row:row+config.inarow, col])
                if check_window(window, num_discs, piece, config):
                    num_windows += 1
        # positive diagonal (upper left part of board, extend right and downwards)
        for row in range(config.rows-(config.inarow-1)):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[range(row, row+config.inarow), range(col, col+config.inarow)])
                if check_window(window, num_discs, piece, config):
                    num_windows += 1
        # negative diagonal (lower left part of board, extend right and upwards)
        for row in range(config.inarow-1, config.rows):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[range(row, row-config.inarow, -1), range(col, col+config.inarow)])
                if check_window(window, num_discs, piece, config):
                    num_windows += 1
        return num_windows
    
    # Helper function for minimax: calculate heuristic for grid
    def get_heuristic(grid, mark, config):
        num_threes = count_windows(grid, 3, mark, config)
        num_fours = count_windows(grid, 4, mark, config)
        num_threes_opp = count_windows(grid, 3, mark%2+1, config)
        num_fours_opp = count_windows(grid, 4, mark%2+1, config)
        score = 1*num_threes + 1e5*num_fours - 100*num_threes_opp - 1000*num_fours_opp
        return score
    
    # Uses minimax to calculate value of dropping piece in selected column
    def score_move(grid, col, mark, config, nsteps):
        next_grid = drop_piece(grid, col, mark, config)
        score = minimax(next_grid, nsteps-1, False, mark, config)
        return score
    
    # Helper function for minimax: Check if agent or opponent has 4 in a row in the window
    def is_terminal_window(window, config):
        return window.count(1) == config.inarow or window.count(2) == config.inarow
    
    # Helper function for minimax: Check if game has ended
    def is_terminal_node(grid, config):
        # Check for draw 
        if list(grid[0, :]).count(0) == 0:
            return True
        # Check for win: horizontal, vertical, or diagonal
        # horizontal 
        for row in range(config.rows):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[row, col:col+config.inarow])
                if is_terminal_window(window, config):
                    return True
        # vertical
        for row in range(config.rows-(config.inarow-1)):
            for col in range(config.columns):
                window = list(grid[row:row+config.inarow, col])
                if is_terminal_window(window, config):
                    return True
        # positive diagonal
        for row in range(config.rows-(config.inarow-1)):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[range(row, row+config.inarow), range(col, col+config.inarow)])
                if is_terminal_window(window, config):
                    return True
        # negative diagonal
        for row in range(config.inarow-1, config.rows):
            for col in range(config.columns-(config.inarow-1)):
                window = list(grid[range(row, row-config.inarow, -1), range(col, col+config.inarow)])
                if is_terminal_window(window, config):
                    return True
        return False

    def minimax(node, depth, maximizingPlayer, mark, config):
        is_terminal = is_terminal_node(node, config)
        valid_moves = [c for c in range(config.columns) if node[0][c] == 0]
        if depth == 0 or is_terminal:
            return get_heuristic(node, mark, config)
        if maximizingPlayer:
            value = -np.Inf
            for col in valid_moves:
                child = drop_piece(node, col, mark, config)
                value = max(value, minimax(child, depth-1, False, mark, config))
            return value
        else:
            value = np.Inf
            for col in valid_moves:
                child = drop_piece(node, col, mark%2+1, config)
                value = min(value, minimax(child, depth-1, True, mark, config))
            return value

    #########################
    # Agent makes selection #
    #########################
    N_STEPS = 3
    valid_moves = [col for col in range(config.columns) if obs.board[col] == 0]
    grid = np.asarray(obs.board).reshape(config.rows, config.columns)
    scores = dict(zip(valid_moves, [score_move(grid, col, obs.mark, config, N_STEPS) for col in valid_moves]))
    print(scores)
    max_cols = [key for key in scores.keys() if scores[key] == max(scores.values())]
    return random.choice(max_cols)

分数输出

{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0}
{0: -99.0, 1: -99.0, 2: -99.0, 3: -99.0, 4: -99.0, 5: -99.0, 6: -99.0}
{0: -99.0, 1: -99.0, 2: -99.0, 3: -99.0, 4: -99.0, 5: -99.0, 6: -99.0}
{0: -99.0, 1: -99.0, 2: -99.0, 3: -99.0, 4: -99.0, 5: -99.0, 6: -99.0}
{0: -99.0, 1: -99.0, 2: -99.0, 3: -99.0, 4: -99.0, 5: -99.0}
{0: -99.0, 1: -99.0, 2: -99.0, 3: -99.0, 4: -99.0, 5: -99.0}

我肯定犯了一个低级错误,但始终无法找出,能否帮忙排查?另外我是StackOverflow新手,若我的提问有问题请指出。


问题排查结果

核心错误:赋值运算符误用

在drop_piece函数中,你使用了比较运算符==而不是赋值运算符=:

next_grid[row][col] == mark  # 错误:仅判断相等,不会修改棋盘

正确写法应该是:

next_grid[row][col] = mark  # 正确:将棋子放置到对应位置

这个错误导致所有drop_piece调用都不会修改棋盘——返回的next_grid和输入的grid完全一致。因此,minimax算法实际上一直在评估同一个初始棋盘的分数,无法区分不同落子的优劣,最终只能随机选择列,自然无法完成阻挡对手的操作。

次要优化建议

  1. 导入语句位置:将import random和import numpy as np移到函数外部,避免每次调用agent时重复导入,提升运行效率。
  2. 代码一致性:minimax函数中valid_moves的判断逻辑与主逻辑一致,无需修改,但可以保持写法统一增强可读性。

修正后的drop_piece函数

def drop_piece(grid, col, mark, config):
    next_grid = grid.copy()
    for row in range(config.rows-1, -1 ,-1):
        if next_grid[row][col] == 0:
            break
    next_grid[row][col] = mark  # 修正赋值运算符
    return next_grid

修正后,agent应该能正确计算不同落子的分数,优先选择能阻挡对手或自身形成连子的列。


内容的提问来源于stack exchange,提问作者Andrew Loo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 13:55:21