社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python制作AI贪吃蛇,很多很多细节、思路都写下来了!

python • 4 年前 • 357 次点击  


前提:本文实现AI贪吃蛇自行对战,加上人机对战,读者可再次基础上自行添加电脑VS电脑和玩家VS玩家(其实把人机对战写完,这2个都没什么了,思路都一样)

实现效果:

具体功能:

1.智能模式:电脑自己玩(自己吃食物)

2.人机对战:电脑和人操作(在上步的基础上加一个键盘控制的贪吃蛇即可)

实现环境:

Pycharm + Python3.6 + Curses + Win10

具体过程:

一:配置环境:

Curses: 参考链接 (Cp后面代表本地Python环境,别下错了)

Stackoverflow 真的是个非常好的地方)

二:

1.灵感来源+参考链接:

http://www.hawstein.com/posts/snake-ai.html (Chrome有时候打不开,Firefox可以打开)

2.算法思路:

A*算法: https://www.cnblogs.com/21207-iHome/p/6048969.html (本人之前接触过,当时讲课老师说是自动寻路算法,我感觉和BFS+DFS一样,结果没想到居然是A*算法)

BFS+DFS(略)

第一步是能制作一个 基本的贪吃蛇 ,熟悉Curses的相关环境(最好别对蛇和食物使用特殊字符,在windows环境下会导致像素延迟,非常丑)


#curses官方手册:https://docs.python.org/3.5/library/curses.html#module-curses
#curses参考手册:https://blog.csdn.net/chenxiaohua/article/details/2099304

具体思路:

熟悉Curses中相关指令后基本就没什么了, 保证按的下一个键不导致蛇死亡,保证蛇吃食物后食物不在蛇身上,保证蛇碰到自己和边框就死亡,如果按其他键,会导致头被插入2次,从而让蛇死亡。(具体见代码分析)

 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # @Time : 2018/11/5 17:08
