├── .gitattributes ├── README.md ├── ai.py ├── chessboard.py ├── gobangGUI.py ├── img ├── black.png ├── chessboard.jpg └── white.png └── sound ├── defeated.wav ├── move.wav └── win.wav /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoBang 2 | 基于PyQt5的五子棋编程(人机对弈) 3 | -------------------------------------------------------------------------------- /ai.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | 5 | # evaluation: 棋盘评估类,给当前棋盘打分用 6 | # ---------------------------------------------------------------------- 7 | class evaluation(object): 8 | def __init__(self): 9 | self.POS = [] 10 | for i in range(15): 11 | row = [(7 - max(abs(i - 7), abs(j - 7))) for j in range(15)] 12 | self.POS.append(tuple(row)) 13 | self.POS = tuple(self.POS) 14 | self.STWO = 1 # 冲二 15 | self.STHREE = 2 # 冲三 16 | self.SFOUR = 3 # 冲四 17 | self.TWO = 4 # 活二 18 | self.THREE = 5 # 活三 19 | self.FOUR = 6 # 活四 20 | self.FIVE = 7 # 活五 21 | self.DFOUR = 8 # 双四 22 | self.FOURT = 9 # 四三 23 | self.DTHREE = 10 # 双三 24 | self.NOTYPE = 11 25 | self.ANALYSED = 255 # 已经分析过 26 | self.TODO = 0 # 没有分析过 27 | self.result = [0 for i in range(30)] # 保存当前直线分析值 28 | self.line = [0 for i in range(30)] # 当前直线数据 29 | self.record = [] # 全盘分析结果 [row][col][方向] 30 | for i in range(15): 31 | self.record.append([]) 32 | self.record[i] = [] 33 | for j in range(15): 34 | self.record[i].append([0, 0, 0, 0]) 35 | self.count = [] # 每种棋局的个数:count[黑棋/白棋][模式] 36 | for i in range(3): 37 | data = [0 for i in range(20)] 38 | self.count.append(data) 39 | self.reset() 40 | 41 | # 复位数据 42 | 43 | def reset(self): 44 | TODO = self.TODO 45 | count = self.count 46 | for i in range(15): 47 | line = self.record[i] 48 | for j in range(15): 49 | line[j][0] = TODO 50 | line[j][1] = TODO 51 | line[j][2] = TODO 52 | line[j][3] = TODO 53 | for i in range(20): 54 | count[0][i] = 0 55 | count[1][i] = 0 56 | count[2][i] = 0 57 | return 0 58 | 59 | # 四个方向(水平,垂直,左斜,右斜)分析评估棋盘,再根据结果打分 60 | 61 | def evaluate(self, board, turn): 62 | score = self.__evaluate(board, turn) 63 | count = self.count 64 | if score < -9000: 65 | stone = turn == 1 and 2 or 1 66 | for i in range(20): 67 | if count[stone][i] > 0: 68 | score -= i 69 | elif score > 9000: 70 | stone = turn == 1 and 2 or 1 71 | for i in range(20): 72 | if count[turn][i] > 0: 73 | score += i 74 | return score 75 | 76 | # 四个方向(水平,垂直,左斜,右斜)分析评估棋盘,再根据结果打分 77 | 78 | def __evaluate(self, board, turn): 79 | record, count = self.record, self.count 80 | TODO, ANALYSED = self.TODO, self.ANALYSED 81 | self.reset() 82 | # 四个方向分析 83 | for i in range(15): 84 | boardrow = board[i] 85 | recordrow = record[i] 86 | for j in range(15): 87 | if boardrow[j] != 0: 88 | if recordrow[j][0] == TODO: # 水平没有分析过? 89 | self.__analysis_horizon(board, i, j) 90 | if recordrow[j][1] == TODO: # 垂直没有分析过? 91 | self.__analysis_vertical(board, i, j) 92 | if recordrow[j][2] == TODO: # 左斜没有分析过? 93 | self.__analysis_left(board, i, j) 94 | if recordrow[j][3] == TODO: # 右斜没有分析过 95 | self.__analysis_right(board, i, j) 96 | 97 | FIVE, FOUR = self.FIVE, self.FOUR 98 | THREE, TWO = self.THREE, self.TWO 99 | SFOUR, STHREE, STWO = self.SFOUR, self.STHREE, self.STWO 100 | check = {} 101 | 102 | # 分别对白棋黑棋计算:FIVE, FOUR, THREE, TWO等出现的次数 103 | for c in (FIVE, FOUR, SFOUR, THREE, STHREE, TWO, STWO): 104 | check[c] = 1 105 | for i in range(15): 106 | for j in range(15): 107 | stone = board[i][j] 108 | if stone != 0: 109 | for k in range(4): 110 | ch = record[i][j][k] 111 | if ch in check: 112 | count[stone][ch] += 1 113 | 114 | # 如果有五连则马上返回分数 115 | BLACK, WHITE = 1, 2 116 | if turn == WHITE: # 当前是白棋 117 | if count[BLACK][FIVE]: 118 | return -9999 119 | if count[WHITE][FIVE]: 120 | return 9999 121 | else: # 当前是黑棋 122 | if count[WHITE][FIVE]: 123 | return -9999 124 | if count[BLACK][FIVE]: 125 | return 9999 126 | 127 | # 如果存在两个冲四,则相当于有一个活四 128 | if count[WHITE][SFOUR] >= 2: 129 | count[WHITE][FOUR] += 1 130 | if count[BLACK][SFOUR] >= 2: 131 | count[BLACK][FOUR] += 1 132 | 133 | # 具体打分 134 | wvalue, bvalue, win = 0, 0, 0 135 | if turn == WHITE: 136 | if count[WHITE][FOUR] > 0: return 9990 137 | if count[WHITE][SFOUR] > 0: return 9980 138 | if count[BLACK][FOUR] > 0: return -9970 139 | if count[BLACK][SFOUR] and count[BLACK][THREE]: 140 | return -9960 141 | if count[WHITE][THREE] and count[BLACK][SFOUR] == 0: 142 | return 9950 143 | if count[BLACK][THREE] > 1 and \ 144 | count[WHITE][SFOUR] == 0 and \ 145 | count[WHITE][THREE] == 0 and \ 146 | count[WHITE][STHREE] == 0: 147 | return -9940 148 | if count[WHITE][THREE] > 1: 149 | wvalue += 2000 150 | elif count[WHITE][THREE]: 151 | wvalue += 200 152 | if count[BLACK][THREE] > 1: 153 | bvalue += 500 154 | elif count[BLACK][THREE]: 155 | bvalue += 100 156 | if count[WHITE][STHREE]: 157 | wvalue += count[WHITE][STHREE] * 10 158 | if count[BLACK][STHREE]: 159 | bvalue += count[BLACK][STHREE] * 10 160 | if count[WHITE][TWO]: 161 | wvalue += count[WHITE][TWO] * 4 162 | if count[BLACK][TWO]: 163 | bvalue += count[BLACK][TWO] * 4 164 | if count[WHITE][STWO]: 165 | wvalue += count[WHITE][STWO] 166 | if count[BLACK][STWO]: 167 | bvalue += count[BLACK][STWO] 168 | else: 169 | if count[BLACK][FOUR] > 0: return 9990 170 | if count[BLACK][SFOUR] > 0: return 9980 171 | if count[WHITE][FOUR] > 0: return -9970 172 | if count[WHITE][SFOUR] and count[WHITE][THREE]: 173 | return -9960 174 | if count[BLACK][THREE] and count[WHITE][SFOUR] == 0: 175 | return 9950 176 | if count[WHITE][THREE] > 1 and \ 177 | count[BLACK][SFOUR] == 0 and \ 178 | count[BLACK][THREE] == 0 and \ 179 | count[BLACK][STHREE] == 0: 180 | return -9940 181 | if count[BLACK][THREE] > 1: 182 | bvalue += 2000 183 | elif count[BLACK][THREE]: 184 | bvalue += 200 185 | if count[WHITE][THREE] > 1: 186 | wvalue += 500 187 | elif count[WHITE][THREE]: 188 | wvalue += 100 189 | if count[BLACK][STHREE]: 190 | bvalue += count[BLACK][STHREE] * 10 191 | if count[WHITE][STHREE]: 192 | wvalue += count[WHITE][STHREE] * 10 193 | if count[BLACK][TWO]: 194 | bvalue += count[BLACK][TWO] * 4 195 | if count[WHITE][TWO]: 196 | wvalue += count[WHITE][TWO] * 4 197 | if count[BLACK][STWO]: 198 | bvalue += count[BLACK][STWO] 199 | if count[WHITE][STWO]: 200 | wvalue += count[WHITE][STWO] 201 | 202 | # 加上位置权值,棋盘最中心点权值是7,往外一格-1,最外圈是0 203 | wc, bc = 0, 0 204 | for i in range(15): 205 | for j in range(15): 206 | stone = board[i][j] 207 | if stone != 0: 208 | if stone == WHITE: 209 | wc += self.POS[i][j] 210 | else: 211 | bc += self.POS[i][j] 212 | wvalue += wc 213 | bvalue += bc 214 | 215 | if turn == WHITE: 216 | return wvalue - bvalue 217 | 218 | return bvalue - wvalue 219 | 220 | # 分析横向 221 | 222 | def __analysis_horizon(self, board, i, j): 223 | line, result, record = self.line, self.result, self.record 224 | TODO = self.TODO 225 | for x in range(15): 226 | line[x] = board[i][x] 227 | self.analysis_line(line, result, 15, j) 228 | for x in range(15): 229 | if result[x] != TODO: 230 | record[i][x][0] = result[x] 231 | return record[i][j][0] 232 | 233 | # 分析横向 234 | 235 | def __analysis_vertical(self, board, i, j): 236 | line, result, record = self.line, self.result, self.record 237 | TODO = self.TODO 238 | for x in range(15): 239 | line[x] = board[x][j] 240 | self.analysis_line(line, result, 15, i) 241 | for x in range(15): 242 | if result[x] != TODO: 243 | record[x][j][1] = result[x] 244 | return record[i][j][1] 245 | 246 | # 分析左斜 247 | 248 | def __analysis_left(self, board, i, j): 249 | line, result, record = self.line, self.result, self.record 250 | TODO = self.TODO 251 | if i < j: 252 | x, y = j - i, 0 253 | else: 254 | x, y = 0, i - j 255 | k = 0 256 | while k < 15: 257 | if x + k > 14 or y + k > 14: 258 | break 259 | line[k] = board[y + k][x + k] 260 | k += 1 261 | self.analysis_line(line, result, k, j - x) 262 | for s in range(k): 263 | if result[s] != TODO: 264 | record[y + s][x + s][2] = result[s] 265 | return record[i][j][2] 266 | 267 | # 分析右斜 268 | 269 | def __analysis_right(self, board, i, j): 270 | line, result = self.line, self.result 271 | record = self.record 272 | TODO = self.TODO 273 | if 14 - i < j: 274 | x, y, realnum = j - 14 + i, 14, 14 - i 275 | else: 276 | x, y, realnum = 0, i + j, j 277 | k = 0 278 | while k < 15: 279 | if x + k > 14 or y - k < 0: 280 | break 281 | line[k] = board[y - k][x + k] 282 | k += 1 283 | self.analysis_line(line, result, k, j - x) 284 | for s in range(k): 285 | if result[s] != TODO: 286 | record[y - s][x + s][3] = result[s] 287 | return record[i][j][3] 288 | 289 | # 分析一条线:五四三二等棋型 290 | 291 | def analysis_line(self, line, record, num, pos): 292 | TODO, ANALYSED = self.TODO, self.ANALYSED 293 | THREE, STHREE = self.THREE, self.STHREE 294 | FOUR, SFOUR = self.FOUR, self.SFOUR 295 | while len(line) < 30: line.append(0xf) 296 | while len(record) < 30: record.append(TODO) 297 | for i in range(num, 30): 298 | line[i] = 0xf 299 | for i in range(num): 300 | record[i] = TODO 301 | if num < 5: 302 | for i in range(num): 303 | record[i] = ANALYSED 304 | return 0 305 | stone = line[pos] 306 | inverse = (0, 2, 1)[stone] 307 | num -= 1 308 | xl = pos 309 | xr = pos 310 | while xl > 0: # 探索左边界 311 | if line[xl - 1] != stone: break 312 | xl -= 1 313 | while xr < num: # 探索右边界 314 | if line[xr + 1] != stone: break 315 | xr += 1 316 | left_range = xl 317 | right_range = xr 318 | while left_range > 0: # 探索左边范围(非对方棋子的格子坐标) 319 | if line[left_range - 1] == inverse: break 320 | left_range -= 1 321 | while right_range < num: # 探索右边范围 322 | if line[right_range + 1] == inverse: break 323 | right_range += 1 324 | 325 | # 如果该直线范围小于 5,则直接返回 326 | if right_range - left_range < 4: 327 | for k in range(left_range, right_range + 1): 328 | record[k] = ANALYSED 329 | return 0 330 | 331 | # 设置已经分析过 332 | for k in range(xl, xr + 1): 333 | record[k] = ANALYSED 334 | 335 | srange = xr - xl 336 | 337 | # 如果是 5连 338 | if srange >= 4: 339 | record[pos] = self.FIVE 340 | return self.FIVE 341 | 342 | # 如果是 4连 343 | if srange == 3: 344 | leftfour = False # 是否左边是空格 345 | if xl > 0: 346 | if line[xl - 1] == 0: # 活四 347 | leftfour = True 348 | if xr < num: 349 | if line[xr + 1] == 0: 350 | if leftfour: 351 | record[pos] = self.FOUR # 活四 352 | else: 353 | record[pos] = self.SFOUR # 冲四 354 | else: 355 | if leftfour: 356 | record[pos] = self.SFOUR # 冲四 357 | else: 358 | if leftfour: 359 | record[pos] = self.SFOUR # 冲四 360 | return record[pos] 361 | 362 | # 如果是 3连 363 | if srange == 2: # 三连 364 | left3 = False # 是否左边是空格 365 | if xl > 0: 366 | if line[xl - 1] == 0: # 左边有气 367 | if xl > 1 and line[xl - 2] == stone: 368 | record[xl] = SFOUR 369 | record[xl - 2] = ANALYSED 370 | else: 371 | left3 = True 372 | elif xr == num or line[xr + 1] != 0: 373 | return 0 374 | if xr < num: 375 | if line[xr + 1] == 0: # 右边有气 376 | if xr < num - 1 and line[xr + 2] == stone: 377 | record[xr] = SFOUR # XXX-X 相当于冲四 378 | record[xr + 2] = ANALYSED 379 | elif left3: 380 | record[xr] = THREE 381 | else: 382 | record[xr] = STHREE 383 | elif record[xl] == SFOUR: 384 | return record[xl] 385 | elif left3: 386 | record[pos] = STHREE 387 | else: 388 | if record[xl] == SFOUR: 389 | return record[xl] 390 | if left3: 391 | record[pos] = STHREE 392 | return record[pos] 393 | 394 | # 如果是 2连 395 | if srange == 1: # 两连 396 | left2 = False 397 | if xl > 2: 398 | if line[xl - 1] == 0: # 左边有气 399 | if line[xl - 2] == stone: 400 | if line[xl - 3] == stone: 401 | record[xl - 3] = ANALYSED 402 | record[xl - 2] = ANALYSED 403 | record[xl] = SFOUR 404 | elif line[xl - 3] == 0: 405 | record[xl - 2] = ANALYSED 406 | record[xl] = STHREE 407 | else: 408 | left2 = True 409 | if xr < num: 410 | if line[xr + 1] == 0: # 左边有气 411 | if xr < num - 2 and line[xr + 2] == stone: 412 | if line[xr + 3] == stone: 413 | record[xr + 3] = ANALYSED 414 | record[xr + 2] = ANALYSED 415 | record[xr] = SFOUR 416 | elif line[xr + 3] == 0: 417 | record[xr + 2] = ANALYSED 418 | record[xr] = left2 and THREE or STHREE 419 | else: 420 | if record[xl] == SFOUR: 421 | return record[xl] 422 | if record[xl] == STHREE: 423 | record[xl] = THREE 424 | return record[xl] 425 | if left2: 426 | record[pos] = self.TWO 427 | else: 428 | record[pos] = self.STWO 429 | else: 430 | if record[xl] == SFOUR: 431 | return record[xl] 432 | if left2: 433 | record[pos] = self.STWO 434 | return record[pos] 435 | return 0 436 | 437 | 438 | # ---------------------------------------------------------------------- 439 | 440 | 441 | # DFS: 博弈树搜索 442 | # ---------------------------------------------------------------------- 443 | class searcher(object): 444 | # 初始化 445 | def __init__(self): 446 | self.evaluator = evaluation() 447 | self.board = [[0 for n in range(15)] for i in range(15)] 448 | self.gameover = 0 449 | self.overvalue = 0 450 | self.maxdepth = 3 451 | 452 | # 产生当前棋局的走法 453 | 454 | def genmove(self, turn): 455 | moves = [] 456 | board = self.board 457 | POSES = self.evaluator.POS 458 | for i in range(15): 459 | for j in range(15): 460 | if board[i][j] == 0: 461 | score = POSES[i][j] 462 | moves.append((score, i, j)) 463 | moves.sort() 464 | moves.reverse() 465 | return moves 466 | 467 | # 递归搜索:返回最佳分数 468 | 469 | def __search(self, turn, depth, alpha, beta): 470 | 471 | # 深度为零则评估棋盘并返回 472 | if depth <= 0: 473 | score = self.evaluator.evaluate(self.board, turn) 474 | return score 475 | 476 | # 如果游戏结束则立马返回 477 | score = self.evaluator.evaluate(self.board, turn) 478 | if abs(score) >= 9999 and depth < self.maxdepth: 479 | return score 480 | 481 | # 产生新的走法 482 | moves = self.genmove(turn) 483 | bestmove = None 484 | 485 | # 枚举当前所有走法 486 | for score, row, col in moves: 487 | 488 | # 标记当前走法到棋盘 489 | self.board[row][col] = turn 490 | 491 | # 计算下一回合该谁走 492 | nturn = turn == 1 and 2 or 1 493 | 494 | # 深度优先搜索,返回评分,走的行和走的列 495 | score = - self.__search(nturn, depth - 1, -beta, -alpha) 496 | 497 | # 棋盘上清除当前走法 498 | self.board[row][col] = 0 499 | 500 | # 计算最好分值的走法 501 | # alpha/beta 剪枝 502 | if score > alpha: 503 | alpha = score 504 | bestmove = (row, col) 505 | if alpha >= beta: 506 | break 507 | 508 | # 如果是第一层则记录最好的走法 509 | if depth == self.maxdepth and bestmove: 510 | self.bestmove = bestmove 511 | 512 | # 返回当前最好的分数,和该分数的对应走法 513 | return alpha 514 | 515 | # 具体搜索:传入当前是该谁走(turn=1/2),以及搜索深度(depth) 516 | 517 | def search(self, turn, depth=3): 518 | self.maxdepth = depth 519 | self.bestmove = None 520 | score = self.__search(turn, depth, -0x7fffffff, 0x7fffffff) 521 | if abs(score) > 8000: 522 | self.maxdepth = depth 523 | score = self.__search(turn, 1, -0x7fffffff, 0x7fffffff) 524 | row, col = self.bestmove 525 | return score, row, col -------------------------------------------------------------------------------- /chessboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | # ---------------------------------------------------------------------- 5 | # 定义棋子类型,输赢情况 6 | # ---------------------------------------------------------------------- 7 | EMPTY = 0 8 | BLACK = 1 9 | WHITE = 2 10 | 11 | 12 | # ---------------------------------------------------------------------- 13 | # 定义棋盘类,绘制棋盘的形状,切换先后手,判断输赢等 14 | # ---------------------------------------------------------------------- 15 | class ChessBoard(object): 16 | def __init__(self): 17 | self.__board = [[EMPTY for n in range(15)] for m in range(15)] 18 | self.__dir = [[(-1, 0), (1, 0)], [(0, -1), (0, 1)], [(-1, 1), (1, -1)], [(-1, -1), (1, 1)]] 19 | # (左 右) (上 下) (左下 右上) (左上 右下) 20 | 21 | def board(self): # 返回数组对象 22 | return self.__board 23 | 24 | def draw_xy(self, x, y, state): # 获取落子点坐标的状态 25 | self.__board[x][y] = state 26 | 27 | def get_xy_on_logic_state(self, x, y): # 获取指定点坐标的状态 28 | return self.__board[x][y] 29 | 30 | def get_next_xy(self, point, direction): # 获取指定点的指定方向的坐标 31 | x = point[0] + direction[0] 32 | y = point[1] + direction[1] 33 | if x < 0 or x >= 15 or y < 0 or y >= 15: 34 | return False 35 | else: 36 | return x, y 37 | 38 | def get_xy_on_direction_state(self, point, direction): # 获取指定点的指定方向的状态 39 | if point is not False: 40 | xy = self.get_next_xy(point, direction) 41 | if xy is not False: 42 | x, y = xy 43 | return self.__board[x][y] 44 | return False 45 | 46 | def anyone_win(self, x, y): 47 | state = self.get_xy_on_logic_state(x, y) 48 | for directions in self.__dir: # 对米字的4个方向分别检测是否有5子相连的棋 49 | count = 1 50 | for direction in directions: # 对落下的棋子的同一条线的两侧都要检测,结果累积 51 | point = (x, y) 52 | while True: 53 | if self.get_xy_on_direction_state(point, direction) == state: 54 | count += 1 55 | point = self.get_next_xy(point, direction) 56 | else: 57 | break 58 | if count >= 5: 59 | return state 60 | return EMPTY 61 | 62 | def reset(self): # 重置 63 | self.__board = [[EMPTY for n in range(15)] for m in range(15)] 64 | -------------------------------------------------------------------------------- /gobangGUI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | 5 | from chessboard import ChessBoard 6 | from ai import searcher 7 | 8 | WIDTH = 540 9 | HEIGHT = 540 10 | MARGIN = 22 11 | GRID = (WIDTH - 2 * MARGIN) / (15 - 1) 12 | PIECE = 34 13 | EMPTY = 0 14 | BLACK = 1 15 | WHITE = 2 16 | 17 | import sys 18 | from PyQt5 import QtCore, QtGui 19 | from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QMessageBox 20 | from PyQt5.QtCore import Qt 21 | from PyQt5.QtGui import QPixmap, QIcon, QPalette, QPainter 22 | from PyQt5.QtMultimedia import QSound 23 | 24 | 25 | # ---------------------------------------------------------------------- 26 | # 定义线程类执行AI的算法 27 | # ---------------------------------------------------------------------- 28 | class AI(QtCore.QThread): 29 | finishSignal = QtCore.pyqtSignal(int, int) 30 | 31 | # 构造函数里增加形参 32 | def __init__(self, board, parent=None): 33 | super(AI, self).__init__(parent) 34 | self.board = board 35 | 36 | # 重写 run() 函数 37 | def run(self): 38 | self.ai = searcher() 39 | self.ai.board = self.board 40 | score, x, y = self.ai.search(2, 2) 41 | self.finishSignal.emit(x, y) 42 | 43 | 44 | # ---------------------------------------------------------------------- 45 | # 重新定义Label类 46 | # ---------------------------------------------------------------------- 47 | class LaBel(QLabel): 48 | def __init__(self, parent): 49 | super().__init__(parent) 50 | self.setMouseTracking(True) 51 | 52 | def enterEvent(self, e): 53 | e.ignore() 54 | 55 | 56 | class GoBang(QWidget): 57 | def __init__(self): 58 | super().__init__() 59 | self.initUI() 60 | 61 | def initUI(self): 62 | 63 | self.chessboard = ChessBoard() # 棋盘类 64 | 65 | palette1 = QPalette() # 设置棋盘背景 66 | palette1.setBrush(self.backgroundRole(), QtGui.QBrush(QtGui.QPixmap('img/chessboard.jpg'))) 67 | self.setPalette(palette1) 68 | # self.setStyleSheet("board-image:url(img/chessboard.jpg)") # 不知道这为什么不行 69 | self.setCursor(Qt.PointingHandCursor) # 鼠标变成手指形状 70 | self.sound_piece = QSound("sound/luozi.wav") # 加载落子音效 71 | self.sound_win = QSound("sound/win.wav") # 加载胜利音效 72 | self.sound_defeated = QSound("sound/defeated.wav") # 加载失败音效 73 | 74 | self.resize(WIDTH, HEIGHT) # 固定大小 540*540 75 | self.setMinimumSize(QtCore.QSize(WIDTH, HEIGHT)) 76 | self.setMaximumSize(QtCore.QSize(WIDTH, HEIGHT)) 77 | 78 | self.setWindowTitle("GoBang") # 窗口名称 79 | self.setWindowIcon(QIcon('img/black.png')) # 窗口图标 80 | 81 | # self.lb1 = QLabel(' ', self) 82 | # self.lb1.move(20, 10) 83 | 84 | self.black = QPixmap('img/black.png') 85 | self.white = QPixmap('img/white.png') 86 | 87 | self.piece_now = BLACK # 黑棋先行 88 | self.my_turn = True # 玩家先行 89 | self.step = 0 # 步数 90 | self.x, self.y = 1000, 1000 91 | 92 | self.mouse_point = LaBel(self) # 将鼠标图片改为棋子 93 | self.mouse_point.setScaledContents(True) 94 | self.mouse_point.setPixmap(self.black) # 加载黑棋 95 | self.mouse_point.setGeometry(270, 270, PIECE, PIECE) 96 | self.pieces = [LaBel(self) for i in range(225)] # 新建棋子标签,准备在棋盘上绘制棋子 97 | for piece in self.pieces: 98 | piece.setVisible(True) # 图片可视 99 | piece.setScaledContents(True) # 图片大小根据标签大小可变 100 | 101 | self.mouse_point.raise_() # 鼠标始终在最上层 102 | self.ai_down = True # AI已下棋,主要是为了加锁,当值是False的时候说明AI正在思考,这时候玩家鼠标点击失效,要忽略掉 mousePressEvent 103 | 104 | self.setMouseTracking(True) 105 | self.show() 106 | 107 | def paintEvent(self, event): # 画出指示箭头 108 | qp = QPainter() 109 | qp.begin(self) 110 | self.drawLines(qp) 111 | qp.end() 112 | 113 | def mouseMoveEvent(self, e): # 黑色棋子随鼠标移动 114 | # self.lb1.setText(str(e.x()) + ' ' + str(e.y())) 115 | self.mouse_point.move(e.x() - 16, e.y() - 16) 116 | 117 | def mousePressEvent(self, e): # 玩家下棋 118 | if e.button() == Qt.LeftButton and self.ai_down == True: 119 | x, y = e.x(), e.y() # 鼠标坐标 120 | i, j = self.coordinate_transform_pixel2map(x, y) # 对应棋盘坐标 121 | if not i is None and not j is None: # 棋子落在棋盘上,排除边缘 122 | if self.chessboard.get_xy_on_logic_state(i, j) == EMPTY: # 棋子落在空白处 123 | self.draw(i, j) 124 | self.ai_down = False 125 | board = self.chessboard.board() 126 | self.AI = AI(board) # 新建线程对象,传入棋盘参数 127 | self.AI.finishSignal.connect(self.AI_draw) # 结束线程,传出参数 128 | self.AI.start() # run 129 | 130 | def AI_draw(self, i, j): 131 | if self.step != 0: 132 | self.draw(i, j) # AI 133 | self.x, self.y = self.coordinate_transform_map2pixel(i, j) 134 | self.ai_down = True 135 | self.update() 136 | 137 | def draw(self, i, j): 138 | x, y = self.coordinate_transform_map2pixel(i, j) 139 | 140 | if self.piece_now == BLACK: 141 | self.pieces[self.step].setPixmap(self.black) # 放置黑色棋子 142 | self.piece_now = WHITE 143 | self.chessboard.draw_xy(i, j, BLACK) 144 | else: 145 | self.pieces[self.step].setPixmap(self.white) # 放置白色棋子 146 | self.piece_now = BLACK 147 | self.chessboard.draw_xy(i, j, WHITE) 148 | 149 | self.pieces[self.step].setGeometry(x, y, PIECE, PIECE) # 画出棋子 150 | self.sound_piece.play() # 落子音效 151 | self.step += 1 # 步数+1 152 | 153 | winner = self.chessboard.anyone_win(i, j) # 判断输赢 154 | if winner != EMPTY: 155 | self.mouse_point.clear() 156 | self.gameover(winner) 157 | 158 | def drawLines(self, qp): # 指示AI当前下的棋子 159 | if self.step != 0: 160 | pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine) 161 | qp.setPen(pen) 162 | qp.drawLine(self.x - 5, self.y - 5, self.x + 3, self.y + 3) 163 | qp.drawLine(self.x + 3, self.y, self.x + 3, self.y + 3) 164 | qp.drawLine(self.x, self.y + 3, self.x + 3, self.y + 3) 165 | 166 | def coordinate_transform_map2pixel(self, i, j): 167 | # 从 chessMap 里的逻辑坐标到 UI 上的绘制坐标的转换 168 | return MARGIN + j * GRID - PIECE / 2, MARGIN + i * GRID - PIECE / 2 169 | 170 | def coordinate_transform_pixel2map(self, x, y): 171 | # 从 UI 上的绘制坐标到 chessMap 里的逻辑坐标的转换 172 | i, j = int(round((y - MARGIN) / GRID)), int(round((x - MARGIN) / GRID)) 173 | # 有MAGIN, 排除边缘位置导致 i,j 越界 174 | if i < 0 or i >= 15 or j < 0 or j >= 15: 175 | return None, None 176 | else: 177 | return i, j 178 | 179 | def gameover(self, winner): 180 | if winner == BLACK: 181 | self.sound_win.play() 182 | reply = QMessageBox.question(self, 'You Win!', 'Continue?', 183 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 184 | else: 185 | self.sound_defeated.play() 186 | reply = QMessageBox.question(self, 'You Lost!', 'Continue?', 187 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 188 | 189 | if reply == QMessageBox.Yes: # 复位 190 | self.piece_now = BLACK 191 | self.mouse_point.setPixmap(self.black) 192 | self.step = 0 193 | for piece in self.pieces: 194 | piece.clear() 195 | self.chessboard.reset() 196 | self.update() 197 | else: 198 | self.close() 199 | 200 | 201 | if __name__ == '__main__': 202 | app = QApplication(sys.argv) 203 | ex = GoBang() 204 | sys.exit(app.exec_()) 205 | -------------------------------------------------------------------------------- /img/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/img/black.png -------------------------------------------------------------------------------- /img/chessboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/img/chessboard.jpg -------------------------------------------------------------------------------- /img/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/img/white.png -------------------------------------------------------------------------------- /sound/defeated.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/sound/defeated.wav -------------------------------------------------------------------------------- /sound/move.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/sound/move.wav -------------------------------------------------------------------------------- /sound/win.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFred/GoBang/473ca018f9c486077e84fc101555d724563b91d7/sound/win.wav --------------------------------------------------------------------------------