接十二,今天重点搞定简单基础版本的AI下棋。
画个大纲(跟随慢慢开发过程不断完善)
1.用户
两个用户对战 一黑一白
用户可以是人,也可以是AI。对战模式支持人人,人机,机机。
- 属性
本次比赛执棋颜色
用户名
密码
游戏得分(赢得局数)
存档棋盘信息(二维数组,chessShape类型数组,黑、白、总棋子个数,下一次下棋的棋权)
-
方法
下棋
输赢
设置、获取属性接口
2.比赛规则
一黑一白交替轮流下棋
可以决定哪个玩家先手
不可以重复下棋到同一个位置
不可以将棋子下到边界外
可以撤回刚刚下的棋,不可以撤回上一步的棋
哪一方横竖斜到达5个棋子赢一局
点击存档,当前用户储存当前棋面所有信息
读档读取当前用户保存的棋盘,一个用户只能存一个棋盘(人机模式加进来可以分开储存)
3.界面
- 登录界面
用户登录:
用户名、密码输入栏(带提示,密码隐藏,用户密码匹配检验),登录按键,注册按键(注册新建user储存到用户txt文档,并弹出储存成功界面选择是否直接登录);
登录图像。
小logo图标。
- 菜单界面
选择功能:
新游戏:对战模式 —— 人人,人机,机机
游戏积分 —— 获胜局数
退出游戏:关闭游戏
可以有设置按键:设置游戏背景音乐,音量等,添加用户设置棋子花色功能(现阶段默认登录用户为黑子)。
- 游戏界面
下棋主界面:
设置棋盘、背景板、菜单栏、棋子计数板、计分板、棋子、下棋指示器,刷新窗体后这些都不会消失。
选择游戏先手为黑棋还是白棋。
背景板、棋盘:17*17(16行,17根线,在中间画分界小黑点),棋盘在背景板之上。
菜单栏:撤回、清空、存档、帮助功能——撤回/清空时,计数器要跟着变化。
棋子计数板:记录当前棋盘上黑白棋子个数。(图像不重叠,随撤回清空等操作实时刷新)。
计分板:记录目前双方赢得局数。
棋子:下到交叉线(棋子校准)、不重复、不越出棋盘、刷新保存,可以撤回,可以识别获胜。
当前局数计时器:距离游戏开始的耗时。
- 获胜界面
当有一方获胜后弹出
显示哪方获胜
显示棋面棋子数
显示获胜图片
菜单:
再战一局:触发游戏界面(棋盘清空)
退出游戏:回到菜单界面(棋盘清空)
回顾棋局:显示重新下棋步骤(撤回步骤显示,回顾结束后弹窗返回)
乱七八糟的功能
存档
读档
软件使用日志
第五天 —— 完善AI下棋逻辑,界面功能
实现AI判断合适的下棋位置,完善登录界面,构建储存用户txt。
实现方式以及一些大佬的优秀小tips
1.AI下棋:
计算棋盘上每个空位上的权值,权值越大,越有可能下在这个位置。
权值如何设计:
下棋特点:
棋子最容易下在同色棋子周围,最容易下在连棋长度最长的地方,最容易堵别人的活三连……
一个棋点周围有8路(8个方向)与其有关,列出这8个方向的所有可能,并根据情况赋予不同权值而达到计算权值做出最佳决策的效果。
在初始阶段我们并不考虑垂直、水平这种同一方向(感觉情况会复杂很多),将8个方向先分开考虑,每个方向的思路都一样,所以共用。
从这个要计算权值的点出发朝某一个方向算棋,先分为两类:
ps:从这个点朝某个方向出发,如果没连到棋就不算权值直接为0。
活棋:
从这个点朝某个方向出发,连到几个同色棋子之后是空棋。代码为010,0110,01110,011110等等。
活棋有一、二、三、四连。
死棋:
从这个点朝某个方向出发,连到几个同色棋子之后是空另一方的棋或者边界。代码为01,011,0111,01111等等。
死棋有一、二、三、四连。
图示:
从左上方向顺时针分别是:活棋一、二、三、四连,死棋一、二、三、四连。
给这8种情况一次赋权值,并以HashMap这种数据结构储存。key为符号编码例如010,011等;value的设计思路:
连数越高,权值应该越大,同等连数活连要大于死连。
又因为8个方向是独立计算的,合并造成的关系应该也有数值大小关系。
为了测试可以先大致设,后续数据关系思路:
棋面权值如何统计:
因为是棋盘上每一个空着的棋点都有可能是放置下一个棋子的位置,因此要遍历棋盘计算权值。
首先这个位置必须是空位置才有可能下棋。
其次这个位置有8个延伸方向,因此需要写8个方向的循环。每个方向最多有四个棋子连棋的延伸。
对某个方向来说周围有棋子才可能连棋,如果周围第一个地方就没有棋子,那么直接认为权值为0,不进入连棋判断的循环;有棋子的话要先记录这颗棋子的颜色,为了后面连棋判断(会突然发现并没有区别己方和敌方,仔细想想好像可以放一起,因为己方、敌方连棋都是我们要放的位置。但再仔细想发现也有区别,可以设置优先级,后面升级再设置)
记录颜色之后进入4个的循环(因为最多四子连棋,否则上次就赢了)——查看棋子颜色计算有多少子连棋,每次循环结果三种情况:
没有棋子:记录连棋记录,break,是活棋,不必再循环下去。
同色:记录连棋记录,继续。
异色:break,直接为死棋。
每个方向的连棋记录生成后(就是key),通过HashMap的据key查值方法,将值叠加起来,8次值的和就是最终这个点的最后权值分数。
代码:
public void fillChessMap(){ //考虑在最一开始就加入,没用每次计算都重新添加一遍 this.chessHashMap.addCode(); //为了检测棋盘输出 System.out.println("chesses棋盘:"); for(int i=0;i<=ROW;i++){ for(int j=0;j<=LINE;j++){ System.out.print(goBangUI.chesses[i][j]+"\t"); } System.out.println(); } System.out.println(); //System.out.println(this.chessHashMap.getCodeWeight("010")); //算法开始遍历棋盘 for(int i=0;i<=ROW;i++){ for(int j=0;j<=LINE;j++){ if(goBangUI.chesses[i][j]==0){ //这个位置首先必须是个空位子才有可能放棋子 int score=0; //分数随棋盘算的位置才更新一次 for(int l=0;l<8;l++) { //8个方向循环 if(i+this.changeLocation[l][0]>ROW || j+this.changeLocation[l][1]>LINE || i+this.changeLocation[l][0]<0 || j+this.changeLocation[l][1]<0){ System.out.println("i:"+i+this.changeLocation[l][0]+" j:"+j+this.changeLocation[l][1]); continue; } //先判断是否出了边界,出边界跳过 String strCode="0"; //没出就开始记录 int chessColor = goBangUI.chesses[i+changeLocation[l][0]][j+changeLocation[l][1]]; //记录某方向第一个棋子的颜色 System.out.println("chessColor:"+chessColor); if(chessColor!=0) { for (int k = 1; k <= 4; k++) { //连棋循环 int nowX=i + k * changeLocation[l][0]; //记录连到的棋子坐标 int nowY=j + k * changeLocation[l][1]; if (nowX > ROW || nowY > LINE || nowX < 0 || nowY < 0) { //边界==死棋 break break; } if (goBangUI.chesses[nowX][nowY] == chessColor) { //同色,记录 strCode += chessColor; } else if(goBangUI.chesses[nowX][nowY] == 0){ //空棋 记录,break strCode += "0"; break; } else //异色==死棋 break break; } } System.out.println("提前str:"+strCode); if(!strCode.equals("0")) { //因为没有 0 的单独编码所以要避开,否则或中断异常结束 System.out.println("str:"+strCode); score += this.chessHashMap.getCodeWeight(strCode); //8个方向分数相加 System.out.println("score:"+score); } } System.out.println("外面要加的score:"+score); this.chessWeight[i][j]=score; //最终分数 } } } }
怎么下棋:
遍历棋盘,权值最大的地方就是本次最下下棋位置。
记录最权值,对应坐标。遍历完之后,下载这个最大位置即可。
代码:
public void paintChess(){ int maxWeight=0,maxI=0,maxJ=0; for (int i=0;i<=ROW;i++){ for(int j=0;j<=LINE;j++){ if(chessWeight[i][j]>maxWeight){ maxI=i; maxJ=j; } } } //pen.fillOval(X+maxJ*SIZE-SIZE/2,Y+maxI*SIZE-SIZE/2,SIZE,SIZE); }
2.一点小tips:
- 在棋盘上画小点点——类似这种
实现思路:就是在画完棋盘格之后再像画小棋子一样画上去就可以,位置自行设计,大小控制一下就可以。
- 当棋盘回放的棋盘有撤回的时候,调用的是paint函数实现的,就会有卡顿的感觉,为了使它更加流畅
实现思路:不要调用paint方法,直接将paint方法内容放在调用的这个位置(我的理解是没有刷新的那个过程?直接摞上去了)
本次待解决的问题
1.因为代码设计问题,想要那个key——value对应的内容值储存一次就够,不用每次遍历都调用,目前设置方法来填充,然后再fill函数之前,调用一次填充函数。不知道还有没有更加自然的方式存疑。
2.用户注册之后可以以流的形式存入txt文档,登录的时候可以读取txt文档并检查用户名密码匹配。
3.设置用户登录之后,显示用户信息,将用户和下棋、棋局结果、存档读档等绑定,实现赢棋加分,战绩记录。