☰
Python+tkinter实战:古诗词填字游戏的三层架构开发
2026/10/6 9:12:40 网站建设 项目流程

系列前两篇写完,不少读者已经跟着把古诗词数据的清洗和命令行版“出题-作答-判定”流程跑通了。这一篇是收尾篇,目标很纯粹:把之前散落的功能模块整合成一个真正能玩的图形界面小游戏——鼠标点击格子、键盘输入汉字、实时判断对错、提示要扣分、最后还能算个总分。适合两类人看:一类是已经能写基础Python、想找个完整项目练手的中级学习者;另一类是手上真有一批诗词内容、想做点教学工具或者文化类小应用的非技术背景朋友。这篇不讲花架子,只讲怎么把一个脚本改造成一个可持续维护的小项目。

很多人跟着教程写到第二篇就散了,因为代码全摞在一个文件里,想加一个功能就要改三处,改完又冒出两个新问题。这篇我给自己定的规矩是:数据、逻辑、界面三层分开,宁可多拆几个函数,也绝不为了省事把东西全塞进主循环。代码我会按模块给出,你可以照着组合,也可以直接拿去改。

1. 先想清楚:这一版到底要做什么样的填字游戏

1.1 最终效果与功能清单

先对着成品说需求。这一版的游戏界面大致分三个区域:

  • 顶部:关卡名称、当前得分、已填字数、当前关卡进度;
  • 中间:画布绘制的棋盘,棋盘由若干格子组成,横向词条和纵向词条会在某些格子交叉;
  • 底部:操作按钮区,包括“检查”“提示”“重置”“下一关”。

玩家用鼠标点选一个格子,格子会高亮,直接敲键盘输入汉字。程序立刻判断对错:

  • 正确:格子变成浅绿色,并且被锁定,防止玩家手误改掉;
  • 错误:格子变成浅红色,但不会被锁定,玩家可以继续改;
  • 空格:保持白色,等待输入。

功能对应的实现方式,我整理成了一张表:

功能实现方式备注
棋盘渲染tkinter Canvas 绘制方格不用控件堆格子,性能好且易扩展
输入汉字绑定<Key>事件,配合正则过滤只接收单字中文,其余一律忽略
正确判定输入后立即和标准答案比较正确则锁定
错误提示输入后状态置为错误,格子变红不锁定,允许重输
检查按钮遍历所有格子,汇总错误数只提示,不自动改
提示按钮自动填一个未填格每次扣 10 分
重置按钮清空所有未锁定格子分数还原到当前关卡初始分
关卡切换读取 JSON 关卡配置换关时重建棋盘和记分

这个功能清单,就是一个完整填字游戏的最小可用集,再往下减就不像游戏了,往上加可以后续扩展排行榜、倒计时、音效等等,但基础版先守住这些功能。

1.2 为什么第三篇还要重新梳理项目结构

前两篇的代码偏脚本式:一个文件从头写到尾,变量、函数、入口混在一起。当时数据量小、逻辑简单,这样写省事。但到了第三篇,界面事件、关卡切换、计分逻辑、校验逻辑交织在一起,如果还堆在一个文件里,基本没法维护。

所以我建议把项目拆成三个文件:

  • data/levels.json:存放所有关卡配置;
  • game_logic.py:纯逻辑层,负责棋盘建模、输入校验、计分,完全不知道界面的存在;
  • main_ui.py:界面层,负责画棋盘、处理鼠标键盘事件、调用逻辑层接口、刷新显示。

这样拆最大的好处是:逻辑层可以脱离界面单独测试。比如在命令行里直接创建一个PuzzleLogic对象,用假数据调input_char,就能验证功能是否正确,不用每次打开图形窗口手动点。如果你后期想换成网页版,只用重写界面层,棋盘和计分规则原样搬过去就行。对个人项目来说,这个投入产出比非常划算。

