java版数独游戏
闲来无事,想到了手机里面有的数独游戏,感觉太简单,所以我就想着自己写一个,设定不同的难度,正好熟悉一下swing编程,练练手
1 import java.awt.Color; 2 import java.awt.Font; 3 import java.awt.GridLayout; 4 import java.util.ArrayList; 5 import java.util.HashMap; 6 import java.util.List; 7 import java.util.Map; 8 import java.util.Random; 9 import java.util.regex.Pattern; 10 11 import javax.swing.BorderFactory; 12 import javax.swing.JFrame; 13 import javax.swing.JPanel; 14 import javax.swing.JTextField; 15 import javax.swing.event.DocumentEvent; 16 import javax.swing.event.DocumentListener; 17 import javax.swing.text.BadLocationException; 18 import javax.swing.text.Document; 19 20 public class SudokuGame extends JFrame implements DocumentListener { 21 22 private static final long serialVersionUID = 1L; 23 private JPanel[] pnlGame; 24 private JTextField[][][] txtGame; 25 private Map<JTextField, JTextField> warnFiledMap = new HashMap<JTextField, JTextField>(); 26 private Document doc; 27 private static final int YES = 1;// 单元格可取值存在 ; 已回滚;已冲突 28 private static final int NO = 0;// 单元格可取值不存在 ;未回滚;未冲突 29 private static final int NEED_ROLLBACK = 10;// 需要回滚 30 private int is_num = YES; //输入的是不是数字标记 31 private int is_remove_by_insert = NO;//remove是不是在已有值的前提下insert一个新值导致的 32 33 private Grid[][][] grids = new Grid[9][3][3]; 34 35 public SudokuGame() { 36 pnlGame = new JPanel[9]; 37 txtGame = new JTextField[9][3][3]; 38 gameInit(); 39 } 40 41 /** 42 * 游戏初始化 43 */ 44 @SuppressWarnings("static-access") 45 public void gameInit() { 46 this.setDefaultCloseOperation(this.EXIT_ON_CLOSE); 47 this.setSize(300, 300); 48 this.setResizable(false); 49 this.setTitle("数独游戏"); 50 this.setLayout(new GridLayout(3, 3)); 51 for (int i = 0; i < 9; i++) { 52 pnlGame[i] = new JPanel(); 53 pnlGame[i].setBorder(BorderFactory.createLineBorder(Color.black)); 54 pnlGame[i].setLayout(new GridLayout(3, 3)); 55 this.add(pnlGame[i]); 56 } 57 58 for (int z = 0; z < 9; z++) { 59 for (int x = 0; x < 3; x++) { 60 for (int y = 0; y < 3; y++) { 61 txtGame[z][x][y] = new JTextField(); 62 txtGame[z][x][y].setBorder(BorderFactory 63 .createEtchedBorder()); 64 txtGame[z][x][y] 65 .setFont(new Font("Dialog", Font.ITALIC, 20));// 设置字体大小 66 txtGame[z][x][y].setHorizontalAlignment(JTextField.CENTER);// 设置字体居中 67 pnlGame[z].add(txtGame[z][x][y]); 68 } 69 } 70 71 } 72 this.init();// 九宫格数据初始化,生成完整正确的九宫格 73 this.setGameLevel(3);// 根据完整的九宫格生成不同难度的数独游戏 74 for (int z = 0; z < 9; z++) {// 9宫格的行 75 for (int x = 0; x < 3; x++) {// 9宫格的列 76 for (int y = 0; y < 3; y++) { 77 if (grids[z][x][y].getValue() != 10) { 78 txtGame[z][x][y].setText(grids[z][x][y].getValue() + ""); 79 txtGame[z][x][y].setEditable(false); 80 } else { 81 txtGame[z][x][y].getDocument().addDocumentListener(this); 82 83 } 84 } 85 } 86 } 87 this.setVisible(true); 88 } 89 90 /** 91 * 生成完整的正确填完的九宫格 92 */ 93 void init() { 94 int value = 0; 95 int[] backlocation; 96 int i = 0, j = 0; 97 int tag_rollback = NO, tag_validValues_exist;// 是否回滚到上一个单元格的标记,单元格的可取值是否已过滤过,可取值是否存在 98 back: for (int z = 0; z < 9; z++) {// 9宫格的行 99 for (int x = i; x < 3; x++) {// 9宫格的列 100 for (int y = j; y < 3; y++) { 101 if (tag_rollback == 1) { 102 i = 0; 103 j = 0; 104 tag_rollback = NO;// 回滚了一次之后,回归正常状态继续遍历 105 } 106 if (null == grids[z][x][y]) { 107 grids[z][x][y] = new Grid(z, x, y); 108 tag_validValues_exist = NO;// 表示单元格的值是刚初始化的,需要过滤 109 } else { 110 tag_validValues_exist = YES;// 表示单元格的值是过滤过的 111 } 112 value = getValidValue(grids[z][x][y], tag_validValues_exist); 113 if (value == NEED_ROLLBACK) { 114 115 backlocation = backlocation(z, x, y); 116 reset(grids[z][x][y]);// 将该单元格重置 117 118 z = backlocation[0] - 1;// 这里z-2是因为跳到back之后,z会直接自增一次,所以减去2个才能跳转成上一个单元格 119 i = backlocation[1]; 120 j = backlocation[2]; 121 tag_rollback = YES;// 从i,j,k指定的位置开始再次遍历 122 continue back;// 重新开始前一个单元格的遍历 123 } else { 124 grids[z][x][y].setValue(value); 125 } 126 } 127 } 128 } 129 } 130 131 // 游戏难度分级 0-3 分别为:简单、中等、困难、骨灰级 132 void setGameLevel(int level) { 133 134 int blank_max, blank_min;// blank代表每个单元格最多和最少空几个让玩家填, 135 136 switch (level) { 137 case 0: 138 blank_max = 4; 139 blank_min = 2; 140 blankInit(blank_max, blank_min); 141 break; 142 case 1: 143 blank_max = 6; 144 blank_min = 4; 145 blankInit(blank_max, blank_min); 146 break; 147 case 2: 148 blank_max = 8; 149 blank_min = 6; 150 blankInit(blank_max, blank_min); 151 break; 152 case 3: 153 blank_max = 9; 154 blank_min = 6; 155 blankInit(blank_max, blank_min); 156 break; 157 } 158 } 159 160 void blankInit(int blank_max, int blank_min) { 161 Random rd = new Random(); 162 List<Integer> list = new ArrayList<Integer>(); 163 int count, index;// count代表实际的空白数,index代表空白的单元格位置 164 165 for (int z = 0; z < 9; z++) { 166 count = 0; 167 while (count < blank_min) {// 每个小9宫格随机出现最大和最小空白之间的空白个数 168 count = rd.nextInt(blank_max + 1); 169 } 170 for (int i = 1; i <= count; i++) { 171 index = rd.nextInt(9); 172 if (list.contains(index)) {// 如果空白选项里已经含有当前位置就重新取 173 i--; 174 } else {// 如果空白选项里已经没有含有当前位置就添加 175 list.add(index); 176 } 177 } 178 for (int j : list) { 179 grids[z][j / 3][j % 3].setValue(10);// 根据j让一个小9宫格里面的9个格子随机空白 180 // 10就代表这个格子空白 181 } 182 list.clear(); 183 } 184 } 185 186 /** 187 * 每个单元格的类 坐标 取值 可取值队列 188 * 189 * @author yhd 190 * 191 */ 192 class Grid { 193 private int x;// 对应每个小单元格的横坐标 194 private int y;// 对应每个小单元格的纵坐标 195 private int z;// 对应每个小9宫格 196 private int value;// 最终取得值 197 private List<Integer> validValues;// 可以取的值 198 199 public Grid(int z, int x, int y) { 200 this.x = x; 201 this.y = y; 202 this.z = z; 203 if (validValues == null) { 204 validValues = new ArrayList<Integer>(); 205 for (int i = 1; i < 10; i++) { 206 validValues.add(i); 207 } 208 } 209 } 210 211 public int getX() { 212 return x; 213 } 214 215 public void setX(int x) { 216 this.x = x; 217 } 218 219 public int getY() { 220 return y; 221 } 222 223 public void setY(int y) { 224 this.y = y; 225 } 226 227 public int getZ() { 228 return z; 229 } 230 231 public void setZ(int z) { 232 this.z = z; 233 } 234 235 public List<Integer> getValidValues() { 236 return validValues; 237 } 238 239 public void setValidValues(List<Integer> validValues) { 240 this.validValues = validValues; 241 } 242 243 public int getValue() { 244 return value; 245 } 246 247 public void setValue(int value) { 248 this.value = value; 249 } 250 251 } 252 253 // 0 1 2 z的布局 整体布局 254 // 3 4 5 255 // 6 7 8 256 // 0 1 257 // (0,0,0) (0,0,1) (0,0,2) (1,0,0) 258 // (0,1,0) (0,1,1) (0,1,2) (1,1,0) 259 // (0,2,0) (0,2,1) (0,2,2) (1,2,0) 260 // 3 4 261 // (3,0,0) (3,0,1) (3,0,2) (4,0,0) 详细布局 262 263 // 当某单元格没有可取值时,回到上一个单元格 264 int[] backlocation(int z, int x, int y) { 265 int[] location = new int[3];// 存贮上一个单元格的位置信息 z x y 266 switch (x + y) {// 根据当前单元格的坐标和找到上一个元素的坐标 267 case 0: 268 location[0] = z - 1; 269 location[1] = 2; 270 location[2] = 2; 271 break; 272 case 1: 273 if (x < y) { 274 location[0] = z; 275 location[1] = 0; 276 location[2] = 0; 277 break; 278 } else { 279 location[0] = z; 280 location[1] = 0; 281 location[2] = 2; 282 break; 283 } 284 case 2: 285 if (x < y) { 286 location[0] = z; 287 location[1] = 0; 288 location[2] = 1; 289 break; 290 } else if (x > y) { 291 location[0] = z; 292 location[1] = 1; 293 location[2] = 2; 294 break; 295 } else { 296 location[0] = z; 297 location[1] = 1; 298 location[2] = 0; 299 break; 300 } 301 case 3: 302 if (x < y) { 303 location[0] = z; 304 location[1] = 1; 305 location[2] = 1; 306 break; 307 } else { 308 location[0] = z; 309 location[1] = 2; 310 location[2] = 0; 311 break; 312 } 313 case 4: 314 location[0] = z; 315 location[1] = 2; 316 location[2] = 1; 317 break; 318 } 319 return location; 320 } 321 322 /** 323 * 一旦无数字可填,就回溯到上一个单元格,当前单元格的可取值列表和值重置 324 * 325 * @param grid 326 */ 327 // 这里开发时遇到了java引用传递的问题,之前的代码如:grid=null 328 // 结果发现只要有回溯,就会一直回溯到0 0 0,不是回溯一次改个值继续下面的遍历 329 // 原来是这里的问题,引用地址传递过来后,这个grid副本存的地址清空了,并不是传进来的那个 330 // grid清空了,所以导致了回溯的那些单元格一直没有被重置,无值可取,才会不断回溯的 331 void reset(Grid grid) { 332 int z = grid.getZ(); 333 int x = grid.getX(); 334 int y = grid.getY(); 335 grids[z][x][y] = null; 336 } 337 338 /** 339 * 获取某个单元格可以取的值 340 */ 341 int getValidValue(Grid grid, int tag_validValues_exist) { 342 Random rd = new Random(); 343 int index, grid_validValues_isExist = YES, last; 344 345 int existValue, value; 346 347 int zx_index = 0;// 以z为标准,x轴方向的遍历起点 348 int zy_index = 0;// 以z为标准,y轴方向的遍历起点 349 350 if (tag_validValues_exist == NO) { 351 switch (grid.getZ() % 3) { 352 case 0: 353 zx_index = grid.getZ(); 354 break; // 表示如果是0 3 6对应的小九宫格 355 case 1: 356 zx_index = grid.getZ() - 1; 357 break; // 同理表示 1 4 7 358 case 2: 359 zx_index = grid.getZ() - 2; 360 break; // 同理表示2 5 8 361 } 362 363 if (grid.getZ() >= 6) { 364 zy_index = grid.getZ() - 6; // 对应6 7 8 y轴方向起点为0 1 2 365 } else if (grid.getZ() >= 3) { 366 zy_index = grid.getZ() - 3; // 对应3 4 5 y轴方向起点为0 1 2 367 } else { 368 zy_index = grid.getZ(); // 对应0 1 2 y轴方向起点为0 1 2 369 } 370 371 for (; zx_index < grid.getZ(); zx_index++) { 372 // 去除同一行已存在的数据 373 for (int y = 0; y < 3; y++) { 374 existValue = grids[zx_index][grid.getX()][y].getValue();// 获取该单元格之前同一行的不同列的值 375 grid_validValues_isExist = removeExistValue(grid, 376 existValue); 377 } 378 } 379 if (grid_validValues_isExist == NO) { 380 return NEED_ROLLBACK; 381 } 382 // 去除同一列已存在的数据 383 for (; zy_index < grid.getZ(); zy_index += 3) { 384 for (int x = 0; x < 3; x++) { 385 existValue = grids[zy_index][x][grid.getY()].getValue();// 获取该单元格之前同一列的不同列的值 386 grid_validValues_isExist = removeExistValue(grid, 387 existValue); 388 } 389 } 390 if (grid_validValues_isExist == NO) { 391 return NEED_ROLLBACK; 392 } 393 // 去除同一个小九宫格里面的不同的值 394 for (int x = 0; x <= grid.getX(); x++) { 395 if (x == grid.getX()) {// 如果已经遍历到和该单元格一行时 396 for (int y = 0; y < grid.getY(); y++) { 397 existValue = grids[grid.getZ()][x][y].getValue(); 398 grid_validValues_isExist = removeExistValue(grid, 399 existValue); 400 } 401 } else {// 在这单元格所在行之前行时就完全遍历列 402 for (int y = 0; y < 3; y++) { 403 existValue = grids[grid.getZ()][x][y].getValue(); 404 grid_validValues_isExist = removeExistValue(grid, 405 existValue); 406 } 407 } 408 } 409 if (grid_validValues_isExist == NO) { 410 return NEED_ROLLBACK; 411 } 412 } 413 if (grid.getValidValues().size() == 0) { 414 return NEED_ROLLBACK; 415 } else { 416 last = grid.getValidValues().size(); 417 } 418 if (last == 0) { 419 index = 0; 420 } else { 421 index = rd.nextInt(last);// 返回该单元格的值 422 // 因为random的nextInt范围必须是[0,last),size和index有区别,index从0开始 423 } 424 value = grid.getValidValues().get(index); 425 removeExistValue(grid, value);// 一旦取过该值,就把这个值从该单元格中去掉 426 // System.out.println(grid.getZ()+" "+grid.getX()+" "+grid.getY()+" "+value); 427 return value; 428 } 429 430 /** 431 * 移除单元格不能取得值和已经取过的值 432 * 433 * @param grid 434 * @param existValue 435 * @return 436 */ 437 int removeExistValue(Grid grid, int existValue) { 438 int index, grid_validValues_isExist = YES; 439 if (grid.getValidValues().size() == 0) { 440 grid_validValues_isExist = NO; 441 } 442 443 if (grid.getValidValues().contains(existValue))// 如果同一列已存在该数据,则去除 444 { 445 index = grid.getValidValues().indexOf(existValue);// 找到该值存在的index 446 grid.getValidValues().remove(index); 447 } 448 return grid_validValues_isExist; 449 } 450 /** 451 * 检查输入的数字是否有冲突 452 * @param z1 453 * @param x1 454 * @param y1 455 */ 456 private void checkValue(int z1, int x1, int y1) { 457 int[] zx = new int[2];// 以z为标准,x轴方向的遍历 458 int[] zy = new int[2];// 以z为标准,y轴方向的遍历 459 String value = txtGame[z1][x1][y1].getText(); 460 value = value.substring(value.length()-1, value.length()); 461 switch (z1 % 3) { 462 case 0: 463 zx[0] = z1 + 1; 464 zx[1] = z1 + 2; 465 break; // 表示如果是0 3 6对应的小九宫格,检查右边2个小九宫格 466 case 1: 467 zx[0] = z1 - 1; 468 zx[1] = z1 + 1; 469 break; // 同理表示 1 4 7 检查两侧小九宫格 470 case 2: 471 zx[0] = z1 - 2; 472 zx[1] = z1 - 1; 473 ; 474 break; // 同理表示2 5 8 检查左侧两个小九宫格 475 } 476 477 if (z1 >= 6) { 478 zy[0] = z1 - 6; // 对应6 7 8 检查它上面2个小九宫格 479 zy[1] = z1 - 3; 480 } else if (z1 >= 3) { 481 zy[0] = z1 - 3; // 对应3 4 5 检查它上面和下面2个小九宫格 482 zy[1] = z1 + 3; 483 } else { 484 zy[0] = z1 + 3; // 对应0 1 2 检查它下面2个小九宫格 485 zy[1] = z1 + 6; 486 } 487 488 int z; 489 490 // 检查同一列已存在的数据 491 for (int i = 0; i < 2; i++) { 492 z = zy[i]; 493 for (int x = 0; x < 3; x++) { 494 if (txtGame[z][x][y1].getText().equals(value)) { 495 txtGame[z][x][y1].setForeground(Color.red); 496 txtGame[z1][x1][y1].setForeground(Color.red); 497 //将冲突的两个文本框的值交替放入map 498 warnFiledMap.put(txtGame[z][x][y1],txtGame[z1][x1][y1]); 499 warnFiledMap.put(txtGame[z1][x1][y1],txtGame[z][x][y1]); 500 return; 501 } 502 } 503 } 504 505 // 检查同一行已存在的数据 506 for (int i = 0; i < 2; i++) { 507 z = zx[i]; 508 for (int y = 0; y < 3; y++) { 509 if (txtGame[z][x1][y].getText().equals(value)) { 510 txtGame[z][x1][y].setForeground(Color.red); 511 txtGame[z1][x1][y1].setForeground(Color.red); 512 //将冲突的两个文本框的值交替放入map 513 warnFiledMap.put(txtGame[z][x1][y],txtGame[z1][x1][y1]); 514 warnFiledMap.put(txtGame[z1][x1][y1],txtGame[z][x1][y]); 515 return; 516 } 517 } 518 } 519 520 // 检查同一个小九宫格里面的不同的值 521 for (int x = 0; x < 3; x++) { 522 for (int y = 0; y < 3; y++) { 523 if (!(x == x1 && y == y1) 524 && txtGame[z1][x][y].getText().equals(value)) { 525 526 txtGame[z1][x][y].setForeground(Color.red); 527 txtGame[z1][x1][y1].setForeground(Color.red); 528 //将冲突的两个文本框的值交替放入map 529 warnFiledMap.put(txtGame[z1][x][y],txtGame[z1][x1][y1]); 530 warnFiledMap.put(txtGame[z1][x1][y1],txtGame[z1][x][y]); 531 return; 532 } 533 } 534 } 535 536 } 537 538 // 文本属性的变化 539 public void changedUpdate(DocumentEvent documentEvent) { 540 } 541 //插入了新数据 542 public void insertUpdate(DocumentEvent documentEvent) { 543 int[] location = new int[3]; 544 int x,y,z; 545 String value; 546 doc = (Document) documentEvent.getDocument(); 547 // 控制文本框显示的数字始终1个并且是刚键入的数字,没有冲突 548 this.validateInput(NO); 549 //如果是数字继续下面的操作 550 if(is_num == YES){ 551 //当存在冲突的时候接收不能冲突field以外的输入 552 if (warnFiledMap.size() != 0) { 553 // 根据触发的docment找到填充这个docment的field坐标 554 this.findFeildLocationByDoc(location); 555 z = location[0]; 556 x = location[1]; 557 y = location[2]; 558 //如果修改的是冲突域的值 559 if(warnFiledMap.get(txtGame[z][x][y])!=null){ 560 value = txtGame[z][x][y].getText(); 561 value = value.substring(value.length()-1, value.length()); 562 if(!value.equals(warnFiledMap.get(txtGame[z][x][y]).getText())){ 563 txtGame[z][x][y].setForeground(Color.black); 564 warnFiledMap.get(txtGame[z][x][y]).setForeground(Color.black); 565 warnFiledMap.clear();//清空冲突域 566 } 567 // 根据坐标去检查填入数字是否正确 568 this.checkValue(location[0], location[1], location[2]); 569 } 570 //禁止其它文本域输入 571 else{ 572 // 有冲突存在,无法输入 573 this.validateInput(YES); 574 } 575 } 576 else{ 577 this.findFeildLocationByDoc(location); 578 // 根据坐标去检查填入数字是否正确 579 this.checkValue(location[0], location[1], location[2]); 580 } 581 } 582 else{ 583 is_num = YES;//将数字开关重置,等待下次重新输入 584 } 585 } 586 587 /** 588 * 控制文本框只能输入数字,并且是1个 并且显示当前刚输入的数字 589 */ 590 void validateInput(int conflict) { 591 String value; 592 // 下面都是开启线程来重置输入的值 否则insertUpdate的写锁会导致无法控制输入 593 594 //如果不是数字就禁止输入 595 try { 596 value = doc.getText(0, doc.getLength()); 597 value = value.substring(value.length()-1, value.length()); 598 if(!Pattern.matches("\\d*", value)){ 599 new Thread(new Thread() { 600 public void run() { 601 try { 602 doc.remove(doc.getLength()-1, 1);// 这里会触发一个removeUpdate事件 将刚输入的非法字符清空 603 } catch (Exception exp) { 604 System.out.println("Error: " + exp.toString()); 605 } 606 } 607 }).start(); 608 is_num = NO;//输入的不是数字 609 is_remove_by_insert = YES;//remove由insert导致 610 return ;//不是数字就直接返回 611 } 612 } catch (BadLocationException e) { 613 e.printStackTrace(); 614 } 615 616 if(conflict==YES && doc.getLength() > 0){ 617 try { 618 new Thread(new Thread() { 619 public void run() { 620 try { 621 doc.remove(0, doc.getLength());// 这里会触发一个removeUpdate事件,输入全部清空 622 } catch (Exception exp) { 623 System.out.println("Error: " + exp.toString()); 624 } 625 } 626 }).start(); 627 is_remove_by_insert = YES;//remove由insert导致 628 } catch (Exception ex) { 629 System.out.println("Error: " + ex.toString()); 630 } 631 } 632 633 if (doc.getLength() > 1 && conflict==NO) { 634 try { 635 new Thread(new Thread() { 636 public void run() { 637 try { 638 doc.remove(0, doc.getLength() - 1);// 这里会触发一个removeUpdate事件 639 } catch (Exception exp) { 640 System.out.println("Error: " + exp.toString()); 641 } 642 } 643 }).start(); 644 is_remove_by_insert = YES;//remove由insert导致 645 } catch (Exception ex) { 646 System.out.println("Error: " + ex.toString()); 647 } 648 } 649 650 int z; 651 //检验所有是否都填完并且无冲突 652 if(conflict==NO){ 653 for(z=0;z<9;z++){ 654 for(int x=0;x<3;x++){ 655 for(int y=0;y<3;y++){ 656 if(txtGame[z][x][y].getText().equals("")){ 657 return; 658 } 659 } 660 } 661 } 662 //如果所有的都填完就代表通关 663 if(z==9){ 664 System.out.println("恭喜通关!"); 665 } 666 } 667 } 668 669 /** 670 * 根据doc找到doc所在的field 671 * @param location 672 */ 673 void findFeildLocationByDoc(int[] location) { 674 for (int z = 0; z < 9; z++) { 675 for (int x = 0; x < 3; x++) { 676 for (int y = 0; y < 3; y++) { 677 if (txtGame[z][x][y].getDocument() == doc) { 678 location[0] = z; 679 location[1] = x; 680 location[2] = y; 681 } 682 } 683 } 684 } 685 } 686 /** 687 * 移除数字的时候更新 688 */ 689 public void removeUpdate(DocumentEvent documentEvent) { 690 doc = (Document) documentEvent.getDocument(); 691 int[] location = new int[3]; 692 int x,y,z; 693 694 if(is_remove_by_insert == NO){ 695 this.findFeildLocationByDoc(location); 696 z = location[0]; 697 x = location[1]; 698 y = location[2]; 699 if(warnFiledMap.get(txtGame[z][x][y])!=null){ 700 txtGame[z][x][y].setForeground(Color.black); 701 warnFiledMap.get(txtGame[z][x][y]).setForeground(Color.black); 702 } 703 } 704 } 705 706 public static void main(String[] args) { 707 new SudokuGame(); 708 } 709 }