news 2026/10/14 16:15:06

Python网络版中国象棋实战:从socket通信到联机对弈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python网络版中国象棋实战:从socket通信到联机对弈

简介:基于Python的网络版中国象棋是一份包含服务器端与双客户端的完整项目源码,面向需要实战练习Python网络编程和桌面游戏开发的学习者,帮助解决在局域网环境下实现双人实时对战的问题。压缩包共31个文件,大小6.67MB,内含9个py源文件、14个pyc编译文件及各类型素材,其中py源码覆盖服务器通信、客户端交互、棋盘与棋子逻辑、红黑双方设置等模块,素材则包括jpg/png/bmp图片、ttf字库和txt配置说明。项目采用一个server加两个main的运行模式,通过修改config中的IP和端口即可实现跨电脑对弈,适合学习套接字编程、并发消息处理、pygame或tkinter界面开发以及中国象棋规则的代码化实现。已有2151人学习下载,无论用于课程设计、毕业课题还是个人技术进阶,都能从中获得架构设计和排错思路,是理论与实践结合紧密的Python实战资料。

1. 从单人摆棋到双人联网:Python网络版中国象棋到底做了什么

之前接手过一个局域网联机象棋的小项目,需求很简单——两个人能在一台电脑上玩很久的本地象棋,搬到网上,让人远程对弈。那时候我第一反应是“走子规则不是早就有人写烂了?”结果真拆下来才发现,最大的坑根本不在棋盘和车马炮,而在于两个客户端之间的状态同步:A走的棋怎么稳定地到B那边、谁先谁后、断线了怎么办、B的界面卡住时A要不要等。这套基于Python网络版中国象棋的资源,正是把“单机走子”和“网络传输”这两件事完整打通的一套参考实现,适合拿去做课程设计、毕业设计、或者自己从零搭一个联机棋类游戏的脚手架。它解决的问题非常具体:各端独立、协议清晰、能跑得通、踩过的坑都在代码里标注好。

2. 拆解架构:通信协议、棋盘表示与服务端/客户端的分工

2.1 选型理由:C/S结构还是P2P结构

做网络象棋,第一个要定的不是界面,而是网络拓扑。常见做法是服务器中转(C/S)或者双方直连(P2P)。直连看着省一台机器,但在NAT后面基本没法用——很多家庭和办公网络不允许主动入站连接,双方都要公网IP才能连得上。常见做法是选一台云服务器做中转,客户端只负责主动连接,服务端把一方的消息转给另一方。这样穿透问题被绕开了,整个系统只需要服务端有公网地址就行。

这份资源里采用的也是C/S中转方案。服务端本身不关心谁赢谁输,也不保存棋局状态,它只干一件事情:维护在线客户端的列表,把某条消息从发送方转发给对局方。这么做的好处是可以把“对局管理”“登录校验”“观战广播”这类扩展功能挂到服务端上,客户端逻辑简单,也容易做断线重连。

数据流向是这样的:玩家A在界面上走了一步棋,客户端A把走棋消息打包发给服务端,服务端根据房间号查找到玩家B的连接,原样转发给客户端B,B收到消息后直接按里面携带的坐标更新棋盘。整个过程不要求两边客户端保持完全一致的实时帧同步,因为回合制游戏天然是“事件同步”而不是“状态同步”:你传的是“谁从哪走到哪”,不传“当时的完整局面”。

2.2 棋盘表示:二维数组还是坐标字符串

棋盘数据结构是整个项目的地基。中国象棋是10行9列,最自然的表示就是二维列表。资源里用board[10][9]存局面,0表示空,正数表示红方棋子,负数表示黑方棋子,对应的字典把数字映射成“车”“马”“炮”等字符。这个表示法在老项目中很常见,打印调试也直观。

但走棋消息不能直接把整个board塞进去,那样消息体积大、而且没法做校验。资源里的做法是用坐标字符串:把行号列号拼成目标字符串,比如红车从(0,0)走到(0,1),消息体就携带from=“00”,to=“01”。这样一条走棋消息固定只有几十字节,放大到局域网或者弱网环境都非常轻松。