从这一篇开始,所有功能都围绕这三层结构展开。下面我把每一层的关键实现拆开讲。

2. 数据层:用 JSON 描述一个关卡

2.1 关卡 JSON 的基本结构

填字游戏的核心是“布局”:哪个词条横向放、哪个词条纵向放、从哪个格子开始、交叉在哪个字上。我选择把布局写进 JSON,而不是让程序自动生成,理由后面会讲。

一个关卡的最小配置长这样:

[ { "name": "静夜思·月", "rows": 5, "cols": 5, "words": [ {"text": "床前明月光", "row": 0, "col": 0, "dir": "across"}, {"text": "月落乌啼霜", "row": 0, "col": 3, "dir": "down"} ], "hints": { "across_0": "《静夜思》第一句", "down_0": "《枫桥夜泊》第一句" } } ]

字段说明:

  • name:关卡名,会显示在界面顶部;
  • rows/cols:棋盘的行数和列数,这里等于棋盘的坐标范围;
  • words:词条列表,每个词条有四要素:内容、起始行、起始列、方向;
  • hints:提示信息,键名用across_0/down_0表示第几个横向或纵向词条,值是给玩家的提示文字。

坐标规则很简单:row为 0 表示最上面一行,col为 0 表示最左边一列。across方向从左往右写,每写一字col加一;down方向从上往下写,每写一字row加一。

上面这个关卡配置,实际摆放出来就是一个 L 形:

床 前 明 月 光 落 乌 啼 霜

第一行横向是“床前明月光”,第 4 列(下标 3)向下是“月落乌啼霜”,交叉点正好是“月”字。

2.2 从词条列表生成棋盘矩阵

布局数据写好了,程序怎么把它变成二维矩阵?这是逻辑层最基础的一个函数。我直接给出完整实现:

def build_grid(level): rows = level["rows"] cols = level["cols"] # 初始化空棋盘 grid = [["" for _ in range(cols)] for _ in range(rows)] for word in level["words"]: text = word["text"] r, c = word["row"], word["col"] direction = word["dir"] for ch in text: if grid[r][c] != "" and grid[r][c] != ch: raise ValueError( f"坐标冲突 {r},{c}:已有 '{grid[r][c]}'," f"又要写入 '{ch}'" ) grid[r][c] = ch if direction == "across": c += 1 elif direction == "down": r += 1 else: raise ValueError(f"未知方向: {direction}") return grid

这个函数返回一个二维列表,每一格存放该位置的正确答案。比如上面那个 L 形关卡的棋盘矩阵就是:

[ ["床", "前", "明", "月", "光"], ["", "", "", "落", ""], ["", "", "", "乌", ""], ["", "", "", "啼", ""], ["", "", "", "霜", ""] ]

函数里做了坐标冲突检查,这是关键。我在写第一版的时候没有加这个检查,结果手工拼关卡数据时眼花,两个词条在同一个格子里写了不同汉字,程序一直等到用户输入到那个格子才发现问题,非常难排查。现在加了ValueError,任何布局不对都会在加载关卡时立刻爆出来。这一行防御代码,能帮你省下大量调试时间。

2.3 一个两难问题:为什么不做全自动布局

很多朋友拿到填字游戏的思路,第一反应是“能不能自动生成整个棋盘,自动匹配交叉点”。我一开始也是这么想的,还花了几天试图写一个算法:拿几百句诗去碰撞,找到共享字,再自动摆放位置。最后发现这条路短期走不通。

原因有三条:

  • 共享字命中率低:五言诗里两句话能共享同一个字的组合比例不高。你拿到的几百句诗,能自动配出十几组优质交叉就算很不错了,剩下的全是勉强拼凑、毫无美感的布局。
  • 自动排版不可控:就算匹配到了共享字,词条互相挤在一起,可能产生狭长棋盘或者大量空格,新手玩家打开就懵了。
  • 关卡设计感为零:填字游戏的乐趣在于难度曲线。第一关应该给一个明显的 L 形或者十字形,让玩家快速体验到第一个交叉点带来的惊喜;之后的关卡才逐步增加词条数量和交叉点。自动生成完全照顾不到这种体验节奏。

