news 2026/10/6 13:34:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
古诗词填字游戏功能升级:输入校验、提示计分与工程化重构

做了前面的基础版本和核心算法之后,我原本以为古诗词填字游戏已经能跑起来了,但在实际给朋友试玩的过程中,问题马上就暴露了:命令行虽然能玩,但提示很弱,输错字没有任何容错,词库一多布局就乱,更别说计分和关卡这些基本功了。所以这第三篇,我决定把游戏从“能玩的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 摆词的比重分配:先长后短、先中心后边缘

第二个大改动是摆词顺序的策略。原来的算法是随机挑选诗句,按入队顺序摆放,结果经常是先放一句短诗占了中心位置,后面长诗没办法摆,整体密度上不去。这版的策略改成:

  1. 按诗句长度从长到短排序,优先摆放长句。因为长句对棋盘面积的要求最苛刻,先占位可以避免后期放不下。
  2. 第一句固定在棋盘中心附近,让整个棋盘的生长从中心向外扩展,避免所有内容挤在一个角落。
  3. 交叉优先:尽量让新词和已有的至少一个词交叉,而不是单独开辟新区块。这样棋盘是一个连通图,玩家填的时候能根据已有提示推理未知字。

实现上,我在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, key=lambda p: max(len(line) for line in p.lines), reverse=True) 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_id=entry.poem_id, line_idx=line_idx) else: # 放不下就跳过这句诗,不影响整体 continue return self.placed_count >= 2 # 至少成功摆下两句才算有效棋盘

这里我特别说一下undo_place的作用。try_place会临时占用格子,如果只是测试而最终不用,必须把所有格子状态回滚,否则后续摆放会基于一个虚假的占用状态。前面两篇我偷懒没有做撤销,结果测试的时候棋盘经常“神秘地多出几个字”,后来发现是测试过程没有回滚。这次老实了,每次评估都严格走“试放-打分-撤销”的流程。

3.3 空白格处理与棋盘裁剪

还有一个实用细节是棋盘裁剪。生成完棋盘后,边缘可能残留大量空白行/列,直接展示给玩家很浪费屏幕空间。我写了一个trim_board方法,扫描整个棋盘,计算非空格的行列边界,然后裁剪成紧凑矩阵。这样生成的棋盘尺寸是动态的,小诗少则七八行,大诗多则二十多行,不存在“棋盘固定30x30但只用了中央一小块”的问题。

裁剪之后,棋盘的显示尺寸和内容尺寸完全一致,后面做图形界面时画布大小也能根据棋盘尺寸自适应,不会出现大片白边。

4. 核心交互逻辑:输入校验、提示与计分

4.1 玩家输入的“多层校验”

命令行版的早期版本里,玩家输入坐标和字,程序直接拿去和答案比对,不对就提示“错误”。听起来没问题,但实际玩起来很糟心——有的玩家会输入全角字母,有的会输入拼音,有的会输入“1,2,明”这种格式,有的干脆输入“123”。如果我直接要求格式对称,体验会很差。

这次我在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(valid=True, action_type="quit") if text in ("hint", "help", "?"): return PlayerAction(valid=True, action_type="hint") if text in ("restart", "r"): return PlayerAction(valid=True, action_type="restart") # 尝试匹配: 行,列,字 或 行 列 字 m = re.match(r"^(\d{1,2})[,\s,]+(\d{1,2})[,\s,]+([\u4e00-\u9fff])$", text) if not m: return PlayerAction(valid=False, action_type="fill", 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(valid=False, action_type="fill", message="坐标超出棋盘范围") return PlayerAction(valid=True, action_type="fill", row=row, col=col, char=char)

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"试试第 {row+1} 行第 {col+1} 列" 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"{i+1:2}" for i in range(len(board[0]))) lines.append(header) for r, row in enumerate(board): line = f"{r+1: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, text=cell.char, font=("SimSun", 18), fill="#333") elif cell.is_filled: canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=cell.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", row=row, col=col, char=char, score=score),图形界面里提前注册好一个刷新标签的函数。以后想加音效、加震动反馈,只需要注册新的 handler,核心代码一行都不用改。

6. 常见问题与排查技巧实录

6.1 词库滤完就没剩几首诗了,怎么办

