Skip to content

使用 Python 进行 AI – 游戏

使用 Python 实现 AI – 构建智能游戏代理

Section titled “使用 Python 实现 AI – 构建智能游戏代理”

游戏的核心在于策略。无论是棋盘游戏、视频游戏还是体育比赛,玩家或团队在开始前都会制定策略,并根据不断变化的游戏状态进行调整。游戏中的 AI 旨在创建表现出智能行为的非玩家角色 (NPC) 或对手,使游戏更具吸引力和挑战性。

许多传统游戏 AI 系统依赖于搜索算法。这些算法探索未来可能的游戏状态,以确定最佳移动。

这些搜索算法的目标是找到通往胜利或高价值游戏状态的最佳移动序列。它们使用游戏规则和胜利条件来评估移动。

将游戏想象成一棵树,当前状态是根节点。每个分支代表一个可能的移动,通向一个新的游戏状态(节点)。搜索算法遍历这棵树,分析潜在结果以做出明智的决策。每个节点都可以分配一个分数(启发式值),表示其受欢迎程度。

组合搜索和启发式方法 (Combinatorial Search and Heuristics)

Section titled “组合搜索和启发式方法 (Combinatorial Search and Heuristics)”

基本搜索算法的一个主要挑战是它们可能是穷举的,会探索整个搜索空间。对于复杂游戏,这在计算上是不可行的,会导致可能的州数量出现“组合爆炸”。这就是启发式方法发挥作用的地方。

由启发式方法引导的组合搜索旨在“剪枝”搜索空间。启发式方法是一种有根据的猜测或经验法则,用于估计游戏状态的价值或特定移动的可能性,使 AI 能够专注于博弈树中更有前途的分支并节省资源。一些使用启发式方法的算法包括:

Minimax 是一种用于双人零和博弈(一方的收益是另一方的损失)的基本决策算法。它遵循的原则是:每个玩家都试图最大化自己的分数,同时假设对手会试图最小化它(即最大化对手自己的分数,这相当于第一个玩家分数的负值)。

该算法会探索博弈树到一定的深度。在每个级别,它在最大化(轮到 AI 时)和最小化(轮到对手时)游戏状态的启发式评估之间交替。启发式函数为终止或深度受限的游戏状态分配一个分数,表示该状态对于当前玩家有多好。

Minimax 仍然可能探索许多不相关的分支。Alpha-Beta 剪枝是 Minimax 算法的一种优化技术,可以显著减少搜索树中评估的节点数量。

它的工作原理是维护两个值:Alpha(到根节点路径上,对于最大化玩家找到的最佳分数)和 Beta(到根节点路径上,对于最小化玩家找到的最佳分数)。如果发现某个移动对于任一玩家来说比之前探索过的移动更差,那么该分支的树就可以被“剪枝”(忽略),因为它不会带来更好的整体结果。

Negamax 算法是 Minimax 的一个变体,它简化了实现,特别是对于启发式评估始终从当前玩家角度出发的游戏。Negamax 利用了 max(a, b) = -min(-a, -b) 这一观察结果,而不是拥有单独的最大化和最小化函数。

这意味着算法总是可以最大化对手得分的负值。这通常会使代码更简洁,因为它无需根据轮到谁来明确切换最大化和最小化逻辑。

虽然经典搜索算法是基础,但现代游戏 AI 通常采用更先进的技术:

  • 蒙特卡洛树搜索 (Monte Carlo Tree Search, MCTS):对分支因子较大(例如,围棋、复杂策略游戏)的游戏有效。MCTS 通过随机模拟构建搜索树,以估计移动的价值。
  • 强化学习 (Reinforcement Learning, RL):智能体通过与游戏环境互动并根据其行为获得奖励或惩罚来学习最优策略。深度强化学习 (Deep Reinforcement Learning)(例如,DQN、AlphaZero)结合了 RL 和深度神经网络来应对高度复杂的游戏。

要学习这些高级主题,OpenAI Gym 等环境提供了出色的平台。

对于初学者学习游戏 AI 原理,easyAI 库为创建双人游戏的 AI 提供了一个直观的框架。它抽象了一些博弈树搜索的复杂性。您可以使用 pip 进行安装:

pip install easyAI

注意:easyAI 非常适合教育目的和简单的游戏。对于商业或高度复杂的游戏,通常使用更强大的引擎和算法(如 MCTS 或 RL)。

示例:“最后一枚硬币” (Last Coin Standing) 游戏的 Bot

Section titled “示例:“最后一枚硬币” (Last Coin Standing) 游戏的 Bot”

在这个游戏中,玩家轮流从一堆硬币中拿走硬币。被迫拿走最后一枚硬币的玩家输掉(或者,根据规则,赢;这里是避免拿走最后一枚硬币)。我们将使用 easyAI 的 TwoPlayersGame 类。

首先,从 easyAI 导入必要的组件:

from easyAI import TwoPlayerGame, Human_Player, AI_Player, Negamax
from easyAI.AI import id_solve # For iterative deepening solve

定义游戏类,继承自 TwoPlayerGame:

