用 Trae AI 编程实战:从零开发 Flutter Web 2048 小游戏

发布时间:2026/9/15 15:14:32
用 Trae AI 编程实战:从零开发 Flutter Web 2048 小游戏 最近我把主力开发工具换成了 Trae起因是看到别人用 AI 对话就把一个完整的游戏做出来了几乎没怎么手写代码。我决定亲自验证一下这件事到底靠谱不靠谱于是给自己定了个目标用 Trae 从 0 到 1 开发一个 Flutter Web 小游戏 2048。为什么选 2048因为这个游戏的玩法足够简单但算法核心——滑动、合并、随机生成、胜负判定——一个都不少非常适合检验 AI 辅助编程的真实能力。而 Flutter Web 是我一直想补上的短板平时写 Flutter 都是跑移动端Web 端的编译、调试、部署链路和移动端有不少差别刚好借这个项目一并摸清楚。最终做出来的成品已经能流畅跑在浏览器里代码也整理成了完整可复用的工程。这篇文章把整个开发过程、核心算法、踩坑记录和完整代码都拆开讲一遍适合三类人看想入门 AI 辅助编程的开发者、想用 Flutter 做 Web 小游戏的初学者、以及想快速了解 Trae 这个工具到底能做多少事的朋友。1. 为什么是 Trae Flutter Web 2048 这个组合1.1 Trae 到底解决了什么问题Trae 是一个 AI 原生的集成开发环境最核心的能力是对话式编程——你在侧边栏用自然语言描述需求它直接帮你生成、修改、解释代码。和传统的自己写代码、再让 AI 补全的模式相比Trae 更接近你当产品经理它当程序员的协作方式。我最早有点怀疑这种模式的效率因为 AI 生成代码有一个老毛病生成出来的东西能用但你可能看不懂更不知道它为什么这么写。实际操作下来发现Trae 比我想象中强的地方有两点一是它能结合整个工程上下文来分析而不是只盯着当前打开的那个文件二是你让它修 bug 的时候它会给出修改思路而不是直接甩一段代码让你自己琢磨。拿这次的 2048 项目来说我一开始给的指令只是创建一个 Flutter Web 项目实现 2048 的核心逻辑它直接把棋盘数据结构、滑动合并算法、UI 布局全部搭好了。之后我又通过十几轮对话逐步加入了手势支持、分数统计、重新开始按钮、颜色主题调整等功能整个开发过程几乎没有从空白文件开始写过代码。1.2 为什么选 Flutter Web 来做小游戏Flutter 本来是移动端跨平台框架但它的 Web 支持这几年已经成熟了不少。用 Flutter 写 2048 这种 2D 网格游戏有几个天然优势组件树和游戏场景天然契合棋盘就是一个 4x4 的网格用GridView或者Column嵌套Row就能搭出来不需要像 Canvas 那样手动算坐标。动画系统省心2048 的格子移动和合并动画用 Flutter 的AnimatedContainer或者AnimatedPositioned几行代码就能做出来效果还非常流畅。同一套代码还能跑移动端虽然目标是 Web但 Flutter 工程天然支持 Android、iOS、桌面端。做完 Web 版之后以后想打包成 App或者加个桌面版成本很低。当然 Flutter Web 也有它的短板最明显的是首屏加载体积比较大这点我在第 5 节会详细讲怎么优化。1.3 2048 这个游戏本身适合做什么2048 的核心机制是4x4 棋盘上每次滑动所有方块朝一个方向移动相同数字的方块相遇时合并成它们的和每次滑动后会在空白位置随机生成一个新的 2 或 4当棋盘被填满且没有相邻相同数字时游戏结束。这个规则的复杂度刚好卡在一个很有意思的位置比猜数字九宫格这类纯逻辑游戏要复杂但又比俄罗斯方块消消乐这类需要实时物理和复杂消除判定的游戏简单得多。做一个能玩的 2048核心代码量大概在 300 到 500 行之间非常适合作为一个学习 AI 辅助编程的入门项目——你能在半天内看到完整的成果过程又足够有挑战性不会觉得这根本是 AI 一键生成的毫无学习价值。2. 开工前必须搞定的环境Flutter SDK、Web 支持和 Trae 的第一轮对话2.1 安装 Flutter 并启用 Web 支持如果你之前没装过 Flutter这一步是绕不开的。安装流程其实并不复杂去 Flutter 官网下载对应你操作系统的 SDK 压缩包解压到一个路径不含中文和空格的目录比如D:\flutterWindows或者~/development/fluttermacOS/Linux。把flutter/bin目录添加到系统 PATH 环境变量。打开终端运行flutter doctor它会检查环境缺什么比如 Android SDK、Chrome、Visual Studio 等。这里有一个常见误区做 Flutter Web 并不需要安装 Android SDK 和 Visual Studio只需要在浏览器里调试的话装个 Chrome 就够了。如果你不打算编译 Android Appflutter doctor提示 Android toolchain 缺失可以直接忽略。装完之后还需要确认 Web 支持已经启用flutter config --enable-web flutter devicesflutter devices输出列表里如果出现了Chrome (web)说明 Web 支持已经就绪。2.2 用 Trae 创建 Flutter 工程Trae 本身就是个 IDE它内置了终端和代码编辑能力。打开 Trae 后先确保它已经配置好了 Flutter SDK 路径。不同版本的 Trae 设置入口不太一样但一般都能在设置里找到 Flutter SDK 路径的配置项。创建 Flutter 工程有两种方式一是在 Trae 的终端里手动执行flutter create二是直接新建项目。我习惯用终端命令创建因为可控性更高flutter create game_2048 cd game_2048创建完成后工程目录里会出现lib/main.dart、pubspec.yaml、web/等文件。这个初始工程默认是 Flutter 的计数器 Demo。接下来就轮到 Trae 上场了——我把首页main.dart的内容全部清空然后在 Trae 的对话面板里输入了第一条指令。这里要提醒一个非常重要的细节AI 对话式的开发关键在于把需求描述清楚。我当时的完整指令是创建一个 Flutter Web 游戏 2048。要求4x4 棋盘所有方块用彩色卡片展示数字越大颜色越深支持键盘方向键和鼠标滑动操作实现完整的滑动合并算法显示当前分数和最高分游戏结束时弹出提示并支持重新开始界面要自适应浏览器窗口大小。Trae 收到这条指令后会在工程上下文里分析需求然后直接改写main.dart。如果它生成的代码有遗漏的地方比如某个方法没定义你直接在对话里回复这里有报错请修复它会自动修正。2.3 环境里最容易踩的几个坑我后来帮朋友在这个项目上做环境配置时发现了几类高频报错提前写在前面帮你们避雷坑一Chrome 启用了 Web 安全限制导致运行失败如果你在flutter run -d chrome启动时遇到页面白屏、端口被拒绝、或者调试连接失败先检查是不是 Chrome 版本太老或者被公司安全策略限制了。最简单的解决方案是直接用flutter run -d web-server --web-port 8080然后手动打开浏览器访问http://localhost:8080。坑二flutter create创建后的工程版本和 Trae 内置 SDK 版本不一致Trae 的 AI 可能生成基于旧版 Flutter API 的代码而本地 SDK 已经升级到了新版运行时会有一堆编译错误。遇到这种“莫名其妙编译不过”的情况先跑一下flutter upgrade flutter pub get把依赖和 SDK 版本对齐。如果某个 API 弃用了直接把报错信息粘贴给 Trae让它帮你改成新版写法。坑三Windows 上弹unable to find suitable Visual Studio toolchain这个报错只在编译 Windows 桌面版或 Android 原生代码时出现做 Web 版完全不需要直接忽略就行。如果你以后打算打包 Windows 桌面应用再去安装 Visual Studio 并勾选 C 桌面开发工作负载。3. 2048 的大脑棋盘建模、滑动合并算法与胜负判定这一节是整个项目的核心也是无论用不用 AI 辅助开发你都应该真正搞懂的部分。Trae 能帮你写代码但如果你不知道算法原理后面改 bug、加功能、调优化都会抓瞎。3.1 棋盘数据结构的设计2048 的棋盘就是 4x4 的二维数组。最直接的定义方式late ListListint grid; ListListint get grid _grid; void initGrid() { _grid List.generate(4, (_) List.filled(4, 0)); }每个格子存一个整数0表示空2、4、8等表示对应的方块数字。这里有个细节值得展开说为什么要用二维数组而不是一维数组加坐标换算用二维数组的优点是代码可读性高grid[row][col]直接对应棋盘位置。但滑动合并算法实现的时候横向和纵向的处理需要分别写逻辑略显繁琐。用一维数组Listint board长度 16 索引换算row index ~/ 4, col index % 4的优点是算法可以统一按一维处理缺点是代码晦涩一点。我最终选择的是二维数组因为代码清晰度对后续维护的收益远大于那一点算法统一性的收益。而且 Trae 生成算法时二维数组的出错率明显更低——AI 在一维数组索引换算上特别容易算错在二维数组上就很少犯这种逻辑错误。3.2 滑动合并算法以左滑为例2048 算法最核心的部分是滑动合并。以左滑为例它的逻辑是每一行单独处理。把这一行中的所有非零数字提取出来按原来的顺序排列比如[2, 0, 2, 4]提取为[2, 2, 4]。从左到右合并相邻的相同数字[2, 2, 4]合并后变成[4, 4]合并得的4累加到分数里。把结果后面补0补到长度 4得到[4, 4, 0, 0]。写成 Dart 代码Listint mergeRow(Listint row, void Function(int) addScore) { // 取出非零数字 Listint nums row.where((e) e ! 0).toList(); // 相邻相同数字合并 for (int i 0; i nums.length - 1; i) { if (nums[i] nums[i 1]) { nums[i] * 2; addScore(nums[i]); nums.removeAt(i 1); } } // 补零到长度 4 while (nums.length 4) { nums.add(0); } return nums; }为什么合并的时候只遍历一遍而不是用 while 循环这是很多初学者最容易写错的地方。2048 的规则是一次滑动中一个格子最多参与一次合并。比如[2, 2, 2, 2]左滑后应该是[4, 4, 0, 0]而不是[8, 0, 0, 0]。for 循环从左到右遍历一遍、合并后立即移除被合并的元素可以天然保证每个元素只合并一次。如果用 while 循环反复遍历就会把连串相同的数字合并成一次大的这不符合游戏规则。右滑的实现也不难——把每一行反转后做左滑再反转回来Listint mergeRowRight(Listint row, void Function(int) addScore) { return mergeRow(row.reversed.toList(), addScore).reversed.toList(); }上滑和下滑则是按列处理把每一列取出来当成一个行做左滑再放回去void moveUp() { for (int col 0; col 4; col) { Listint column [for (int row 0; row 4; row) grid[row][col]]; Listint merged mergeRow(column, _increaseScore); for (int row 0; row 4; row) { grid[row][col] merged[row]; } } }这个四种方向统一转换成左滑的思路是我个人觉得 2048 算法最优雅的地方。你只需要把左滑写好其他三个方向都是转置和反转的组合一旦理解了这一层整个游戏的算法就通了。3.3 随机生成新方块与胜负判定每次滑动完成后要在所有值为0的格子中随机选一个生成2或4。标准规则里生成 2 的概率是 90%生成 4 的概率是 10%。实现起来很简单void spawnRandomTile() { List(int, int) emptyCells []; for (int row 0; row 4; row) { for (int col 0; col 4; col) { if (grid[row][col] 0) { emptyCells.add((row, col)); } } } if (emptyCells.isEmpty) return; final cell emptyCells[Random().nextInt(emptyCells.length)]; grid[cell.$1][cell.$2] Random().nextDouble() 0.9 ? 2 : 4; }注意这里用Random().nextDouble() 0.9而不是nextInt(10) 0从概率分布上讲是一样的但代码意图更清晰。游戏结束的判定也很直观棋盘满了而且相邻的格子上下左右没有任何一组数字相等。翻译成代码bool isGameOver() { for (int row 0; row 4; row) { for (int col 0; col 4; col) { if (grid[row][col] 0) return false; if (col 3 grid[row][col] grid[row][col 1]) return false; if (row 3 grid[row][col] grid[row 1][col]) return false; } } return true; }isGameOver()里判断的其实是还能不能继续滑动只要有空格或者有任何两个相邻格子数字相同游戏就还能继续。这个逻辑很容易漏掉“合并后产生新空格”的情况但实际上合并产生的空格在每次滑动结束时的spawnRandomTile()里已经引入了新数字所以判断时不需要额外考虑。3.4 关于滑动合并算法的边界情况最后补充两个我在测试中容易被忽略的边界情况情况一滑动前后棋盘没有变化不应该触发新方块生成。如果玩家往左滑但最左边已经全是数字没有发生任何移动和合并这时候如果也生成一个新方块就会多出一步无效步数破坏游戏的公平性。解决方案是移动前用jsonEncode(grid)或其他方式深度复制一份原棋盘移动后对比如果有变化才执行spawnRandomTile()。情况二2048 格子合并不只是 2 变 4、4 变 8也可能是 8 和 8 合并成 16。合并逻辑应该对任意数字通用nums[i] * 2这一行已经自动处理了不需要单独为 2048 写特判。但如果你想让游戏在出现 2048 时弹胜利提示就需要额外加一个标志位在合并时判断nums[i] 2048。4. 用对话把界面聊出来UI、手势、动画和分数的演进过程解决了算法之后真正的工程量在 UI 和交互。这一节我会用对话式的开发回顾来展示 Trae 在整个过程中的角色——它不是一个简单的代码生成器更像是一个随叫随到的结对编程伙伴。4.1 第一轮对话搭建棋盘 UI第一轮对话后Trae 生成的界面是纯静态的棋盘——写着数字的格子排成 4x4看起来像个简陋的表格完全没有 2048 那种质感。但这其实是个非常好的起点因为棋盘的数据结构已经通了后面所有工作都只是美化和加交互。棋盘 UI 我用了GridView.builder来渲染GridView.builder( physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 8, crossAxisSpacing: 8, ), itemCount: 16, itemBuilder: (context, index) { int row index ~/ 4; int col index % 4; return _buildTile(grid[row][col]); }, )这里有一个细节容易踩坑GridView默认是可滚动的在游戏棋盘里必须把physics设置成NeverScrollableScrollPhysics()否则鼠标滚轮或者触摸滑动会被GridView拦截导致手势识别出问题。_buildTile根据格子数字返回不同颜色的卡片。我用了一个Mapint, Color存储不同数字对应的颜色从 2 到 2048 一共 11 档颜色。这部分代码完全由 Trae 生成我只提了一个要求颜色要符合主流 2048 游戏风格浅色底配深色数字数字越大颜色越深。4.2 第二轮对话接入手势和键盘棋盘 UI 搭好之后下一步就是让棋子能动起来。2048 的输入方式有两种键盘方向键和触摸滑动。Flutter Web 里监听键盘事件推荐用KeyboardListener或者FocusonKeyEvent。Trae 帮我实现的是KeyboardListener方案KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: (event) { if (event is KeyDownEvent) { switch (event.logicalKey) { case LogicalKeyboardKey.arrowLeft: _moveLeft(); case LogicalKeyboardKey.arrowRight: _moveRight(); case LogicalKeyboardKey.arrowUp: _moveUp(); case LogicalKeyboardKey.arrowDown: _moveDown(); } } }, child: ... )这里要注意autofocus: true必须设置否则页面加载后焦点不在棋盘上键盘事件根本收不到。我第一次跑的时候按方向键没反应排查了半天发现是焦点问题。触摸滑动则用GestureDetector的onPanEnd回调根据手指数和滑动方向判断GestureDetector( onPanEnd: (details) { Offset velocity details.velocity.pixelsPerSecond; if (velocity.dx.abs() velocity.dy.abs()) { if (velocity.dx 0) _moveRight(); else _moveLeft(); } else { if (velocity.dy 0) _moveDown(); else _moveUp(); } }, )这个实现判断的主要依据是速度向量的主方向。如果想要更灵敏的体验应该用起始点和结束点的位移差来判断但速度方式在小屏设备上更稳定不容易误触。两种方案各有适用场景大家可以根据自己的目标设备调整。4.3 第三轮对话加入动画和音效静态界面上滑动数字是瞬移的体验比较生硬。2048 的精髓之一就是方块移动和合并时的丝滑动画。这一步我让 Trae 把动画加上了核心逻辑是把每个格子包在一个AnimatedPositioned里格子的位置由它在棋盘里的坐标决定AnimatedPositioned( duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, left: left, top: top, child: _buildTile(...) )动画时长我建议 80~120 毫秒之间。短于 80 毫秒几乎看不出动画效果长于 150 毫秒会拖慢游戏节奏玩起来会觉得卡。合并的反馈动画新合并的方块有个弹跳效果也可以用AnimatedScale实现从 0.8 放大到 1.0效果很自然。音效这一块我提了一嘴新方块出现时播放一个轻提示音合并大数字播放更响的音效Trae 建议用浏览器内置的 Web Audio API 生成简单提示音这样不需要额外引入音频资源。我试了一下效果还行不过考虑到部分浏览器会自动拦截未经过用户交互的音频播放最终把音效默认关了只在设置里留了个开关。4.4 第四轮对话完善分数、最高分和游戏结束逻辑游戏性完善之后剩下的是外围功能。分数在每次合并时累加mergeRow里合并的数值加到总分数最高分则用SharedPreferences存到浏览器 localStorage 里刷新页面后不会丢。这里踩了一个小坑Flutter Web 的SharedPreferences插件在 Web 端实际上是封装了浏览器 localStorage但在老版本插件上引入时需要用SharedPreferences.getInstance()异步初始化否则会有空指针。Trae 生成的代码用了一个异步初始化函数在main()里通过WidgetsFlutterBinding.ensureInitialized()确保初始化完成后再加载游戏这个模式在后续的 Flutter 开发里也值得复用。游戏结束的弹窗我用的是showDialog里面显示当前分数和最高分并且提供一个再来一局按钮。重新开始时只需要重置棋盘、把分数归零再生成两个初始方块2 和 2 或者 2 和 4保证游戏能继续。4.5 和 Trae 对话的实用技巧把需求说清楚这一段是给读者的对话式编程经验总结因为有几轮对话里我自己吃了不少亏。技巧一一次对话只提一类需求。如果你在对话里同时说帮我加上键盘操作、触摸操作、动画效果、音效、深色模式AI 生成的结果大概率会很乱甚至会改崩之前的正常逻辑。拆分诉求每次对话解决一个模块及时运行验证效率反而更高。技巧二给出具体目标和约束条件。与其说让动画好看点不如说格子移动动画需要 100 毫秒合并时要有放大反馈使用 easeInOut 曲线。AI 对明确参数的响应远好于对模糊形容词的响应。技巧三报错信息直接粘贴给 Trae。终端里显示的红色报错原样复制到对话里它自己会分析错误并修复。这一点的效率远超自己读堆栈去猜。技巧四让 AI 解释代码而不是只生成代码。我每让 Trae 生成一个核心方法都会追加一句解释一下这个方法的实现思路。这样你不仅得到了代码还同时学到了为什么这么写——AI 辅助开发最大的价值不是替代你思考而是帮你加速理解。5. 让游戏跑在浏览器里Web 编译、调试与部署的实战问题Flutter Web 的开发和移动端有个显著不同代码写好了要让它真正在浏览器里以最佳状态跑起来还需要处理不少 Web 特有的问题。这一节记录我在这个项目上遇到的每一个实际问题和解法。5.1 本地运行与热重载体验开发阶段我用的命令是flutter run -d chrome这个命令会用 Chrome 打开页面并且支持热重载按r键和热重启按大写R键。修改代码后保存按r就能在毫秒级看到 UI 更新体验和移动端一致。但如果你的 Chrome 受限或者无法自动启动可以用flutter run -d web-server --web-port 8080然后用任意浏览器访问http://localhost:8080调试。这里--web-port参数很实用——默认随机端口如果你需要固定端口排查问题或者做局域网联调一定要手动指定。5.2 Service Worker 与缓存导致的更新不生效Flutter Web 编译产物默认启用 Service Worker 缓存这在生产环境能显著加快二次加载速度但开发调试时非常坑。典型症状是你改了代码重新 build部署到服务器后发现浏览器加载的还是旧版游戏按 CtrlF5 强制刷新也没用。根本原因是flutter_service_worker.js在浏览器里注册后缓存了旧的 JS 文件而新版本的 Service Worker 文件中 URL 的 hash 变了浏览器没能正确失效旧缓存。解决方案有两个层面开发环境在web/index.html里添加一段脚本让开发模式不注册 Service Workerscript if (serviceWorker in navigator) { navigator.serviceWorker.getRegistrations().then(function(registrations) { for (let registration of registrations) { registration.unregister(); } }); } /script生产环境如果你更新频繁可以在 Nginx 的配置里设置Cache-Control: no-cache确保每次请求都回源校验location / { add_header Cache-Control no-cache, no-store, must-revalidate; }这一块我和 Trae 来回聊了几轮它在排查这个 bug 时表现很好——它建议我先用浏览器开发者工具查看 Application 面板里的 Service Worker 状态确认是不是有旧缓存再针对性地处理。这个排查思路非常值得参考先定位问题产生的环节再去修改对应的配置。5.3 构建发布flutter build web 和静态部署准备发布时执行flutter build web --release产物在build/web目录。这个目录就是完整的静态站点直接扔到任何静态文件服务器Nginx、Apache、GitHub Pages、对象存储 CDN 都可以就能跑。但这里有个部署陷阱Flutter Web 默认资源路径是根路径/。如果你把build/web部署到一个子路径比如https://example.com/game/直接访问会白屏因为页面找不到/main.dart.js。解决方案是在构建时加--base-href参数flutter build web --release --base-href/game/或者更灵活一点直接用相对路径构建。这会改变资源引用方式让站点部署在任何子路径下都能正常访问。想验证本地的构建产物是否正常可以用 Python 起一个临时静态服务器cd build/web python3 -m http.server 8080然后浏览器访问http://localhost:8080。这里我多提一个细节不要直接双击build/web/index.html用file://协议打开Service Worker 和部分浏览器特性在file://下会失效必须通过 HTTP 服务访问。5.4 真机预览与局域网访问开发过程中如果你想用手机真机测试 Web 游戏的手势操作不需要构建完整的部署包。用flutter run -d web-server --web-port 8080 --web-hostname 0.0.0.0启动然后让手机和电脑连同一个 WiFi访问http://你的电脑IP:8080就能测。这里有几个注意点--web-hostname 0.0.0.0必须加否则 Flutter 默认只监听 localhost外部设备访问不到。手机浏览器需要允许 HTTP 访问部分 iOS 版本默认可能拦截。游戏体验在手机上可能会有点卡因为 Flutter Web 默认渲染方案是 CanvasKit对移动端性能优化一般帧率不稳定是正常的。如果遇到明显的卡顿可以试试构建时加--web-renderer html参数Flutter 老版本支持新版本已经移除了这个选项所以这一段看看就好。5.5 首屏加载优化从 5MB 到 1.6MBFlutter Web 一直被诟病的点就是首屏加载体积。一个 2048 小游戏构建完后所有产物加起来往往有 5MB 左右这在手机上通过 4G 网络加载需要好几秒体验非常差。我做了一些尝试后效果最为明显的是使用--wasm编译选项。Flutter 3.22 版本支持编译成 WebAssembly这能显著减小产物体积flutter build web --release --wasm实测下来体积大约从 5MB 降到 1.6MB加载速度提升非常明显。代价是网页需要在支持 WebAssembly 的现代浏览器上运行而 2024 年之后的主流浏览器都已经支持了所以这个选择基本没有兼容性负担。第二招是用 Nginx 开启 gzip 或 Brotli 压缩。main.dart.js这种文本类文件压缩比很高gzip 之后能再减 60% 以上体积gzip on; gzip_types application/javascript text/css application/json;这两招叠加首屏加载时间能缩短到原来的三分之一左右。6. 完整代码导览与后续还能玩出的花样6.1 工程结构说明游戏的主要逻辑都在lib/main.dart一个文件里整个文件大约 400 行。这在 Flutter 单页面小游戏中是正常体量因为所有状态管理、UI、算法都在同一个 State 类里。工程其他关键文件的作用路径作用lib/main.dart游戏主体包含棋盘数据、算法、UI 和交互逻辑web/index.htmlWeb 入口页面可以在这里做加载动画、SEO 标题等定制pubspec.yaml依赖声明本项目仅用到shared_preferencesassets/放自定义图片、音频资源本项目没有额外资源如果你想把代码拆分得更有条理可以参考这个结构lib/ main.dart // 入口 UI game_board.dart // 棋盘逻辑 算法 tile.dart // 单个格子组件 storage.dart // SharedPreferences 封装6.2 核心代码精读整个项目最值得精读的部分我在第 3 节已经拆解过了。这里再给出完整版main.dart的关键骨架import package:flutter/material.dart; import package:flutter/services.dart; import package:shared_preferences/shared_preferences.dart; void main() async { WidgetsFlutterBinding.ensureInitialized(); SharedPreferences prefs await SharedPreferences.getInstance(); runApp(GameApp(prefs: prefs)); } class GameApp extends StatelessWidget { final SharedPreferences prefs; const GameApp({super.key, required this.prefs}); override Widget build(BuildContext context) { return MaterialApp( title: 2048, theme: ThemeData.dark(), home: GamePage(prefs: prefs), ); } }GamePage是核心 StatefulWidget包含所有状态class GamePage extends StatefulWidget { final SharedPreferences prefs; const GamePage({super.key, required this.prefs}); override StateGamePage createState() _GamePageState(); } class _GamePageState extends StateGamePage { late ListListint grid List.generate(4, (_) List.filled(4, 0)); int score 0; int bestScore 0; override void initState() { super.initState(); bestScore widget.prefs.getInt(best_score) ?? 0; _startNewGame(); } void _startNewGame() { setState(() { grid List.generate(4, (_) List.filled(4, 0)); score 0; _spawnTile(); _spawnTile(); }); } }_spawnTile、mergeRow、moveLeft、moveUp等方法我在第 3 节已经写过了这里不再重复。完整可运行的代码建议你直接跟着第 2 节创建工程然后用 Trae 逐条生成这样你能在对话过程中看到每一步的细节。6.3 进阶玩法一撤销Undo功能2048 玩到后期手滑一步就是全盘皆输。加一个撤销功能能极大提升游戏友好度。实现撤销的思路是每次移动前把整个棋盘和分数存到一个历史栈里最多记录 10 步。撤销时从栈顶弹出上一次的状态并恢复。final ListGameState _history []; void _moveLeft() { var before encode(); setState(() { grid moveLeftImpl(grid); _mergeScore(); _spawnTile(); }); var after encode(); if (after ! before) { _history.add(GameState(before, score)); if (_history.length 10) _history.removeAt(0); } }这个功能 Trae 只用一轮对话就写出来了让我有点意外的是它主动处理了无效步数不入历史这个细节——说明 AI 对 2048 这个经典游戏还是相当熟悉的。6.4 进阶玩法二不同棋盘尺寸和难度2048 除了标准 4x4还有人玩 3x3、5x5、6x6 模式。算法完全通用只是把4替换成可配置的棋盘大小。初始生成方块的数量和随机生成新 4 的概率也可以随难度调整。我在代码里把棋盘大小和初始方块的个数提取成了两个配置项int boardSize 4; int initialTiles 2;你可以在游戏设置面板里让玩家选择这也是一个很好的 Trae 对话练习——把棋盘大小改成可配置的难度选项区分 3x3、4x4、5x5。6.5 进阶玩法三动画和主题的进一步打磨Flutter 的动画系统能做出比默认效果精致得多的体验。目前实现的动画还停留在移动 放大的阶段如果想要更高级的效果可以考虑新方块生成的弹出动画类似果冻效果的ElasticOut曲线。2048 格子出现的闪烁和辉光效果可以用BoxShadow 渐变动画实现。深色模式和浅色模式的自动切换通过监听系统的ThemeMode实现。这些功能单独看都不难但合在一起会让游戏的质感上一个台阶。如果专门去做商业化的小游戏这类细节打磨是拉开产品差距的关键。6.6 个人体会AI 协作开发的上限在哪里用 Trae 从 0 到 1 开发完这个项目之后我对 AI 辅助编程有了一个更清醒的认知。AI 最擅长的是把你描述清楚的、有明确规则的需求转化成代码并且能快速迭代修改。它不擅长的是帮你做产品决策——比如棋盘底色用米色还是灰色、动画时长是 80 毫秒还是 120 毫秒、分数面板放在棋盘上方还是左侧这些需要审美和用户体验经验的判断你如果不明确说需求AI 生成的方案就会比较中规中矩。所以我现在的协作方式是我负责定方向、定规则、定参数Trae 负责把这些需求快速实现出来。遇到 bug我描述现象、粘贴报错它负责分析原因和给出修复方案。这种分工的好处是我能在一晚上完成一个从零到可部署的完整游戏项目同时每一行代码的意图我都清楚。2048 已经在 GitHub 上开源过无数版本但自己亲手从功能设计、算法实现、界面打磨到部署上线的过程价值永远不是跑通一个 Demo就能替代的。尤其是配合 Trae 这种对话式 AI 工具你更像是带领一个随叫随到的结对编程伙伴完整经历了一个真实项目的开发流程。这个流程走一遍之后我对 Flutter Web、对 2048 算法、对 AI 协作的边界都比之前有了具体得多的理解——这大概就是做小项目最大的收获。