20253331靳淏童 2025-2026-2 《Python程序设计》实验四报告
课程:《Python程序设计》
班级: 2533
姓名: 靳淏童
学号: 20253331
实验教师:王志强
实验日期:2026年5月18日
必修/选修: 公选课
1.实验要求
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
课代表和各小组负责人收集作业(源代码、视频、综合实践报告)
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。
评分标准:
(1)程序能运行,功能丰富(至少5个功能)。(需求提交源代码,并建议录制程序运行的视频)15分
(2)综合实践报告,要体现实验分析、设计、实现过程、结果等信息,格式规范,逻辑清晰,结构合理。20分。
(3)在实践报告中,需要对全课进行总结,并写课程感想体会、意见和建议等。10分
2 实验选题:中国象棋(游戏制作)
2.1 选题背景
中国象棋是中华民族的文化瑰宝,拥有悠久的传承历史。借助编程实现象棋对弈系统,不仅能够巩固Python面向对象编程、图形界面开发等课堂所学,还能深入理解规则引擎、碰撞检测和博弈算法的设计思路。我选择tkinter作为图形库,因为它是Python标准库的一部分,无需安装任何第三方依赖,任何具备Python环境的机器都能直接运行,这在课程作业提交和演示时非常方便。
2.2 主要功能清单
本系统一共实现了八项核心功能:第一,标准棋盘与棋子初始化,在9×10交叉点网格上正确摆放红黑双方共32个棋子。第二,完整走法生成,支持帅(将)、仕(士)、相(象)、马、车、炮、兵(卒)的全部走法规则,包括马脚、象田、炮架、将帅照面等特殊约束。第三,回合制交互,红方先行,黑方后行,只有当前回合的玩家才能操作己方棋子。第四,将军与将死判定,实时检测哪一方被将军,并在无应将走法时判定胜负。第五,人机对战模式,电脑采用随机合法走法,为单人练习提供对手。第六,悔棋功能,支持撤销一步并带有确认对话框,防止误操作。第七,模式切换,一键在双人对战与人机对战之间切换,无需重启程序。第八,可视化反馈,选中棋子高亮、合法走法以绿色圆点提示、将军时状态栏变红并显示“将军!”字样,用户体验友好。
3 本地实验环境
本次开发使用的操作系统为Windows 11,开发语言为Python 3.12,集成开发环境为IDLE(Python 3.12 64-bit)以及VS Code。图形界面库选择tkinter(Python内置),因此没有额外安装任何第三方库。版本管理采用Git并托管到Gitee平台。
4 实验设计
4.1 系统架构
本程序遵循MVC(模型-视图-控制器)设计模式。模型层由board二维数组和ChessPiece类组成,负责存储所有棋子的状态(类型、颜色、位置、合法走法列表)。视图层是Canvas组件,负责绘制棋盘网格、棋子图案、高亮边框以及合法走法提示,同时状态栏标签Label负责显示当前回合和将军警告。控制器层则是鼠标事件绑定函数on_canvas_click,它处理用户的点击操作,调用走法生成、移动执行、AI走棋等业务逻辑。三层分离使得代码职责清晰,便于维护和扩展。
4.2 核心模块划分
程序主要包含以下几个模块:棋子对象模块(ChessPiece类),定义了棋子的显示名称和颜色获取方法。走法生成模块(calculate_valid_moves方法),针对每一种棋子类型分别实现走法规则。规则判定模块(is_in_check和is_checkmate方法),负责检测将军和将死状态。移动执行模块(move_piece方法),执行走棋、记录历史、判断胜负并切换玩家。AI模块(ai_move方法),随机选择合法走法。悔棋模块(undo_move方法),撤销最近一步并恢复棋盘状态。绘制模块(draw_board、draw_grid、draw_pieces等),负责将所有图形元素渲染到画布上。
4.3 关键算法设计
走法生成是整系统的核心。对于每一种棋子,程序按照规则计算出所有可到达的位置。例如马走“日”字,但必须检查“马腿”是否被蹩,即直走一格处是否有棋子阻挡;象走“田”字,需检查“象眼”是否有棋子,且不能过河;炮走直线,但吃子时必须隔一个棋子(炮架);车走直线且不可越子;帅(将)只能在九宫格内移动一步,且不能与对方将帅在同一列照面(中间无棋子)。将军检测通过遍历所有对方棋子,看它们的合法走法是否包含己方将帅的位置来实现。将死判定则在被将军的基础上,进一步检查是否所有己方棋子均无法应将。AI采用最简单的随机策略,每次从当前所有己方棋子的合法走法中随机选取一个执行,虽然智能水平不高,但足够演示人机对战的基本流程。
4.4 界面布局设计
游戏窗口分为左右两大部分。左侧是画布区域,尺寸由SQUARE_SIZE=65动态计算,确保所有交叉点精确对齐。画布上绘制了完整的棋盘网格,包括横线、竖线(河界处断开)、九宫格斜线、以及“楚河汉界”文字。棋子绘制在交叉点上,每个棋子用一个白色填充、红色或黑色边框的圆表示,内部显示对应的汉字(红方为帅仕相兵,黑方为将士象卒)。右侧是控制面板,包含模式切换按钮(双人对战、人机对战)、悔棋、重新开始、退出,以及一个状态标签,实时显示当前回合和将军信息。整个界面简洁直观,符合用户习惯。
5 实验过程
5.1 环境搭建
由于采用了tkinter,无需安装任何第三方库。我直接在IDLE中新建Python文件,导入tkinter、messagebox、random和enum模块。tkinter是Python标准库,messagebox用于弹出对话框,random用于AI随机选择,enum用于定义棋子类型枚举。整个过程没有任何安装障碍。
5.2 项目文件组织
在本地创建文件夹ChineseChess,其中仅包含一个主程序文件chess.py。所有图形均通过代码绘制,未使用外部图片资源,因此无需额外资源文件夹。该设计降低了项目复杂度,也方便代码迁移和分享。
5.3 关键代码编写与调试
我按顺序实现了以下核心逻辑。第一步,定义常量SQUARE_SIZE、BOARD_ROWS、BOARD_COLS、MARGIN等,并编写坐标转换函数to_pixel和to_board,确保点击画布时能正确映射到最近的交叉点。第二步,定义PieceType枚举和ChessPiece类,实现get_display_name区分红黑显示文字。第三步,实现calculate_valid_moves,逐个棋子类型完善走法规则,特别关注马腿、象田、炮架、将帅照面等细节。第四步,实现move_piece,包含移动执行、历史记录、捕获检测、胜负判定(包括吃将直接获胜)。第五步,实现is_in_check和is_checkmate,反复测试各种将军局面是否被准确识别。第六步,实现ai_move,收集所有合法走法并随机选取。第七步,实现undo_move,弹出确认框后恢复上一步状态。第八步,实现绘制函数,确保网格、棋子、高亮和提示正确显示。第九步,实现事件处理on_canvas_click,处理棋子选中、移动和取消选择。最后编写主循环和启动代码。
调试过程中遇到的主要问题及解决方式记录如下。
第一个问题是棋子被画在格子中心而非交叉点。起初我使用了MARGIN + colSIZE + SIZE//2,这不符合中国象棋规则。修正方案是将坐标改为MARGIN + colSIZE,并将点击检测改为取最近交叉点,且限制点击距离不超过半个格子,从而准确捕捉用户意图。
第二个问题是黑方棋子显示的文字错误,黑方帅显示为“帅”、士显示为“仕”等。原因在于get_display_name未根据颜色区分。修正后黑方正确显示“将、士、象、卒”。
第三个问题是吃掉对方将帅后游戏并未结束。原代码只检查了将死状态,而直接将死的情况没有被捕获。我在move_piece中添加了对captured类型的判断,一旦捕获到GENERAL立即判定获胜。
第四个问题是将军没有界面提示,仅在控制台输出。我在游戏状态中增加了in_check变量,在update_status中判断并修改状态标签的文字颜色为红色,同时添加了额外的画布文字提示。
第五个问题是AI走棋时界面短暂卡顿。这是因为AI逻辑在事件循环中同步执行,阻塞了界面更新。我使用master.after(300,self.aimove)延迟调用,给绘制留出时间。
第六个问题是悔棋后将军状态不刷新。悔棋恢复棋盘后,我重新计算self.in_check = self.is_in_check(self.currentplayer),确保状态正确。
5.4 最终代码与托管
import tkinter as tk
from tkinter import messagebox
import random
from enum import Enum
==================== 常量 ====================
SQUARE_SIZE = 65
BOARD_ROWS = 10
BOARD_COLS = 9
MARGIN = 40
BOARD_PIXEL_WIDTH = (BOARD_COLS - 1) * SQUARE_SIZE
BOARD_PIXEL_HEIGHT = (BOARD_ROWS - 1) * SQUARE_SIZE
CANVAS_WIDTH = BOARD_PIXEL_WIDTH + 2 * MARGIN
CANVAS_HEIGHT = BOARD_PIXEL_HEIGHT + 2 * MARGIN
BG_COLOR = "#EED0A1"
LINE_COLOR = "#000000"
RED_COLOR = "#DC0000"
BLACK_COLOR = "#000000"
HIGHLIGHT_COLOR = "#00FF0066"
PIECE_RADIUS = 28
==================== 棋子类型 ====================
class PieceType(Enum):
GENERAL = "帅"
ADVISOR = "仕"
ELEPHANT = "相"
HORSE = "马"
CHARIOT = "车"
CANNON = "炮"
SOLDIER = "兵"
==================== 棋子类 ====================
class ChessPiece:
def init(self, piece_type, is_red, row, col):
self.type = piece_type
self.is_red = is_red
self.row = row
self.col = col
self.valid_moves = []
def get_display_name(self):
if self.type == PieceType.GENERAL:
return "帅" if self.is_red else "将"
elif self.type == PieceType.ADVISOR:
return "仕" if self.is_red else "士"
elif self.type == PieceType.ELEPHANT:
return "相" if self.is_red else "象"
elif self.type == PieceType.SOLDIER:
return "兵" if self.is_red else "卒"
else:
return self.type.value
def get_color(self):
return RED_COLOR if self.is_red else BLACK_COLOR
==================== 游戏主类 ====================
class ChineseChessGame:
def init(self, master):
self.master = master
master.title("中国象棋 - 交叉点版")
master.resizable(False, False)
self.board = [[None for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]
self.current_player = True
self.game_over = False
self.winner = None
self.move_history = []
self.selected_piece = None
self.ai_enabled = False
self.in_check = False
self.create_widgets()
self.setup_board()
self.draw_board()
def create_widgets(self):
main_frame = tk.Frame(self.master)
main_frame.pack(padx=10, pady=10)
self.canvas = tk.Canvas(main_frame, width=CANVAS_WIDTH, height=CANVAS_HEIGHT,
bg=BG_COLOR, highlightthickness=0)
self.canvas.pack(side=tk.LEFT)
self.canvas.bind("
panel = tk.Frame(main_frame, padx=10)
panel.pack(side=tk.RIGHT, fill=tk.Y)
tk.Label(panel, text="中国象棋", font=("宋体", 18, "bold")).pack(pady=5)
tk.Label(panel, text="综合实践作业", font=("宋体", 12)).pack(pady=2)
self.mode_label = tk.Label(panel, text="当前模式:双人对战", font=("宋体", 10))
self.mode_label.pack(pady=5)
tk.Button(panel, text="双人对战", width=12, command=self.set_pvp_mode).pack(pady=2)
tk.Button(panel, text="人机对战", width=12, command=self.set_ai_mode).pack(pady=2)
tk.Button(panel, text="悔棋", width=12, command=self.undo_move).pack(pady=2)
tk.Button(panel, text="重新开始", width=12, command=self.reset_game).pack(pady=2)
tk.Button(panel, text="退出", width=12, command=self.master.quit).pack(pady=2)
self.status_var = tk.StringVar()
self.status_var.set("红方走棋")
self.status_label = tk.Label(panel, textvariable=self.status_var,
font=("宋体", 12, "bold"), fg="blue")
self.status_label.pack(pady=10)
def set_pvp_mode(self):
self.ai_enabled = False
self.mode_label.config(text="当前模式:双人对战")
self.reset_game()
def set_ai_mode(self):
self.ai_enabled = True
self.mode_label.config(text="当前模式:人机对战")
self.reset_game()
def setup_board(self):
self.board = [[None for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]
# 红方(下方)
self.board[0][0] = ChessPiece(PieceType.CHARIOT, True, 0, 0)
self.board[0][1] = ChessPiece(PieceType.HORSE, True, 0, 1)
self.board[0][2] = ChessPiece(PieceType.ELEPHANT, True, 0, 2)
self.board[0][3] = ChessPiece(PieceType.ADVISOR, True, 0, 3)
self.board[0][4] = ChessPiece(PieceType.GENERAL, True, 0, 4)
self.board[0][5] = ChessPiece(PieceType.ADVISOR, True, 0, 5)
self.board[0][6] = ChessPiece(PieceType.ELEPHANT, True, 0, 6)
self.board[0][7] = ChessPiece(PieceType.HORSE, True, 0, 7)
self.board[0][8] = ChessPiece(PieceType.CHARIOT, True, 0, 8)
self.board[2][1] = ChessPiece(PieceType.CANNON, True, 2, 1)
self.board[2][7] = ChessPiece(PieceType.CANNON, True, 2, 7)
for i in range(0, 9, 2):
self.board[3][i] = ChessPiece(PieceType.SOLDIER, True, 3, i)
# 黑方(上方)
self.board[9][0] = ChessPiece(PieceType.CHARIOT, False, 9, 0)
self.board[9][1] = ChessPiece(PieceType.HORSE, False, 9, 1)
self.board[9][2] = ChessPiece(PieceType.ELEPHANT, False, 9, 2)
self.board[9][3] = ChessPiece(PieceType.ADVISOR, False, 9, 3)
self.board[9][4] = ChessPiece(PieceType.GENERAL, False, 9, 4)
self.board[9][5] = ChessPiece(PieceType.ADVISOR, False, 9, 5)
self.board[9][6] = ChessPiece(PieceType.ELEPHANT, False, 9, 6)
self.board[9][7] = ChessPiece(PieceType.HORSE, False, 9, 7)
self.board[9][8] = ChessPiece(PieceType.CHARIOT, False, 9, 8)
self.board[7][1] = ChessPiece(PieceType.CANNON, False, 7, 1)
self.board[7][7] = ChessPiece(PieceType.CANNON, False, 7, 7)
for i in range(0, 9, 2):
self.board[6][i] = ChessPiece(PieceType.SOLDIER, False, 6, i)
self.current_player = True
self.game_over = False
self.winner = None
self.move_history.clear()
self.selected_piece = None
self.in_check = False
self.update_status()
def reset_game(self):
self.setup_board()
self.draw_board()
# ---------- 坐标转换 ----------
def to_pixel(self, row, col):
x = MARGIN + col * SQUARE_SIZE
y = MARGIN + row * SQUARE_SIZE
return x, y
def to_board(self, px, py):
col = round((px - MARGIN) / SQUARE_SIZE)
row = round((py - MARGIN) / SQUARE_SIZE)
if 0 <= row < BOARD_ROWS and 0 <= col < BOARD_COLS:
x, y = self.to_pixel(row, col)
if abs(px - x) <= SQUARE_SIZE//2 and abs(py - y) <= SQUARE_SIZE//2:
return row, col
return None, None
def get_piece_at(self, row, col):
if 0 <= row < BOARD_ROWS and 0 <= col < BOARD_COLS:
return self.board[row][col]
return None
def is_valid_position(self, row, col):
return 0 <= row < BOARD_ROWS and 0 <= col < BOARD_COLS
# ---------- 走法生成 ----------
def calculate_valid_moves(self, piece):
piece.valid_moves.clear()
row, col = piece.row, piece.col
if piece.type == PieceType.GENERAL:
palace = [(7,3),(7,4),(7,5),(8,3),(8,4),(8,5),(9,3),(9,4),(9,5)] if not piece.is_red else
[(0,3),(0,4),(0,5),(1,3),(1,4),(1,5),(2,3),(2,4),(2,5)]
for dr, dc in [(0,1),(1,0),(0,-1),(-1,0)]:
nr, nc = row+dr, col+dc
if (nr, nc) in palace:
target = self.get_piece_at(nr, nc)
if not target or target.is_red != piece.is_red:
if not self.is_generals_facing(nr, nc, piece.is_red):
piece.valid_moves.append((nr, nc))
elif piece.type == PieceType.ADVISOR:
palace = [(7,3),(7,5),(8,4),(9,3),(9,5)] if not piece.is_red else
[(0,3),(0,5),(1,4),(2,3),(2,5)]
for dr, dc in [(1,1),(1,-1),(-1,1),(-1,-1)]:
nr, nc = row+dr, col+dc
if (nr, nc) in palace:
target = self.get_piece_at(nr, nc)
if not target or target.is_red != piece.is_red:
piece.valid_moves.append((nr, nc))
elif piece.type == PieceType.ELEPHANT:
if (piece.is_red and row < 5) or (not piece.is_red and row >= 5):
for dr, dc in [(2,2),(2,-2),(-2,2),(-2,-2)]:
nr, nc = row+dr, col+dc
if self.is_valid_position(nr, nc):
er, ec = row+dr//2, col+dc//2
if not self.get_piece_at(er, ec):
target = self.get_piece_at(nr, nc)
if not target or target.is_red != piece.is_red:
piece.valid_moves.append((nr, nc))
elif piece.type == PieceType.HORSE:
moves = [(row+2,col+1),(row+2,col-1),(row-2,col+1),(row-2,col-1),
(row+1,col+2),(row+1,col-2),(row-1,col+2),(row-1,col-2)]
legs = [(row+1,col),(row+1,col),(row-1,col),(row-1,col),
(row,col+1),(row,col+1),(row,col-1),(row,col-1)]
for i, (nr, nc) in enumerate(moves):
if self.is_valid_position(nr, nc):
lr, lc = legs[i]
if not self.get_piece_at(lr, lc):
target = self.get_piece_at(nr, nc)
if not target or target.is_red != piece.is_red:
piece.valid_moves.append((nr, nc))
elif piece.type == PieceType.CHARIOT:
for dr, dc in [(0,1),(1,0),(0,-1),(-1,0)]:
for step in range(1, 10):
nr, nc = row+drstep, col+dcstep
if not self.is_valid_position(nr, nc):
break
target = self.get_piece_at(nr, nc)
if not target:
piece.valid_moves.append((nr, nc))
else:
if target.is_red != piece.is_red:
piece.valid_moves.append((nr, nc))
break
elif piece.type == PieceType.CANNON:
for dr, dc in [(0,1),(1,0),(0,-1),(-1,0)]:
for step in range(1, 10):
nr, nc = row+drstep, col+dcstep
if not self.is_valid_position(nr, nc):
break
target = self.get_piece_at(nr, nc)
if not target:
piece.valid_moves.append((nr, nc))
else:
for step2 in range(step+1, 10):
nr2, nc2 = row+drstep2, col+dcstep2
if not self.is_valid_position(nr2, nc2):
break
target2 = self.get_piece_at(nr2, nc2)
if target2:
if target2.is_red != piece.is_red:
piece.valid_moves.append((nr2, nc2))
break
break
elif piece.type == PieceType.SOLDIER:
directions = [(1,0)] if piece.is_red else [(-1,0)]
if (piece.is_red and row >= 5) or (not piece.is_red and row < 5):
directions += [(0,1),(0,-1)]
for dr, dc in directions:
nr, nc = row+dr, col+dc
if self.is_valid_position(nr, nc):
target = self.get_piece_at(nr, nc)
if not target or target.is_red != piece.is_red:
piece.valid_moves.append((nr, nc))
def is_generals_facing(self, new_row, new_col, is_red):
col = new_col
step = 1 if is_red else -1
start = new_row + step
end = 9 if is_red else 0
for r in range(start, end, step):
p = self.get_piece_at(r, col)
if p:
if p.type == PieceType.GENERAL and p.is_red != is_red:
for mid in range(min(new_row, r)+1, max(new_row, r)):
if self.get_piece_at(mid, col):
return False
return True
return False
return False
# ---------- 移动与规则 ----------
def move_piece(self, piece, row, col):
self.move_history.append((piece, (piece.row, piece.col), (row, col), self.board[row][col]))
self.board[piece.row][piece.col] = None
captured = self.board[row][col]
self.board[row][col] = piece
piece.row, piece.col = row, col
piece.valid_moves.clear()
# ========== 新增:吃将直接获胜 ==========
if captured and captured.type == PieceType.GENERAL:
self.game_over = True
self.winner = "红方" if piece.is_red else "黑方"
self.update_status()
self.draw_board()
if messagebox.askyesno("游戏结束", f"{self.winner}获胜!\n是否重新开始?"):
self.reset_game()
else:
self.master.quit()
return
# 原有将死检测
enemy = not piece.is_red
self.in_check = self.is_in_check(enemy)
if self.is_checkmate(enemy):
self.game_over = True
self.winner = "红方" if piece.is_red else "黑方"
self.update_status()
self.draw_board()
if messagebox.askyesno("游戏结束", f"{self.winner}获胜!\n是否重新开始?"):
self.reset_game()
else:
self.master.quit()
return
self.current_player = not self.current_player
self.update_status()
self.draw_board()
if self.ai_enabled and not self.current_player and not self.game_over:
self.master.after(300, self.ai_move)
def is_in_check(self, is_red):
general = None
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
p = self.get_piece_at(i, j)
if p and p.type == PieceType.GENERAL and p.is_red == is_red:
general = (i, j)
break
if general:
break
if not general:
return False
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
attacker = self.get_piece_at(i, j)
if attacker and attacker.is_red != is_red:
self.calculate_valid_moves(attacker)
if general in attacker.valid_moves:
return True
return False
def is_checkmate(self, is_red):
if not self.is_in_check(is_red):
return False
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
p = self.get_piece_at(i, j)
if p and p.is_red == is_red:
self.calculate_valid_moves(p)
if p.valid_moves:
return False
return True
# ---------- 状态更新 ----------
def update_status(self):
if self.game_over:
self.status_var.set(f"{self.winner}获胜!")
self.status_label.config(fg="red")
return
turn = "红方" if self.current_player else "黑方"
if self.in_check:
self.status_var.set(f"{turn}走棋 ⚠️ 将军!")
self.status_label.config(fg="red")
else:
self.status_var.set(f"{turn}走棋")
self.status_label.config(fg="blue")
# ---------- AI ----------
def ai_move(self):
if self.game_over:
return
pieces = []
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
p = self.get_piece_at(i, j)
if p and not p.is_red:
self.calculate_valid_moves(p)
if p.valid_moves:
pieces.append(p)
if not pieces:
return
chosen = random.choice(pieces)
target = random.choice(chosen.valid_moves)
self.move_piece(chosen, target[0], target[1])
# ---------- 悔棋 ----------
def undo_move(self):
if self.game_over:
messagebox.showinfo("提示", "游戏已结束,不能悔棋")
return
if not self.move_history:
messagebox.showinfo("提示", "没有可以悔的棋")
return
if not messagebox.askyesno("确认悔棋", "确定要悔棋一步吗?"):
return
last = self.move_history.pop()
piece, (old_r, old_c), (new_r, new_c), captured = last
self.board[old_r][old_c] = piece
self.board[new_r][new_c] = captured
if captured:
captured.row, captured.col = new_r, new_c
piece.row, piece.col = old_r, old_c
self.current_player = not self.current_player
self.game_over = False
self.winner = None
self.selected_piece = None
self.in_check = self.is_in_check(self.current_player)
self.update_status()
self.draw_board()
# ---------- 绘制 ----------
def draw_board(self):
self.canvas.delete("all")
self.draw_grid()
self.draw_pieces()
self.draw_valid_moves()
if self.in_check and not self.game_over:
self.canvas.create_text(CANVAS_WIDTH//2, MARGIN + BOARD_PIXEL_HEIGHT + 20,
text="⚡ 将军!", font=("宋体", 16, "bold"), fill="red")
def draw_grid(self):
for i in range(BOARD_ROWS):
y = MARGIN + i * SQUARE_SIZE
self.canvas.create_line(MARGIN, y, MARGIN + (BOARD_COLS-1)SQUARE_SIZE, y,
fill=LINE_COLOR, width=2)
for i in range(BOARD_COLS):
x = MARGIN + i * SQUARE_SIZE
if i == 0 or i == BOARD_COLS-1:
self.canvas.create_line(x, MARGIN, x, MARGIN + (BOARD_ROWS-1)SQUARE_SIZE,
fill=LINE_COLOR, width=2)
else:
self.canvas.create_line(x, MARGIN, x, MARGIN + 4SQUARE_SIZE,
fill=LINE_COLOR, width=2)
self.canvas.create_line(x, MARGIN + 5SQUARE_SIZE, x, MARGIN + (BOARD_ROWS-1)SQUARE_SIZE,
fill=LINE_COLOR, width=2)
for r, c in [(0,3), (0,5), (7,3), (7,5)]:
x1 = MARGIN + c * SQUARE_SIZE
y1 = MARGIN + r * SQUARE_SIZE
x2 = MARGIN + (c+2) * SQUARE_SIZE
y2 = MARGIN + (r+2) * SQUARE_SIZE
self.canvas.create_line(x1, y1, x2, y2, fill=LINE_COLOR, width=2)
self.canvas.create_line(x2, y1, x1, y2, fill=LINE_COLOR, width=2)
self.canvas.create_text(MARGIN + (BOARD_COLS-1)SQUARE_SIZE//2,
MARGIN + 4SQUARE_SIZE + 10,
text="楚 河", font=("楷体", 20), fill="#888888")
self.canvas.create_text(MARGIN + (BOARD_COLS-1)SQUARE_SIZE//2,
MARGIN + 5*SQUARE_SIZE - 10,
text="汉 界", font=("楷体", 20), fill="#888888")
def draw_pieces(self):
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
p = self.get_piece_at(i, j)
if p:
x, y = self.to_pixel(i, j)
color = p.get_color()
self.canvas.create_oval(x-PIECE_RADIUS, y-PIECE_RADIUS,
x+PIECE_RADIUS, y+PIECE_RADIUS,
outline=color, width=3, fill="white")
self.canvas.create_text(x, y, text=p.get_display_name(),
font=("宋体", 24, "bold"), fill=color)
if self.selected_piece and self.selected_piece is p:
self.canvas.create_oval(x-PIECE_RADIUS-4, y-PIECE_RADIUS-4,
x+PIECE_RADIUS+4, y+PIECE_RADIUS+4,
outline="yellow", width=4, tags="selected")
def draw_valid_moves(self):
if self.selected_piece:
for (r, c) in self.selected_piece.valid_moves:
x, y = self.to_pixel(r, c)
self.canvas.create_oval(x-10, y-10, x+10, y+10,
fill=HIGHLIGHT_COLOR, outline="")
# ---------- 事件 ----------
def on_canvas_click(self, event):
if self.game_over:
return
row, col = self.to_board(event.x, event.y)
if row is None or col is None:
return
clicked = self.get_piece_at(row, col)
if clicked and clicked.is_red == self.current_player:
if self.selected_piece:
self.selected_piece.valid_moves.clear()
self.selected_piece = clicked
self.calculate_valid_moves(clicked)
self.draw_board()
return
if self.selected_piece and (row, col) in self.selected_piece.valid_moves:
self.move_piece(self.selected_piece, row, col)
self.selected_piece = None
return
if self.selected_piece:
self.selected_piece.valid_moves.clear()
self.selected_piece = None
self.draw_board()
==================== 启动 ====================
if name == "main":
root = tk.Tk()
game = ChineseChessGame(root)
root.mainloop()
完成全部功能后,我将代码整理并添加了注释。使用Git初始化仓库,关联Gitee远端,提交并推送。代码地址为
象棋 · Issue #IJUZ24 · jinhaotong/靳淏童的仓库 - Gitee.com
6 运行结果与演示
https://www.bilibili.com/video/BV1mDj36RE27/?spm_id_from=333.1387.upload.video_card.click&vd_source=ec70ce3f7f7540a31d426d779f51c9ea
程序启动后,窗口左侧展示标准象棋棋盘,红方(下方)和黑方(上方)棋子均位于交叉点,布局与真实棋盘完全一致。状态栏显示“红方走棋”。
点击红方棋子,该棋子被黄色边框高亮,同时所有合法走法位置出现绿色半透明圆点。再次点击某个绿色圆点,棋子即移动到该位置,若目标位置有对方棋子则吃掉,并播放成功移动。黑方回合同理。当某一方被将军时,状态栏变为红色并显示“黑方走棋 ⚠️ 将军!”同时棋盘底部出现红色“⚡ 将军!”文字。
当一方被将死或帅被吃掉时,弹出对话框显示“红方获胜!”并提供“重新开始”和“退出”选项。
点击“悔棋”按钮会弹出确认框,确认后撤销最近一步。
点击“人机对战”切换后,电脑黑方会自动走棋。所有功能运行正常,交互流畅。
7 功能总结
经过全面测试,本系统成功实现了以下八个功能:第一,标准棋盘与棋子初始化。第二,全部棋子的合法走法生成。第三,回合制对弈与交互。第四,将军与将死判定。第五,吃将直接获胜。第六,人机对战。第七,悔棋(带确认)。第八,模式切换与状态显示。程序界面清晰,反馈及时,基本具备了一款完整象棋对弈软件的雏形。
8 实验中遇到的问题及解决方法汇总
问题现象 原因分析 解决措施
棋子画在格子中心 坐标偏移错误 改为交叉点坐标,并调整点击检测
黑方文字显示错误 未按颜色区分 重写get_display_name
吃帅后游戏不结束 未处理捕获将帅 添加captured.type == GENERAL判定
将军无提示 只在控制台输出 增加in_check状态和界面变色提示
AI走棋卡顿 同步执行阻塞UI 使用after延迟调用
悔棋后状态未刷新 未重新计算将军 悔棋后重新计算in_check
9 实验感想、全课总结及课程体会
9.1 实验感想
这次中国象棋的开发是我第一次完全使用tkinter完成图形界面项目。我深刻体会到,面向对象设计在复杂业务逻辑中的巨大优势——将棋子、棋盘、走法生成、规则判定等模块分开,不仅让代码结构清晰,更使得排错变得容易。例如在实现马腿蹩脚时,只需修改马走法的生成部分,不影响其他棋子。另外,对用户交互细节的关注同样重要,比如选中高亮、走法提示、将军警示,这些虽然不直接影响逻辑正确性,却极大提升了用户体验。这次实验让我明白,优秀的程序既要“算得对”,也要“用得顺”。
9.2 全课总结
本课程覆盖了Python从基础语法到高级应用的完整知识体系。我系统学习了变量、运算符、流程控制、序列(列表、元组、字典、集合)、字符串与正则表达式、函数与面向对象(封装、继承、多态)、异常处理、文件操作、数据库(SQLite)、网络爬虫(requests+BeautifulSoup)以及图形界面编程。通过多次实验,我将理论付诸实践,完成了爬取天气数据、数据可视化、简单Web服务以及本次的游戏开发。编程能力从最初只会写简单脚本,到现在能够独立设计并实现一个中等规模的项目,进步显著。更重要的是,我学会了如何查阅文档、调试错误、优化代码,这些都是终身受用的技能。
9.3 课程感想体会
王老师的教学风格令我印象深刻。课堂上,老师总是将抽象的概念用生动的比喻化解(比如“蛋炒饭”与“盖浇饭”形象地解释了继承与组合),让知识点更易理解。实验课上,老师带着我们一步步完成每个实验,从易到难,循序渐进,即使到了下课时间也坚持帮助同学解决bug。老师的热情和耐心感染了我,让我对编程产生了更浓厚的兴趣。此外,老师经常分享校园生活中的趣事,让课堂气氛轻松活泼,这种亦师亦友的关系让我倍感温暖。
9.4 意见与建议
对于课程改进,我有两点建议。第一,部分章节内容密集度较高,尤其是网络爬虫和数据库部分,希望能增加一些课后补充资料或案例,帮助消化。第二,建议在实验课前发布预习任务(例如相关库的简介、示例代码片段),这样课堂上更能跟上节奏,实验效率也会更高。整体而言,这门课程收获满满,感谢老师的辛勤付出。