class LastCoinStanding(TwoPlayerGame):
""" In this game, there's a pile of N coins. Players take 1 to K coins.
The player who avoids taking the last coin wins. """
def __init__(self, players, num_coins=15, max_coins_per_turn=4):
self.players = players
self.num_coins = num_coins
self.max_coins_per_turn = max_coins_per_turn
self.nplayer = 1 # Player 1 starts
def possible_moves(self):
# A player can take 1 to max_coins_per_turn, but not more than remaining coins
return [str(i) for i in range(1, min(self.max_coins_per_turn, self.num_coins) + 1)]
def make_move(self, move):
self.num_coins -= int(move)
def win(self):
# The player who makes num_coins <= 0 makes the opponent take the last coin (or no coin)
# So, the current player wins if they leave 0 or less coins for the opponent effectively.
# In easyAI, win() is called AFTER the move. So if num_coins is 0 or less, current player won.
return self.num_coins <= 0
def is_over(self):
return self.win()
def scoring(self):
# The player who wins gets 100, loser gets -100 (or 0 if not zero-sum by default)
return 100 if self.win() else 0
def show(self):
print(f"{self.num_coins} coins left in the pile")
# For transposition table (memoization to speed up search)
# ttentry gives a unique representation of the game state
def ttentry(self):
return self.num_coins

设置和运行游戏:

# Define the AI algorithm (Negamax with a search depth of 5)
ai_algo = Negamax(5)
# To solve the game (find optimal strategy if possible)
# r: result (1 for win, 0 for draw, -1 for loss for player 1)
# d: depth at which solution found
# m: optimal move for player 1 at the start
# This can take time for larger games.
# print("Solving game...")
# r, d, m = id_solve(LastCoinStanding, range(2, 20), win_score=100)
# print(f"Result: {r}, Depth: {d}, Optimal Move: {m}")
# Play the game: AI vs Human
if __name__ == "__main__":
game = LastCoinStanding([AI_Player(ai_algo), Human_Player()])
game.play()

当您运行此代码时,easyAI 将管理游戏流程。输出将显示游戏进程,例如:

15 coins left in the pile
Move #1: player 1 (AI) plays 4:
11 coins left in the pile
Player 2 (Human) what do you play? 2
Move #2: player 2 plays 2:
9 coins left in the pile
Move #3: player 1 (AI) plays 3:
6 coins left in the pile
Player 2 (Human) what do you play? 1
Move #4: player 2 plays 1:
5 coins left in the pile
Move #5: player 1 (AI) plays 4:
1 coins left in the pile
Player 2 (Human) what do you play? 1
Move #6: player 2 plays 1:
0 coins left in the pile
Player 2 (Human) wins!

注意:原始示例的 win_game 略有不同。在 easyAI 中,如果 刚结束回合 的玩家做出了获胜的移动,win() 应该返回 true。然后,scoring 函数确定游戏的得分结果。id_solve 的输出(例如,d:2, a:0, m:1)表示深度、得分和移动;这特定于 easyAI 的求解器输出格式。

井字棋是一个经典的、非常适合说明游戏 AI 的游戏。以下是如何使用 easyAI 实现它:

导入:

from easyAI import TwoPlayerGame, AI_Player, Negamax
from easyAI.Player import Human_Player

游戏类定义:

class TicTacToe(TwoPlayerGame):
""" The classic game of Tic-Tac-Toe. """
def __init__(self, players):
self.players = players
self.board = [0] * 9 # 0: empty, 1: player 1 (O), 2: player 2 (X)
self.nplayer = 1 # Player 1 starts
def possible_moves(self):
return [i + 1 for i, e in enumerate(self.board) if e == 0]
def make_move(self, move):
self.board[int(move) - 1] = self.nplayer
def unmake_move(self, move): # Optional, for AI search
self.board[int(move) - 1] = 0
def lose(self):
""" Returns True if the current player has lost. """
# Check if opponent (self.nopponent) has three in a row
winning_combinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Columns
[0, 4, 8], [2, 4, 6] # Diagonals
]
return any(
all(self.board[c] == self.nopponent for c in combo)
for combo in winning_combinations
)
def is_over(self):
# Game is over if someone has lost or if there are no more moves (draw)
return self.lose() or (self.possible_moves() == [])
def show(self):
print("\n" + "\n".join([
" ".join([['.', 'O', 'X'][self.board[3 * j + i]] for i in range(3)])
for j in range(3)
]))
def scoring(self):
# Returns 100 if current player wins, -100 if opponent wins (current player loses), 0 for draw
return -100 if self.lose() else 0
# For transposition table
def ttentry(self):
return tuple(self.board) # Tuple of board state is a good unique key

运行井字棋游戏:

# AI algorithm: Negamax with search depth 7 (Tic-Tac-Toe is small enough to solve)
# Higher depth means stronger AI but more computation time.
if __name__ == "__main__":
ai_algo_ttt = Negamax(7)
game_ttt = TicTacToe([Human_Player(), AI_Player(ai_algo_ttt)]) # Human (O) vs AI (X)
# game_ttt = TicTacToe([AI_Player(ai_algo_ttt), Human_Player()]) # AI (O) vs Human (X)
game_ttt.play()

一个示例游戏会话可能看起来像这样(玩家 1 是人类 ‘O’,玩家 2 是 AI ‘X’):

. . .
. . .
. . .
Player 1 what do you play? 1
Move #1: player 1 plays 1:
O . .
. . .
. . .
Move #2: player 2 (AI) plays 5:
O . .
. X .
. . .
Player 1 what do you play? 3
Move #3: player 1 plays 3:
O . O
. X .
. . .
Move #4: player 2 (AI) plays 2:
O X O
. X .
. . .
Player 1 what do you play? 7
Move #5: player 1 plays 7:
O X O
. X .
O . .
Move #6: player 2 (AI) plays 4:
O X O
X X .
O . .
Player 1 what do you play? 6
Move #7: player 1 plays 6:
O X O
X X O
O . .
Move #8: player 2 (AI) plays 9:
O X O
X X O
O . X
Player 1 what do you play? 8
Move #9: player 1 plays 8:
O X O
X X O
O O X
Game over. It's a draw!