恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C#实战:从递归算法到可视化交互的汉诺塔游戏开发指南
首页
资讯中心
/
C#实战:从递归算法到可视化交互的汉诺塔游戏开发指南
C#实战:从递归算法到可视化交互的汉诺塔游戏开发指南
发布时间:2026/8/4 3:49:54
1. 项目概述从经典算法到实战应用汉诺塔这个源自古老传说的数学游戏相信每一位计算机科学或软件工程专业的朋友都不陌生。它不仅是数据结构与算法课程中的“常客”更是理解递归思想的绝佳范例。但你是否想过将这个经典的算法问题从一个控制台的黑白字符演示转变为一个拥有可视化交互、完整游戏逻辑的桌面应用程序这正是我们这次要深入探讨的“C#实现汉诺塔游戏项目”的核心价值。这个项目远不止于完成一道算法题。它是一次将理论算法递归与面向对象编程OOP、图形用户界面GUI设计、事件驱动编程以及软件工程基础实践相结合的综合性演练。对于C#开发者尤其是正在巩固基础或准备技术面试的开发者而言通过实现一个完整的汉诺塔游戏你能系统地锻炼以下能力如何用C#的类来抽象“柱子”和“圆盘”实体如何设计清晰的数据结构来维护游戏状态如何将递归移动算法与UI动画平滑对接如何处理用户的拖拽、点击等交互事件以及如何组织项目结构使得代码易于阅读、测试和维护。无论你是想深化对递归的理解还是希望有一个拿得出手的、能体现综合能力的小型项目作品这个指南都将为你提供一条清晰的路径。我们将从最核心的递归算法出发一步步构建出一个功能完备、界面友好的Windows窗体应用程序。2. 核心需求与整体设计思路在动手写第一行代码之前我们必须明确这个项目要达成什么目标以及如何架构它。一个基础的汉诺塔游戏至少包含几个核心部分游戏规则引擎、图形化界面、用户交互逻辑。我们的设计需要让这些部分各司其职又协同工作。2.1 功能需求拆解首先我们需要明确这个汉诺塔游戏应具备的基本功能可配置的初始状态允许用户选择圆盘的数量例如3-8个游戏难度随之变化。可视化呈现在窗体上清晰地绘制三根柱子A B C以及叠放在柱子上的若干圆盘。圆盘通常用不同宽度和颜色的矩形表示。交互式操作玩家可以通过鼠标拖放或点击选择的方式将一个柱子顶部的圆盘移动到另一根柱子的顶部。规则验证游戏引擎必须实时验证玩家的每一步操作是否符合汉诺塔规则一次只能移动一个圆盘且移动过程中任何时候都不能将较大的圆盘放在较小的圆盘之上。游戏状态管理记录当前每一步、总步数并能判断游戏何时成功完成所有圆盘都从A柱移动到C柱。辅助与重置功能提供“重置游戏”、“撤销一步”、“提示下一步”甚至自动求解等功能增强用户体验。信息反馈在界面上显示当前步数、最少完成步数2^n - 1以及操作成功或失败的提示。2.2 技术架构选型与思路基于上述需求我们选择经典的Windows Forms (WinForms)作为GUI框架。虽然WPF在表现力和数据绑定上更强大但WinForms对于此类2D图形绘制和直接事件处理的项目来说更加轻量、直观且易于上手能让我们更专注于游戏逻辑本身。项目整体将采用典型的模型-视图-控制器MVC变体思路进行架构模型Model负责核心数据和游戏规则。这包括Tower类代表一根柱子内部用一个Stackint或ListDisk来存储圆盘。int可以代表圆盘的尺寸编号。GameEngine类游戏的核心引擎。它持有三根Tower的实例封装了移动圆盘的规则验证方法CanMove、执行移动的方法MoveDisk、判断游戏是否完成的方法IsGameCompleted以及实现自动求解的递归算法。GameState类可选用于记录游戏状态方便实现撤销/重做功能。可以保存每一步移动的快照。视图View即主窗体MainForm。它负责所有可视化工作在Paint事件中根据GameEngine中的模型数据绘制柱子、圆盘。提供按钮Button用于控制游戏开始、重置、撤销、求解。提供标签Label用于显示步数等信息。控制器Controller在WinForms中控制器逻辑通常分散在窗体的事件处理方法中。我们将通过处理鼠标事件MouseDown,MouseMove,MouseUp来实现拖放交互并在事件处理程序中调用GameEngine的方法来更新模型然后触发视图重绘this.Invalidate()。为什么选择这个架构它将游戏逻辑模型与界面显示视图和用户输入控制器分离。这样做的好处非常明显GameEngine可以独立进行单元测试无需依赖UIUI的改动比如换一种绘制圆盘的方式不会影响核心规则代码结构清晰易于理解和扩展。例如未来如果你想将WinForms界面替换为控制台界面或WPF界面绝大部分模型代码都可以直接复用。3. 核心模块实现详解有了清晰的设计蓝图我们就可以开始动手搭建项目的核心模块了。这是将想法转化为代码的关键一步。3.1 数据模型定义Tower与Disk类首先我们定义游戏世界中的基本元素。虽然圆盘可以用一个简单的整数代表其宽度或大小等级表示但为了未来扩展比如给每个圆盘单独的颜色、ID我们创建一个Disk类。// Disk.cs public class Disk { public int Size { get; } // 圆盘大小值越小圆盘越小顶部 public Color Color { get; set; } // 圆盘颜色可根据Size计算 public Disk(int size) { if (size 0) throw new ArgumentException(Disk size must be positive.); Size size; // 示例根据大小生成渐变色 int colorValue 200 - (size * 20); colorValue Math.Max(50, colorValue); // 确保颜色不太暗 this.Color Color.FromArgb(colorValue, 100, 150); } }接下来是Tower类它代表一根柱子。核心是使用StackDisk来存储圆盘因为汉诺塔的操作只能移动顶部圆盘完美契合栈的“后进先出”LIFO特性。// Tower.cs public class Tower { public string Name { get; } // 柱子名称如 A, B, C private StackDisk disks; // 存储圆盘的栈 public Tower(string name) { Name name; disks new StackDisk(); } // 获取顶部圆盘不移除 public Disk PeekTopDisk() { return disks.Count 0 ? disks.Peek() : null; } // 移除并返回顶部圆盘 public Disk PopTopDisk() { return disks.Count 0 ? disks.Pop() : null; } // 放入一个圆盘到顶部 public void PushDisk(Disk disk) { // 规则检查放入的圆盘必须比当前顶部圆盘小如果存在 if (disks.Count 0 disk.Size disks.Peek().Size) { throw new InvalidOperationException($Cannot place larger disk (Size:{disk.Size}) on top of smaller disk (Size:{disks.Peek().Size}).); } disks.Push(disk); } // 获取所有圆盘用于绘制由于栈是反的可能需要反转顺序来从底向上绘制 public IEnumerableDisk GetAllDisks() { return disks.Reverse(); // 注意这里返回的是从底部到顶部的顺序 } public int DiskCount disks.Count; public bool IsEmpty disks.Count 0; }注意在PushDisk方法中我们内置了核心规则验证。这是一个关键设计点将业务规则约束在数据模型内部可以确保任何外部操作无论是用户交互还是自动求解都必须通过这个“关卡”保证了数据的一致性。3.2 游戏引擎核心GameEngine类GameEngine是整个游戏的大脑它管理三根柱子并提供了游戏进行的所有逻辑接口。// GameEngine.cs public class GameEngine { public Tower TowerA { get; } public Tower TowerB { get; } public Tower TowerC { get; } public int TotalMoves { get; private set; } public int MinPossibleMoves { get; } // 最少完成步数 2^n - 1 private StackGameState undoStack; // 用于实现撤销功能 private int numberOfDisks; public GameEngine(int diskCount) { if (diskCount 1 || diskCount 10) // 限制范围避免UI绘制问题 throw new ArgumentOutOfRangeException(nameof(diskCount), Disk count should be between 1 and 10.); numberOfDisks diskCount; TowerA new Tower(A); TowerB new Tower(B); TowerC new Tower(C); TotalMoves 0; MinPossibleMoves (int)Math.Pow(2, diskCount) - 1; undoStack new StackGameState(); InitializeGame(diskCount); } // 初始化游戏将所有圆盘放到A柱 private void InitializeGame(int diskCount) { for (int i diskCount; i 1; i--) { TowerA.PushDisk(new Disk(i)); } SaveState(); // 保存初始状态 } // 根据名称获取柱子 public Tower GetTowerByName(string name) { return name switch { A TowerA, B TowerB, C TowerC, _ throw new ArgumentException(Invalid tower name.) }; } // 核心方法1判断能否从源柱子移动一个圆盘到目标柱子 public bool CanMove(Tower fromTower, Tower toTower) { if (fromTower.IsEmpty) return false; // 源柱子为空 Disk topDisk fromTower.PeekTopDisk(); if (toTower.IsEmpty) return true; // 目标柱子为空可以放 return topDisk.Size toTower.PeekTopDisk().Size; // 比较大小 } // 核心方法2执行移动操作 public bool MoveDisk(Tower fromTower, Tower toTower) { if (!CanMove(fromTower, toTower)) { return false; // 移动非法 } Disk diskToMove fromTower.PopTopDisk(); try { toTower.PushDisk(diskToMove); TotalMoves; SaveState(); // 移动成功后保存状态用于撤销 return true; } catch (InvalidOperationException) { // 理论上不会发生因为CanMove已经检查过但为了健壮性可以回滚 fromTower.PushDisk(diskToMove); return false; } } // 核心方法3经典的汉诺塔递归求解算法 public List(string From, string To) SolvePuzzle() { List(string, string) moves new List(string, string)(); SolveRecursive(numberOfDisks, A, C, B, moves); return moves; } private void SolveRecursive(int n, string from, string to, string aux, List(string From, string To) moveList) { if (n 1) { moveList.Add((from, to)); return; } SolveRecursive(n - 1, from, aux, to, moveList); moveList.Add((from, to)); SolveRecursive(n - 1, aux, to, from, moveList); } // 判断游戏是否完成所有圆盘是否都在C柱 public bool IsGameCompleted() { return TowerA.IsEmpty TowerB.IsEmpty TowerC.DiskCount numberOfDisks; } // 保存当前游戏状态用于撤销 private void SaveState() { // 简单实现记录每一步移动的“从”和“到”。更复杂的实现可以克隆整个引擎状态。 // 这里我们保存一个快照三根柱子上圆盘大小的列表。 var state new GameState( TowerA.GetAllDisks().Select(d d.Size).ToList(), TowerB.GetAllDisks().Select(d d.Size).ToList(), TowerC.GetAllDisks().Select(d d.Size).ToList(), TotalMoves ); undoStack.Push(state); } // 撤销上一步操作 public bool Undo() { if (undoStack.Count 1) return false; // 只剩初始状态无法撤销 undoStack.Pop(); // 丢弃当前状态 var previousState undoStack.Peek(); // 获取上一个状态 // 根据previousState恢复游戏状态这里需要实现状态恢复逻辑 // 为了简洁此处省略具体的恢复代码。一种方法是清空柱子然后重新Push。 RestoreState(previousState); return true; } private void RestoreState(GameState state) { // 清空所有柱子 ClearTower(TowerA); ClearTower(TowerB); ClearTower(TowerC); // 根据state中的列表重新构建圆盘并放入对应柱子 // ... 实现细节略 TotalMoves state.MoveCount; } private void ClearTower(Tower tower) { while (!tower.IsEmpty) tower.PopTopDisk(); } } // 用于保存状态的简单类 public class GameState { public Listint DisksOnA { get; } public Listint DisksOnB { get; } public Listint DisksOnC { get; } public int MoveCount { get; } public GameState(Listint a, Listint b, Listint c, int moves) { DisksOnA a; DisksOnB b; DisksOnC c; MoveCount moves; } }设计要点解析单一职责GameEngine只负责游戏规则和状态不关心UI。SolvePuzzle方法返回的是移动步骤的列表如(A, C)而不是直接操作UI或模型这保持了引擎的纯净性。状态管理SaveState和Undo的实现展示了如何为功能留出扩展接口。简单的实现可以只记录步骤复杂的实现可以保存完整快照。递归算法的封装SolveRecursive是经典的汉诺塔递归解法。将其封装在引擎内UI层只需调用SolvePuzzle获取步骤列表然后按步骤驱动引擎执行即可实现了算法与交互的解耦。4. 用户界面与交互实现模型和引擎准备好后我们需要一个窗口来让用户看到并操作它们。这是WinForms发挥所长的地方。4.1 窗体布局与控件绘制首先创建一个Windows窗体应用程序项目设计主窗体MainForm。界面布局在窗体上放置以下控件Panel命名为gamePanel作为游戏绘制区域占据窗体主要部分。我们将在这个Panel的Paint事件中绘制柱子和圆盘。NumericUpDown命名为numDisks让用户选择圆盘数量例如1-8。ButtonbtnStart开始/重置btnUndo撤销btnSolve自动求解btnHint提示。LabellblMoves显示当前步数lblMinMoves显示最少步数lblStatus显示操作状态或游戏结果。绘制游戏元素在gamePanel_Paint方法中我们需要根据GameEngine的当前状态进行绘制。计算坐标这是最需要细心的地方。我们需要根据gamePanel的尺寸动态计算三根柱子的位置、底部位置、每根柱子的宽度和间隔。圆盘的宽度则根据其Size属性按比例计算高度固定。绘制柱子使用Graphics.DrawRectangle或Graphics.FillRectangle绘制三个垂直的矩形。绘制圆盘遍历每根柱子上的圆盘从底部开始根据其Size计算宽度使用其Color属性填充一个圆角矩形可以用Graphics.FillPath实现使其看起来更像圆盘。// MainForm.cs 部分代码 private GameEngine gameEngine; private const int TowerWidth 20; private const int DiskHeight 25; private const int BaseHeight 20; private int panelCenterY; private void InitializeDrawingParameters() { // 计算绘制区域和坐标 int panelWidth gamePanel.Width; int panelHeight gamePanel.Height; panelCenterY panelHeight - BaseHeight - 50; // 柱子底部Y坐标 // 计算三根柱子的X坐标等间距 towerPositions[A] panelWidth / 4; towerPositions[B] panelWidth / 2; towerPositions[C] panelWidth * 3 / 4; } private void gamePanel_Paint(object sender, PaintEventArgs e) { if (gameEngine null) return; Graphics g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 抗锯齿让图形更平滑 // 1. 绘制底座和柱子 Pen towerPen new Pen(Color.SaddleBrown, TowerWidth); foreach (char towerName in new[] { A, B, C }) { int x towerPositions[towerName]; // 绘制柱子 g.DrawLine(towerPen, x, panelCenterY - 300, x, panelCenterY); // 绘制底座 g.FillRectangle(Brushes.Peru, x - 60, panelCenterY, 120, BaseHeight); // 绘制柱子标签 g.DrawString(towerName.ToString(), this.Font, Brushes.Black, x - 5, panelCenterY BaseHeight 5); } // 2. 绘制圆盘 DrawDisksOnTower(g, gameEngine.TowerA, A); DrawDisksOnTower(g, gameEngine.TowerB, B); DrawDisksOnTower(g, gameEngine.TowerC, C); } private void DrawDisksOnTower(Graphics g, Tower tower, char towerKey) { int towerX towerPositions[towerKey]; var disks tower.GetAllDisks().ToList(); // 获取从底到顶的顺序 int diskBottomY panelCenterY; for (int i 0; i disks.Count; i) { Disk disk disks[i]; // 圆盘宽度与Size成正比 int diskWidth 40 disk.Size * 20; // 基础宽度 大小增量 int diskLeft towerX - diskWidth / 2; int diskTop diskBottomY - DiskHeight * (i 1); // 从下往上叠放 // 使用圆角矩形绘制圆盘 Rectangle rect new Rectangle(diskLeft, diskTop, diskWidth, DiskHeight); using (GraphicsPath path GetRoundedRectPath(rect, 10)) { using (SolidBrush brush new SolidBrush(disk.Color)) { g.FillPath(brush, path); g.DrawPath(Pens.DarkSlateGray, path); // 绘制边框 } } } }4.2 鼠标拖放交互逻辑为了让玩家能通过拖拽来移动圆盘我们需要处理gamePanel的鼠标事件。逻辑是鼠标按下时判断点击了哪个柱子的顶部圆盘选中源柱子鼠标移动时如果正在拖拽可以高亮目标柱子鼠标松开时判断目标位置是哪个柱子并尝试执行移动。private Tower selectedFromTower null; private Point dragStartPoint; private void gamePanel_MouseDown(object sender, MouseEventArgs e) { // 1. 找到被点击的柱子 Tower clickedTower GetTowerAtPoint(e.Location); if (clickedTower null || clickedTower.IsEmpty) return; // 2. 选中该柱子作为移动源 selectedFromTower clickedTower; dragStartPoint e.Location; // 可以在这里给选中的圆盘一个视觉反馈比如改变颜色 gamePanel.Invalidate(); } private void gamePanel_MouseMove(object sender, MouseEventArgs e) { if (selectedFromTower ! null (Math.Abs(e.X - dragStartPoint.X) 5 || Math.Abs(e.Y - dragStartPoint.Y) 5)) { // 开始拖拽可以绘制一个跟随鼠标的圆盘虚影高级功能 // 或者简单地在鼠标移动到可放置的柱子上时高亮该柱子 Tower hoverTower GetTowerAtPoint(e.Location); if (hoverTower ! null gameEngine.CanMove(selectedFromTower, hoverTower)) { // 高亮目标柱子例如改变底座颜色 } gamePanel.Invalidate(); } } private void gamePanel_MouseUp(object sender, MouseEventArgs e) { if (selectedFromTower null) return; // 找到鼠标释放位置对应的柱子 Tower targetTower GetTowerAtPoint(e.Location); bool moveSuccess false; if (targetTower ! null targetTower ! selectedFromTower) { // 尝试移动 moveSuccess gameEngine.MoveDisk(selectedFromTower, targetTower); } if (moveSuccess) { UpdateUIAfterMove(); if (gameEngine.IsGameCompleted()) { MessageBox.Show($恭喜你完成了游戏\n总步数{gameEngine.TotalMoves} 最优步数{gameEngine.MinPossibleMoves}, 游戏完成); } } else { lblStatus.Text 移动无效; } // 清理选中状态 selectedFromTower null; gamePanel.Invalidate(); } // 辅助方法根据屏幕坐标点判断点击了哪根柱子 private Tower GetTowerAtPoint(Point point) { foreach (var kvp in towerPositions) { // 简单判断如果点击位置在柱子图形的矩形区域内 int towerX kvp.Value; Rectangle hitArea new Rectangle(towerX - 30, panelCenterY - 300, 60, 300 BaseHeight); if (hitArea.Contains(point)) { return gameEngine.GetTowerByName(kvp.Key.ToString()); } } return null; } private void UpdateUIAfterMove() { lblMoves.Text $步数{gameEngine.TotalMoves}; lblStatus.Text $移动成功; // 触发重绘 gamePanel.Invalidate(); }交互设计心得视觉反馈至关重要在MouseDown和MouseMove事件中提供视觉反馈如高亮选中圆盘、提示可放置区域能极大提升用户体验让操作更直观。防误触处理在MouseUp中我们检查了移动是否有效并给出了明确的提示lblStatus。对于无效操作除了提示还可以考虑添加一个轻微的动画或声音反馈。性能考虑在MouseMove事件中频繁调用Invalidate()会引发重绘如果绘制逻辑复杂可能会卡顿。一个优化是只重绘需要更新的区域或者使用双缓冲技术SetStyle(ControlStyles.OptimizedDoubleBuffer, true)。5. 功能增强与项目扩展基础功能实现后我们可以为项目添加更多实用和有趣的功能让它从一个简单的演示程序变成一个更完整的“游戏”。5.1 自动求解与动画演示我们已经在GameEngine中实现了SolvePuzzle方法它返回最优移动步骤的列表。在UI上实现自动求解并伴有动画会非常直观地展示递归算法的执行过程。private async void btnSolve_Click(object sender, EventArgs e) { btnSolve.Enabled false; var moveSequence gameEngine.SolvePuzzle(); lblStatus.Text $正在自动求解 ({moveSequence.Count}步)...; // 使用异步方法避免UI线程阻塞 foreach (var move in moveSequence) { var fromTower gameEngine.GetTowerByName(move.From); var toTower gameEngine.GetTowerByName(move.To); // 执行移动 gameEngine.MoveDisk(fromTower, toTower); UpdateUIAfterMove(); // 添加延迟形成动画效果 await Task.Delay(500); // 延迟500毫秒 } lblStatus.Text 自动求解完成; btnSolve.Enabled true; }注意这里使用了async/await和Task.Delay来实现非阻塞的延时动画。务必确保在延时期间禁用相关按钮防止用户操作干扰动画流程。5.2 撤销/重做与游戏状态持久化撤销功能在GameEngine中已经预留了接口。在UI上我们需要处理btnUndo的点击事件。private void btnUndo_Click(object sender, EventArgs e) { if (gameEngine.Undo()) { UpdateUIAfterMove(); lblStatus.Text 已撤销上一步; } else { lblStatus.Text 无法撤销; } }状态持久化指的是将当前游戏状态保存到文件或数据库下次启动时可以加载继续。我们可以定义一个简单的数据契约如JSON格式保存圆盘数量、各柱子上的圆盘大小列表、总步数等信息。public class GameSaveData { public int DiskCount { get; set; } public Listint TowerA { get; set; } public Listint TowerB { get; set; } public Listint TowerC { get; set; } public int TotalMoves { get; set; } } // 保存游戏 private void SaveGame(string filePath) { var saveData new GameSaveData { DiskCount gameEngine.TowerA.DiskCount gameEngine.TowerB.DiskCount gameEngine.TowerC.DiskCount, TowerA gameEngine.TowerA.GetAllDisks().Select(d d.Size).ToList(), // ... 类似填充 TowerB, TowerC TotalMoves gameEngine.TotalMoves }; string json JsonSerializer.Serialize(saveData); File.WriteAllText(filePath, json); } // 加载游戏 private void LoadGame(string filePath) { string json File.ReadAllText(filePath); var saveData JsonSerializer.DeserializeGameSaveData(json); // 根据saveData重新初始化gameEngine和UI }5.3 界面美化与用户体验优化双缓冲绘制在窗体构造函数或Load事件中设置DoubleBuffered true可以极大减少绘制时的闪烁。public MainForm() { InitializeComponent(); this.DoubleBuffered true; // 启用双缓冲 gamePanel.Paint gamePanel_Paint; gamePanel.MouseDown gamePanel_MouseDown; // ... 其他事件绑定 }圆盘绘制优化使用GraphicsPath绘制圆角矩形并添加简单的渐变或阴影效果可以让圆盘看起来更立体。音效与提示在移动成功、失败、游戏完成时可以播放简短的音效。Hint提示功能可以调用SolvePuzzle只取出下一步并高亮显示对应的柱子。自适应布局在窗体的Resize事件中重新计算towerPositions和panelCenterY并调用gamePanel.Invalidate()使游戏画面能随窗体大小变化而自适应。6. 常见问题排查与调试技巧在开发过程中你可能会遇到一些典型问题。这里记录了一些“踩坑”经验和解决方案。6.1 绘制坐标计算错误问题圆盘画在了柱子外面或者堆叠顺序不对。排查在DrawDisksOnTower方法中在绘制每个圆盘前用g.DrawRectangle把计算出的Rectangle边框画出来检查其位置和大小是否正确。确认GetAllDisks()返回的顺序是否是从底部到顶部。我们的Stack.Reverse()就是为了解决这个问题。检查panelCenterY、towerPositions等基准坐标是否在窗体大小改变时得到了更新。6.2 鼠标点击检测不准确问题点击柱子没反应或者点击柱子之间的空白处却被判定为点击了柱子。排查GetTowerAtPoint方法中的hitArea矩形范围需要仔细调试。可以在Paint事件中把这个矩形也画出来看看它是否和视觉上的柱子区域吻合。考虑使用更精确的碰撞检测比如只检测柱子顶部一定高度内的区域因为只有顶部圆盘可移动或者结合圆盘的位置进行判断。6.3 自动求解动画卡顿或UI无响应问题点击“求解”按钮后界面卡住直到所有步骤完成才一次性刷新。解决确保你使用了async/await和Task.Delay而不是Thread.Sleep。Thread.Sleep会阻塞UI线程。在动画执行期间禁用“求解”按钮防止重复点击。如果步骤非常多比如圆盘数10可以考虑增加每一步的延迟时间或者提供“加速”选项。6.4 撤销功能状态恢复异常问题撤销后游戏状态没有正确回到上一步或者圆盘数量不对。排查检查SaveState方法保存的数据是否完整。是否包含了所有三根柱子的状态检查RestoreState方法是否正确地从保存的数据重建了Disk对象并放入了正确的柱子。特别注意Disk的Size属性是只读的重建时需要new Disk(size)。确保undoStack的推入(Push)和弹出(Pop)逻辑正确特别是在游戏初始化、重置时是否清空了栈或保存了正确状态。6.5 项目结构混乱难以维护问题所有代码都写在MainForm.cs里导致文件庞大逻辑纠缠。建议严格遵守我们最初的设计将Disk,Tower,GameEngine,GameState等类放在独立的.cs文件中。在MainForm中只保留与UI交互和绘制相关的代码。业务逻辑尽量调用GameEngine的方法。使用区域(#region ... #endregion)来折叠代码块让MainForm的结构更清晰。考虑使用更高级的架构模式如将GameEngine作为ObservableUI作为Observer通过事件通知来更新进一步降低耦合度。这个C#汉诺塔游戏项目从算法核心到可视化交互完整地走了一遍小型桌面应用开发的全流程。它像一块试金石能检验你对C#基础语法、面向对象、递归思想、WinForms图形编程和事件处理的理解深度。当你亲手解决掉那些坐标计算不准、事件响应异常的bug并最终看到圆盘按照你设定的规则平滑移动时所获得的成就感远非单纯理解算法可比。你可以在此基础上继续探索比如加入关卡系统、移动步数排行榜、更炫酷的动画效果或者尝试用WPF、Avalonia重写界面甚至移植到WebBlazor或移动端.NET MAUI让这个经典问题在不同的技术栈上焕发新的生命力。