ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Trae从零开发Flutter Web版2048:AI辅助编程实战记录

用Trae从零开发Flutter Web版2048:AI辅助编程实战记录 先交代一下背景我用 Trae 从零开始写了一个 Flutter Web 版的 2048从立项到最终能部署到静态站点上前后花了一个下午加一个晚上。我本来以为这种小程序最难的是“合并逻辑”结果真正花时间的全在细节上——比如和 Trae 对需求时规则没说清楚导致合并没有按预期执行、Flutter Web 在浏览器里的键盘焦点问题、CanvasKit 渲染下的加载体积优化。这篇就把完整过程拆开来讲适合想了解 Trae 实际开发工作流的读者也适合想用 2048 作为 Flutter Web 入门项目的朋友。我没用现成模板也没有去 GitHub 找别人写好的 2048 改一改而是从创建 Flutter 工程开始每一行核心逻辑都由 Trae 辅助完成。中间经历了几轮“我发现 bug、回贴提示词、Trae 修复、再验证”的循环这些过程比代码本身更有参考价值。所以这篇文章不会只贴一份完整代码而是会重点说明每一步我为什么这么设计、怎么和 Trae 沟通、最终落地时踩了哪些坑。1. 为什么拿 2048 练手它刚好卡在“玩具”和“工程”之间2048 看起来简单4x4 的网格、数字方块、上下左右滑动、相同数字合并。但如果你认真做一遍会发现它的复杂度很微妙刚好比普通 Demo 高一个档次又比业务系统低很多。这种体量非常合适用来测一个 AI 编程工具的真实水平。我把 2048 拆成了几个独立模块每块都可以让 Trae 单独完成然后再拼起来棋盘数据模型一个 4x4 的二维数组所有移动和合并都是对数组状态的操作。游戏核心逻辑判断游戏是否结束、能否移动、随机生成新方块2 或 4。界面渲染Flutter 里的GridView或手写布局按状态刷新数字块。动画层合并和滑动时的过渡效果。交互层移动端手势、PC 端键盘方向键。状态管理游戏得分、最高分、重新开始、游戏结束弹窗。其中“游戏核心逻辑”是最容易出 bug 的地方。很多人写 2048 时会在“合并”这个环节翻车——比如一行里出现[2, 2, 4, 0]按右移后是按“先移动后合并”还是“边移动边合并”两种思路结果完全不一样。这就是一个很典型的、适合在和 Trae 对话时讲清楚规则的点。另一个原因是2048 的状态流转非常适合单元测试。一个函数输入棋盘状态输出移动后的状态纯函数、无副作用。这意味着 Trae 生成的代码我能立刻写测试去验证验证不过就贴错误信息让它继续改。这种“生成-测试-反馈”的循环正是 AI 编程工具最擅长的地方。和 Trae 配合开发这样的项目我能直观看到它在三个维度上的表现对 Flutter Widget 树和布局的熟悉程度、对 Dart 语言特性的把握比如不可变数据、集合操作、以及对游戏规则这种隐含业务逻辑的理解能力。坦白说第一轮对话时我踩了一个坑我以为 AI 会自动理解 2048 的规则所以第一次提示词写得比较随意让它“实现 2048 的核心逻辑”结果合并逻辑写得一塌糊涂。后面我改用结构化描述明确每一步的规则代码质量立刻就上来了。这部分会在第 3 节详细展开。2. 开局准备装环境、建工程明确 Trae 的工作边界2.1 环境安装比预想中简单但要注意版本我这边本机是 macOS安装流程比较顺。如果你用 Windows过程也类似只是环境变量配置稍有一点点差异。首先是 Flutter SDK。我用的是 stable 分支当时最新稳定版本是 3.x。这里有个建议尽量别用 beta 或 master 分支因为 Trae 这类 AI 工具训练的语料大多基于稳定版本的 API用非稳定分支时它给出的代码偶尔会踩到行为变更的地方徒增调试成本。flutter --version确认 Flutter 已安装后还需要确保 Web 支持已经启用。新版本的 Flutter 默认已经包含了 Web 支持不需要像早期那样手动执行flutter config --enable-web。但保险起见可以看一眼flutter config --list如果列表里enable-web是 true就不用管。如果不是执行一下flutter config --enable-web然后是 Trae。我直接用官网下载的安装包装完后登录账号。这里要注意一点Trae 是以窗口 IDE 的形式工作的底层类似 VSCode 的分支所以如果你是 VSCode 用户上手几乎没有学习成本。2.2 创建 Flutter 工程先别急着写代码在让 Trae 写逻辑之前我先把工程初始化好。这一步我没让 Trae 代劳因为命令行操作直接自己跑更快flutter create game_2048 cd game_2048创建完工程之后我先跑了一次默认的 counter demo确认 Flutter Web 在浏览器里能正常跑起来flutter run -d chrome这里有个容易忽略的细节flutter run -d chrome会启动开发调试模式首次编译需要一点时间但后续热重载非常快。我开发全程主要靠 Chrome 调试没有在模拟器上跑因为 2048 这种游戏本身就是桌面和移动通吃的交互模型浏览器里能模拟移动端手势也更接近最终 Web 发布的形态。2.3 给 Trae 设定工作边界哪些让它做哪些自己写这一步是我用 AI 编程工具一段时间后总结出的习惯。在打开 Trae 写第一段提示词之前我在心里先做了分工交给 Trae棋盘数据模型、核心逻辑、基础 UI 布局、动画实现、手势识别、键盘事件。自己来项目初始化、样式细节微调、性能优化、构建发布以及和浏览器特性相关的兼容性处理。这个分工不是绝对的但有一条原则很关键涉及项目骨架的部分AI 生成后你要亲自 review涉及独立函数的逻辑可以放手让 AI 去写。因为骨架错误会导致后续所有代码都在错误的架构上叠加改起来成本极高而独立函数就算有问题报错信息也会直接告诉你哪里不对。另外我在 Trae 里开了新的会话专门给这个项目用。这样做的原因是避免上下文污染——AI 编程工具在一个会话里聊得太久容易记混你之前的需求。每个大功能模块开一个新会话效果会好很多。3. 和 Trae 对需求写提示词比写代码更需要动脑3.1 第一轮对话我犯了太相信“常识”的错我的第一轮提示词写得特别随意帮我实现一个 2048 游戏的核心逻辑使用 Dart 语言要有移动、合并、生成随机方块和判断游戏结束的函数。结果 Trae 确实生成了完整代码结构也看着像模像样但跑起来之后我发现了一个经典错误当一行是[2, 2, 4, 0]时向左移动后它给出的结果是[4, 4, 0, 0]而正确的 2048 规则下应该是[4, 4, 0, 0]没有错但换成[4, 4, 4, 0]时它合并成了[8, 4, 0, 0]这个是对的。换成[2, 2, 2, 2]时它变成了[4, 4, 0, 0]这个也没问题。真正出错的是[2, 2, 2, 0]——它给的是[4, 2, 0, 0]符合规则但[2, 0, 2, 2]它给的答案是[4, 0, 2, 0]这个不对因为移动后应该先把所有非零元素堆到一侧也就是变成[2, 2, 2, 0]这时候再合并才得到[4, 2, 0, 0]。问题出在它没有执行“先移动、后合并”的两步流程。很多人觉得 2048 的核心就是遍历数组然后合并其实不是。正确逻辑是先把这一行所有非零元素取出来按方向排列。对取出的元素从左到右或从右到左进行“相邻相同则合并”操作。合并后的结果补零到固定长度。比如[2, 0, 2, 2]向左移动取非零元素[2, 2, 2]合并位置 0 和 1 都是 2合并为 4位置 2 是 2保留。得到[4, 2]补零[4, 2, 0, 0]这样写就完全规避了顺序问题。3.2 如何把规则“翻译”成 AI 能理解的结构化描述发现错误后我没有直接说“你错了应该先移动再合并”而是把规则改成结构化描述再次发给 Trae实现一个 2048 游戏的移动和合并逻辑。规则如下每次移动前先把行/列中的所有非零元素取出按移动方向排列。从排列后的第一个元素开始如果相邻两个元素值相同则合并为一个元素值为两数之和每个元素在一次移动中只能被合并一次。合并完成后将结果放回原数组长度不足位置补 0。如果移动前后数组没有任何变化则视为无效移动。这样一写Trae 生成的代码就正确了。核心函数大致长这样Listint mergeLine(Listint line) { // 先去掉所有 0按行方向排列 Listint tiles line.where((e) e ! 0).toList(); Listint result []; for (int i 0; i tiles.length; i) { if (i 1 tiles.length tiles[i] tiles[i 1]) { result.add(tiles[i] * 2); i; // 跳过被合并的元素 } else { result.add(tiles[i]); } } // 补零到原长度 while (result.length 4) { result.add(0); } return result; }这个函数最关键的两行是tiles[i] * 2和i。i保证了每个元素在一次移动中只会参与一次合并。如果你去掉这行[4, 4, 8, 0]这种输入就会被错误地合并成[16, 0, 0, 0]。3.3 二次对话让 Trae 处理四个方向的共性问题移动方向有四个但如果直接让 Trae 写四个函数代码会重复且冗余。更好的做法是定义好方向规则利用矩阵转置来处理上下方向。第二轮对话我给的提示词是2048 游戏有上、下、左、右四个方向的移动。请实现四个方向的移动逻辑。要求左移动直接对每行调用 mergeLine。右移动对每行先反转再调用 mergeLine最后再反转。上移动将矩阵转置后对每行调用 mergeLine再转置回来。下移动将矩阵转置后对每行先反转再调用 mergeLine再反转最后转置回来。 请用 Dart 实现不要重复造轮子。Trae 很快给出了代码。核心也就是这两个辅助函数ListListint transpose(ListListint grid) { return List.generate(4, (i) List.generate(4, (j) grid[j][i])); } ListListint reverseLines(ListListint grid) { return grid.map((row) row.reversed.toList()).toList(); }方向处理变成了四个非常轻量的组合函数。这套设计的可读性相当好后期我想要增加“撤销”功能时也只需要在移动前保存一份棋盘快照整体侵入性很小。3.4 让 Trae 判断“能不能移动”和“游戏是否结束”接着我继续让 Trae 实现了两个辅助判定函数canMove(grid)棋盘里是否有空格或者任意相邻格子值相同。isGameOver(grid)在canMove返回 false 时游戏结束。这两个函数可以和move逻辑共享判断规则。我给 Trae 的提示词是请实现 canMove 和 isGameOver 两个函数。canMove 返回布尔值表示当前棋盘是否还有可移动的空间isGameOver 表示游戏是否结束。注意isGameOver 等于 !canMove不要重复实现逻辑。Trae 给出的实现类似bool canMove(ListListint grid) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (grid[i][j] 0) return true; if (j 3 grid[i][j] grid[i][j 1]) return true; if (i 3 grid[i][j] grid[i 1][j]) return true; } } return false; }这个逻辑不难但让 AI 写能节省一点时间更重要的是验证 AI 是否理解了我刚才定的规则。验证方式很简单写一个单测输入一个“没有空格且相邻没有相同值”的棋盘例如[ [2, 4, 2, 4], [4, 2, 4, 2], [2, 4, 2, 4], [4, 2, 4, 2] ]canMove应该返回 false。如果 AI 的逻辑漏掉了相邻格子判断这个用例会第一时间暴露问题。4. 核心逻辑落地数据模型、状态管理和 UI 解耦4.1 数据模型用不可变棋盘还是可变棋盘和 Trae 讨论数据模型时我明确让它设计一个Game2048类持有ListListint _grid和int _score。这里有一个设计选择是让棋盘状态不可变每次移动生成新棋盘还是可变直接修改原棋盘对于 Flutter 的setState模式可变棋盘是更自然的选择。因为 UI 层只需要在每次移动后调用setState通知刷新即可。而不可变棋盘更适合 Redux 或 Riverpod 这类状态管理方案。我这次选择的是可变棋盘 ChangeNotifier原因很简单这是 Flutter 里最基础、最容易理解的状态管理方式Trae 对这个模式也最熟。class Game2048 extends ChangeNotifier { ListListint _grid List.generate(4, (_) List.filled(4, 0)); int _score 0; bool _gameOver false; ListListint get grid _grid; int get score _score; bool get gameOver _gameOver; void reset() { _grid List.generate(4, (_) List.filled(4, 0)); _score 0; _gameOver false; addRandomTile(); addRandomTile(); notifyListeners(); } }4.2 随机生成方块几行代码里的概率陷阱新方块生成的规则很简单在空位里随机挑一个位置以 90% 概率放 210% 概率放 4。这个逻辑我让 Trae 一次性生成但后来检查时发现一个问题它用Random随机选位置时有极小概率选不到有效空位导致死循环风险。Trae 一开始给的实现是void addRandomTile() { final random Random(); while (true) { final row random.nextInt(4); final col random.nextInt(4); if (_grid[row][col] 0) { _grid[row][col] random.nextInt(10) 0 ? 4 : 2; break; } } }这个实现如果棋盘只有一个空位会循环很多次才能随机命中。虽然概率上不会无限循环但这不是最优写法。我让 Trae 优化成“先收集所有空位再从空位列表中随机选一个”void addRandomTile() { final emptyCells (int, int)[]; for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (_grid[i][j] 0) emptyCells.add((i, j)); } } if (emptyCells.isEmpty) return; final random Random(); final (r, c) emptyCells[random.nextInt(emptyCells.length)]; _grid[r][c] random.nextInt(10) 0 ? 4 : 2; }这种优化在棋盘刚开局时看不出来但到游戏后期棋盘快满了差距会非常明显。我和 Trae 沟通这类优化时通常会在提示词里点名写“不要使用随机尝试直到成功的方式要先收集候选列表”这样 AI 会直接给出正确方案。4.3 UI 布局让 Trae 用 Flutter Widget 还原网格棋盘 UI 我让 Trae 用LayoutBuilderAspectRatio做了自适配。关键是外层一个正方形网格内部 4x4 个格子。Trae 生成的布局大概是这样的结构Column包含得分区和棋盘区。棋盘区用LayoutBuilder拿到可用宽高取较小值设为棋盘尺寸。每个格子用Expanded均分宽度。每个格子的背景色根据数值大小从浅到深变化。数字 2 和 4 用深色文字大数字用白色文字。颜色映射表直接让 Trae 按经典 2048 的配色生成。这里我给它的提示词是每个格子的背景色根据数值大小变化2 使用浅黄色4 使用淡橙色8 使用橙色16 使用橙红色32 使用红色64 使用深红128 及以上使用黄色系渐变。文字颜色请根据背景亮度自适应调整。实际上 Trae 生成的配色表基本不用改动。唯一的微调是 128 以上的字体会显得过大我在FittedBox里加了自适应缩放。Widget buildTile(int value) { return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: _bgColor(value), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: value 0 ? null : FittedBox( fit: BoxFit.scaleDown, child: Text( $value, style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: value 4 ? Colors.black54 : Colors.white, ), ), ), ); }4.4 状态管理ChangeNotifier 和 Listener 的配合我把Game2048类设计为ChangeNotifier在 Widget 里用ListenableBuilder监听变化每次移动更新后自动重建棋盘区域。这里有个性能细节整个棋盘每次移动都会完全重建一次但在 4x4 的规模下完全没问题不需要做更精细的局部更新。这个设计也方便以后加“动画”。每次移动时我开始是直接刷新数据后来发现没有动画的 2048 玩起来很生硬。于是让 Trae 在移动函数中给每个 tile 加一个位移偏移量然后用AnimatedPositioned或AnimationController去驱动。动画这块我放在第 5 节展开这里先不过多展开。5. 交互和动画键盘、手势和 Flutter Web 的坑5.1 键盘输入的焦点问题Flutter Web 特有的坑我在 PC 上开发第一件事就是让键盘方向键能控制游戏。Trae 给的建议是用KeyboardListener或Focus包裹棋盘区域监听onKeyEvent。实际操作后发现Flutter Web 页面加载后焦点并不自动落在Focus节点上必须点一下页面才能响应按键。这个问题不是 Trae 生成的代码有误而是 Flutter Web 的焦点机制决定的。我的做法是在initState里给FocusNode请求焦点override void initState() { super.initState(); _focusNode FocusNode(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); }同时在GestureDetector上支持点击点击时也请求焦点这样用户随便点一下棋盘或页面方向键就能用。这里还遇到一个细节浏览器页面自身有一些快捷键行为。比如 Safari 里按方向键会滚动页面Chrome 里也有类似行为。解决方式是在onKeyEvent里对方向键事件调用event.event.preventDefault()。Flutter 的KeyEvent没有直接暴露 preventDefault 方法我换用了HardwareKeyboard提供的KeyEvent处理。确切说我让 Trae 用的是Focus( onKeyEvent: (node, event) { if (event is KeyDownEvent) { // 处理方向键 } return KeyEventResult.handled; }, )。返回KeyEventResult.handled会阻止事件继续冒泡也就避免了浏览器默认行为。5.2 移动端手势原生手势 vs 自定义手势判断Flutter 里没有原生的“滑动方向”手势但我可以用GestureDetector的onPanEnd或onHorizontalDragEnd/onVerticalDragEnd来监听。因为 2048 只关心上下左右四个方向不关心滑动距离只关心滑动方向。我让 Trae 实现的方式是在onPanStart记录起始点在onPanEnd比较end - start的向量取 x 轴和 y 轴中位移更大的那个方向作为移动方向。Offset _startPos Offset.zero; void _onPanStart(DragStartDetails details) { _startPos details.globalPosition; } bool _onPanEnd(DragEndDetails details) { final dx details.globalPosition.dx - _startPos.dx; final dy details.globalPosition.dy - _startPos.dy; if (dx.abs() 20 dy.abs() 20) return false; // 过滤误触 if (dx.abs() dy.abs()) { _move(dx 0 ? Direction.right : Direction.left); } else { _move(dy 0 ? Direction.down : Direction.up); } return true; }20 像素的阈值是目前最舒服的值。太低了容易误触太高了滑动不灵敏。5.3 动画让 Trae 给 tile 加过渡效果2048 体验不好的一个重要原因是合并和滑动看起来“太瞬间”。我让 Trae 加了两层动画滑块位移每次移动后tile 的新位置应该从旧位置平滑过渡过去。新生成方块的缩放淡入每次新增的 2 或 4 应该从小变大出现。合并时的“弹一下”效果两个格子合并成一个时新格子有短暂放大再缩回的效果。实现位移动画我采用的是“给每个 tile 用 Stack AnimatedPositioned 定位”的方案。Trae 生成的代码结构大致是Stack( children: boardTiles.map((tile) AnimatedPositioned( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, left: tile.x * tileSize, top: tile.y * tileSize, child: _buildTile(tile), )).toList(), )这里有一个需要特别注意的点合并后的数字是立即变化的如果只靠位置动画两个 tile 会在同一时刻重叠并变成新数字观感上没有“合并”的爆发感。后来我加了合并动画层在合并发生的那一帧对新生成的 tile 施加一个从 1.0 放大到 1.2 再回到 1.0 的 scale 动画。这个效果需要AnimationControllerTrae 生成的是使用TweenSequence的版本。不过我后来发现TweenSequence在 Web 端某些浏览器下可能出现一个轻微卡顿原因是动画驱动导致重建整个棋盘。我的优化方式是只在合并的 tile 上做动画而不是对整个棋盘做。这一轮的提示词大概是给 2048 添加滑动和合并动画。滑动持续 150ms合并后的新 tile 需要在 100ms 内从 1.0 放大到 1.2再在 50ms 内回到 1.0。请用 Flutter 动画实现保持代码简洁。Trae 给出的实现里使用了AnimatedScale的思路说实话它不是直接用AnimatedScale而是用ScaleTransition 一个_AnimationController。核心代码能跑但性能一般我在性能优化那一步做了重点调整。5.4 无效移动的处理别让动画和状态不同步2048 有个隐式规则如果移动方向没有导致任何变化不生成新方块也不切换回合。一开始 Trae 生成的move函数会在每次按键后无条件调用addRandomTile这就导致了“无效移动也会出现新方块”的 bug。修复很简单在移动后对比移动前后的棋盘状态如果完全一样则直接返回不生成新方块。bool move(Direction direction) { final before _grid.map((row) Listint.from(row)).toList(); // 执行移动逻辑... if (_isSameGrid(before, _grid)) { return false; } addRandomTile(); if (!canMove(_grid)) { _gameOver true; } notifyListeners(); return true; }这个改动看似不起眼但直接影响游戏节奏。如果无效移动也生成新方块游戏难度会失衡后期玩家需要精确操作时会被莫名塞方块。6. 调试、构建和发布把 Trae 生成的代码推向生产环境6.1 单元测试的重要性AI 代码尤其需要验证我前面说过2048 的核心逻辑是纯函数所以非常适合写单元测试。我让 Trae 也生成了一份测试代码。不过我没有完全信任它而是自己先梳理了测试用例。关键用例至少包括左移[2, 2, 2, 0]→[4, 2, 0, 0]右移[0, 2, 2, 2]→[0, 0, 2, 4]一次移动合并一次[2, 2, 2, 2]→ 左移[4, 4, 0, 0]空棋盘添加随机方块只添加一个方块游戏结束判断满格且无可合并时返回 truetest(mergeLine 左移合并, () { final result mergeLine([2, 2, 2, 0]); expect(result, [4, 2, 0, 0]); }); test(mergeLine 一次移动只合并一次, () { final result mergeLine([2, 2, 2, 2]); expect(result, [4, 4, 0, 0]); }); test(canMove 满格不结束, () { final grid [ [2, 4, 2, 4], [4, 2, 4, 2], [2, 4, 2, 4], [4, 2, 4, 2] ]; expect(canMove(grid), false); });执行flutter test这些测试帮我在改动动画代码后能快速确认核心逻辑没被碰坏。AI 编程的代码往往看起来对但边界情况非常容易埋雷没有测试兜底我是不敢直接发布的。6.2 和 Trae 沟通 Bug把现象和期望结果一起贴给它开发过程中我最常用的一种反馈方式是把错误现象、当前代码片段、期望结果一起发到 Trae 对话框里。比如遇到移动方向反了的问题我的提示词是按下方向键上时方块向下移动了。请检查 move 方法中方向和矩阵坐标的映射关系。目前移动代码用 dx 和 dy 表示位移但键盘方向对应的是 -1, 0 / 1, 0 / 0, -1 / 0, 1 的偏移请修正并确保上移是行号减小。这种提示方式比单纯说“方向反了”有效得多。Trae 看到具体的方向映射关系后基本一次就能改对。还有一个经典问题移动后得分没有累加。这个 bug 相当隐蔽因为mergeLine函数只返回新数组没有返回合并产生的分数。我让 Trae 把 mergeLine 改成返回记录和新数组合在一起的方案({Listint line, int score}) mergeLineWithScore(Listint line) { int gained 0; Listint tiles line.where((e) e ! 0).toList(); Listint result []; for (int i 0; i tiles.length; i) { if (i 1 tiles.length tiles[i] tiles[i 1]) { result.add(tiles[i] * 2); gained tiles[i] * 2; i; } else { result.add(tiles[i]); } } while (result.length 4) { result.add(0); } return (line: result, score: gained); }Dart 3 的 record 语法在这里确实很好用。Trae 对这种新语法支持得也还可以没出什么错。6.3 构建体积优化从 6MB 到 2MBflutter build web默认使用 CanvasKit 渲染器这个渲染器效果更好但加载体积也大。我构建后的输出文件里main.dart.js有 1.9MBCanvasKit相关资源有 3.5MB合计接近 6MB。作为一个小游戏这个体积有点说不过去。我的优化方案包括开启--web-renderer html模式实测后发现 html 模式在很多浏览器上的渲染字体和 CanvasKit 有差异但体积确实更小。最终我还是选择保留 CanvasKit因为 2048 这种游戏的动画在 CanvasKit 下更流畅兼容性也更好。开启 gzip 压缩。静态服务器对main.dart.js启用 gzip 后实际传输体积降到 600KB 左右加载速度已经可以接受。用flutter build web --release而不是 debug 模式构建release 模式的 treeshaking 会让 JS 体积小很多。发布时我用的是 GitHub Pages直接把build/web目录传到 gh-pages 分支。这个流程本身很简单和 Flutter 或者 Trae 无关但如果你用其他静态托管平台比如 Cloudflare Pages 或 Vercel流程是一样的。一个实测中需要注意的细节2048 的网页要适配不同屏幕尺寸。我在布局上用了LayoutBuilder让棋盘始终保持在视口的 90% 宽度以内同时限制最大宽度为 500 像素避免在大屏上棋盘显得过大。这个适配在手机上也没问题。6.4 后续可能的功能扩展做完基础版后这个项目还可以扩展而 Trae 能一件件帮你实现。我在考虑的是撤销功能保存每次移动前的棋盘快照支持最多撤销一次。动画回放记录每一步操作支持回放整个游戏过程。最高分持久化用shared_preferences保存本地最高分Web 端也可以用 localStorage。AI 自动玩写一个简单的启发式算法让电脑自动玩 2048。其中“AI 自动玩”这个扩展很有意思核心算法是从当前棋盘开始模拟每种移动方向并评估结果棋盘的“空位数”和“平滑度”然后选择最优方向。这类算法代码本身不复杂让 Trae 写完全没有问题。7. 几点个人体会用 Trae 写这个项目的过程中我最深的体会是它不是一个能替你思考的工具而是一个能帮你把思考快速落地的工具。尤其是从 0 到 1 的项目你脑子里得先有“2048 该怎么做”的草图知道哪些模块怎么切分、哪些规则必须说清楚然后 Trae 才能把你的意图翻译成代码。具体来说有三条经验是可以复用的第一提示词里要写“规则”而不是“意图”。如果你只说“实现 2048 的逻辑”AI 会自动脑补一套自认为正确的规则但未必是你要的。把“先移动后合并”“每次移动只能合并一次”“无效移动不生成新方块”这些动作边界写明白生成结果基本不需要返工。第二让 AI 生成代码后一定要自己写测试用例去验证。AI 编程最怕的不是它写不出来而是它写得“看起来对但边界全错”的代码。2048 这种规则明确、状态可穷举的游戏是最适合用单元测试兜底的场景。测试通过后面加动画、加手势、加发布配置才敢放手改。第三把大任务拆成小任务分多次和 Trae 对话。一次对话塞太多需求容易生成一堆“缝合怪”代码。我这次是按“数据模型 → 移动合并 → 随机生成 → 判定 → UI → 动画 → 发布”这样逐块拆的每块一个小目标每块都验证通过后再进下一个。效率和准确率都比一上来让它生成整个项目高很多。
返回列表