五子棋人机对战实践项目
总的任务和目标
完成一个人机对战的五子棋项目,基本效果如下:
第一部分 Java绘图原理
1. 基本概念
像素,坐标
2. 组件自定义绘图原理
参见:https://www.cnblogs.com/leier/archive/2012/03/31/2426520.html
3. Graphics对象的常用方法
setFont(), setColor(), drawLine(), drawString(), drawOval(), drawRect(), fillOval(), fillRect(), drawImage()
第二部分 绘制棋盘
1. 基本思路
在一个JPanel上绘制一个背景,然后绘制水平和垂直的若干条线,使其构成等距离的格子,通常是15*15(条线)。
2. 代码实现
(1)主类代码:
(2) ChessPanel代码:
第三部分 绘制棋子
1. 基本思路
使用drawOval()可以绘制空心的圆,使用fillOval()可以填充实心的圆。
2. 坐标计算
由于格子是水平和垂直的有下标的,而绘制时需要使用实际的像素坐标,所以,需要进行行列下标到像素坐标的转换:
int x = col * GRID_WIDTH;
int y = row * GRID_WIDTH;
3. 代码实现
第四部分 鼠标下棋
1. 基本思路
需要处理鼠标单点事件,获取鼠标所在的位置,然后计算出应该绘制棋子的行列下标,并使用一个二维数组来全局存储棋子的位置。
2. 鼠标位置与行列下标计算
int x = e.getX();
int y = e.getY();
int row = y / GRID_WIDTH;
int col = x / GRID_WIDTH;
3. 代码实现
(1) ChessPanel属性和构造方法代码:
(2)监听器类(内部类)代码:
(3)绘图代码:
第五部分 判断胜负
1. 基本思路
判断胜负是因为在当前位置(row, col)落子导致了胜负,所以,判断胜负其实是在当前落子位置为中心,横向搜索左边第4个位置开始到右边第4个位置(其余位置不需要考虑),或者从上到下,或者正向45度,或者反向45度位置。
2. 处理方法
处理方法有很多,可以采用计数的方式,也可以采用字符串连接的方式,此处采用了将从左边第4颗开始,到右边第4颗结束,将每颗的颜色表示成字符1(黑色)或者2(白色),只需要判断其中是否有连续的5个1或5个2,即“11111”或“22222”即可知道胜负。
3. 代码实现
(1) 监听器类(内部类)代码:
(2)checkWin判断胜负的代码:
/** 判断胜负 * @param row 落子的行下标 * @param col 落子的列下标 * @return 是否获胜,true-是,false-否 */ public boolean checkWin(int row, int col) {}
(3)重置游戏状态
第六部分 人机对战
1. 基本思路
当人点了鼠标落子以后,轮到电脑下棋,电脑的基本思想就是,在棋盘的空白处的每个位置,进行判断,当前位置的进攻指数和防守指数分别为多少,在进攻指数和防守指数中取一个较大值作为当前位置的评估值,在整个棋盘的所有空白处找到一个最大值,最大值的那个位置即为应该落子的位置。
2. 某个位置的进攻指数和防守指数的评估方法
可以参见:https://www.cnblogs.com/songdechiu/p/5768999.html
本例中简化此问题,依据第五部分中胜负判断的方式,对连续9个位置形成的字符串进行搜索、评分,得出一个评估方案。如“11111”代表连续5个黑色,评分100,“011110”代表连续4个黑色,两端为空位置,评分90……。
3. 代码实现
(1)监听器类(内部类)代码:
(2) 电脑下棋的代码:
(3) 评估关键参数代码:
(4) 评估方法代码:
以上为本次人机对战的实践项目要求及部分实现代码,下面展示自己的完整代码(有所改变):
【主类DrawFrame】
1 package qipan; 2 3 import java.awt.BorderLayout; 4 import java.awt.Container; 5 import java.awt.*; 6 import javax.swing.JFrame; 7 8 public class DrawFrame extends JFrame{ 9 //属性 10 Mypanel panel=null; 11 12 //方法 13 public DrawFrame(){ 14 //设置主面板 15 setTitle("五子棋"); 16 setSize(620,640); 17 setDefaultCloseOperation(DISPOSE_ON_CLOSE); 18 setLocationRelativeTo(null); 19 setResizable(false); 20 21 //设置绘图面板 22 Container cp =getContentPane(); 23 panel=new Mypanel(); 24 panel.setBackground(Color.ORANGE); 25 cp.add(panel,BorderLayout.CENTER); 26 } 27 28 public static void main(String[] args) { 29 JFrame frame=new DrawFrame(); 30 frame.setVisible(true); 31 32 } 33 34 }
【Mypanel】
1 package qipan; 2 3 import java.awt.Color; 4 import java.awt.Graphics; 5 import java.awt.Image; 6 import java.awt.Point; 7 import java.awt.event.MouseAdapter; 8 import java.awt.event.MouseEvent; 9 import java.awt.event.MouseListener; 10 import java.io.File; 11 import java.io.IOException; 12 13 import javax.imageio.ImageIO; 14 import javax.swing.JOptionPane; 15 import javax.swing.JPanel; 16 17 public class Mypanel extends JPanel { 18 // 属性 19 /** 棋盘宽度 */ 20 public static final int GRID_WIDTH = 40; 21 /** 棋盘行数 */ 22 public static final int LINE_COUNT = 15; 23 /** 黑子与白子 */ 24 public static final int ITEM_BLACK = 1; 25 public static final int ITEM_WHITE = 2; 26 27 /**黑棋和白棋的图片*/ 28 Image imgblack = null; 29 Image imgwhite = null; 30 31 /** 设置分数机制 */ 32 String[] blackKey = { 33 "11111", "011110", "11110", "01111", "10111", 34 "11011", "11101", "01110", "11100", "00111", 35 "0111", "1011","1101", "1110", "111", 36 "01100", "00110", "011", "110", "11" }; 37 38 String[] whiteKey = { 39 "22222", "022220", "22220", "02222", "20222", 40 "22022", "22202", "02220", "22200", "00222", 41 "0222", "2022","2202", "2220", "222", 42 "02200", "00220", "022", "220", "22" }; 43 44 int[] Keyvalue = { 45 100, 90, 80, 80, 80, 46 80, 80, 70, 60, 60, 47 50, 50, 50, 50, 40, 48 30, 30, 20, 20, 10 }; 49 /** 设置计算机为黑色(true),人为白色(false)*/ 50 private boolean isblack = true; 51 /**绘制棋盘数组*/ 52 int[][] chessItems = new int[LINE_COUNT][LINE_COUNT]; 53 /**创建监听器对象*/ 54 private static MouseListener listener = null; 55 56 private Point lastItemPoint = new Point(); 57 // 方法 58 public Mypanel() { 59 60 //加载图片 61 try { 62 imgblack = ImageIO.read(new File("imgs/black.png " )); 63 imgwhite = ImageIO.read(new File("imgs/white.png " )); 64 } catch (IOException e) { 65 e.printStackTrace(); 66 } 67 //添加监听器 68 listener = new MyListener(); 69 this.addMouseListener(listener); 70 } 71 72 // 添加鼠标监听器 73 public class MyListener extends MouseAdapter { 74 @Override 75 public void mouseClicked(MouseEvent e) { 76 int x = e.getX(); 77 int y = e.getY(); 78 int row = y / GRID_WIDTH; 79 int col = x / GRID_WIDTH; 80 // 绘制落点棋子,并置换一次棋子颜色 81 if (chessItems[row][col] == 0) { 82 if (isblack) { 83 chessItems[row][col] = ITEM_WHITE; 84 isblack = !isblack; 85 } else { 86 isblack = !isblack; 87 chessItems[row][col] = ITEM_BLACK; 88 } 89 Mypanel.this.repaint(); 90 } 91 // 判断胜负 92 boolean Result = checkWin(row, col); 93 94 // 重置棋盘 95 if (Result) { 96 JOptionPane.showMessageDialog(null, "you won"); 97 reset(); 98 } 99 100 // 电脑下棋 101 computerPlay(); 102 } 103 /** 电脑 */ 104 private void computerPlay() { 105 int temRow = -1, temCol = -1, maxValue = 0; 106 int current = isblack ? 2 : 1; 107 for (int row = 0; row < LINE_COUNT; row++) { 108 for (int col = 0; col < LINE_COUNT; col++) { 109 if (chessItems[row][col] > 0) 110 continue; 111 int attack = checkMax(row, col, current);// 计算进攻值 112 /**3-current是为了置换颜色,判断防守哪个棋,减5是为了在进攻值和防守值相等时优先选择进攻*/ 113 int defend = checkMax(row, col, 3-current) - 5;// 计算防守值 114 int max = Math.max(attack, defend); 115 if (max > maxValue) { 116 temRow = row; 117 temCol = col; 118 maxValue = max; 119 } 120 } 121 } 122 //在计算出来的地方落子 123 if(temCol<0||temRow<0) 124 { 125 /**用sum来计算棋盘当前有没有下棋,如果没有下棋,则电脑不执行任何操作,直到我下棋为止*/ 126 int sum=0; 127 for(int i=0;i<LINE_COUNT;i++){ 128 for(int j=0;j<LINE_COUNT;j++){ 129 sum=sum+chessItems[i][j]; 130 } 131 } 132 if(sum==0){} 133 else{ 134 JOptionPane.showMessageDialog(null, "平局"); 135 reset(); 136 } 137 138 }else { 139 chessItems[temRow][temCol] = current; 140 lastItemPoint.x=temRow; 141 lastItemPoint.y=temCol; 142 isblack=!isblack;//换一次手 143 repaint();//重画 144 boolean result = checkWin(temRow, temCol); 145 //判断胜负 146 if (result) { 147 JOptionPane.showMessageDialog(null, "you lost"); 148 reset(); 149 } 150 } 151 } 152 153 /** 154 * 评估落子位置的分值 155 * @param row 156 * @param col 157 * @param current 158 * @return 159 */ 160 private int checkMax(int row, int col, int current) { 161 int max=0,temMax=0; 162 String [] strings = blackKey; 163 if(current==2){ 164 strings = whiteKey; 165 } 166 167 //水平方向 168 StringBuilder builder = new StringBuilder(); 169 for(int i=-4;i<=4;i++) 170 { 171 int newCol=col+i; 172 if(newCol<0||newCol>=LINE_COUNT) continue; 173 if(i==0){ 174 builder.append(current); 175 }else { 176 builder.append(chessItems[row][newCol]); 177 } 178 } 179 180 for(int i=0;i<strings.length;i++) 181 { 182 String key = strings[i]; 183 if(builder.indexOf(key)>=0){ 184 max=Keyvalue[i]; 185 break; 186 } 187 } 188 if(max==100) 189 return max; 190 191 //竖直方向 192 builder.delete(0, builder.length()); 193 for(int i=-4;i<=4;i++) 194 { 195 int newRow=row+i; 196 if(newRow<0||newRow>=LINE_COUNT) continue; 197 if(i==0){ 198 builder.append(current); 199 }else { 200 builder.append(chessItems[newRow][col]); 201 } 202 } 203 204 for(int i=0;i<strings.length;i++) 205 { 206 String key = strings[i]; 207 if(builder.indexOf(key)>=0){ 208 temMax=Keyvalue[i]; 209 break; 210 } 211 } 212 if(temMax>max) 213 max=temMax; 214 if(max==100) 215 return max; 216 217 //正45度方向 218 builder.delete(0, builder.length()); 219 for(int i=-4;i<=4;i++) 220 { 221 int newRow=row-i; 222 int newCol=col+i; 223 if(newCol<0||newCol>=LINE_COUNT||newRow<0||newRow>=LINE_COUNT) continue; 224 if(i==0){ 225 builder.append(current); 226 }else { 227 builder.append(chessItems[newRow][newCol]); 228 } 229 } 230 231 for(int i=0;i<strings.length;i++) 232 { 233 String key = strings[i]; 234 if(builder.indexOf(key)>=0){ 235 temMax=Keyvalue[i]; 236 break; 237 } 238 } 239 if(temMax>max) 240 max=temMax; 241 if(max==100) 242 return max; 243 244 //负45度方向 245 builder.delete(0, builder.length()); 246 for(int i=-4;i<=4;i++) 247 { 248 int newRow=row+i; 249 int newCol=col+i; 250 if(newCol<0||newCol>=LINE_COUNT||newRow<0||newRow>=LINE_COUNT) continue; 251 if(i==0){ 252 builder.append(current); 253 }else { 254 builder.append(chessItems[newRow][newCol]); 255 } 256 } 257 258 for(int i=0;i<strings.length;i++) 259 { 260 String key = strings[i]; 261 if(builder.indexOf(key)>=0){ 262 temMax=Keyvalue[i]; 263 break; 264 } 265 } 266 if(temMax>max) 267 max=temMax; 268 if(max==100) 269 return max; 270 return max; 271 } 272 // 将数组的值置为0,也就将棋盘重画为空 273 private void reset() { 274 for (int row = 0; row < LINE_COUNT; row++) { 275 for (int col = 0; col < LINE_COUNT; col++) { 276 chessItems[row][col] = 0; 277 } 278 } 279 isblack = true; 280 repaint(); 281 } 282 283 private boolean checkWin(int row, int col) { 284 StringBuilder builder = new StringBuilder(); 285 // 判断水平方向 286 for (int i = -4; i <= 4; i++) { 287 int newcol = col + i; 288 if (newcol >= 0 && newcol < LINE_COUNT) { 289 builder.append(chessItems[row][newcol]); 290 } 291 } 292 if (builder.indexOf("11111") >= 0 || builder.indexOf("22222") >= 0) { 293 return true; 294 } 295 // 判断竖直方向 296 builder.delete(0, builder.length()); 297 for (int i = -4; i <= 4; i++) { 298 int newrow = row + i; 299 if (newrow >= 0 && newrow < LINE_COUNT) { 300 builder.append(chessItems[newrow][col]); 301 } 302 } 303 if (builder.indexOf("11111") >= 0 || builder.indexOf("22222") >= 0) { 304 return true; 305 } 306 // 判断正45度方向 307 builder.delete(0, builder.length()); 308 for (int i = 4; i >= -4; i--) { 309 int newrow = row + i; 310 int newcol = col - i; 311 if (newrow >= 0 && newrow < LINE_COUNT && newcol >= 0 312 && newcol < LINE_COUNT) { 313 builder.append(chessItems[newrow][newcol]); 314 } 315 } 316 if (builder.indexOf("11111") >= 0 || builder.indexOf("22222") >= 0) { 317 return true; 318 } 319 // 判断负45度方向 320 builder.delete(0, builder.length()); 321 for (int i = -4; i <= 4; i++) { 322 int newrow = row + i; 323 int newcol = col + i; 324 if (newrow >= 0 && newrow < LINE_COUNT && newcol >= 0 325 && newcol < LINE_COUNT) { 326 builder.append(chessItems[newrow][newcol]); 327 } 328 } 329 if (builder.indexOf("11111") >= 0 || builder.indexOf("22222") >= 0) { 330 return true; 331 } 332 return false; 333 } 334 } 335 336 //绘制棋盘 337 @Override 338 protected void paintComponent(Graphics g) { 339 super.paintComponent(g); 340 g.setColor(Color.black); 341 342 // 绘制水平线 343 for (int i = 0; i < LINE_COUNT; i++) { 344 int x1 = GRID_WIDTH / 2; 345 int y1 = GRID_WIDTH / 2 + i * GRID_WIDTH; 346 int x2 = 14 * GRID_WIDTH + GRID_WIDTH / 2; 347 int y2 = y1; 348 g.drawLine(x1, y1, x2, y2); 349 } 350 // 绘制垂直线 351 for (int i = 0; i < LINE_COUNT; i++) { 352 int x1 = GRID_WIDTH / 2 + i * GRID_WIDTH; 353 int y1 = GRID_WIDTH / 2; 354 int x2 = x1; 355 int y2 = GRID_WIDTH / 2 + 14 * GRID_WIDTH; 356 g.drawLine(x1, y1, x2, y2); 357 } 358 //ChessItem(0, 2, Color.BLUE, g); 359 // 绘制一个棋子 360 for (int row = 0; row < LINE_COUNT; row++) { 361 for (int col = 0; col < LINE_COUNT; col++) { 362 switch (chessItems[row][col]) { 363 case ITEM_BLACK: 364 ChessItem(col, row, Color.BLACK, g); 365 drawlimit(lastItemPoint,Color.RED,g);//去跟踪电脑所下的棋 366 break; 367 case ITEM_WHITE: 368 ChessItem(col, row, Color.WHITE, g); 369 break; 370 } 371 372 } 373 } 374 } 375 /** 376 * 追踪画框 377 * @param lasPoint 上一个棋子,也就是电脑所下的棋 378 * @param color 框体颜色 379 * @param g 画笔 380 */ 381 private void drawlimit(Point lasPoint, Color color, Graphics g) { 382 int x = lasPoint.y * GRID_WIDTH; 383 int y = lasPoint.x* GRID_WIDTH; 384 g.setColor(color); 385 g.drawRect(x, y, GRID_WIDTH, GRID_WIDTH); 386 } 387 /** 388 * 构建绘制棋子的方法 389 * @param col 棋子所在列坐标 390 * @param row 棋子所在行坐标 391 * @param color 棋子颜色 392 * @param g 393 */ 394 public void ChessItem(int col, int row, Color color, Graphics g) { 395 int x = col * GRID_WIDTH; 396 int y = row * GRID_WIDTH; 397 // g.setColor(color); 398 // g.fillOval(x, y, GRID_WIDTH, GRID_WIDTH); 399 Image image = Color.BLACK.equals(color) ? imgblack:imgwhite; 400 g.drawImage(image, x, y, GRID_WIDTH, GRID_WIDTH, this); 401 } 402 }