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

基于Minimax算法的Pygame井字棋游戏TypeError问题求助

井字棋Minimax算法TypeError问题排查

问题描述

开发人机对战井字棋游戏,采用Minimax算法实现AI逻辑时,运行代码出现TypeError: cannot unpack non-iterable NoneType object错误,触发位置在main函数第258行:row, col = comp.evaluation(board)。


代码文件

ttt1.py(变量定义文件)

import random
WIDTH = 600
HEIGHT = 600

ROWS = 3 # 网格行数
COLS = 3 # 网格列数

SQUARE_SIZE = WIDTH // COLS # 单个格子大小
LINE_WIDTH = 15 # 网格线宽度

# 圆圈样式
CIRCLE_WIDTH = 20
RADIUS = SQUARE_SIZE // 3

# 叉号样式
CROSS_WIDTH = 20

OFFSET = 50

# 颜色定义(随机生成)
BACKCOLOR = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
LINE_COLOR = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
CIRCLE = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
CROSS = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))

主代码文件

import sys # 用于退出应用
import copy
import random
import pygame
import numpy as np

from ttt1 import * # 导入ttt1中的变量

# pygame初始化
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("带Minimax算法的井字棋")
screen.fill(BACKCOLOR)

class Board:

    def __init__(self): # 初始化空棋盘(二维数组全0)
        self.squares = np.zeros((ROWS, COLS))
        self.empty_square = self.squares # 此处与方法名冲突
        self.marked_square = 0  # 已标记格子数

    def final_condition(self, show=False):
        # 返回0表示未分胜负,1表示玩家1获胜,2表示玩家2获胜
        # 纵向获胜检测
        for col in range(COLS):
            if self.squares[0][col] == self.squares[1][col] == self.squares[2][col] != 0:
                if show:
                    color = CIRCLE if self.squares[0][col] == 2 else CROSS
                    start_pos = (col * SQUARE_SIZE + SQUARE_SIZE // 2, 20)
                    final_pos = (col * SQUARE_SIZE + SQUARE_SIZE // 2, HEIGHT - 20)
                    pygame.draw.line(screen, color, start_pos, final_pos, LINE_WIDTH)
                return self.squares[0][col]
        # 横向获胜检测
        for row in range(ROWS):
            if self.squares[row][0] == self.squares[row][1] == self.squares[row][2] != 0:
                if show:
                    color = CIRCLE if self.squares[row][0] == 2 else CROSS
                    start_pos = (20, row * SQUARE_SIZE + SQUARE_SIZE // 2)
                    final_pos = (WIDTH - 20, row * SQUARE_SIZE + SQUARE_SIZE // 2)
                    pygame.draw.line(screen, color, start_pos, final_pos, LINE_WIDTH)
                return self.squares[row][0]

        # 主对角线获胜检测
        if self.squares[0][0] == self.squares[1][1] == self.squares[2][2] != 0:
            if show:
                color = CIRCLE if self.squares[1][1] == 2 else CROSS
                start_pos = (20, 20)
                final_pos = (WIDTH - 20, HEIGHT - 20)
                pygame.draw.line(screen, color, start_pos, final_pos, CROSS_WIDTH)
            return self.squares[1][1]
        # 副对角线获胜检测
        if self.squares[2][0] == self.squares[1][1] == self.squares[0][2] != 0:
            if show:
                color = CIRCLE if self.squares[1][1] == 2 else CROSS
                start_pos = (20, HEIGHT - 20)
                final_pos = (WIDTH - 20, 20)
                pygame.draw.line(screen, color, start_pos, final_pos, CROSS_WIDTH)
            return self.squares[1][1]
        
        return 0 # 未分胜负

    def mark_squares(self, row, col, player): # 标记格子,player为1或2
        self.squares[row][col] = player
        self.marked_square += 1

    def empty_square(self, row, col): # 判断格子是否为空(与实例属性同名,冲突)
        return self.squares[row][col] == 0  
    def return_empty(self):
        empty = []
        for row in range(ROWS):
            for col in range(COLS):
                if self.empty_square[row, col]: # 此处实际访问的是数组而非方法
                    empty.append((row, col))
        return empty

    def isfull(self):
        return self.marked_square == 9 # 棋盘已满
    def isempty(self):
        return self.marked_square == 0 # 棋盘全空

class MinMax:
    def __init__(self, level=1, player=2):
        self.level = level # AI难度:0为随机,1为Minimax
        self.player = player # AI玩家编号(2)

    def random_choice(self, board): # 随机选择空格子
        empty = board.return_empty()
        index = random.randrange(0, len(empty))
        return empty[index] # 返回(row, col)

    def minmax(self, board, max):
        # 终端状态检测
        case = board.final_condition()
        # 玩家1获胜
        if case == 1:
            return 1, None
        # 玩家2获胜
        if case == 2:
            return -1, None
        # 平局
        elif board.isfull():
            return 0, None
        # 最大化玩家逻辑(玩家1)
        if max:
            max_eval = -100
            best_move = None
            empty_square = board.return_empty()
            for(row, col) in empty_square:
                temp_board = copy.deepcopy(board)
                temp_board.mark_squares(row, col, 1)
                eval = self.minmax(temp_board, False)[0]
                if eval > max_eval: 
                    max_eval = eval
                    best_move = (row, col)
            return max_eval, best_move
        # 最小化玩家逻辑(AI玩家2)
        elif not max:
            min_eval = 100
            best_move = None
            empty_square = board.return_empty()
            for(row, col) in empty_square:
                temp_board = copy.deepcopy(board)
                temp_board.mark_squares(row, col, self.player)
                eval = self.minmax(temp_board, True)[0]
                if eval < min_eval: 
                    min_eval = eval
                    best_move = (row, col)
            return min_eval, best_move

    def evaluation(self, main_board):
        if self.level == 0:
            eval = 'random'
            move = self.random_choice(main_board)
        else:
            eval, move = self.minmax(main_board, False)
        print(f'Minimax选择在位置{move}落子,评估值为{eval}')
        return move

# 游戏绘制逻辑类
class TicTac:
    def __init__(self):
        self.board = Board()
        self.comp = MinMax()
        self.player = 2 # 初始玩家为2(圆圈)
        self.gamemode = 'computer' # 默认人机对战
        self.run = True
        self.lines() # 绘制网格线

    def make_move(self, row, col):
        self.board.mark_squares(row, col, self.player)
        self.draw_figure(row, col) # 绘制棋子
        self.another_player() # 切换玩家

    def reset(self):
        self.__init__() # 重置游戏

    def lines(self): # 绘制网格线
        screen.fill(BACKCOLOR)
        # 竖线
        pygame.draw.line(screen, LINE_COLOR,(SQUARE_SIZE, 0), (SQUARE_SIZE, HEIGHT), LINE_WIDTH)
        pygame.draw.line(screen, LINE_COLOR,(WIDTH - SQUARE_SIZE, 0), (WIDTH - SQUARE_SIZE, HEIGHT), LINE_WIDTH)
        # 横线
        pygame.draw.line(screen, LINE_COLOR,(0,SQUARE_SIZE), (WIDTH, SQUARE_SIZE), LINE_WIDTH)
        pygame.draw.line(screen, LINE_COLOR,(0, HEIGHT - SQUARE_SIZE), (WIDTH, HEIGHT - SQUARE_SIZE), LINE_WIDTH)

    def draw_figure(self, row, col):
        if self.player == 1:
            # 绘制叉号
            start_down_line = (col * SQUARE_SIZE + OFFSET, row * SQUARE_SIZE + OFFSET)
            end_down_line = (col * SQUARE_SIZE + SQUARE_SIZE - OFFSET, row * SQUARE_SIZE + SQUARE_SIZE - OFFSET)
            pygame.draw.line(screen, CROSS, start_down_line, end_down_line, CROSS_WIDTH)
            start_up_line = (col * SQUARE_SIZE + OFFSET, row * SQUARE_SIZE + SQUARE_SIZE - OFFSET)
            end_up_line = (col * SQUARE_SIZE + SQUARE_SIZE - OFFSET, row * SQUARE_SIZE + OFFSET) 
            pygame.draw.line(screen, CROSS, start_up_line, end_up_line, CROSS_WIDTH)
        elif self.player == 2:
            # 绘制圆圈
            center = (col * SQUARE_SIZE + SQUARE_SIZE // 2, row * SQUARE_SIZE + SQUARE_SIZE // 2)
            pygame.draw.circle(screen, CIRCLE, center, RADIUS, CIRCLE_WIDTH)

    def another_player(self): # 切换玩家:1↔2
        self.player = self.player % 2 + 1

    def change_gamemode(self): # 切换游戏模式:人机↔人人
        if self.gamemode == 'user':
            self.gamemode = 'computer'
        else:
            self.gamemode = 'user'
    
    def isover(self): # 判断游戏是否结束
        return self.board.final_condition(show=True) != 0 or self.board.isfull()

def main():
    tictac = TicTac()
    board = tictac.board
    comp = tictac.comp

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            if event.type == pygame.KEYDOWN:
                # G键切换游戏模式
                if event.key == pygame.K_g:
                    tictac.change_gamemode()
                # R键重置游戏
                if event.key == pygame.K_r:
                    tictac.reset()
                    board = tictac.board
                    comp = tictac.comp
                # 0键切换AI为随机模式
                if event.key == pygame.K_0:
                    comp.level = 0
                # 1键切换AI为Minimax模式
                if event.key == pygame.K_1:
                    comp.level = 1

            if event.type == pygame.MOUSEBUTTONDOWN:
                pos = event.pos
                row = pos[1]//SQUARE_SIZE
                col = pos[0]//SQUARE_SIZE
                if board.empty_square[row,col] and tictac.run:
                    tictac.make_move(row, col)
                    if tictac.isover():
                        tictac.run = False

        # AI回合逻辑
        if tictac.gamemode == 'computer' and tictac.player == comp.player and tictac.run:
            pygame.display.update()
            # 此处触发TypeError,因为comp.evaluation可能返回None
            row, col = comp.evaluation(board)
            tictac.make_move(row, col)
            if tictac.isover():
                tictac.run = False

        pygame.display.update()

if __name__ == "__main__":
    main()

问题原因与修复方案

核心原因

  1. Board类的命名冲突:同时定义了实例属性self.empty_square = self.squares和同名方法def empty_square(self, row, col),导致return_empty()中调用self.empty_square[row, col]时,实际访问的是numpy数组而非判断方法,无法正确识别空格子,最终return_empty()返回空列表,AI无法获取有效落子位置,返回None。
  2. 未处理游戏结束后的AI调用:当游戏已结束(平局或胜负已分)时,AI仍会尝试调用evaluation方法,此时无空格子可选择,返回None,解包时触发TypeError。

修复步骤

1. 解决命名冲突

重命名Board类的判断方法,避免与实例属性同名:

class Board:
    def __init__(self):
        self.squares = np.zeros((ROWS, COLS))
        self.marked_square = 0  # 删除原有的self.empty_square属性

    # 重命名方法为is_empty_square
    def is_empty_square(self, row, col):
        return self.squares[row][col] == 0

    def return_empty(self):
        empty = []
        for row in range(ROWS):
            for col in range(COLS):
                # 修改调用为新方法名
                if self.is_empty_square(row, col):
                    empty.append((row, col))
        return empty

同时修改main函数中的判断:

if board.is_empty_square(row,col) and tictac.run:

2. 增加游戏结束判断

在MinMax类的evaluation方法中,先检查游戏是否已结束,避免无效计算:

def evaluation(self, main_board):
    # 先判断游戏是否已结束
    if main_board.final_condition() != 0 or main_board.isfull():
        return None
    if self.level == 0:
        eval = 'random'
        move = self.random_choice(main_board)
    else:
        eval, move = self.minmax(main_board, False)
    print(f'Minimax选择在位置{move}落子,评估值为{eval}')
    return move

3. 修复AI回合的解包逻辑

在main函数中,先判断返回值是否为None,再进行解包:

if tictac.gamemode == 'computer' and tictac.player == comp.player and tictac.run:
    pygame.display.update()
    move = comp.evaluation(board)
    if move is not None:
        row, col = move
        tictac.make_move(row, col)
        if tictac.isover():
            tictac.run = False

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 11:16:01