所以,最终方案是:布局靠人工设计,程序只负责加载和渲染。填字游戏本质是内容编排的艺术,关卡编辑器的价值远大于随机生成器。当然,如果你确实想做自动生成,可以把“找共享字”做成辅助工具,先把候选词条按共享字聚合,再由你手工挑选和定位,这个方向是可以推进的。

3. 逻辑层:棋盘建模、输入校验与计分

3.1 一个格子需要记录哪些状态

棋盘不只是“正确答案”和“当前答案”两张表那么简单。从交互角度看,每个格子至少还要知道:当前状态的类型、是否被锁定。我建议用一个字典或者一个小类来管理:

class Cell: def __init__(self, answer): self.answer = answer # 标准答案 self.current = "" # 玩家当前输入 self.locked = False # 正确后锁定 self.status = "empty" # empty / correct / wrong

为什么answer由构造时传入,而不是后面赋值?因为逻辑层需要在你输入之前就知道这个格子是否有效。如果玩家点了一个不在任何词条上的空格,answer就是空字符串,程序可以直接忽略输入。

status字段是界面层的“翻译器”。逻辑层不需要知道格子是什么颜色,它只需要把correct或者wrong状态告诉界面层,具体变绿还是变红由 UI 负责。这个解耦很重要,以后想换主题色或者改成移动端样式,逻辑层一行都不用动。

3.2 输入事件的处理流程

玩家每敲一个字,程序就会走一遍下面的流程:

  1. 检查有没有选中格子,如果没有,直接忽略;
  2. 检查格子是否允许输入:如果锁定或者是空白格,忽略;
  3. 检查输入字符是不是单个汉字;
  4. 把字符写入格子的current;
  5. 和answer比较,更新status;
  6. 如果正确,把格子设为locked;
  7. 通知界面层刷新这个格子。

对应逻辑层的核心方法:

import re HANZI_RE = re.compile(r"^[\u4e00-\u9fff]$") def input_char(self, row, col, ch): cell = self.grid[row][col] if not cell.answer: return False if cell.locked: return False if not HANZI_RE.match(ch): return False cell.current = ch if ch == cell.answer: cell.locked = True cell.status = "correct" self.correct_count += 1 else: cell.status = "wrong" self.penalty(2) # 每次填错扣 2 分 return True

这里有一个细节:我用正则^[\u4e00-\u9fff]$校验单字汉字。这个范围覆盖了常用的 20000 多个汉字,对古诗词素材来说足够用。注意不要用\w,因为\w会把英文字母、数字、下划线都算进去,玩家敲一个空格或者标点就全放行了。

还有一个容易踩的坑:古诗词里偶尔会有异体字或者生僻字,比如“閒”这种不在基础汉字区的情况。为了保证正常显示,你可以在预处理阶段把这些字映射成常用简体字,或者把正则范围放宽到扩展 A 区\u3400-\u4dbf,但那可能引入玩家输入法打不出来的字。我建议素材制作时统一用简体字,在数据层面就规避这个问题。

3.3 计分规则与提示系统

基础的计分规则我设计得很简单:

  • 每关初始得分 100 分;
  • 填错一次扣 2 分;
  • 使用一次提示扣 10 分;
  • 全关完成后额外加 20 分;
  • 分数下限是 0,不会出现负分。

计分模块单独抽一个类:

class ScoreBoard: def __init__(self, initial=100): self.score = initial def use_hint(self): self.score = max(0, self.score - 10) def wrong_penalty(self, count=1): self.score = max(0, self.score - 2 * count) def complete_bonus(self): self.score += 20