这是我清洗词库时遇到的最大的坑。本来准备了300多首古诗,清洗完之后发现能用的可能不到一半。原因有几个:七言律诗动辄56个字,单句拆出来有7字、5字,但生僻字多;五言绝句相对干净,但内容短的又不够支撑一个棋盘。

解决办法:一是扩大原始诗词库的采集维度,把《唐诗三百首》《宋词三百首》《千家诗》里常见的篇目都加进来,增加候选量;二是对生僻字表做适度放宽——只要不是非常冷僻的字都接受,避免把“月”和“明”这种常见字误杀;三是控制每关的诗句数量在5到6首左右,这样即使单句长度短,组合起来也能形成一个还算密集的棋盘。

6.2 命令行下输入中文变成乱码

我在 Windows 的命令行里试运行时,遇到了输入中文变成“????”的情况。排查之后发现是终端编码的问题——Windows 控制台默认使用 cp936(GBK),而 Python 脚本按 UTF-8 输出汉字时会出现错乱。

解决方式:在脚本开头加一行环境兼容代码:

import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-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 版本、加上诗词背景卡片展示,甚至是把你的词库按主题(山水、离别、边塞)拆分出更细的关卡。每一步都能让这个项目更贴合你的使用场景,重要的是先把当前这版跑起来,玩一局再说。

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

SpringBoot电商平台全栈实战:从订单库存到秒杀优化

简介&#xff1a;这是一份基于SpringBoot的电商平台毕业设计完整资料&#xff0c;面向计算机相关专业的学生或需要快速搭建电商后端项目的开发者&#xff0c;用以解决课程设计、毕业设计选题及实际开发中从零搭建功能模块耗时的问题。压缩包内含1个doc文档&#xff0c;大小约4.…

作者头像 李华
网站建设 2026/10/6 13:33:58

Agent-Reach:多智能体系统触达治理的工程实践

Agent-Reach 是我们从实际业务里抽出来的一套工程实践&#xff0c;名字看着像一个框架&#xff0c;其实本质就一句话&#xff1a;让每个 Agent 都能被稳定、准确、低成本地触达。之前带团队做多 Agent 协作时&#xff0c;最头疼的从来不是模型效果&#xff0c;而是 Agent 之间互…

作者头像 李华
网站建设 2026/10/6 13:31:31

Superpowers:开源浏览器游戏开发平台,从安装到实战全记录

前些天有朋友问我&#xff1a;有一个叫 Superpowers 的开源项目&#xff0c;想装来玩玩&#xff0c;到底值不值得折腾&#xff1f;我先说结论——如果你正好对游戏开发感兴趣&#xff0c;又不想一上来就碰那些动辄几个 GB 的大家伙&#xff0c;Superpowers 绝对是个值得试一试的…

作者头像 李华
网站建设 2026/10/6 13:31:30

Notepad++ 安装与绿色便携版配置:从安装包选型到配置迁移全指南

简介&#xff1a;面向程序员与Web开发者的Notepad 7.5.8安装包&#xff0c;适合需要轻量级代码编辑器、并希望直接使用插件管理器的Windows用户。安装包内集成插件管理器&#xff0c;可便捷检索并安装语法高亮、代码折叠、自动完成等扩展插件&#xff1b;同时附带emeet插件&…

作者头像 李华
网站建设 2026/10/6 13:30:58

EF Core核心原理与高频面试题深度解析:从DbContext到性能优化

1. 为什么我劝你认真对待EF这门技术先说句实话&#xff1a;很多人在简历上写着“熟练使用Entity Framework”&#xff0c;但对着一道“为什么不要在每个请求中都new一个DbContext”的面试题就开始支支吾吾。这不是个别现象&#xff0c;而是国内.NET开发者一个普遍的老毛病——会…

作者头像 李华
网站建设 2026/10/6 13:30:56

HIS系统Oracle数据库性能优化实战:AWR诊断、SQL改写与索引重构全复盘

去年接了一个有点典型的活儿——给某市中心医院的HIS系统做数据库性能优化。这个系统上线跑了差不多六年&#xff0c;业务高峰期的卡顿已经严重到门诊护士想摔鼠标、收费窗口排队排到大厅的程度。对于搞数据库的人来说&#xff0c;HIS系统大概是所有OLTP系统里最“拧巴”的一种…

作者头像 李华