还有一套替代方案是用一维索引0到89,配合行列转换函数。我一般不建议在协议层用一维索引,因为调试的时候从抓包工具里看一条“move 12 34”还得自己心算行号列号,太容易翻车。坐标字符串的可读性对排障非常有帮助,尤其是联机对弈出问题时,谁走的哪一步一目了然。

2.3 协议字段:JSON消息体的约定与扩展性

协议层成败决定整个系统能不能扩展。资源里的消息统一采用JSON文本格式,最外层固定包含type字段,后续字段随type不同而变化。这样设计的好处是调试方便、跨语言兼容、加字段不需要改底层。下面是核心消息类型:

type方向关键字段说明
join客户端→服务端name, room加入指定房间
match服务端→客户端rival_name, color匹配成功,告知执红/执黑
move客户端→服务端from, to, role本次走棋的起止坐标
move_ok服务端→客户端from, to, fen权威走棋结果,附带局面哈希
move_fail服务端→客户端reason拒绝走棋并返回原因
retract客户端→服务端step_id请求悔棋
end服务端→客户端winner宣布对局结束

move里带一个role字段,用来标记当前是谁的回合,服务端可以据此拒绝越权走子。move_ok里带fen字段是可选的,用来做局面哈希校验,防止两端棋盘状态悄悄跑偏。如果只是课程设计,这个字段可以省掉;想做严谨一点的平台,建议保留——它相当于两端对局的校验和,每一步都对一下,能提前发现同步问题。

2.4 走子规则模块:合法性与被“吃掉”的棋子

规则判断是整个系统里最容易被复制错的部分。资源里把合法性校验抽成了一个独立函数,输入是棋盘、起点、终点、轮到哪一方,输出是布尔值和错误原因。它内部要依次做五件事:

第一,检查起点是不是当前方的棋子,不能拿对方的子走棋。第二,检查终点是不是同色棋子,中国象棋不允许吃自己人。第三,按棋子类型走不同的移动逻辑,车走直线、炮隔子吃、马走日蹩马腿、象走田塞象眼、士只能在九宫斜线、将不能出九宫。第四,检查落点是否导致老将被直接照面——也就是走后形成将帅对脸的局面要判非法。第五,兵过河前只能前进,过河后才能横移。

这里我提一个很多人踩过的坑:马的蹩马腿判定。网上很多版本只判断“目标位置在马的日字对角线上”,不检查马腿所在位置的中间点是不是有棋子,导致马能“飞过去”。资源里的实现把蹩马腿条件明确写成了中间点的坐标棋盘值是否为0,测试用例里也专门覆盖了“马被别在中间时不能跳”的场景。

3. 基于socket实现联机模块:服务端骨架与帧编解码实现

3.1 服务端骨架:监听、房间分发与连接列表管理

服务端是整个联机系统的心脏。下面这份代码是服务端最核心的连接管理骨架。它要做的事情有三件:同时监听多个客户端连接、把新连接分配到指定房间、把消息从一个连接转发到同房间的另一个连接。这里直接用多线程模型,每个客户端连接跑一个线程,用一把全局锁保护共享的连接字典。

import socket import threading import json # 管理连接: room_id -> {client_id: socket} rooms = {} rooms_lock = threading.Lock() def register_client(sock, room_id, client_id): with rooms_lock: if room_id not in rooms: rooms[room_id] = {} rooms[room_id][client_id] = sock def send_to_room(room_id, exclude_cid, payload: bytes): with rooms_lock: if room_id not in rooms: return for cid, conn in rooms[room_id].items(): if cid == exclude_cid: continue try: conn.sendall(payload) except OSError: # 连接已断开,交给清理逻辑处理 pass def handle_client(conn, addr): # 实际项目中在这个函数里完成三件事: # 1. 读取并解析消息 # 2. 先查房间和身份,再做转发 # 3. 连接断开时从 rooms 中移除 pass def start_server(host, port): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((host, port)) server.listen(5) while True: conn, addr = server.accept() threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()

逻辑说明:rooms是一个二级字典,第一层key是房间号,第二层key是客户端唯一标识。send_to_room遍历某个房间里的所有连接,把payload原样转发给每个客户端。handle_client函数在完整资源里承担消息解析和路由,上面只保留了结构骨架。start_server中socket.SO_REUSEADDR参数很关键,不加它的话,服务端重启时经常报“端口被占用”,导致开发期反复重启很痛苦。