这个设计的逻辑是:错误扣分要轻,因为填字游戏本身有试错成分,太重容易让人不敢尝试;提示扣分要重,因为提示是直接给答案,扣太少玩家会无脑提示过关,整个游戏失去意义。分数下限设成 0,免得玩家填错太多出现负分影响心情。

提示功能本身很直接:找到第一个还没有被正确填写的格子,把正确答案填进去,同时扣分。要注意的是,如果所有格子已经填对,提示按钮应该无效,避免白扣分:

def hint(self): if self.is_complete(): return False for row in self.grid: for cell in row: if cell.answer and not cell.locked: cell.current = cell.answer cell.locked = True cell.status = "correct" self.correct_count += 1 self.score_board.use_hint() return (row_index, col_index, cell.answer) return None

“检查”按钮的逻辑更简单:遍历所有格子,统计status == "wrong"的数量,弹个消息告诉玩家错了几处。这个功能不是为了替玩家改错,而是给一个全局反馈,让玩家知道还有哪些地方需要回头看。

4. 界面层:用 tkinter 画一个能玩的棋盘

4.1 Canvas 画棋盘的基本框架

tkinter 里画棋盘有两条路:一是用Button或Label控件排布成网格,二是在一个Canvas上画矩形和文字。我强烈建议用Canvas。理由很现实:一个 7x7 的棋盘要放 49 个控件,事件绑定和管理都很啰嗦,而且控件多了之后界面刷新会掉帧;Canvas这一张画布几十个绘图对象轻松搞定,性能好,代码也清爽。

核心绘制函数:

CELL = 64 def draw_grid(canvas, level, cell_size=CELL): canvas.delete("all") rows, cols = level["rows"], level["cols"] for r in range(rows): for c in range(cols): x0, y0 = c * cell_size, r * cell_size x1, y1 = x0 + cell_size, y0 + cell_size fill = "#efefef" canvas.create_rectangle( x0, y0, x1, y1, outline="#666", fill=fill, width=2 )

注意一个问题:关卡配置里可能会出现整行或整列都是空的情况,也就是说某些格子不属于任何词条。这种格子画出来没有任何意义,玩家点进去也没有反应。我建议在绘制时判断一下answer是否为空,为空就把格子画成浅灰色,并禁止点击。具体判断可以通过逻辑层暴露一个接口,比如logic.is_cell_valid(r, c)。

4.2 点击选中与键盘输入的事件绑定

棋盘是画布,但玩家要能选中格子、输入汉字。事件绑定分两步。

第一步,鼠标点击画布,把点击坐标换算成格子坐标:

def on_canvas_click(self, event): col = event.x // CELL row = event.y // CELL if self.logic.is_cell_valid(row, col): self.selected = (row, col) self._highlight_cell(row, col)

这里有三个细节值得注意:

  • event.x // CELL是整除,得到的是格子下标。如果距离超出棋盘范围,需要再判断一次row < rows and col < cols。
  • 边界情况:点击在两格交界线上时,整除结果仍会落到某一格,玩家不会感知到明显的选择偏差,可以接受。
  • 点击空白格时不要高亮,否则玩家会困惑“为什么选中了却没法输入”。

第二步,给根窗口绑定键盘事件:

root.bind("<Key>", self.on_key_press) def on_key_press(self, event): if self.selected is None: return row, col = self.selected if event.keysym == "BackSpace": self.logic.clear_cell(row, col) else: self.logic.input_char(row, col, event.char) self._refresh_cell(row, col) self._update_progress()

BackSpace用于清空格子,这是一个易用性很强的设计:很多玩家填错了第一反应是按退格,而不是重新输入覆盖。注意clear_cell只作用于没有锁定的格子,锁定格不能清除。

4.3 中文输入法的一个大坑

用bind("<Key>")直接捕获输入,在英文输入法下很顺畅,但在中文输入法下会遇到一个麻烦:当你敲拼音时,event.char可能拿到的是英文字母或空字符串,等按下空格或者数字上屏后,事件已经过去了,程序什么都没收到。

