前提:本文实现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.算法思路:

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

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/envpython2#-*-coding:utf-8-*-3#@Time:2018/11/517:084#@Author:Empirefree5#@File:贪吃蛇-01.py6#@Software:PyCharmCommunityEdition78#curses官方手册:https://docs.python.org/3.5/library/curses.html#module-curses9#curses参考手册:https://blog.csdn.net/chenxiaohua/article/details/20993041011#基本思路:while循环,让蛇一直右走(直到按键,如果按了其他键就会导致蛇头被重复插入1次到snake中,12#继而第二次循环就会退出),蛇是每次自动增长,但是每次没吃到食物就会pop尾部(snake放在dict中,类似链表),按键检查就是只能按方向键13#按方向键也存在判别是否出错(按了up后又按down),然后对于死亡情况就是碰到周围和自己1415#1.蛇的移动和吃食物后的变化16#2.按键:按其他键和方向键17#3.死亡判断1819importcurses20importrandom2122#开启curses23defInit_Curse():24globals25s=curses.initscr()26curses.curs_set(0)#能见度光标,写错了哇27curses.noecho()28curses.cbreak()#立即得到响应29s.keypad(True)#特殊处理键位,返回KEY_LEFT3031#关闭并回到终端32defExit_Curse():33curses.echo()34curses.nocbreak()35s.keypad(False)36curses.endwin()3738defStart_Game():39#窗口化操作40y,x=s.getmaxyx()#curses中是y,x41w=curses.newwin(y,x,0,0)42w.keypad(1)43w.timeout(100)4445#初始化蛇的位置,并用dict存储46snake_x=int(x/4)47snake_y=int(y/2)48snake=[[snake_y,snake_x],[snake_y,snake_x-1],[snake_y,snake_x-2]]4950#初始化食物51food_pos=[int(y/2),int(x/2)]52w.addch(food_pos[0],food_pos[1],'@')#用@显示食物字元5354key=curses.KEY_RIGHT#得到右方向键5556#开始,为什么我感觉True比1看的爽一些57whileTrue:58next_key=w.getch()#等待输入,传回整数59print(next_key,'QAQ')60#防止Error61ifnext_key!=-1:62ifkey==curses.KEY_RIGHTandnext_key!=curses.KEY_LEFT63orkey==curses.KEY_LEFTandnext_key!=curses.KEY_RIGHT64orkey==curses.KEY_DOWNandnext_key!=curses.KEY_UP65orkey==curses.KEY_UPandnext_key!=curses.KEY_DOWN:66key=next_key6768#蛇死亡,当蛇头碰到蛇身或墙壁69ifsnake[0][0]in[0,y]orsnake[0][1]in[0,x]orsnake[0]insnake[1:]:70#print(snake[0],snake[1])按下其他键就会导致,new_head被插入2次,从而退出71curses.endwin()72print('!!!游戏结束!!!')73quit()7475#按键移动76tempy=snake[0][0]77tempx=snake[0][1]78new_head=[tempy,tempx]79ifkey==curses.KEY_RIGHT:80new_head[1]+=181elifkey==curses.KEY_LEFT:82new_head[1]-=183elifkey==curses.KEY_UP:84new_head[0]-=185elifkey==curses.KEY_DOWN:86new_head[0]+=187snake.insert(0,new_head)#保留蛇头,根据按键更新蛇头8889#食物位置90ifsnake[0]==food_pos:91food_pos=None92whilefood_posisNone:93new_food=[random.randint(1,y-1),random.randint(1,x-1)]94ifnew_foodnotinsnake:95food_pos=new_food96w.addch(food_pos[0],food_pos[1],'@')#再次添加食物,保证食物不在蛇上97else:98tail=snake.pop()#dict直接pop尾部99w.addch(tail[0],tail[1],'')100101w.addch(snake[0][0],snake[0][1],'Q')102103if__name__=='__main__':104Init_Curse()105Start_Game()106107print('QAQ')108Exit_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/envpython2#-*-coding:utf-8-*-3#@Time:2018/11/1614:264#@Author:Empirefree5#@File:贪吃蛇-03.py6#@Software:PyCharmCommunityEdition78importcurses9fromcursesimportKEY_RIGHT,KEY_LEFT,KEY_UP,KEY_DOWN10fromrandomimportrandint1112#必须要弄成全局哇,不然需要用到的数据太多了13#1.初始化界面14#2.更新地图,判断是否可以吃到食物15#3.如果可以吃到,放出虚拟蛇(这里又设计到地图更新(board_reset),记录距离(board_refresh)操作)16#3.1虚拟蛇若吃食物距离蛇尾有路径(直接吃),否则,追蛇尾17#3.2若吃不到,则追蛇尾18#4.更新best_move,改变距离19###########################################################################################20#作者:21print('**************************************************************************')22print('*****************!!!欢迎使用AI贪吃蛇!!!*************************')23print('*****************作者:胡宇乔*********************')24print('*****************工具:Pycharm*********************')25print('*****************时间:2018/11/1614:26********************')26print('*****************(按Esc结束贪吃蛇游戏)**********************')27print('**************************************************************************')28#场地29HEIGHT,WIDTH=map(int,input('请输入长度长宽[20 40]:').split())30FIELD_SIZE=HEIGHT*WIDTH3132#蛇和食物33HEAD=034FOOD=035UNDEFINED=(HEIGHT+1)*(WIDTH+1)36SNAKE=2*UNDEFINED3738#四个方向的移动39LEFT=-140RIGHT=141UP=-WIDTH42DOWN=WIDTH4344#错误码45ERR=-11114647#用一维数组来表示二维的东西48#board表示蛇运动的矩形场地49#初始化蛇头在(1,1)的地方,第0行,HEIGHT行,第0列,WIDTH列为围墙,不可用50#初始蛇长度为151board=[0]*FIELD_SIZE52snake=[0]*(FIELD_SIZE+1)53snake[HEAD]=1*WIDTH+154snake_size=155#tmpsnake即虚拟蛇56tmpboard=[0]*FIELD_SIZE57tmpsnake=[0]*(FIELD_SIZE+1)58tmpsnake[HEAD]=1*WIDTH+159tmpsnake_size=16061#food:食物位置(0~FIELD_SIZE-1),初始在(3,3)62#best_move:运动方向63food=3*WIDTH+364best_move=ERR6566#运动方向数组67mov=[LEFT,RIGHT,UP,DOWN]68#接收到的键和分数69key=KEY_RIGHT70score=1#分数也表示蛇长7172#cueses初始化73curses.initscr()74win=curses.newwin(HEIGHT,WIDTH,0,0)75win.keypad(1)76curses.noecho()77curses.curs_set(0)78win.border(0)79win.nodelay(1)80win.addch(food//WIDTH,food%WIDTH,'@')8182###########################################################################################83#判断是否为空(可走)84defis_cell_free(idx,psize,psnake):85returnnot(idxinpsnake[:psize])868788#检查某个位置idx是否可向move方向运动89defis_move_possible(idx,move):90flag=False91ifmove==LEFT:92flag=Trueifidx%WIDTH>1elseFalse93elifmove==RIGHT:94flag=Trueifidx%WIDTH(2*WIDTH-1)elseFalse#即idx/WIDTH>197elifmove==DOWN:98flag=Trueifidx<(FIELD_SIZE-2*WIDTH)elseFalse#即idx/WIDTH<HEIGHT-299returnflag100101102#计算出board中每个非SNAKE元素到达食物的路径长度,并判断是否可以找到食物103defboard_reset(psnake,psize,pboard):104foriinrange(FIELD_SIZE):105ifi==food:106pboard[i]=FOOD107elifis_cell_free(i,psize,psnake):#该位置为空108pboard[i]=UNDEFINED109else:#该位置为蛇身110pboard[i]=SNAKE111112113#广度优先搜索遍历整个board,114#计算出board中每个非SNAKE元素到达食物的路径长度115defboard_refresh(pfood,psnake,pboard):116queue=[]117queue.append(pfood)118inqueue=[0]*FIELD_SIZE119found=False120#while循环结束后,除了蛇的身体,121#其它每个方格中的数字代码从它到食物的路径长度122whilelen(queue)!=0:123idx=queue.pop(0)124ifinqueue[idx]==1:continue125inqueue[idx]=1126foriinrange(4):127ifis_move_possible(idx,mov[i]):128ifidx+mov[i]==psnake[HEAD]:129found=True130ifpboard[idx+mov[i]]pboard[idx]+1:133pboard[idx+mov[i]]=pboard[idx]+1134ifinqueue[idx+mov[i]]==0:135queue.append(idx+mov[i])136137returnfound138139140#蛇头开始,根据蛇的4个领域选择最远路径(安全一点)141defchoose_shortest_safe_move(psnake,pboard):142best_move=ERR143min=SNAKE144foriinrange(4):145ifis_move_possible(psnake[HEAD],mov[i])andpboard[psnake[HEAD]+mov[i]]<min:146min=pboard[psnake[HEAD]+mov[i]]147best_move=mov[i]148returnbest_move149150151#从蛇头开始,根据board中元素值,152#从蛇头周围4个领域点中选择最远路径153defchoose_longest_safe_move(psnake,pboard):154best_move=ERR155max=-1156foriinrange(4):157ifis_move_possible(psnake[HEAD],mov[i])andpboard[psnake[HEAD]+mov[i]]max:158max=pboard[psnake[HEAD]+mov[i]]159best_move=mov[i]160returnbest_move161162163#检查是否可以追着蛇尾运动,即蛇头和蛇尾间是有路径的164#为的是避免蛇头陷入死路165#虚拟操作,在tmpboard,tmpsnake中进行166defis_tail_inside():167globaltmpboard,tmpsnake,food,tmpsnake_size168tmpboard[tmpsnake[tmpsnake_size-1]]=0#虚拟地将蛇尾变为食物(因为是虚拟的,所以在tmpsnake,tmpboard中进行)169tmpboard[food]=SNAKE#放置食物的地方,看成蛇身170result=board_refresh(tmpsnake[tmpsnake_size-1],tmpsnake,tmpboard)#求得每个位置到蛇尾的路径长度171foriinrange(4):#如果蛇头和蛇尾紧挨着,则返回False。即不能follow_tail,追着蛇尾运动了172ifis_move_possible(tmpsnake[HEAD],mov[i])andtmpsnake[HEAD]+mov[i]==tmpsnake[173tmpsnake_size-1]andtmpsnake_size>3:174result=False175returnresult176177178#让蛇头朝着蛇尾运行一步179#不管蛇身阻挡,朝蛇尾方向运行180deffollow_tail():181globaltmpboard,tmpsnake,food,tmpsnake_size182tmpsnake_size=snake_size183tmpsnake=snake[:]184board_reset(tmpsnake,tmpsnake_size,tmpboard)#重置虚拟board185tmpboard[tmpsnake[tmpsnake_size-1]]=FOOD#让蛇尾成为食物186tmpboard[food]=SNAKE#让食物的地方变成蛇身187board_refresh(tmpsnake[tmpsnake_size-1],tmpsnake,tmpboard)#求得各个位置到达蛇尾的路径长度188tmpboard[tmpsnake[tmpsnake_size-1]]=SNAKE#还原蛇尾189190returnchoose_longest_safe_move(tmpsnake,tmpboard)#返回运行方向(让蛇头运动1步)191192193#在各种方案都不行时,随便找一个可行的方向来走(1步),194defany_possible_move():195globalfood,snake,snake_size,board196best_move=ERR197board_reset(snake,snake_size,board)198board_refresh(food,snake,board)199min=SNAKE200201foriinrange(4):202ifis_move_possible(snake[HEAD],mov[i])andboard[snake[HEAD]+mov[i]]<min:203min=board[snake[HEAD]+mov[i]]204best_move=mov[i]205returnbest_move206207#虚拟蛇蛇移动208defshift_array(arr,size):209foriinrange(size,0,-1):210arr[i]=arr[i-1]211212#产生新食物213defnew_food():214globalfood,snake_size215cell_free=False216whilenotcell_free:217w=randint(1,WIDTH-2)218h=randint(1,HEIGHT-2)219food=h*WIDTH+w220cell_free=is_cell_free(food,snake_size,snake)221win.addch(food//WIDTH,food%WIDTH,'@')222223224#真正的蛇在这个函数中,朝pbest_move走1步225defmake_move(pbest_move):226globalkey,snake,board,snake_size,score227shift_array(snake,snake_size)228snake[HEAD]+=pbest_move229230#按esc退出,getch同时保证绘图的流畅性,没有它只会看到最终结果231win.timeout(10)232event=win.getch()233key=keyifevent==-1elseevent234ifkey==27:return235236p=snake[HEAD]237win.addch(p//WIDTH,p%WIDTH,'*')238239#如果新加入的蛇头就是食物的位置240#蛇长加1,产生新的食物,重置board(因为原来那些路径长度已经用不上了)241ifsnake[HEAD]==food:242board[snake[HEAD]]=SNAKE#新的蛇头243snake_size+=1244score+=1245ifsnake_size<FIELD_SIZE:new_food()246else:#如果新加入的蛇头不是食物的位置247board[snake[HEAD]]=SNAKE#新的蛇头248board[snake[snake_size]]=UNDEFINED#蛇尾变为空格249win.addch(snake[snake_size]//WIDTH,snake[snake_size]%WIDTH,'')250251252#虚拟蛇最短移动253defvirtual_shortest_move():254globalsnake,board,snake_size,tmpsnake,tmpboard,tmpsnake_size,food255tmpsnake_size=snake_size256tmpsnake=snake[:]#如果直接tmpsnake=snake,则两者指向同一处257tmpboard=board[:]#board中已经是各位置到达食物的路径长度了,不用再计算258board_reset(tmpsnake,tmpsnake_size,tmpboard)259260food_eated=False261whilenotfood_eated:262board_refresh(food,tmpsnake,tmpboard)263move=choose_shortest_safe_move(tmpsnake,tmpboard)264shift_array(tmpsnake,tmpsnake_size)265tmpsnake[HEAD]+=move#在蛇头前加入一个新的位置266#如果新加入的蛇头的位置正好是食物的位置267#则长度加1,重置board,食物那个位置变为蛇的一部分(SNAKE)268iftmpsnake[HEAD]==food:269tmpsnake_size+=1270board_reset(tmpsnake,tmpsnake_size,tmpboard)#虚拟运行后,蛇在board的位置271tmpboard[food]=SNAKE272food_eated=True273else:#如果蛇头不是食物的位置,则新加入的位置为蛇头,最后一个变为空格274tmpboard[tmpsnake[HEAD]]=SNAKE275tmpboard[tmpsnake[tmpsnake_size]]=UNDEFINED276277278#如果蛇与食物间有路径,则调用本函数279deffind_safe_way():280globalsnake,board281safe_move=ERR282#虚拟地运行一次,因为已经确保蛇与食物间有路径,所以执行有效283#运行后得到虚拟下蛇在board中的位置,即tmpboard,见label101010284virtual_shortest_move()#该函数唯一调用处285ifis_tail_inside():#如果虚拟运行后,蛇头蛇尾间有通路,则选最短路运行(1步)286returnchoose_shortest_safe_move(snake,board)287safe_move=follow_tail()#否则虚拟地follow_tail1步,如果可以做到,返回true288returnsafe_move289290if__name__=='__main__':291292whilekey!=27:293win.border(0)294win.addstr(0,2,'分数:'+str(score)+'')295win.timeout(10)296#接收键盘输入,同时也使显示流畅297event=win.getch()298key=keyifevent==-1elseevent299#重置矩阵300board_reset(snake,snake_size,board)301302#如果蛇可以吃到食物,board_refresh返回true303#并且board中除了蛇身(=SNAKE),其它的元素值表示从该点运动到食物的最短路径长304ifboard_refresh(food,snake,board):305best_move=find_safe_way()#find_safe_way的唯一调用处306else:307best_move=follow_tail()308309ifbest_move==ERR:310best_move=any_possible_move()311#上面一次思考,只得出一个方向,运行一步312ifbest_move!=ERR:313make_move(best_move)314else:315break316317curses.endwin()318print("得分:"+str(score))贪吃蛇-02

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

细节:

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

1#产生新食物2defnew_food():3globalfood,snake_size,myfood4cell_free=False5whilenotcell_free:6food1=[random.randint(1,HEIGHT-2),random.randint(1,WIDTH-2)]7w=randint(1,WIDTH-2)8h=randint(1,HEIGHT-2)9myfood=[h,w]10food=h*WIDTH+w11if(is_cell_free(food,snake_size,snake)and[w,h]notinsnake1):12cell_free=True13win.addch(food//WIDTH,food%WIDTH,'@')

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

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

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

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

更多精彩教程欢迎B站搜索“千锋教育”

千锋教育Python全套视频教程,轻松掌握Excel、Word、PPT、邮件、爬虫、office办公自动化(宋如宁主讲)