参数说明:host建议设置成0.0.0.0而不是127.0.0.1,否则远端客户端根本连不进来。port选一个未被占用的端口,课程设计环境建议用10000以上的高位端口,避免和系统服务冲突。listen(5)表示内核排队等待accept的连接队列长度,这个数字不用调太大,对象棋这种并发极低的场景够用。

3.2 帧编解码:提取消息边界的标准姿势

TCP是流协议,没有“一条消息”的概念。如果收发两端各自调sendall和recv,很可能出现一次send的数据被拆成两次recv,或者两次send的数据被拼到一次recv里。这就是开发联机程序时最容易翻车的地方。

业界常用解决办法是自定义帧格式:每条报文前面用固定4个字节表示后面JSON数据的字节长度。接收方先读取定长4字节,解析出长度N,再循环读取直到攒够N字节,才算拿到一条完整消息。下面给出封包和拆包的参考实现:

import struct def build_frame(payload: bytes) -> bytes: # 4字节大端无符号整数 = 荷载长度 length = len(payload) header = struct.pack("!I", length) return header + payload def recv_exact(conn, n: int) -> bytes: data = b"" while len(data) < n: chunk = conn.recv(n - len(data)) if not chunk: raise ConnectionError("connection closed while reading") data += chunk return data def recv_frame(conn): header = recv_exact(conn, 4) length = struct.unpack("!I", header)[0] if length > 1024 * 1024: raise ValueError("frame too large, possible protocol error") payload = recv_exact(conn, length) return json.loads(payload.decode("utf-8"))

逻辑说明:build_frame把待发送的JSON字节序列加了一个4字节长度前缀;recv_exact是一个循环读满指定字节数的工具函数,它解决了recv单次返回不足字节数的问题。recv_frame先读4字节头,再按照头里声明的长度把消息体完整读出来,最后用json.loads还原成字典。中间做了最大长度限制,避免恶意或异常数据导致内存被撑爆。

参数说明:struct.pack的格式串“!I”表示用网络字节序(大端)打包一个无符号整数。网络字节序与机器无关,两端都是同样的格式串才不至于读到错乱的长度值。字节序不一致是另一个隐蔽坑——有些开发者图省事直接发host字节序的整数,在x86小端机器上自测没问题,一换到ARM或交叉编译环境就乱套。

3.3 客户端收发线程:把网络事件降到主线程之外

客户端是带界面的,不能在UI线程里直接调recv,否则界面会卡死。常见的做法是单独拉一个接收线程,每次收到一条完整消息后,通过队列扔给界面线程处理。这里给出一段客户端收发核心代码,展示怎样用queue安全传递消息:

import threading import queue import socket class GameClient: def __init__(self, host, port): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) self.msg_queue = queue.Queue() self.running = True threading.Thread(target=self.recv_loop, daemon=True).start() def recv_loop(self): while self.running: try: msg = recv_frame(self.sock) except (ConnectionError, ValueError): self.msg_queue.put({"type": "system", "event": "disconnect"}) break self.msg_queue.put(msg) def send_move(self, src, dst): frame = build_frame(json.dumps({ "type": "move", "from": src, "to": dst, "role": self.my_role }).encode()) self.sock.sendall(frame)

逻辑说明:recv_loop是阻塞循环,它持续调用recv_frame,每解析出一条消息就放入标准库queue.Queue。queue是线程安全的,可以放心把msg_queue传递到任何线程去读取。界面线程开一个QTimer定时从msg_queue里取消息,一旦取到就更新棋盘,这样网络线程和UI线程不会因为共享变量而打架。

参数说明:send_move里的my_role是客户端加入房间后服务端回传的执红/执黑标识。客户端所有走棋请求都带这个字段,服务端可以据此判断当前请求是否为合法方发起——非回合方的走子请求应当被拒绝。这里有一个设计选择:合法性第一道关在客户端本地做,第二道关在服务端做。本地校验反馈快、能提示具体错误;服务端校验防止有人修改客户端后绕过限制。

4. 基于PyQt5的走棋界面:绘制棋盘、点击判定与悔棋实现