这个问题当年让我很崩溃。很长一段时间里,我的方案是“点击格子后弹出一个输入小窗口”,玩家在小窗口里用任意输入法输入,回车确认后把字填进去。这个方案兼容性最好,任何输入法都能正常工作。后来发现在 Windows 11 自带输入法和搜狗输入法下,直接bind("<KeyRelease>")也能稳定拿到上屏汉字,因为真实汉字是在KeyRelease阶段才进入事件队列的。但不同系统行为不一致,Linux 下有的输入法还是拿不到。

所以这一版我推荐一个更稳妥的方案:在界面底部放一个隐藏的Entry,玩家点击格子后,自动焦点放到这个Entry上,输入汉字后按回车确认。说它“隐藏”不是真的看不见,而是平时不显示输入提示,需要时给一点点视觉反馈。这样绕开输入法事件捕获的兼容性问题,实现成本低,而且玩家体验并不差。

def _setup_entry(self): self.entry = tk.Entry(self.root, font=("微软雅黑", 14)) self.entry.place(x=0, y=0, width=1, height=1) # 极小,不可见 self.entry.bind("<Return>", self._on_entry_confirm) def _on_entry_confirm(self, event): text = self.entry.get().strip() if len(text) == 1 and HANZI_RE.match(text): row, col = self.selected self.logic.input_char(row, col, text) self._refresh_cell(row, col) self.entry.delete(0, tk.END) self.root.focus_set() # 把焦点还给主窗口,避免误触

玩过填字游戏的朋友都知道,连续输入的感觉很重要。这里root.focus_set()是把焦点交还给主窗口,这样玩家点下一个格子时不会碰到 Entry 还残留焦点的问题。

4.4 按钮区与状态栏

底部按钮区用tk.Frame和tk.Button实现:

  • “检查”:调用逻辑层统计错误数,用messagebox.showinfo显示结果;
  • “提示”:调用logic.hint(),如果返回坐标就刷新对应格子,并刷新分数;
  • “重置”:调用logic.reset_level(),清空所有未锁定格子并重置分数;
  • “下一关”:调用load_level(current + 1),载入新关卡。

状态栏用一个tk.Label,每次输入、提示、重置后调用_update_progress(),刷新以下内容:

def _update_progress(self): total = self.logic.total_cells() done = self.logic.correct_count text = f"{self.level['name']} 得分:{self.logic.score} 进度:{done}/{total}" self.status_label.config(text=text)

这里有个提示:显示分数和进度不要用多个 Label 分散更新,一个 Label 一次性config(text=...)最省事,也避免界面闪烁。

5. 整合与打包:从模块到可双击的桌面程序

5.1 GameApp 类的完整结构

下面把界面层的主角GameApp类完整搭出来,它负责串联数据、逻辑和界面三部分。

class GameApp: def __init__(self, root, levels): self.root = root self.levels = levels self.current_index = 0 self.selected = None self.canvas = None self.status_label = None self.logic = None self._build_ui() self.load_level(0) def _build_ui(self): self.root.title("古诗词填字游戏") self.root.geometry("560x520") self.status_label = tk.Label( self.root, text="", font=("微软雅黑", 12), pady=10 ) self.status_label.pack(side=tk.TOP, fill=tk.X) self.canvas = tk.Canvas( self.root, width=500, height=400, bg="#fafafa", highlightthickness=0 ) self.canvas.pack(pady=10) btns = tk.Frame(self.root) btns.pack(pady=10) for text, cmd in [ ("检查", self.check_all), ("提示", self.use_hint), ("重置", self.reset_level), ("下一关", self.next_level), ]: tk.Button( btns, text=text, command=cmd, width=8, font=("微软雅黑", 11) ).pack(side=tk.LEFT, padx=5) self.canvas.bind("<Button-1>", self.on_canvas_click) self.root.bind("<Key>", self.on_key_press)

