在当今的游戏世界中,游戏环境变得越来越复杂,众多不同的因素都会影响决策的制定。人工智能领域的一个重要分支就是致力于构建能够像专业玩家一样做出正确决策的游戏代理。在这篇教程中,我们将详细探讨如何仅使用遗传算法来构建一个能够玩名为 CoinTex 游戏的代理。
CoinTex 游戏概述
CoinTex 是一款使用 Kivy 框架开发的开源跨平台 Python 3 游戏。该游戏的源代码可以在 GitHub 上找到,它是《使用 Kivy 和 Android Studio 在 Python 中构建 Android 应用》这本书的一部分。游戏的主要 Python 脚本名为 main.py,其 GUI(小部件树)则在名为 cointex.kv 的 KV 文件中构建。
运行游戏时,只需运行 main.py 脚本。在 Mac/Linux 终端中,要使用 python3 而不是 python,因为 CoinTex 是用 Python 3 开发的。游戏目前有 24 个关卡,玩家需要逐个击败前一个关卡才能激活下一个关卡。进入关卡后,玩家通过触摸屏幕来移动角色,屏幕顶部会显示已收集的硬币数量、本关的总硬币数量、当前关卡编号以及玩家的生命值。玩家的目标是在保持生命值大于 0 的情况下收集所有硬币。
游戏的工作原理
游戏运行后,首先调用的方法是 on_start(),该方法会根据玩家上次通过的关卡激活相应的关卡,之后会打开已激活关卡的网格。当用户选择一个关卡并在进入之前,会调用 screen_on_pre_enter() 方法,在这个方法中,会随机选择屏幕上硬币的位置。进入关卡后,screen_on_enter() 方法会被调用,它会启动怪物和火焰的移动。
怪物和火焰移动时,会为它们的 pos_hint 属性分配新的位置。在 cointex.kv 文件中,为每个怪物和火焰都分配了一个回调方法,当 pos_hint 属性发生变化时会调用这些方法。玩家也有一个 pos_hint 属性,当它改变时,会调用 char_postion_hint() 方法,该方法会比较玩家和所有未收集硬币的位置,以检查是否发生碰撞。
获取关键信息
要构建游戏代理,我们需要获取一些关键信息,包括关卡屏幕的引用、硬币的位置、怪物的位置和火焰的位置。
- 获取屏幕引用:对于每个关卡,都有一个对应的 Kivy 屏幕,通过 app.root.screens[lvl_num] 可以获取该关卡屏幕的引用,其中 app 是 CointexApp 类的实例,lvl_num 是关卡编号。
- 获取硬币位置:屏幕中有一个名为 coins_ids 的字典,它保存着未收集硬币的位置。收集到的硬币会从该字典中移除。通过 curr_screen.coins_ids 可以获取该字典。
- 获取怪物位置:每个屏幕都有一个名为 num_monsters 的属性,它保存着屏幕上怪物的数量。通过 curr_screen.ids[‘monster’+str(monst_num)+‘_image_lvl’+str(lvl_num)] 可以获取怪物的引用,进而获取其位置。
- 获取火焰位置:火焰的处理方式与怪物类似,通过 curr_screen.ids[‘fire’+str(i+1)+‘_lvl’+str(lvl_num)] 可以获取火焰的引用和位置。
代理的工作原理
我们构建的游戏代理仅使用遗传算法来做出决策,告诉玩家角色该移动到哪里,不使用任何机器学习或深度学习模型。代理的任务是在避免与怪物和火焰碰撞的同时收集所有硬币。
代理在决定玩家下一个位置时,会先设定一个目标硬币,然后根据遗传算法当前种群中的解决方案,选择能让玩家尽可能接近目标硬币的解决方案。具体做法是计算种群中每个解决方案(位置)与目标硬币位置之间的距离,为了使遗传算法的适应度函数成为最大化函数,会将计算出的距离取倒数作为适应度值。
同时,代理还会考虑怪物和火焰的位置。如果某个解决方案靠近至少一个怪物或火焰,会对其适应度值进行惩罚,降低其值;如果远离,则增加其值。
实现步骤
- 安装 PyGAD:要继续这个项目,必须安装 PyGAD 库,可以从 PyPI 下载其 wheel 文件,也可以使用 pip 安装,命令为 pip install pygad>=2.4.0(在 Linux/Mac 上使用 pip3)。
- 构建适应度函数:在遗传算法中,适应度函数接受算法产生的解决方案作为输入,并返回一个适应度值作为输出。适应度值越高,解决方案越好。在 PyGAD 中,适应度函数是一个普通的 Python 函数,接受解决方案和解决方案在种群中的索引作为参数。
def fitness_func(solution, solution_idx):
curr_screen = app.root.screens[lvl_num]
coins = curr_screen.coins_ids
if len(coins.items()) == 0:
return 0
curr_coin = coins[list(coins.keys())[0]]
curr_coin_center = [curr_coin.pos_hint['x'], curr_coin.pos_hint['y']]
output = abs(solution[0] - curr_coin_center[0]) + abs(solution[1] - curr_coin_center[1])
output = 1.0 / output
monsters_pos = []
for i in range(curr_screen.num_monsters):
monster_image = curr_screen.ids['monster'+str(i+1)+'_image_lvl'+str(lvl_num)]
monsters_pos.append([monster_image.pos_hint['x'], monster_image.pos_hint['y']])
for monst_pos in monsters_pos:
char_monst_h_distance = abs(solution[0] - monst_pos[0])
char_monst_v_distance = abs(solution[1] - monst_pos[1])
if char_monst_h_distance <= 0.3 and char_monst_v_distance <= 0.3:
output -= 300
else:
output += 100
fires_pos = []
for i in range(curr_screen.num_fires):
fire_image = curr_screen.ids['fire'+str(i+1)+'_lvl'+str(lvl_num)]
fires_pos.append([fire_image.pos_hint['x'], fire_image.pos_hint['y']])
for fire_pos in fires_pos:
char_fire_h_distance = abs(solution[0] - fire_pos[0])
char_fire_v_distance = abs(solution[1] - fire_pos[1])
if char_fire_h_distance <= 0.3 and char_fire_v_distance <= 0.3:
output -= 300
else:
output += 100
fitness = output
return fitness
- 构建世代回调函数:这个函数会在遗传算法的每一代完成后被调用,用于找到上一代进化出的种群中的最佳解决方案,并根据该解决方案的位置移动玩家。
last_fitness = 0
def callback_generation(ga_instance):
global last_fitness
best_sol_fitness = ga_instance.best_solution()[1]
fitness_change = best_sol_fitness - last_fitness
curr_screen = app.root.screens[lvl_num]
last_fitness = best_sol_fitness
coins = curr_screen.coins_ids
if len(coins.items()) == 0 or curr_screen.character_killed:
return "stop"
elif len(coins.items()) != 0 and fitness_change != 0:
best_sol = ga_instance.best_solution()[0]
app.start_char_animation(lvl_num, [float(best_sol[0]), float(best_sol[1])])
- 创建遗传算法实例:使用 PyGAD 的 GA 类创建遗传算法的实例,并设置必要的参数。
import pygad
ga_instance = pygad.GA(num_generations=9999, num_parents_mating=300, fitness_func=fitness_func, sol_per_pop=1000, num_genes=2, init_range_low=0.0, init_range_high=1.0, random_mutation_min_val=0.0, random_mutation_max_val=1.0, mutation_by_replacement=True, callback_generation=callback_generation, delay_after_gen=app.root.screens[lvl_num].char_anim_duration)
- 启动遗传算法:为了避免阻塞应用程序的主线程,我们在一个新线程中运行遗传算法。在 CointexApp 类的 screen_on_enter() 方法中启动这个新线程。
import threading
class CollectCoinThread(threading.Thread):
def __init__(self, screen):
super().__init__()
self.screen = screen
def run(self):
ga_instance = pygad.GA(num_generations=9999, num_parents_mating=300, fitness_func=fitness_func, sol_per_pop=1000, num_genes=2, init_range_low=0.0, init_range_high=1.0, random_mutation_min_val=0.0, random_mutation_max_val=1.0, mutation_by_replacement=True, callback_generation=callback_generation, delay_after_gen=self.screen.char_anim_duration)
ga_instance.run()
global lvl_num
lvl_num = screen_num
collectCoinThread = CollectCoinThread(screen=curr_screen)
collectCoinThread.start()
总结
通过本教程,我们了解了如何使用遗传算法为 CoinTex 游戏创建一个游戏代理。这个代理能够在复杂的游戏环境中,即使在有许多硬币、怪物和火焰的困难关卡中,也能高效地工作。使用开源的 Python 库 PyGAD 实现遗传算法,为我们开发游戏代理提供了便利。如果你对代码感兴趣,可以在 GitHub 上找到 CoinTex 和代理的源代码。