4.1 棋盘绘制:用QPainter一次性画完棋盘和棋子

界面这块用的PyQt5。绘制逻辑全部交给QPainter,棋盘背景、网格线、九宫斜线、河界文字、棋子全部在一个paintEvent里画完。它的优势是不依赖额外图片资源,切到哪台机器都能跑,不用担心素材路径问题。核心绘制逻辑如下:

from PyQt5.QtWidgets import QWidget from PyQt5.QtGui import QPainter, QBrush, QColor, QPen from PyQt5.QtCore import Qt, QPoint class ChessBoard(QWidget): CELL_SIZE = 64 ORIGIN_X = 40 ORIGIN_Y = 40 def paintEvent(self, event): p = QPainter(self) p.setRenderHint(QPainter.Antialiasing) # 画横线,10行共9条横线 for row in range(10): start = QPoint(self.ORIGIN_X, self.ORIGIN_Y + row * self.CELL_SIZE) end = QPoint(self.ORIGIN_X + 8 * self.CELL_SIZE, self.ORIGIN_Y + row * self.CELL_SIZE) p.drawLine(start, end) # 画竖线,9列,中间一行是河界要断开 for col in range(9): x = self.ORIGIN_X + col * self.CELL_SIZE if col == 0 or col == 8: p.drawLine(x, self.ORIGIN_Y, x, self.ORIGIN_Y + 9 * self.CELL_SIZE) else: p.drawLine(x, self.ORIGIN_Y, x, self.ORIGIN_Y + 4 * self.CELL_SIZE) p.drawLine(x, self.ORIGIN_Y + 5 * self.CELL_SIZE, x, self.ORIGIN_Y + 9 * self.CELL_SIZE) # 画斜线(九宫) p.drawLine( self.ORIGIN_X + 3 * self.CELL_SIZE, self.ORIGIN_Y, self.ORIGIN_X + 5 * self.CELL_SIZE, self.ORIGIN_Y + 2 * self.CELL_SIZE ) # 画棋子、文字等 self.drawPieces(p)

逻辑说明:paintEvent里用CELL_SIZE做基准尺寸,所有坐标都由行列换算得出,拖动窗口缩放时不会出现图案错乱。横线10行9条、中间第5行断开是为了画河界,竖线在断开的区域分两段画。九宫斜线只需要画两条主对角线中对应的那两条线段。

参数说明:CELL_SIZE=64表示每个格子边长64像素,这是鼠标点击转换坐标的基础。ORIGIN_X和ORIGIN_Y是棋局左上角距离窗口边缘的偏移量,保证棋盘周边留白,让用户操作时不会误点到边缘。p.setRenderHint(QPainter.Antialiasing)打开抗锯齿,棋子上字边缘才不会有毛刺。

4.2 点击判定与走棋流程:鼠标事件到协议消息的转换

鼠标点击棋盘首先要把屏幕坐标换算成行列坐标,换算公式行列值等于坐标差值除以格子大小。这一步看似简单,坑在取下整还是取上整。我通常会先判定点击落点离哪个交叉点最近,再决定按下哪一格,避免用户稍偏一点就选错棋子。

def find_intersection(self, pos): # 对每个交叉点计算距鼠标的最短距离 best = None best_dist = 1e9 for row in range(10): for col in range(9): px = self.ORIGIN_X + col * self.CELL_SIZE py = self.ORIGIN_Y + row * self.CELL_SIZE dist = (pos.x() - px) ** 2 + (pos.y() - py) ** 2 if dist < best_dist: best_dist = dist best = (row, col) if best_dist > (self.CELL_SIZE // 2) ** 2: return None # 离得太远,判定为空点 return best def mousePressEvent(self, event): pos = event.pos() idx = self.find_intersection(pos) if idx is None: return row, col = idx if self.selected is None: # 未选中任何棋子,尝试选择已方棋子 piece = self.board[row][col] if piece and (piece[0] == self.my_color): self.selected = (row, col) self.update() else: # 已有选中,尝试走棋 self.try_move(self.selected, (row, col))