类初始化时先_build_ui()建好控件,然后load_level(0)载入第一关。这里注意一个问题:按钮和 Canvas 都建好之后才可以开始绘制棋盘,所以load_level必须放在_build_ui之后调用,顺序不能反。

5.2 load_level 的职责边界

load_level是整局的“初始化开关”,它做四件事:

def load_level(self, index): if index < 0 or index >= len(self.levels): messagebox.showinfo("提示", "已经是最后一关啦") return self.current_index = index self.level = self.levels[index] self.logic = PuzzleLogic(self.level) self.selected = None self._draw_board() self._update_progress()

这里我故意让PuzzleLogic在构造时接收整个level字典,并在内部完成build_grid、格子状态数组、得分板的初始化。这样界面层不关心棋盘矩阵怎么构建,只负责调用logic.grid拿到格子数据来画图。

关键点是每次换关都要创建一个全新的PuzzleLogic实例,千万不要复用旧实例然后手动清空状态。因为build_grid内部会做坐标冲突检查,新建实例本身就是一次校验,任何布局错误都会在这里暴露。复用旧实例容易把上一关的残留状态带进新关卡,这类 bug 非常隐蔽。

5.3 主入口与 PyInstaller 打包

入口文件很简单:

if __name__ == "__main__": with open("data/levels.json", "r", encoding="utf-8") as f: levels = json.load(f) root = tk.Tk() app = GameApp(root, levels) root.mainloop()

注意读取文件时一定要写encoding="utf-8"。我之前在 Windows 上遇到过 JSON 里汉字全都变成乱码的怪事,就是因为系统默认编码不是 UTF-8,json.load用了错误编码解析。

打包成可执行文件,我推荐 PyInstaller。一条命令搞定:

pyinstaller -F -w -n 古诗词填字游戏 main_ui.py

参数含义:

  • -F:生成单个可执行文件,方便分发;
  • -w:不显示命令行窗口,避免黑框;
  • -n:指定生成文件的名字。

如果后续加了图标,再加上--icon=puzzle.ico参数。打包完之后,在dist目录里就会得到“古诗词填字游戏.exe”,把这个文件发给朋友就能直接玩,他们机器上不需要装 Python。

一个打包实测经验:如果程序里有用到tkinter,PyInstaller 一般会自动带上,不需要额外配数据文件。但如果你把关卡数据levels.json放在外部文件并且用相对路径读取,打包后很容易出现找不到文件的问题。最简单的解决办法是直接用--add-data "data/levels.json;data"把它打进包里,再把读取路径改成sys._MEIPASS兼容的写法。这个属于进阶话题,基础版先不展开,但你心里要有这根弦。

6. 运行阶段的常见问题与避坑记录

6.1 点击格子没反应,或者输入时字符直接出现在控制台

排查思路按顺序走一遍:

  • 确认canvas.bind("<Button-1>")和root.bind("<Key>")都绑上了;
  • 点击格子时有没有执行_highlight_cell,如果没有,说明selected没有设置成功;
  • 输入时按键事件被 Entry 控件拦截了,这是最常见的原因。如果焦点在按钮或者 Entry 上,root.bind("<Key>")可能不触发,最简单的办法是绑定窗口所有控件而不是只在 root 上绑,或者每次点击格子后调用self.root.focus_set()强制抢回焦点。

这个坑我在做按钮区时踩得很惨:玩家用鼠标点完“提示”按钮后,焦点落在按钮上,再点棋盘输入,键盘事件全被按钮吞了。最后加上focus_set()才解决。

6.2 汉字输入框输入后没有反应

如果你走了隐藏 Entry 的方案,最常见的问题是玩家输入完汉字后没有按回车,而是习惯性地点击了另一个格子。这时候 Entry 的Return事件没触发,输入的内容卡在 Entry 里,棋盘自然不变。

