一:总体思路
1:与人人对战最主要不同,人机对战需要一个能够评价当前棋盘,并作出一个较为合理的抉择的AI。
此版本我用了权值法:权值法是指AI扫描棋盘每一个点,根据这个点周围的情况给点赋一个“权值”来评价在此点下棋的价值多大。
直观判断,如果黑棋方有如下(A)局面,那么在Q点下棋将是他下一步的必然选择(更不必说(B))
(A)
(B)
有趣的是,黑方即使在如下局面,选择Q点依然是必然。(进攻就是最好的防守!!!)
所以我们只要考察这个点某一方向的同样颜色连子的情况,再将各个方向的权值相加,就可以判断该点下棋的价值。
各种情况的权值怎么衡量呢?
1: 活连: 两端都是空位
- 010 10
- 0110 50
- 01110 100
- 011110 5000
- 020 10
- 0220 50
- 02220 100
- 022220 5000
- 2: 半活: 一端是空位,一端是边界或者对方棋子
- 01 8
- 011 40
- 0111 800
- 01111 5000
- 02 8
- 022 40
- 0222 800
- 02222 5000
0表示该点为空(可下棋),1表示黑棋,2表示白棋。在这种判断方法下,只有下在现有棋子旁边才有意义(包括是斜对角线挨着)(当然有些时候下的位置不挨着任何棋子才是妙手,但这就是这个版本是简单版的原因)。
那么怎么得到每一个点每一个方向的棋子情况呢?
构造的连子情况很苛刻,if分支就很多:
public int toRight(int[][] arr, int r, int c) { //c和r是0起步,0——15共16列
String temS;
if (arr[r][c] == 0) { //只有这个0右边相邻的不是0才有意义赋权值
if (c == arr[0].length - 1) {
//System.out.println("1布");
return 0;
}
int color = arr[r][c + 1];
if (color == 0) {
//System.out.println("————2布");
return 0;
} else {
//System.out.println("——————3布");
temS="0"+color;
for (int i = c + 2; i < arr[0].length; i++) { //最右边再向右没有权值
if (arr[r][i] == color) {
temS+=color;
} else if (arr[r][i] == 0) {
temS+="0";
return map.get(temS);
}else{
return map.get(temS);
}
}
}
}
//System.out.println("————————4布");
return 0;
}
这是对于右侧连子情况的判断,首先在此方法外要if判断以下这个点是否可下(为空),接下来判断是否是最右边的点(因为下面的代码对于最右边不兼容,会有数组越界问题),然后再判断该点右侧是否也为空。
接下来遍历右侧每个点考察是否有颜色相同的连子。注意最右边是空和最右边是边界或者棋子是不同情况。
补充一点:我们将各种连子情况与权值构成了hashmap,连子情况以字符串表示做key。
接下来八个方向都走一遍就好
public int[][] getCoderArr(int[][] array) {
int[][] arr = new int[COLS][ROWS];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[0].length; j++) {
if (array[i][j] == 0) { //不等于0的,返回值当默认值0处理
int coderSum = 0;
coderSum += toRight(array, i, j);
coderSum += toLeft(array, i, j);
coderSum += toUp(array, i, j);
coderSum += toDown(array, i, j);
coderSum += toNorthWest(array, i, j);
coderSum += toNorthEast(array, i, j);
coderSum += toSouthWest(array, i, j);
coderSum += toSouthEast(array, i, j);
arr[i][j] = coderSum;
}
}
}
return arr;
}
2:有了一个AI后,我们还需要调整原本的鼠标监听器,从一点下一颗棋到一点下两颗棋。
public void mouseClicked(MouseEvent e) {
int x = e.getX();
int y = e.getY();
int r = (y - Y + SIZE / 2) / SIZE;
int c = (x - X + SIZE / 2) / SIZE;
if (chessFlag == 0) {
JOptionPane.showMessageDialog(null, "还没开始");
return;
}
if (r < 0 || c < 0 || r > ROWS || c > COLS) {
JOptionPane.showMessageDialog(null, "不能在边界外下棋");
return;
}
if (CHESS_ARRAY[r][c] != 0) {
return;
}
CHESS_ARRAY[r][c] = chessFlag;
chessList.add(new Chess(r, c, chessFlag));
//人人对战
if (choice == 1) {
if (chessFlag == 1) {
g.setColor(Color.BLACK);
chessFlag = 2;
} else if (chessFlag == 2) {
g.setColor(Color.WHITE);
chessFlag = 1;
}
int cx = c * SIZE + X - SIZE / 2;
int cy = r * SIZE + Y - SIZE / 2;
g.fillOval(cx, cy, SIZE, SIZE);
if (check(r, c)) {
String ans;
if (CHESS_ARRAY[r][c] == 1) {
ans = "黑方";
blackWin++;
} else {
ans = "白方";
whiteWin++;
}
int a = JOptionPane.showConfirmDialog(null, new Object[]{"再来一局?"}, "游戏结束,胜者是" + ans, JOptionPane.YES_NO_OPTION);
if (a == 0) {
ui.startButton.setText("开始游戏");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
CHESS_ARRAY[i][j] = 0;
}
}
//btn.setText("开始游戏");
HISTORY.add((ArrayList<Chess>) chessList.clone()); //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
chessList.clear();
chessPanel.paint(g);
chessFlag = 0;
} else {
ui.jf.setVisible(false);
}
}
}
//人机对战
else if (choice == 2) {
int cx = c * SIZE + X - SIZE / 2;
int cy = r * SIZE + Y - SIZE / 2;
g.setColor(Color.BLACK);
g.fillOval(cx, cy, SIZE, SIZE); //黑方画完验黑方
if (check(r, c)) {
String ans;
blackWin++;
int a = JOptionPane.showConfirmDialog(chessPanel, new Object[]{"再来一局?"}, "游戏结束,胜者是玩家,太遗憾了" , JOptionPane.YES_NO_OPTION);
if (a == 0) {
ui.startButton.setText("开始游戏");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
CHESS_ARRAY[i][j] = 0;
}
}
//btn.setText("开始游戏");
HISTORY.add((ArrayList<Chess>) chessList.clone()); //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
chessList.clear();
chessPanel.paint(g);
chessFlag = 0;
} else {
ui.jf.setVisible(false);
}
}else {
int xAI = this.findMax()[1];
int yAI = this.findMax()[0];
CHESS_ARRAY[yAI][xAI] = 2;
chessList.add(new Chess(yAI, xAI, 2));
int cxAI = xAI * SIZE + X - SIZE / 2;
int cyAI = yAI * SIZE + Y - SIZE / 2;
g.setColor(Color.WHITE);
g.fillOval(cxAI, cyAI, SIZE, SIZE);
if (check(yAI, xAI)) {
whiteWin++;
int a = JOptionPane.showConfirmDialog(chessPanel, new Object[]{"再来一局?"}, "游戏结束,胜者是电脑,哈哈垃圾" , JOptionPane.YES_NO_OPTION);
if (a == 0) {
ui.startButton.setText("开始游戏");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
CHESS_ARRAY[i][j] = 0;
}
}
//btn.setText("开始游戏");
HISTORY.add((ArrayList<Chess>) chessList.clone()); //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
chessList.clear();
chessPanel.paint(g);
chessFlag = 0;
} else {
ui.jf.setVisible(false);
}
}
}
//权值表
int[][] AICheck = ai.getCoderArr(CHESS_ARRAY);
for (int i = 0; i < AICheck.length; i++) {
for (int j = 0; j < AICheck[i].length; j++) {
System.out.print(AICheck[i][j] + " ");
}
System.out.println(" ");
}
for (int i = 0; i < CHESS_ARRAY.length; i++) {
for (int j = 0; j < CHESS_ARRAY[i].length; j++) {
System.out.print(CHESS_ARRAY[i][j]);
}
System.out.println(" ");
}
}
}
public int[] findMax() {
int[][] codeArr = ai.getCoderArr(CHESS_ARRAY);
int max = 0;
int[] location = {0, 0};
for (int i = 0; i < codeArr.length; i++) {
for (int j = 0; j < codeArr[0].length; j++) {
if (codeArr[i][j] > max) {
max = codeArr[i][j];
location = new int[]{i, j};
}
}
}
System.out.println("最大权值对应坐标为:"+ Arrays.toString(location));
System.out.println("最大权值是:"+max);
return location;
}
代码69行开始是人机对战的监听器。总体思路是,我选择黑棋画完后判断一次输赢,再让AI画白棋,再判断一次输赢。
131-142行打印出了棋盘和权值表。
147-163行定义了从权值表中找到最大值所对应的坐标。
值得注意的是:120-121行:当我们先将数组A加入到另一个数组B,再把A清空,那么B中的A也被清空了,原因是clear断开了对地址的链接。
3:然后我们发现,还需要一个主页面,以及两个选择按钮(甚至更多)来选择模式或者退出。
UI设计我遗留了许多问题,在此记录一部分,以后查看或者求求路过大佬指教:
1:
左上角只有在鼠标滑过的时候按钮才能显示,猜测是JFrame上画的背景遮挡了,但是明明是先画的后加的按钮,具体情况未知
2:关于最小化页面后棋子消失,或者界面移出后棋子消失的问题,虽然问题解决了但我没搞明白。
从观看历史界面返回下棋界面,右侧又出现了遮挡但并没有完全遮挡的情况——接下来我在remove这个panel前先把可视设置为false就好了。原来是remove,但没有完全remove掉。
4:主页面上我加的JLabel莫名消失!
5:关于主界面和下棋界面:最合理的想法当然是一开始只显示主界面,点击对战后再弹出下棋界面。
问题是:UI界面里的初始化方法中,我们从chessPanel中产生了画笔,然后将画笔赋给了监听器。但是这个产生画笔的方法必须要求桌面上这个panel可见,于是一开始只能可视。我的解决方法是一开始就显示两个界面,但用主界面遮挡住棋盘界面。并且可以设置一个boolean,只有点击了主界面某个按钮才能下棋。
这显然并不是最优解,如果我们想要不同按钮唤醒不同棋盘界面怎么办?可以一开始全部显示,在选择某个界面同时其他不显示,但还是无法避免一启动弹出许多界面的问题。
6:关于用for循环创建组件。缺点在于,这个过程一定是在方法中的,每个组件没有名字,更不可能把声明放到成员区。最常用循环创建的可能就是按钮,label等等,某些来回要搞的操作,比如setText(),最好还是指明具体对象:在之前的代码中我用actionevent.getSource()再强转JButton类型得到点击的按钮后,进行操作,这就容易操作到其他按钮,一旦我们在其他代码区想操作这些button,那getSource()的就可能是最近点击的某个按钮了。
7:showPaint(),hidePaint(),showPaintQuick()。现在的一个问题是:showPaintQuick()想要实现从showPaint()或者hidePaint()后一些棋子后开始自动下,我的操作是将表示一局中所有棋子顺序的index也作为showPaintQuick的参数,用index将所有棋子的列表切片。这样操作会跳过之前点下一步的几个棋子重showPaintQuick()。showPaintQuick()重画了棋盘,想要在showPaintQuick启动前画棋子只能想想在方法内,画棋盘后考虑,于是我先for循环画了下过的,再for循环加sleep画后面的。:
public void showPaintQuick(Graphics g, ArrayList<Chess> arrayList,int index){
g.setColor(Color.lightGray);
g.fillRect(0,0,getWidth(),getHeight());
g.setColor(Color.BLACK);
System.out.println("重新画线中");
for (int i = 0; i < ROWS; i++) {
g.drawLine(X,Y+i*SIZE,X+GRID_NUM*SIZE,Y+i*SIZE);
}
for (int i = 0; i < COLS; i++) {
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+GRID_NUM*SIZE);
}
for (Chess chess:arrayList.subList(0,index)) {
int cx = chess.c * SIZE + X - SIZE / 2;
int cy = chess.r * SIZE + Y - SIZE / 2;
if (chess.chessFlag == 1) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.WHITE);
}
g.fillOval(cx, cy, SIZE, SIZE);
}
for (Chess chess:arrayList.subList(index,arrayList.size())) {
int cx=chess.c*SIZE+X-SIZE/2;
int cy=chess.r*SIZE+Y-SIZE/2;
if(chess.chessFlag==1){
g.setColor(Color.BLACK);
}else{
g.setColor(Color.WHITE);
}
g.fillOval(cx,cy,SIZE,SIZE);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
二:较上一版本功能的更新
1:回放功能
首先展示效果:
回放功能内部实现的功能有:
1:记录这一次启动下的所有回合
2:选择回合后,可选择点击上一步,下一步来一个棋子一个棋子观看下棋
3:也可以选择速播速播来观看自动下棋
4:也可以选择观看最终结局来直接下完
5:点击返回则返回
实现是主要思路如下:
1:每一次下棋用于记录下棋顺序的那个列表,我们要存储到一个专门的列表中,围绕此高维列表展开操作。
2:“上一步”是最骚的,我选择用棋盘的一小格遮住下的最后一个棋子。也可以考虑重画前面除了最后一个的所有棋子。
3:“速播”要求电脑知道从顺序表的哪一步开始自动下棋,这就要用index记录一下上一步下一步的点击。同时用Thread.sleep(1000)使下棋过程可视。
代码如下:
1:用于“下一步”,只画一个棋子
public void showPaint(Graphics g, ArrayList<Chess> arrayList, int index) {
int cx = arrayList.get(index).c * SIZE + X - SIZE / 2;
int cy = arrayList.get(index).r * SIZE + Y - SIZE / 2;
if (arrayList.get(index).chessFlag == 1) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.WHITE);
}
g.fillOval(cx, cy, SIZE, SIZE);
}
2:用于”上一步“,遮挡上一个棋子
public void hidePaint(Graphics g, ArrayList<Chess> arrayList, int index){
int cx = arrayList.get(index-1).c * SIZE + X - SIZE / 2;
int cy = arrayList.get(index-1).r * SIZE + Y - SIZE / 2;
Image icon=new ImageIcon("imgs/img_1.png").getImage();
g.drawImage(icon,cx,cy,SIZE,SIZE,null);
}
3:用于快速看局(代码确实有点冗长,重用性确实太差,后面我慢慢优化)
public void showPaintQuick(Graphics g, ArrayList<Chess> arrayList,int index){
g.setColor(Color.lightGray);
g.fillRect(0,0,getWidth(),getHeight());
g.setColor(Color.BLACK);
System.out.println("重新画线中");
for (int i = 0; i < ROWS; i++) {
g.drawLine(X,Y+i*SIZE,X+GRID_NUM*SIZE,Y+i*SIZE);
}
for (int i = 0; i < COLS; i++) {
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+GRID_NUM*SIZE);
}
for (Chess chess:arrayList.subList(0,index)) {
int cx = chess.c * SIZE + X - SIZE / 2;
int cy = chess.r * SIZE + Y - SIZE / 2;
if (chess.chessFlag == 1) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.WHITE);
}
g.fillOval(cx, cy, SIZE, SIZE);
}
for (Chess chess:arrayList.subList(index,arrayList.size())) {
int cx=chess.c*SIZE+X-SIZE/2;
int cy=chess.r*SIZE+Y-SIZE/2;
if(chess.chessFlag==1){
g.setColor(Color.BLACK);
}else{
g.setColor(Color.WHITE);
}
g.fillOval(cx,cy,SIZE,SIZE);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
4:选择对局界面
public void choiceHistory() {
jFrame.setSize(180, 350);
JPanel choiceH = new JPanel();
ButtonGroup buttonGroup = new ButtonGroup();
Box box = Box.createVerticalBox();
for (int i = 0; i < HISTORY.size(); i++) {
JButton jrb = new JButton("第" + (i + 1) + "局");
jrb.addActionListener(this);
buttonGroup.add(jrb);
box.add(jrb);
}
choiceH.add(box);
//JButton ok=new JButton("确定");
jFrame.add(choiceH);
//jFrame.add(ok,BorderLayout.SOUTH);
jFrame.setVisible(true);
}
5:监听器中相关部分
else if (actionStr.equals("历史记录")) {
//JOptionPane.showMessageDialog(null,"黑方总计获胜次数:"+blackWin+"\n白方总计获胜次数:"+whiteWin);
int choice = JOptionPane.showConfirmDialog(null, "黑方总计获胜次数:" + blackWin + "\n白方总计获胜次数:" + whiteWin + "\n是否回看精彩对局?", "请选择", JOptionPane.YES_NO_OPTION);
if (choice == 0) {
if (HISTORY.size() != 0) {
ui.jf.remove(ui.btnPanel);
ui.jf.add(ui.reviewPanel, BorderLayout.EAST);
ui.jf.setVisible(true);
//ui.btnPanel.setVisible(false);
//ui.reviewPanel.setVisible(true);
this.choiceHistory();
} else {
JOptionPane.showMessageDialog(null, "暫無對局");
}
}
}
for (int i = 0; i < HISTORY.size(); i++) {
if (actionStr.equals("第" + (i + 1) + "局")) {
chessPanel.paint(g);
index = 0;
System.out.println("第" + (i + 1) + "局qq");
System.out.println(HISTORY.get(i));
temArray = HISTORY.get(i);
}
}
if (actionStr.equals("上一步")) {
System.out.println("1");
if (index > 0) {
chessPanel.hidePaint(g, temArray, index);
index--;
} else {
JOptionPane.showMessageDialog(null, "没有上一步了");
}
} else if (actionStr.equals("下一步")) {
if (index < temArray.size()) {
chessPanel.showPaint(g, temArray, index);
index++;
} else {
JOptionPane.showMessageDialog(null, "已经结束辣");
}
} else if (actionStr.equals("观看最终结局")) {
int staticIndex = index;
for (int i = 0; i < temArray.size() - staticIndex; i++) { //假如在for内部调整i的上界,是会影响循环的(上界在次次循环中会刷新)
chessPanel.showPaint(g, temArray, index);
index++;
}
} else if (actionStr.equals("速播速播")) {
chessPanel.showPaintQuick(g,temArray,index);
index=temArray.size();
} else if (actionStr.equals("返回")) {
ui.reviewPanel.setVisible(false);
jFrame.setVisible(false);
ui.jf.remove(ui.reviewPanel);
//ui.reviewPanel.setVisible(false);
ui.jf.add(ui.btnPanel, BorderLayout.EAST);
ui.jf.setVisible(true);
}
2:悔棋功能
悔棋我可没用遮住的办法,总体思路是底层修改底层二维数组棋盘,重画界面棋盘。
要注意的是:
我们重新调用chessPanel的paint方法,却不能直观的调用repaint方法,原因我盲猜是repaint内部并不直接重画,这导致每次画的速度赶不上代码进行到下面的速度。所以在此我们手动调用paint方法解决。
代码如下:
else if (actionStr.equals("悔棋")) {
if (chessList.size() == 0) {
JOptionPane.showMessageDialog(null, "没有棋子可以悔棋了");
return;
}
if(choice==1) {
Chess chess = chessList.get(chessList.size() - 1);
//System.out.println(chess.c+"@@@"+chess.r);
CHESS_ARRAY[chess.r][chess.c] = 0;
chessFlag = chess.chessFlag;
chessList.remove(chessList.size() - 1);
}
else if(choice==2){
Chess chess = chessList.get(chessList.size() - 1);
CHESS_ARRAY[chess.r][chess.c] = 0;
chessList.remove(chessList.size() - 1);
Chess chess2 = chessList.get(chessList.size() - 1);
CHESS_ARRAY[chess2.r][chess2.c] = 0;
chessList.remove(chessList.size()-1);
}
chessPanel.paint(g);
(其中choice=1是人人对战,悔一颗棋,choice=2是人机对战,悔两颗棋)