逻辑说明:find_intersection用双重循环计算鼠标位置与棋盘上全部90个交叉点的平方距离,取最近的那个。这个算法简单、不易错,90个点遍历一次毫秒级完成,完全没有性能压力。选棋规则做了限定:只有落在自己方棋子上的点击才会被选中为起点,这个判断放在鼠标事件层,可以大幅减少后面无意义的协议通信。

参数说明:best_dist大于半个格子距离的平方说明点落在偏远处,直接返回None,这一条规则很实用——它避免了玩家在棋盘外区域乱点时引发空指针。try_move内部做了四步操作:本地走子合法性检测、发送move消息到服务端、等待服务端回move_ok后更新棋盘、最后清空selected状态。

4.3 悔棋的实现:撤回协议与一次只能悔一步的约束

悔棋是最容易破坏同步的功能,因为悔棋本质上是对“权威局面”的修正。最常见方案是服务端维护一个step_history列表,每步棋的结果都存进去。收到悔棋请求时,服务端先把上一步棋从history里弹出,计算出悔棋后的局面快照fen,再把这个快照连同撤销的坐标一起广播给双方。两份数据缺一不可:坐标告诉界面“把棋子放回去”,快照则让双方本地棋盘可以做一次整体校对。

def handle_retract(self, cid): room = self.get_room(cid) if not room.history: return self.send_error(cid, "no_step_to_retract") step = room.history.pop() # 用的是栈结构,后手悔棋只能悔最后一步 snapshot = build_snapshot(room.board) payload = build_frame(json.dumps({ "type": "retract", "step": step, "fen": snapshot }).encode()) self.send_to_room(room.id, exclude=None, payload=payload)

逻辑说明:room.history保存的不是简单坐标对,而是包含棋子和吃子信息的完整记录。弹出后需要恢复该位置的棋子,再把被吃的棋子放回目标点。snapshot是合法走子后的棋盘状态序列化结果,它让双方客户端不用逐条回放历史也能直接校准到最新局面。这种设计把“悔棋”从单端操作变成服务端权威操作,最能避免出现两边棋盘不一致。

参数说明:悔棋时只允许撤回最后一步棋,这是很多桌游平台的潜规则。如果你想做“可以连悔两步”或者“对方同意才能悔”的变体,需要额外加房间配置,把retract消息改成“请求-确认”两步交互,而不是当前这种一步广播。从工作量来说,资源里这份一步悔棋的实现已经能覆盖绝大多数交付场景。

5. 联机对战实战排坑:五条必须牢记的踩坑记录

5.1 走了几步就解析失败:粘包与半包问题

现象:客户端和服务端在单步走棋时一切正常,连续走几步后就开始偶发“JSON解析失败”,甚至棋盘上棋子位置错乱。让人头疼的是,出错的频率并不固定,有时候走十步没问题,有时候第二步就崩。

原因:TCP是字节流协议,发送端多次调用sendall后,数据可能被TCP层合并成一个大数据段发出,也可能被拆成多个小段。接收端如果按“一次recv就是一条消息”来读,就必然会出现一次读到了两条消息,或者半条消息的情况。这个现象行业内叫粘包/半包。典型错误代码是没有按长度读取,直接data = conn.recv(4096),然后json.loads。

解决:使用前面讲过的帧格式,先收4字节长度头,再按长度收消息体。核心点有两个:接收循环必须按recv_exact的方式把长度头读满4字节、消息体读满N字节;发送端每个sendall必须只发一条完整帧。只要两端都严格套用这个格式,粘包半包就不可能出现。建议开发期在服务端写一个测试脚本,随机连续发1000条消息,再用抓包工具校验字节序。

5.2 UI线程被recv阻塞后整个窗口假死

现象:客户端启动后,一进入游戏就出现“界面上所有按钮都点了没反应、棋子拖不动”,过几秒后突然恢复,还会跳出大量消息。单机版正常、联机版才出现,这是排查的第一个线索。

原因:在界面主线程里调了socket.recv(),而这个函数在没有数据时会一直阻塞直到收到数据或超时。UI循环得不到执行机会,整个窗口就进入假死状态。这在联机程序新手手里非常常见,问题根源不是socket本身,而是把网络IO放错了线程。