一个体验优化:在 Entry 获得焦点时给输入框一个明显的视觉位置,或者把确认方式改成“输入一个汉字后自动提交”——这需要绑定<KeyRelease>而不是<Return>。我实测下来,绑定KeyRelease后,在 Windows 和主流 Linux 桌面环境下都能稳定拿到上屏汉字,但稳定性不如回车确认。给你的建议是:测试环境允许就用KeyRelease自动提交,体验最顺;要绝对可靠就回车确认,代价是玩家多按一次回车。

6.3 高分屏下窗口和字体发虚

tkinter 在 Windows 高分屏下的表现一直很一般。处理办法有三步,按重要性排序:

import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception: pass

这段代码要放在创建窗口之前。它告诉系统这个程序自己处理 DPI 缩放,否则 tkinter 界面会被系统拉伸,所有格子看起来都是糊的。字体方面,尽量用("微软雅黑", 12)这类明确指定字族和字号的写法,不要用 tkinter 的默认字体。

如果你用的是 4K 屏幕,CELL = 64可能会显得格子偏小。可以把格子大小做成常量,统一改一处就行。我的经验是格子边长在 56 到 72 之间观感最好,小于 56 字会挤,大于 72 棋盘会显得空。

6.4 关卡数据出错:坐标冲突、字多出棋盘

这类问题本质是数据层的问题,表现五花八门:

  • 两个词条在同一个格子放了不同汉字,程序直接ValueError崩溃;
  • 词条太长,写到棋盘外面去了,运行时没有报错但棋盘显示不完整;
  • 词条里夹了标点符号或者空格,玩家永远无法输入。

应对方法除了前面说的build_grid里检查冲突,还可以在加载关卡后加一个校验函数,专门检查每个词条是否越界:

def validate_level(level): rows, cols = level["rows"], level["cols"] for word in level["words"]: text_len = len(word["text"]) if word["dir"] == "across" and word["col"] + text_len > cols: raise ValueError(f"词条 {word['text']} 超出横向边界") if word["dir"] == "down" and word["row"] + text_len > rows: raise ValueError(f"词条 {word['text']} 超出纵向边界")

把这个校验函数放在PuzzleLogic.__init__里,每次加载关卡都自动执行。这类防御性检查虽然不起眼,但在你以后往 JSON 里添加关卡时会救你很多次。

6.5 重置后分数没有还原

这个问题的根源在于:你在界面层可能直接操作了逻辑层的内部属性,比如在重置时写了logic.score = 100,但逻辑层的ScoreBoard内部还有一个自己的计分状态,两边没同步。

我建议所有分数状态统一封装在逻辑层内部,界面层只通过接口访问:

def reset_level(self): self.grid = self._build_grid_from_level(self.level) self.score_board = ScoreBoard(initial=100) self.correct_count = 0

这样每次重置都是新建一个ScoreBoard对象,彻底避免旧数据残留。同样,提示、检查、输入全走逻辑层的方法,界面层不要直接改score字段。工程上这叫“最小暴露接口”,前期可能觉得多写了几行代码,后期真香。

7. 想清楚,然后把代码组合起来

把所有模块组合完,一个完整可玩的古诗词填字游戏就立住了。数据层提供关卡配置,逻辑层管理棋盘和计分,界面层处理交互和渲染。三层之间的接口就几个函数:input_char、hint、check、reset_level、load_level。以后的扩展都可以沿着这个边界推进。

最后分享一个我觉得最值得做的扩展方向:在“提示”上做文章。现在的提示是随机填一格,玩起来比较生硬。如果诗词库足够大,可以基于当前棋盘已有的字反向匹配诗库,把提示升级成“王维写过一句含‘月’的诗,下一句是什么”这种更符合诗词主题的玩法,游戏的文化含量一下就上去了。古诗词填字的乐趣,一半在文字本身,一半在出题的方式。你可以从这句开始,把提示系统做成和诗词库深度联动的模块,这个项目的水准会再上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询