4 # @Author : Empirefree
5 # @File : 贪吃蛇-01.py
6 # @Software: PyCharm Community Edition
7
8 #curses官方手册:https://docs.python.org/3.5/library/curses.html#module-curses
9 #curses参考手册:https://blog.csdn.net/chenxiaohua/article/details/2099304
10
11 # 基本思路:while循环,让蛇一直右走(直到按键,如果按了其他键就会导致蛇头被重复插入1次到snake中,
12 # 继而第二次循环就会退出),蛇是每次自动增长,但是每次没吃到食物就会pop尾部(snake放在dict中,类似链表),按键检查就是只能按方向键
13 # 按方向键也存在判别是否出错(按了up后又按down),然后对于死亡情况就是碰到周围和自己
14
15 # 1.蛇的移动和吃食物后的变化
16 # 2.按键:按其他键和方向键
17 # 3.死亡判断
18
19 import curses
20 import random
21
22 # 开启curses
23 def Init_Curse():
24 global s
25 s = curses.initscr()
26 curses.curs_set(0) #能见度光标,写错了哇
27 curses.noecho()
28 curses.cbreak() #立即得到响应
29 s.keypad(True) #特殊处理键位,返回KEY_LEFT
30
31 #关闭并回到终端
32 def Exit_Curse():
33 curses.echo()
34 curses.nocbreak()
35 s.keypad(False)
36 curses.endwin()
37
38 def Start_Game():
39 # 窗口化操作
40 y, x = s.getmaxyx() # curses中是y,x
41 w = curses.newwin(y, x, 0, 0)
42 w.keypad(1)
43 w.timeout(100)
44
45 # 初始化蛇的位置,并用dict存储
46 snake_x = int(x / 4)
47 snake_y = int(y / 2)
48 snake = [[snake_y, snake_x], [snake_y, snake_x - 1], [snake_y, snake_x - 2]]
49
50 # 初始化食物
51 food_pos = [int(y / 2), int(x / 2)]
52 w.addch(food_pos[0], food_pos[1], '@') # 用@显示食物字元
53
54 key = curses.KEY_RIGHT # 得到右方向键
55
56 # 开始,为什么我感觉True比1看的爽一些
57 while True:
58 next_key = w.getch() # 等待输入,传回整数
59 print(next_key, 'QAQ')
60 # 防止Error
61 if next_key != -1:
62 if key == curses.KEY_RIGHT and next_key != curses.KEY_LEFT
63 or key == curses.KEY_LEFT and next_key != curses.KEY_RIGHT
64 or key == curses.KEY_DOWN and next_key != curses.KEY_UP
65 or key == curses.KEY_UP and next_key != curses.KEY_DOWN:
66 key = next_key
67
68 # 蛇死亡, 当蛇头碰到蛇身或墙壁
69 if snake[0][0] in [0, y] or snake[0][1] in [0, x] or snake[0] in snake[1:]:
70 # print(snake[0], snake[1]) 按下其他键就会导致,new_head被插入2次,从而退出
71 curses.endwin()
72 print('!!!游戏结束!!!')
73 quit()
74
75 #按键移动
76 tempy = snake[0][0]
77 tempx = snake[0][1]
78 new_head = [tempy, tempx]
79 if key == curses.KEY_RIGHT:
80 new_head[1] += 1
81 elif key == curses.KEY_LEFT:
82 new_head[1] -= 1
83 elif key == curses.KEY_UP:
84 new_head[0] -= 1
85 elif key == curses.KEY_DOWN:
86 new_head[0] += 1
87 snake.insert(0, new_head) #保留蛇头,根据按键更新蛇头
88
89 #食物位置
90 if snake[0] == food_pos:
91 food_pos = None
92 while food_pos is None:
93 new_food = [random.randint(1, y - 1), random.randint(1, x - 1)]
94 if new_food not in snake:
95 food_pos = new_food
96 w.addch(food_pos[0], food_pos[1], '@') #再次添加食物,保证食物不在蛇上
97 else:
98 tail = snake.pop() #dict直接pop尾部
99 w.addch(tail[0], tail[1], ' ')
100
101 w.addch(snake[0][0], snake[0][1], 'Q')
102
103 if __name__ == '__main__':
104 Init_Curse()
105 Start_Game()
106
107 print('QAQ')
108 Exit_Curse()
基本贪吃蛇

3.代码剖析:

[红色为代码所需函数]

(蛇每走一步,就更新snake距离food的board距离,涉及 board_rest (更新每个非snake元素距离food的距离)和 board_refresh (本文这里采用BFS算法)),寻找到best_move,然后让蛇移动即可

如果吃的到食物( find_safe_way ):----> 放出虚拟蛇( virtual_shortest_move )(防止蛇吃完食物就被自己绕死)

如果虚拟蛇吃完食物还可以找到 蛇尾(出的去)( is_tail_inside )

直接吃食物( choose_shortest_safe_move )

反之,出不去:

就跟着尾巴走( follow_tail )就好比一直上下绕,就绝对不会死,但是蛇就完全没有灵性

如果吃不到食物

跟着尾巴(走最远的路(

choose_longest_safe_move

)),四个方向走(如果是A*算法需要将8个方向改成4个方向)

如果上诉方法都不行,就涉及到a ny_possible_move ,挑选距离最小的走(这里就会涉及到将自己吃死,有待改进)

(通过以上方法,就可以制造一个基本AI贪吃蛇了,当然,还有很多细节方面东西需要考虑)

报错:

win = curses.newwin(HEIGHT, WIDTH, 0, 0)

_curses.error: curses function returned NULL

原因:Pycharm下面(或者cmd、exe太小,需要拉大点)

	 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # @Time : 2018/11/16 14:26
