基于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()
问题原因与修复方案
核心原因
- Board类的命名冲突:同时定义了实例属性
self.empty_square = self.squares和同名方法def empty_square(self, row, col),导致return_empty()中调用self.empty_square[row, col]时,实际访问的是numpy数组而非判断方法,无法正确识别空格子,最终return_empty()返回空列表,AI无法获取有效落子位置,返回None。 - 未处理游戏结束后的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
相关产品推荐
相关产品推荐

