ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

古诗词填字游戏功能升级:输入校验、提示计分与工程化重构

古诗词填字游戏功能升级:输入校验、提示计分与工程化重构 做了前面的基础版本和核心算法之后我原本以为古诗词填字游戏已经能跑起来了但在实际给朋友试玩的过程中问题马上就暴露了命令行虽然能玩但提示很弱输错字没有任何容错词库一多布局就乱更别说计分和关卡这些基本功了。所以这第三篇我决定把游戏从“能玩的Demo”升级成一个“真正能拿得出手的小工具”——补齐交互细节、完善输入校验、加入提示和计分机制顺便把代码结构重新整理一遍。如果你前两篇已经跟着写完了这篇可以直接接着改如果你是从这一篇开始看我也尽量把关键的数据结构和算法逻辑讲清楚让你能衔接得上。1. 第三篇的改进思路与整体设计1.1 前两篇完成了什么这一篇要解决什么前两篇里我主要完成了两块东西。第一块是词库的构建——从上百首古诗词里提取诗句按字数、朝代、作者做了分类存成结构化的数据文件第二块是核心的摆词引擎——给定若干诗句通过扫描棋盘空位和碰撞检测把诗句往横竖两个方向上排布生成一个交叉填字棋盘。说实话这两块完成后游戏已经能玩了程序随机选几首诗在终端上打印出带空的棋盘玩家输入坐标和字程序判断对不对。但问题也很多。最明显的几个一是提示系统完全缺失玩家卡住就没办法二是输入容错太差英文标点、全角空格、多音字都会导致误判三是计分和关卡都是假的没有连续性四是代码全挤在一个文件里加功能越来越费劲。所以这一篇我把重心放在功能完善和工程化重构上把界面逻辑和游戏逻辑分开把校验机制做成独立模块再加入提示、计分、关卡、答案复盘这几个面向玩家的核心功能。1.2 技术方案选型为什么继续用Python标准库很多读者可能会问既然要做到交互和界面为什么不用Pygame甚至为什么不用Web前端我的选择是基于这几个考量零依赖部署用Tkinter或纯纯的命令行交互玩家只需要装一个Python环境就能跑不需要pip install一堆东西。专注核心逻辑这个项目的核心价值是“古诗词数据 摆词算法 交互逻辑”而不是画多么炫酷的界面。用标准库可以让我把精力集中在游戏逻辑本身。便于教学和修改作为系列文章很多读者是编程初学者标准库的方案更友好拆开看每一块都看得懂。这一篇我采用“命令行交互 图形界面双模式”的思路主体逻辑共用一套展示层分别实现。命令行模式方便调试和快速试玩Tkinter图形模式适合最终分享给朋友。这样既满足了“能玩”又不过度设计。1.3 整体模块规划从单文件到分层结构开始写代码之前我先把项目的目录结构梳理了一遍。前两篇的代码是单文件fill_game.py大概六百多行功能再往后加就会变得很难维护。这一篇我把它拆分成四个模块poetry_fill/ ├── data_loader.py # 词库加载、清洗、格式化 ├── board_engine.py # 棋盘生成、碰撞检测、摆词算法 ├── game_core.py # 游戏状态管理、输入校验、提示/计分 ├── ui_cli.py # 命令行交互界面 └── ui_gui.py # Tkinter 图形界面可选拆完之后每个文件的功能单一、接口清晰调试和扩展都方便很多。下面我会按照这个结构逐步讲解每一部分的实现。2. 词库预处理与数据结构升级2.1 词库清洗去掉“好看但没法用”的诗句前两篇的词库是从公开的诗词集里爬取加人工整理的数据虽然丰富但直接拿来用会有很多坑。这一篇我做了几轮清洗规则如下剔除过长诗句超过12个字的诗句比如杂言诗、乐府诗在棋盘上排布时不好控制如果一首诗里有长句整个棋盘的宽高会被迫拉大玩家滚动起来很累。我统一筛出字数在4到10之间的诗句。剔除生僻字和繁体字这不是说生僻字不好而是在填字游戏场景下玩家大概率认不出、打不出这些字。我维护了一个“常用汉字表”诗句里只要出现一个表外的字整句就过滤掉。繁体字我做了映射到简体实在映射不了的再过滤。去重和版本统一同一句诗可能会有多个版本比如“床前明月光”也有人写成“牀前明月光”我按简体优先的原则做了归一化同时把重复的诗句去重。下面是我在data_loader.py里实现的清洗逻辑的核心代码# data_loader.py import json import re # 常用汉字表采用通用规范汉字表一级字表实际项目中可以加载外部文件 COMMON_CHARS set(床前明月光疑是地上霜举头望明月低头思故乡...) def clean_poem_line(line: str, min_len: int 4, max_len: int 10) - str | None: 清洗单句诗句返回规范化后的诗句不合格则返回 None # 去除所有空格、全角空格、制表符 line re.sub(r[\s\u3000\t], , line) # 去除中文标点和英文标点 line re.sub(r[。、《》.,!?;:()], , line) if not min_len len(line) max_len: return None # 检查是否包含表外字含繁体映射后的字 for ch in line: if ch not in COMMON_CHARS: return None return line这里有一个我踩过的坑一开始我没有过滤标点结果“举头望明月”被当成8个字导致后续在棋盘上预留了8格其中一格是“”玩家填的时候完全懵掉。清洗之后我重新导出了poems_clean.json每个条目包含poem_id、dynasty、author、content拆成单句的列表。2.2 数据结构升级把“诗句”变成“词语单元”前两篇的词库结构偏“整诗”导向每次取用都是一整首。这次我做了个重要调整在载入时就把每句诗拆成可独立使用的“词语单元”每个单元记录它所属的诗句、在句中的位置、字数、以及它的“关键属性”比如是否该句的题目词。这么设计的原因很直接填字游戏玩的是“字与字交叉”而不是“整句输入”。玩家的基本操作是在棋盘某个坐标填一个汉字然后系统判断这个字在该坐标上是不是正确。如果把整句诗当作一个整体判断就会很笨重拆成字以后每次填字只需要查棋盘上该位置的“正确答案”即可。# data_loader.py 中定义的数据结构 dataclass class WordUnit: word: str # 单个汉字如 明 poem_id: int # 所属诗篇 ID line_index: int # 所属诗句在诗中的序号 char_index: int # 该字在句中的位置0 起 line_text: str # 完整诗句方便提示用 dataclass class PoemEntry: poem_id: int title: str dynasty: str author: str lines: list[str] # 清洗后的诗句列表我额外保留line_text字段是因为后面做提示功能时要给玩家看“这句话是‘’里的‘’”只保存单个字是不够的。这个字段在前两篇里没有属于这次重构时补上的。2.3 配置化的关卡与难度参数词库升级之后我又加了一层难度配置。以前是随机选诗难度不可控新手玩家可能碰到四五首七言律诗棋盘巨大直接劝退。这次我在game_core.py里定义了三档难度模板难度诗句数量每句字数范围允许的摆词方向提示次数入门3~4首4~5字横或竖二选一6次进阶4~5首5~7字横竖交叉4次挑战6~8首5~10字横竖交叉2次选择难度后data_loader只从对应区间里取诗句这样既能保证初学者不至于被吓跑又能让高阶玩家有挑战空间。在实际实现中我还在配置里加入了allow_multi_touch参数——控制同一首诗的多个诗句在棋盘上是否必须互相串联避免出现三四个孤立的词块各自为政。3. 棋盘引擎重构从“能摆”到“摆得好”3.1 碰撞检测的边界问题前两篇里我已经实现了基础的碰撞检测新诗向一个方向摆放时检查占用格子和已有格子是否冲突。但仔细复盘后发现这个逻辑还有几个明显的边界问题没处理相邻格错误如果新诗摆在一个已有词的旁边虽然字不重叠但两个词中间紧挨着从视觉上玩家会误以为它们有交叉关系其实没有。这是“伪邻接”问题。穿插但不交叉有些情况下新词会和已有词形成“T”字形但交叉点上的字并不相同。这是真正的错误必须拦截。对角线贴靠新词的尾部斜对角贴着已有词的拐角虽然不影响正确性但在视觉上很丑而且后续再摆词可能产生畸形的棋盘。我在这一版里加了一个is_valid_placement函数除了检查字格重叠还会做一步“邻接格扫描”# board_engine.py def _neighbors_ok(self, row: int, col: int, ch: str) - bool: 检查 (row, col) 位置放置 ch 是否会导致“贴靠但不相交”的情况。 规则如果某个相邻格已被占用那么该相邻格所在词的延伸方向必须穿过本格。 directions [(0, 1), (0, -1), (1, 0), (-1, 0)] for dr, dc in directions: nr, nc row dr, col dc if not (0 nr self.height and 0 nc self.width): continue if self.grid[nr][nc] is None: continue # 相邻格有字但没有通过当前格建立交叉即视为非法贴靠 need_intersect False # 如果是横向邻居那么当前格应该属于同一横向词 if dr 0 and self.horizontal_word_at(row, col) is not None: need_intersect True if dc 0 and self.vertical_word_at(row, col) is not None: need_intersect True if not need_intersect: return False return True这里的逻辑是如果一个格子周围已经有字那么当前字只有两种情况允许放——要么和这个邻居在同一行上组成横向词要么在同一列上组成纵向词。否则就算字是对的也会形成“悬空贴靠”棋盘看起来像乱掉的电线杆。这个规则写起来有点绕但排错效果立竿见影棋盘的整体完成度一下子就上来了。3.2 摆词的比重分配先长后短、先中心后边缘第二个大改动是摆词顺序的策略。原来的算法是随机挑选诗句按入队顺序摆放结果经常是先放一句短诗占了中心位置后面长诗没办法摆整体密度上不去。这版的策略改成按诗句长度从长到短排序优先摆放长句。因为长句对棋盘面积的要求最苛刻先占位可以避免后期放不下。第一句固定在棋盘中心附近让整个棋盘的生长从中心向外扩展避免所有内容挤在一个角落。交叉优先尽量让新词和已有的至少一个词交叉而不是单独开辟新区块。这样棋盘是一个连通图玩家填的时候能根据已有提示推理未知字。实现上我在board_engine.py的generate_board里加了一个贪心循环对每句诗尝试所有可能的起点和方向使用计分函数评估每种摆放方式——交叉数量越多、离中心越近、与已有内容重叠越少分数越高。选最高分的位置落子。# board_engine.py 核心生成函数简写 def generate_board(self, poem_entries: list[PoemEntry]) - bool: self.reset_board() # 1. 长句优先 ordered sorted(poem_entries, keylambda p: max(len(line) for line in p.lines), reverseTrue) for entry in ordered: best_score -1 best_placement None for line_idx, line in enumerate(entry.lines): for row in range(self.height): for col in range(self.width): for direction in (horizontal, vertical): if self.try_place(line, row, col, direction): score self._placement_score(line, row, col, direction) if score best_score: best_score score best_placement (line, row, col, direction, line_idx) self.undo_place(line, row, col, direction) if best_placement is not None: line, row, col, direction, line_idx best_placement self.place(line, row, col, direction, poem_identry.poem_id, line_idxline_idx) else: # 放不下就跳过这句诗不影响整体 continue return self.placed_count 2 # 至少成功摆下两句才算有效棋盘这里我特别说一下undo_place的作用。try_place会临时占用格子如果只是测试而最终不用必须把所有格子状态回滚否则后续摆放会基于一个虚假的占用状态。前面两篇我偷懒没有做撤销结果测试的时候棋盘经常“神秘地多出几个字”后来发现是测试过程没有回滚。这次老实了每次评估都严格走“试放-打分-撤销”的流程。3.3 空白格处理与棋盘裁剪还有一个实用细节是棋盘裁剪。生成完棋盘后边缘可能残留大量空白行/列直接展示给玩家很浪费屏幕空间。我写了一个trim_board方法扫描整个棋盘计算非空格的行列边界然后裁剪成紧凑矩阵。这样生成的棋盘尺寸是动态的小诗少则七八行大诗多则二十多行不存在“棋盘固定30x30但只用了中央一小块”的问题。裁剪之后棋盘的显示尺寸和内容尺寸完全一致后面做图形界面时画布大小也能根据棋盘尺寸自适应不会出现大片白边。4. 核心交互逻辑输入校验、提示与计分4.1 玩家输入的“多层校验”命令行版的早期版本里玩家输入坐标和字程序直接拿去和答案比对不对就提示“错误”。听起来没问题但实际玩起来很糟心——有的玩家会输入全角字母有的会输入拼音有的会输入“1,2,明”这种格式有的干脆输入“”。如果我直接要求格式对称体验会很差。这次我在game_core.py里实现了一套分层的输入校验流程任何不符合规范的内容都会被转成统一的错误类型提示也更友好输入问题检测方式玩家看到的提示坐标格式错误正则匹配r^(\d)[,\s](\d)$“坐标格式应为: 行号,列号例如 3,5”坐标越界与棋盘宽高比较“坐标超出棋盘范围请重新输入”输入的不是汉字Unicode 范围检测“请填写单个汉字”位置上已有字查询 grid“该位置已经有字了不能重复填写”字填错与答案比对“这个字不对哦再想想剩余生命或次数会变化”字正确更新 grid“填对了解锁一句新的提示...”为了支持这个流程我写了一个parse_player_input函数返回统一的PlayerAction对象方便 UI 层直接处理。这里有一个细节坐标的行列号是1开始还是0开始。我最后决定在用户交互层统一使用1开始因为普通人说话都是“第3行第5列”0开始反人类。但在内部数据结构里还是0开始两层之间做个转换用row_1based row_0based 1和col_1based col_0based 1即可。# game_core.py dataclass class PlayerAction: valid: bool action_type: str # fill, hint, quit, restart row: int | None None # 内部 0-based col: int | None None char: str | None None message: str def parse_player_input(raw: str, board_width: int, board_height: int) - PlayerAction: text raw.strip() if text in (quit, exit, q): return PlayerAction(validTrue, action_typequit) if text in (hint, help, ?): return PlayerAction(validTrue, action_typehint) if text in (restart, r): return PlayerAction(validTrue, action_typerestart) # 尝试匹配: 行,列,字 或 行 列 字 m re.match(r^(\d{1,2})[,\s](\d{1,2})[,\s]([\u4e00-\u9fff])$, text) if not m: return PlayerAction(validFalse, action_typefill, message格式应该是 行号,列号,汉字比如 3,5,明) row, col, char int(m.group(1)) - 1, int(m.group(2)) - 1, m.group(3) if not (0 row board_height and 0 col board_width): return PlayerAction(validFalse, action_typefill, message坐标超出棋盘范围) return PlayerAction(validTrue, action_typefill, rowrow, colcol, charchar)4.2 填字判定与连锁解锁玩家填对一个字之后程序不能仅仅把这个字写到棋盘上。更好的体验是填对一个字后自动检测它所在的横向诗句和纵向诗句是否已经补全如果补全了就弹出这句的完整内容作为“解锁奖励”。这个做法既给了正反馈又让玩家在填字的过程中一点点把整首诗拼出来非常有成就感。这个逻辑在game_core.py的fill_cell里实现。核心是四个方向扫描从当前位置出发向左找到这句诗的起点再向右扫到终点检查这一行上的所有格子是否都填了如果都填了记录为“completed_line”并标记为已解锁。def _complete_lines_through(self, row: int, col: int) - list[str]: completed [] for dr, dc in ((0, 1), (1, 0)): # 找到行/列的起点 start_col col while start_col - dc 0 and self.grid[row][start_col - dc] is not None: start_col - dc # 找到行/列的终点 end_col start_col chars [] while end_col self.width and self.grid[row][end_col] is not None: chars.append(self.grid[row][end_col].char) end_col 1 if len(chars) 2 and self.all_filled(row, start_col, end_col - 1, dr, dc): completed.append(.join(chars)) return completed这个“填对一字 - 解锁一句”的机制让游戏的流畅度高了很多。原来玩家填完一个字看到的是一个静态棋盘现在每填对一个关键交叉点就可能触发一两句完整诗句的展示体验明显更接近商业填字游戏的感觉。4.3 提示系统的三种模式提示功能是这个版本的重点之一我实现了三种由浅入深的提示模式坐标提示随机找一个还没填的空格告诉玩家它的行列坐标比如“试试点第2行第4列”。这个模式最弱只在玩家完全没头绪时用。字义提示针对某个空格显示它所在诗句的完整上下文。比如“这句诗出自《静夜思》上一句是‘床前明月光’”。这需要调用WordUnit.line_text和PoemEntry信息。直接填空直接把某个空格的字填上减少一个可提示次数。这三种模式我把它们做成一个“提示漏斗”玩家第一次用提示时先给坐标再用给上下文最后才直接填字。这样可以尽量保留玩家自主推理的乐趣又能在卡死时救场。下面是use_hint的简化实现def use_hint(self) - str: if self.hints_left 0: return 提示次数已经用完了哦 empty_cells self._empty_cells() if not empty_cells: return 棋盘已经填满了太棒了 row, col random.choice(empty_cells) hint_type self._hint_stage % 3 # 依次切换提示模式 if hint_type 0: self._hint_stage 1 return f试试第 {row1} 行第 {col1} 列 elif hint_type 1: self._hint_stage 1 unit self.grid[row][col] return f这句出自{unit.line_text}作者是{self.poem_map[unit.poem_id].author} else: self._place_char_at(row, col, self.grid[row][col].char) self.hints_left - 1 self._hint_stage 1 return f直接告诉你吧这个位置是‘{self.grid[row][col].char}’提示次数用完后再调用use_hint我统一返回“提示次数已经用完了哦”并用一个彩色的命令行输出标红提示这样玩家不会误以为游戏卡住了。4.4 计分模型从“做对题”到“做对选择”计分这一块我不想做成简单的“填对加分填错扣分”因为那样玩家会倾向于乱填碰运气。我设计的计分模型包含三个维度正确填字得分每填对一个格子得10分如果是通过提示“直接填空”填上的得2分。这能鼓励玩家自己动脑而不是狂按提示。错误填字扣分每次填错扣3分但扣分不会让分数变成负数——保底0分防止玩家因连续错误被劝退。连锁完成奖励当填对一个字后连续解锁两句完整诗句奖励15分解锁一句奖励5分。计分状态我放在GameState对象里UI层每次渲染棋盘时把当前分数一起展示出来。还加了一个简单的“连击提示”如果玩家连续5次填对命令行会打印“太强了5连击”图形界面会显示一个小的连击标牌。这个对游戏气氛的提升非常明显。4.5 关卡循环与答案复盘原版游戏玩完一局就结束了玩家看不到任何汇总很没头绪。这次我在game_core.py里加了next_level和summary两个方法next_level在当前棋盘完成后根据玩家得分和填充率决定下一关的难度建议。得分超过本关80%的分数上限就直接升难度低于40%则建议降到初级难度重玩。summary在游戏结束时展示本次游戏的字数统计、正确率、提示使用次数、完成用时、以及所有本次出现的诗句的完整文本。最后这一项特别重要也是我后来加上的——填字游戏本质是“玩着玩着背下诗句”如果结束后不给完整的诗玩家想复习都不知道去哪看。def build_summary(self) - dict: total_cells self.total_cells filled_cells len(self.filled_cells) return { total_cells: total_cells, filled_cells: filled_cells, filled_ratio: filled_cells / total_cells if total_cells else 0, score: self.score, hints_used: self.hints_used, elapsed_seconds: time.time() - self.start_time, completed_lines: list(self.completed_lines), poems: [self.poem_map[pid] for pid in self.poem_map], }5. UI层实现命令行版和图形界面的取舍5.1 命令行版的交互优化很多程序员觉得命令行版做成什么样无所谓能跑就行。但我的看法不一样——命令行版其实就是测试版的用户界面交互体验直接影响调试效率。我在ui_cli.py里做了几件提升体验的小事用不同颜色区分信息用 ANSI 转义序列把正确提示绿色、错误提示红色、普通信息白色区分开。打印棋盘时把空位显示成“·”而不是空格。这样棋盘结构一目了然玩家能快速判断哪些位置是待填的。每轮操作后清屏。这个有争议但经过测试持续滚屏的游玩体验比清屏差很多。我在进入游戏时检测sys.stdout.isatty()只有终端环境才清屏否则保留滚动日志避免用户复制输出时丢掉上下文。# ui_cli.py 中的棋盘渲染部分 def render_board(board: list[list[Cell | None]]) - str: lines [] header .join(f{i1:2} for i in range(len(board[0]))) lines.append(header) for r, row in enumerate(board): line f{r1:2} for cell in row: if cell is None: line · elif cell.is_fixed: line f {cell.char} elif cell.is_filled: line f {cell.char} else: line □ lines.append(line) return \n.join(lines)这里我把“固定字”交叉点上的字游戏开始时已经给出和“待填字”空格区分开固定字直接显示汉字待填空显示“□”填好的显示汉字。这样玩家能清楚地知道哪些字是提示、哪些是自己填的。命令行版的整体交互流程在main_loop里显示棋盘 - 显示当前分数/提示次数 - 等待输入 - 解析输入 - 执行操作 - 重新渲染5.2 Tkinter图形界面的关键实现如果要把游戏分享给不熟悉命令行的朋友Tkinter 是零成本的选择。我在ui_gui.py里用 Tkinter 的 Canvas 和 Frame 搭建了一个简单的 800x600 窗口。主要的实现点有两个。棋盘绘制我用 Canvas 画格子每个格子约 40x40 像素。网格行列数来自棋盘的实际尺寸启动时根据棋盘大小动态计算画布尺寸居中显示。格子的数字标号第几行第几列放在格子左上角的灰色小字汉字居中黑色大字。玩家点击某个空格后会弹出一个simpledialog.askstring输入框输入单个汉字提交。def _draw_board(self): canvas self.canvas canvas.delete(all) rows, cols self.game.board_height, self.game.board_width cell_size 40 for r in range(rows): for c in range(cols): x1, y1 c * cell_size, r * cell_size x2, y2 x1 cell_size, y1 cell_size cell self.game.grid[r][c] if cell is None: continue canvas.create_rectangle(x1, y1, x2, y2, outline#888, fill#f9f5e8) if cell.is_fixed: canvas.create_text((x1 x2) / 2, (y1 y2) / 2, textcell.char, font(SimSun, 18), fill#333) elif cell.is_filled: canvas.create_text((x1 x2) / 2, (y1 y2) / 2, textcell.char, font(SimSun, 18), fill#0a6) else: canvas.create_text((x1 x2) / 2, (y1 y2) / 2, text?, font(SimSun, 14), fill#bbb)事件绑定我用鼠标点击获取行列号然后弹出输入框。Tkinter 的中文输入框本身支持拼音输入法所以玩家可以直接输入汉字不需要额外处理输入法问题。这个方案实测在 Windows 和 Linux 下都能正常使用macOS 下也能跑只是字体渲染略有不同。5.3 双模式共用的MessageBus设计命令行和图形界面共用同一个GameCore但两者对一些事件的响应方式不同。我引入了一个非常轻量的MessageBus本质就是一个事件回调列表GameCore在填对、填错、解锁、计分变化等时机调用注册好的回调UI层各自注册自己的实现。命令行版把回调接到print上图形界面版把回调接到标签刷新上。这样核心逻辑彻底和展示解耦后续想加 Web 版或者 API 版只需要再加一个适配器。class MessageBus: def __init__(self): self._handlers defaultdict(list) def register(self, event: str, handler): self._handlers[event].append(handler) def emit(self, event: str, **kwargs): for handler in self._handlers.get(event, []): handler(**kwargs)使用方式很简单在game_core里填对后调用bus.emit(cell_filled, rowrow, colcol, charchar, scorescore)图形界面里提前注册好一个刷新标签的函数。以后想加音效、加震动反馈只需要注册新的 handler核心代码一行都不用改。6. 常见问题与排查技巧实录6.1 词库滤完就没剩几首诗了怎么办这是我清洗词库时遇到的最大的坑。本来准备了300多首古诗清洗完之后发现能用的可能不到一半。原因有几个七言律诗动辄56个字单句拆出来有7字、5字但生僻字多五言绝句相对干净但内容短的又不够支撑一个棋盘。解决办法一是扩大原始诗词库的采集维度把《唐诗三百首》《宋词三百首》《千家诗》里常见的篇目都加进来增加候选量二是对生僻字表做适度放宽——只要不是非常冷僻的字都接受避免把“月”和“明”这种常见字误杀三是控制每关的诗句数量在5到6首左右这样即使单句长度短组合起来也能形成一个还算密集的棋盘。6.2 命令行下输入中文变成乱码我在 Windows 的命令行里试运行时遇到了输入中文变成“????”的情况。排查之后发现是终端编码的问题——Windows 控制台默认使用 cp936GBK而 Python 脚本按 UTF-8 输出汉字时会出现错乱。解决方式在脚本开头加一行环境兼容代码import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)如果你用的是 Windows Terminal 或 Windows 11 的内置终端一般不需要这个处理但在传统的 cmd 窗口里这行代码是必须的。另外输入中文时如果提示“语法错误”检查一下输入法的全角/半角状态全角逗号会被我的正则里的[]捕获但如果玩家输入的是全角空格\u3000正则的\s也能匹配所以不会出问题。6.3 填字判断时多音字和异体字怎么办这是古诗词项目里最绕不开的问题。比如“远上寒山石径斜”的“斜”在古音里读 xiá但现代读音是 xié还有“乡音无改鬓毛衰”的“衰”古音读 cuī现代读 shuāi。好在我的项目只需要校验“字是否正确”不需要校验“读音是否正确”所以这个问题躲过了。但如果是做语音朗读功能、拼音标注功能就一定要处理多音字词典。我的建议是不要试图在填字游戏里引入读音判断读音问题交给专门的词典模块处理游戏主体只比字形否则会把复杂度拉到一个没法收拾的层级。异体字问题我前面提到过比如“牀前明月光”和“床前明月光”如果词库里有异体字清洗阶段没有转成简体就会导致某个格子用户无法填入常见字。我的处理方式是维护一个“异体字映射表”把常见异体字映射到简体载入时统一替换。6.4 长句摆不下导致棋盘太小有一次我设置的是10字诗但词库里符合这种长度又能通过清洗的句子只有两首结果生成的棋盘只有两行可玩性很差。后来我加了前面提到的“长句优先 贪心评分”策略同时把摆不下句子的跳过机制改成了“备选池”机制——首选的10字句放不下就从备选列表里挑八字的顶上。这样可以保证棋盘至少有足够的密度而不是严格拘泥于某个字数限制。6.5 命令行版清屏后看不到历史提示有玩家反馈清屏之后想看之前的提示内容就看不到了。我一开始觉得这是小事后来发现对游戏体验影响很大因为提示信息往往包含完整诗句玩家需要来回看。后来我加了一个“消息记录区”在棋盘下方固定显示最近5条操作消息包括填对、填错、提示内容。这样即使清屏玩家依然能看到最近几条关键信息。这个改动虽然很小但对可玩性的提升非常明显。7. 打包与分享让朋友不做任何配置就能玩代码写完之后最后一步是打包分发。我不打算要求朋友去安装 Python、pip 安装依赖因为那对非程序员来说门槛太高。我的做法是使用 PyInstaller 把游戏打包成单文件可执行文件。pip install pyinstaller pyinstaller --onefile --windowed ui_gui.py--onefile会把所有依赖打包成一个可执行文件--windowed在 Windows 下不会弹出控制台窗口。打包完成后生成的文件在dist/目录下直接把这份文件发给朋友就能玩。这个过程有几个坑值得注意。PyInstaller 默认不会自动包含你项目里的外部数据文件比如poems_clean.json所以必须用--add-data参数显式声明pyinstaller --onefile --windowed \ --add-data poems_clean.json:. \ ui_gui.py另外如果你的图形界面用了中文字体打包后在别的机器上字体可能缺失Tkinter 会回退到默认字体显示效果会打折扣。一个办法是在代码里做字体回退检测优先使用系统中文字体import tkinter.font as tkfont def get_chinese_font(): for family in (Microsoft YaHei, PingFang SC, WenQuanYi Micro Hei, SimHei): if family in tkfont.families(): return (family, 16) return (TkDefaultFont, 16)打包出来之后我只在自己电脑上测试过后面让朋友在 Windows 10 和 macOS 上各试了一轮Windows 上没问题macOS 上因为权限设置需要右键打开一次才能运行这也算一个常见的新手坑。8. 最后再分享几个小技巧整个第三篇做下来我自己收获最大的其实不是某个单独的功能而是“把游戏逻辑和界面彻底分离”之后带来的自由度。以前想加个新功能总是要小心翼翼地翻到渲染函数的中间去塞一段代码现在只需要在GameCore里加方法UI 层注册回调即可。后续如果你打算做一个 Web 版只需要写一个 Flask 或 FastAPI 的适配器把MessageBus的回调接到 WebSocket 上核心引擎可以原封不动地复用。还有一个建议如果你准备把这个项目作为编程练手项目一定要先跑通一个最小的闭环比如命令行版再去做图形界面不要一上来就折腾 GUI。我在第一版时就是直接上手 Tkinter结果一边调试布局一边想算法两边都没做好。这次是先把board_engine和game_core完整跑通、输出足够多的测试信息确认没有 bug 之后才写的 UI效率高了很多。最后说一个填字游戏特有的小优化当棋盘填到80%以上时接近完成的诗句应该优先显示在提示里。实现方法不难——在use_hint里优先选择那些剩余未填空格最少的区域而不是随机选。这样玩家越到后期越能感受到“快拼出来了”的紧张感游戏的完成度体验会好很多。第三篇的内容就先到这里。古诗词填字游戏这个项目做到这个阶段已经是一个可以日常游玩、可以分享给朋友、可以拿去练手的小作品了。接下来如果你想继续折腾可以考虑的方向有加入多音字拼音提示、做成 Web 版本、加上诗词背景卡片展示甚至是把你的词库按主题山水、离别、边塞拆分出更细的关卡。每一步都能让这个项目更贴合你的使用场景重要的是先把当前这版跑起来玩一局再说。
返回列表