解决:能收网络消息的操作统一放进独立线程或QThread,收到的消息放进queue.Queue。界面线程用QTimer每隔50毫秒检测一次队列,如果有消息就批量取出处理。这样即便socket那边一秒收到200条消息,界面线程仍然按自己的节奏消费,不会出现卡顿。从那以后我做联机界面,第一件事永远是问自己一句话:“这个recv是放在哪个线程里跑的?”

5.3 选择棋子的坐标串错位导致马真的“飞”了

现象:红方马在棋盘上的移动不受蹩马腿约束,黑方马却一切正常;或者同一只棋子,从A点移动到B点在单机测试时合法,联机时服务端返回move_fail。

原因:走子规则模块里的蹩马腿判断被注释掉或者判断逻辑写错,常见错误是只检查目标点横纵坐标差为1和2,而没有检查马腿中间点是否有棋子。联机时服务端和客户端各自维护一份规则代码,如果两份代码不一致,就会出现“客户端本地校验通过,服务端校验拒绝”的诡异局面。另一个制造错位的来源是坐标字符串传输中行列序不一致:一方按“row+col”发送,另一方按“col+row”解析。

解决:把走子校验函数统一放在一个模块里,客户端服务端共用同一份代码。在资源目录下,是实现一个不依赖任何界面代码的rule.py,里面就一个is_valid_move(board, my_color, src, dst)函数,两边都直接import这个文件。如果项目要求服务端用另外一套语言重写,那也要用同一套测试用例先做验证,绝不能各写各的。坐标序这件事上,协议字段名直接命名为from_row、from_col,比再用拼接字符串要安全得多。

5.4 客户端断线重连后服务端还把它当在线

现象:玩家A关闭客户端后重新打开、再次加入同一个房间,A已经能正常收发消息,但玩家B这边一下卡死、或者B发消息时抛“socket send failed”。检查服务端还发现连接列表里有两个完全一样的旧连接。

原因:TCP连接断开后,如果服务端不及时读取数据,可能根本不知道连接已经死亡。特别是客户端直接kill进程,没有机会发送正常关闭帧,服务端那个socket仍然处于半开状态,发数据触发RST之前你根本感知不到异常。旧连接没被清理,导致后来的广播被发送给了已失效的socket。

解决:服务端除了注册连接,还必须做两件事:一是每次send后检查写入是否抛异常,一旦异常就立刻执行清理逻辑;二是给每对客户端连接加心跳。心跳周期通常设10秒固定值,客户端每10秒上报一次存活消息,服务端每30秒检查一次所有连接的最后活跃时间,超过阈值直接断开并回收连接。断线重连的客户端要生成新全局ID注册进房间,而不是复用旧ID。清理函数必须带锁,避免和广播同时操作rooms字典导致线程错乱。

5.5 悔棋造成两端棋盘不一致:忘了同步吃掉的子

现象:双方对战一段时间后,执行一次悔棋,B端棋盘被撤回的格子正确,但B端被吃掉的棋子没有回到B的棋盘上。继续对弈几步之后,发现双方局面开始明显不同步。

原因:后端存储的历史记录只有起点和终点坐标。悔棋时把棋盘起点恢复原棋、终点恢复原棋子,但如果这一手吃掉了棋子,历史记录里没有被吃掉棋子的信息,无法在悔棋时把它挂回终点对应的格子,导致撤回后的局面和撤棋前不一致。

解决:在step记录中除了from和to,必须增加captured字段,记录被吃掉的棋子到底是什么。悔棋时从captured里读到棋子身份和颜色,把它写回终点格。同时,每一次悔棋后的完整局面快照要广播给两端,让任意一端发现自己棋盘和服务端权威快照不一致时,可以直接灌注整盘状态而不是继续戴着错位跑。我实测下来,悔棋功能如果不在协议层做完整,单靠客户端本地拍脑袋还原是必然翻车的。

6. 进阶实践:用状态机驱动断线重连与发布打包建议

到了这个阶段,联机对弈的基本功能已经能跑通了。再往上走一截,就是把“能玩”变成“好用”。我重点说说断线重连的状态机设计,这是所有联机应用里最需要用“状态”来约束逻辑的地方。