4 # @Author : Empirefree
5 # @File : 贪吃蛇-03.py
6 # @Software: PyCharm Community Edition
7
8 import curses
9 from curses import KEY_RIGHT, KEY_LEFT, KEY_UP, KEY_DOWN
10 from random import randint
11
12 # 必须要弄成全局哇,不然需要用到的数据太多了
13 # 1.初始化界面
14 # 2.更新地图,判断是否可以吃到食物
15 # 3.如果可以吃到,放出虚拟蛇(这里又设计到地图更新(board_reset),记录距离(board_refresh)操作)
16 # 3.1虚拟蛇若吃食物距离蛇尾有路径(直接吃),否则,追蛇尾
17 # 3.2若吃不到,则追蛇尾
18 # 4.更新best_move,改变距离
19 ###########################################################################################
20 #作者:
21 print('**************************************************************************')
22 print('*****************!!!欢迎使用AI贪吃蛇 !!!*************************')
23 print('*****************作者:胡宇乔 *********************')
24 print('*****************工具: Pycharm *********************')
25 print('*****************时间: 2018/11/16 14:26 ********************')
26 print('***************** (按Esc结束贪吃蛇游戏) **********************')
27 print('**************************************************************************')
28 # 场地
29 HEIGHT, WIDTH = map(int, input('请输入长度长宽[20 40]:').split())
30 FIELD_SIZE = HEIGHT * WIDTH
31
32 #蛇和食物
33 HEAD = 0
34 FOOD = 0
35 UNDEFINED = (HEIGHT + 1) * (WIDTH + 1)
36 SNAKE = 2 * UNDEFINED
37
38 # 四个方向的移动
39 LEFT = -1
40 RIGHT = 1
41 UP = -WIDTH
42 DOWN = WIDTH
43
44 # 错误码
45 ERR = -1111
46
47 # 用一维数组来表示二维的东西
48 # board表示蛇运动的矩形场地
49 # 初始化蛇头在(1,1)的地方,第0行,HEIGHT行,第0列,WIDTH列为围墙,不可用
50 # 初始蛇长度为1
51 board = [0] * FIELD_SIZE
52 snake = [0] * (FIELD_SIZE + 1)
53 snake[HEAD] = 1 * WIDTH + 1
54 snake_size = 1
55 # tmpsnake即虚拟蛇
56 tmpboard = [0] * FIELD_SIZE
57 tmpsnake = [0] * (FIELD_SIZE + 1)
58 tmpsnake[HEAD] = 1 * WIDTH + 1
59 tmpsnake_size = 1
60
61 # food:食物位置(0~FIELD_SIZE-1),初始在(3, 3)
62 # best_move: 运动方向
63 food = 3 * WIDTH + 3
64 best_move = ERR
65
66 # 运动方向数组
67 mov = [LEFT, RIGHT, UP, DOWN]
68 # 接收到的键 和 分数
69 key = KEY_RIGHT
70 score = 1 # 分数也表示蛇长
71
72 #cueses初始化
73 curses.initscr()
74 win = curses.newwin(HEIGHT, WIDTH, 0, 0)
75 win.keypad(1)
76 curses.noecho()
77 curses.curs_set(0)
78 win.border(0)
79 win.nodelay(1)
80 win.addch(food // WIDTH, food % WIDTH, '@')
81
82 ###########################################################################################
83 #判断是否为空(可走)
84 def is_cell_free(idx, psize, psnake):
85 return not (idx in psnake[:psize])
86
87
88 # 检查某个位置idx是否可向move方向运动
89 def is_move_possible(idx, move):
90 flag = False
91 if move == LEFT:
92 flag = True if idx % WIDTH > 1 else False
93 elif move == RIGHT:
94 flag = True if idx % WIDTH < (WIDTH - 2) else False
95 elif move == UP:
96 flag = True if idx > (2 * WIDTH - 1) else False # 即idx/WIDTH > 1
97 elif move == DOWN:
98 flag = True if idx < (FIELD_SIZE - 2 * WIDTH) else False # 即idx/WIDTH < HEIGHT-2
99 return flag
100
101
102 # 计算出board中每个非SNAKE元素到达食物的路径长度,并判断是否可以找到食物
103 def board_reset(psnake, psize, pboard):
104 for i in range(FIELD_SIZE):
105 if i == food:
106 pboard[i] = FOOD
107 elif is_cell_free(i, psize, psnake): # 该位置为空
108 pboard[i] = UNDEFINED
109 else: # 该位置为蛇身
110 pboard[i] = SNAKE
111
112
113 # 广度优先搜索遍历整个board,
114 # 计算出board中每个非SNAKE元素到达食物的路径长度
115 def board_refresh(pfood, psnake, pboard):
116 queue = []
117 queue.append(pfood)
118 inqueue = [0] * FIELD_SIZE
119 found = False
120 # while循环结束后,除了蛇的身体,
121 # 其它每个方格中的数字代码从它到食物的路径长度
122 while len(queue) != 0:
123 idx = queue.pop(0)
124 if inqueue[idx] == 1: continue
125 inqueue[idx] = 1
126 for i in range(4):
127 if is_move_possible(idx, mov[i]):
128 if idx + mov[i] == psnake[HEAD]:
129 found = True
130 if pboard[idx + mov[i]] < SNAKE: # 如果该点不是蛇的身体
131
132 if pboard[idx + mov[i]] > pboard[idx] + 1:
133 pboard[idx + mov[i]] = pboard[idx] + 1
134 if inqueue[idx + mov[i]] == 0:
135 queue.append(idx + mov[i])
136
137 return found
138
139
140 #蛇头开始,根据蛇的4个领域选择最远路径(安全一点)
141 def choose_shortest_safe_move(psnake, pboard):
142 best_move = ERR
143 min = SNAKE
144 for i in range(4):
145 if is_move_possible(psnake[HEAD], mov[i]) and pboard[psnake[HEAD] + mov[i]] < min:
146 min = pboard[psnake[HEAD] + mov[i]]
147 best_move = mov[i]
148 return best_move
149
150
151 # 从蛇头开始,根据board中元素值,
152 # 从蛇头周围4个领域点中选择最远路径
153 def choose_longest_safe_move(psnake, pboard):
154 best_move = ERR
155 max = -1
156 for i in range(4):
157 if is_move_possible(psnake[HEAD], mov[i]) and pboard[psnake[HEAD] + mov[i]] < UNDEFINED and pboard[psnake[HEAD] + mov[i]] > max:
158 max = pboard[psnake[HEAD] + mov[i]]
159 best_move = mov[i]
160 return best_move
161
162
163 # 检查是否可以追着蛇尾运动,即蛇头和蛇尾间是有路径的
164 # 为的是避免蛇头陷入死路
165 # 虚拟操作,在tmpboard,tmpsnake中进行
166 def is_tail_inside():
167 global tmpboard, tmpsnake, food, tmpsnake_size
168 tmpboard[tmpsnake[tmpsnake_size - 1]] = 0 # 虚拟地将蛇尾变为食物(因为是虚拟的,所以在tmpsnake,tmpboard中进行)
169 tmpboard[food] = SNAKE # 放置食物的地方,看成蛇身
170 result = board_refresh(tmpsnake[tmpsnake_size - 1], tmpsnake, tmpboard) # 求得每个位置到蛇尾的路径长度
171 for i in range(4): # 如果蛇头和蛇尾紧挨着,则返回False。即不能follow_tail,追着蛇尾运动了
172 if is_move_possible(tmpsnake[HEAD], mov[i]) and tmpsnake[HEAD] + mov[i] == tmpsnake[
173 tmpsnake_size - 1] and tmpsnake_size > 3:
174 result = False
175 return result
176
177
178 # 让蛇头朝着蛇尾运行一步
179 # 不管蛇身阻挡,朝蛇尾方向运行
180 def follow_tail():
181 global tmpboard, tmpsnake, food, tmpsnake_size
182 tmpsnake_size = snake_size
183 tmpsnake = snake[:]
184 board_reset(tmpsnake, tmpsnake_size, tmpboard) # 重置虚拟board
185 tmpboard[tmpsnake[tmpsnake_size - 1]] = FOOD # 让蛇尾成为食物
186 tmpboard[food] = SNAKE # 让食物的地方变成蛇身
187 board_refresh(tmpsnake[tmpsnake_size - 1], tmpsnake, tmpboard) # 求得各个位置到达蛇尾的路径长度
188 tmpboard[tmpsnake[tmpsnake_size - 1]] = SNAKE # 还原蛇尾
189
190 return choose_longest_safe_move(tmpsnake, tmpboard) # 返回运行方向(让蛇头运动1步)
191
192
193 # 在各种方案都不行时,随便找一个可行的方向来走(1步),
194 def any_possible_move():
195 global food, snake, snake_size, board
196 best_move = ERR
197 board_reset(snake, snake_size, board)
198 board_refresh(food, snake, board)
199 min = SNAKE
200
201 for i in range(4):
202 if is_move_possible(snake[HEAD], mov[i]) and board[snake[HEAD] + mov[i]] < min:
203 min = board[snake[HEAD] + mov[i]]
204 best_move = mov[i]
205 return best_move
206
207 #虚拟蛇蛇移动
208 def shift_array(arr, size):
209 for i in range(size, 0, -1):
210 arr[i] = arr[i - 1]
211
212 #产生新食物
213 def new_food():
214 global food, snake_size
215 cell_free = False
216 while not cell_free:
217 w = randint(1, WIDTH - 2)
218 h = randint(1, HEIGHT - 2)
219 food = h * WIDTH + w
220 cell_free = is_cell_free(food, snake_size, snake)
221 win.addch(food // WIDTH, food % WIDTH, '@')
222
223
224 # 真正的蛇在这个函数中,朝pbest_move走1步
225 def make_move(pbest_move):
226 global key, snake, board, snake_size, score
227 shift_array(snake, snake_size)
228 snake[HEAD] += pbest_move
229
230 # 按esc退出,getch同时保证绘图的流畅性,没有它只会看到最终结果
231 win.timeout(10)
232 event = win.getch()
233 key = key if event == -1 else event
234 if key == 27: return
235
236 p = snake[HEAD]
237 win.addch(p // WIDTH, p % WIDTH, '*')
238
239 # 如果新加入的蛇头就是食物的位置
240 # 蛇长加1,产生新的食物,重置board(因为原来那些路径长度已经用不上了)
241 if snake[HEAD] == food:
242 board[snake[HEAD]] = SNAKE # 新的蛇头
243 snake_size += 1
244 score += 1
245 if snake_size < FIELD_SIZE: new_food()
246 else: # 如果新加入的蛇头不是食物的位置
247 board[snake[HEAD]] = SNAKE # 新的蛇头
248 board[snake[snake_size]] = UNDEFINED # 蛇尾变为空格
249 win.addch(snake[snake_size] // WIDTH, snake[snake_size] % WIDTH, ' ')
250
251
252 #虚拟蛇最短移动
253 def virtual_shortest_move():
254 global snake, board, snake_size, tmpsnake, tmpboard, tmpsnake_size, food
255 tmpsnake_size = snake_size
256 tmpsnake = snake[:] # 如果直接tmpsnake=snake,则两者指向同一处
257 tmpboard = board[:] # board中已经是各位置到达食物的路径长度了,不用再计算
258 board_reset(tmpsnake, tmpsnake_size, tmpboard)
259
260 food_eated = False
261 while not food_eated:
262 board_refresh(food, tmpsnake, tmpboard)
263 move = choose_shortest_safe_move(tmpsnake, tmpboard)
264 shift_array(tmpsnake, tmpsnake_size)
265 tmpsnake[HEAD] += move # 在蛇头前加入一个新的位置
266 # 如果新加入的蛇头的位置正好是食物的位置
267 # 则长度加1,重置board,食物那个位置变为蛇的一部分(SNAKE)
268 if tmpsnake[HEAD] == food:
269 tmpsnake_size += 1
270 board_reset(tmpsnake, tmpsnake_size, tmpboard) # 虚拟运行后,蛇在board的位置
271 tmpboard[food] = SNAKE
272 food_eated = True
273 else: # 如果蛇头不是食物的位置,则新加入的位置为蛇头,最后一个变为空格
274 tmpboard[tmpsnake[HEAD]] = SNAKE
275 tmpboard[tmpsnake[tmpsnake_size]] = UNDEFINED
276
277
278 # 如果蛇与食物间有路径,则调用本函数
279 def find_safe_way():
280 global snake, board
281 safe_move = ERR
282 # 虚拟地运行一次,因为已经确保蛇与食物间有路径,所以执行有效
283 # 运行后得到虚拟下蛇在board中的位置,即tmpboard,见label101010
284 virtual_shortest_move() # 该函数唯一调用处
285 if is_tail_inside(): # 如果虚拟运行后,蛇头蛇尾间有通路,则选最短路运行(1步)
286 return choose_shortest_safe_move(snake, board)
287 safe_move = follow_tail() # 否则虚拟地follow_tail 1步,如果可以做到,返回true
288 return safe_move
289
290 if __name__ == '__main__':
291
292 while key != 27:
293 win.border(0)
294 win.addstr(0, 2, '分数:' + str(score) + ' ')
295 win.timeout(10)
296 # 接收键盘输入,同时也使显示流畅
297 event = win.getch()
298 key = key if event == -1 else event
299 # 重置矩阵
300 board_reset(snake, snake_size, board)
301
302 # 如果蛇可以吃到食物,board_refresh返回true
303 # 并且board中除了蛇身(=SNAKE),其它的元素值表示从该点运动到食物的最短路径长
304 if board_refresh(food, snake, board):
305 best_move = find_safe_way() # find_safe_way的唯一调用处
306 else:
307 best_move = follow_tail()
308
309 if best_move == ERR:
310 best_move = any_possible_move()
311 # 上面一次思考,只得出一个方向,运行一步
312 if best_move != ERR:
313 make_move(best_move)
314 else:
315 break
316
317 curses.endwin()
318 print("
得分:" + str(score))
贪吃蛇-02

在以上基础上,还需要引入第一步制造的基本贪吃蛇


细节:1.键盘蛇加入后如何与蛇抢分(只需要return即可,但是 new_food()里面是需要更改的)

	 1 # 产生新食物
2 def new_food():
3 global food, snake_size, myfood
4 cell_free = False
5 while not cell_free:
6 food1 = [random.randint(1, HEIGHT - 2), random.randint(1, WIDTH - 2)]
7 w = randint(1, WIDTH - 2)
8 h = randint(1, HEIGHT - 2)
9 myfood = [h, w]
10 food = h * WIDTH + w
11 if (is_cell_free(food, snake_size, snake) and [w, h] not in snake1):
12 cell_free = True
13 win.addch(food // WIDTH, food % WIDTH, '@')

2.一直没说,由于蛇加入后很多变量都需要global,导致变量看起来非常麻烦(读者要有心理准备)

3.curses里面的win.timeout()是控制蛇的速度

好像就没什么了,想起来了在更。我没加入2条蛇不能彼此碰撞(读者也可以弄成2个地图,然后看AI蛇和你自己的蛇如何操作跑,我是放在了一个地图里面)

当然还有很多很多细节,不过主要思路写下来了。其余就靠分析代码自行研究了。Python制作AI贪吃蛇

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

觉得不错,点个“在看”然后转发出去



Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/72637
 
357 次点击