客户端对局状态可以抽象成四个:Lobby(大厅)、Playing(对局中)、Reconnecting(重连中)、Ended(已结束)。切换规则是固定的:Lobby收到match消息后进入Playing;Playing中途网络异常进入Reconnecting;Reconnecting状态下尝试连接服务器并重新加入房间,成功则回到Playing,超过5次失败则进入Ended并告知对局已经无法继续。

class GameState: def __init__(self): self.state = "Lobby" self.retry_count = 0 def on_event(self, event, payload=None): if self.state == "Lobby" and event == "matched": self.state = "Playing" self.retry_count = 0 elif self.state == "Playing" and event == "disconnected": self.state = "Reconnecting" self.retry_count = 0 elif self.state == "Reconnecting" and event == "joined": self.state = "Playing" self.retry_count = 0 elif self.state == "Reconnecting" and event == "retry_fail": self.retry_count += 1 if self.retry_count >= 5: self.state = "Ended"

逻辑说明:状态机把“网络断开”和“重新连上”这件事从一个简单的bool值变成了可追踪的状态。在Reconnecting状态下,客户端保留本地的当前棋盘和history,但停止接收新消息,只发起join请求。服务端要额外提供“继续对局”的协议支持:当客户端重新join到已经存在的房间时,服务端把当前权威局面快照和history长度一起回传,客户端以此校准本地棋盘。

参数说明:重连尝试次数限制为5,每次间隔用指数退避:第一次等1秒、第二次2秒、第三次4秒,最多加到8秒封顶。指数退避比固定间隔更聪明——前几次快速重试能解决临时闪断,后面慢下来避免服务器在高负载下被打爆。这类参数在实际项目中最好做成配置文件,而不是写死在代码里,课程设计交付时也建议附一份config.ini,把端口、超时、重连上限写清楚。

打包成可分发程序同样有讲究。客户端用PyInstaller打包成单目录或单文件,关键在collect数据文件:如果窗口图标、棋盘背景图是从外部路径加载的,打包后运行会出现找不到文件的错误。常见做法是把所有资源用qrc机制嵌进二进制,这样打包出来只有一个exe或app,复制到别的电脑直接能跑。服务端不带界面,直接写一份requirements.txt,部署时用pip按清单装依赖即可。每条报文在后面附真实抓包样例,这份代码闭着眼都能跑通。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 16:14:49

海洋数据机器学习实战:从传统方法到深度学习与物理约束

1. 海洋数据到底长什么样&#xff1a;先搞清楚你面对的是什么很多做机器学习的朋友一听到“海洋科学”四个字&#xff0c;第一反应是“不就是把陆地上的模型搬到海里去吗”。我一开始也这么想&#xff0c;直到真正接触了几个海洋观测项目的数据之后才发现&#xff0c;这个想法有…

作者头像 李华
网站建设 2026/10/14 16:13:43

安卓HTTPS证书信任链断裂原因与解决方案

1. 问题本质&#xff1a;不是“证书错误”&#xff0c;而是信任链断裂的系统级拦截你打开安卓手机浏览器&#xff0c;输入一个明明在电脑上能正常访问的 HTTPS 网站&#xff0c;页面却突然弹出“Your connection is not private”或直接显示白屏&#xff0c;控制台&#xff08;…

作者头像 李华
网站建设 2026/10/14 16:13:29

ArcGIS基础100例实操:坐标系、投影与空间分析的避坑指南

简介&#xff1a;面向GIS初学者与需要系统提升ArcGIS操作能力的用户&#xff0c;《ArcGIS基础100例&#xff08;操作手册操作数据&#xff09;》由汤国安教授编写&#xff0c;以100个真实操作案例为主线&#xff0c;配套讲授界面认识、数据导入导出、图层管理、地图布局设计等基…

作者头像 李华
网站建设 2026/10/14 16:13:07

LightMem离线更新机制详解:记忆如何自动合并、去重与精炼?

【免费下载链接】LightMem [ICLR 2026] LightMem: Lightweight and Efficient Memory-Augmented Generation 项目地址&#xff1a; https://gitcode.com/gh_mirrors/li/LightMem 点击查看 免费下载 LightMem 是一个轻量高效的大模型记忆增强生成&#xff08;Memory-Augmented …

作者头像 李华