news 2026/9/15 7:58:03

Trae + Flutter Web 开发2048小游戏:AI辅助编程到核心算法完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Trae + Flutter Web 开发2048小游戏:AI辅助编程到核心算法完整实践

最近被 Trae 种草得厉害,趁着周末把 2048 这个经典小游戏用 Flutter Web 完整重写了一遍。整个过程从创建 Flutter 项目、写游戏逻辑、做 UI 动效,到打包部署,几乎全程在 Trae 里完成,我自己动手改的代码不到十行。这篇文章就完整记录一下这次实操:如何把需求拆给 Trae、2048 的合并算法到底怎么设计、Flutter Web 在交互和动画上有哪些坑,最后整理出可以一键跑起来的完整代码。无论你是刚接触 AI 辅助编程的新手,还是想快速上手 Flutter Web 的开发者,这篇都能给你一套能直接抄的作业。

1. 项目概述与整体思路拆解

1.1 为什么偏偏是 Trae、Flutter Web 和 2048 这个组合

先聊技术选型,这部分决定了整个开发体验,值得多花点篇幅说清楚。

Trae 是目前对中文开发者很友好的一款 AI 编程 IDE,内置了智能体(Agent)、对话(Chat)和 Builder 三种模式,可以边对话边改代码,也能扔一句话让它从零生成整个项目。我的使用感受是:第一,中文提示词理解得好,不用费劲用英文描述需求;第二,Builder 模式可以直接生成多文件项目,而不是像早期工具那样只能生成单个函数;第三,模型切换自由,写 Dart 这种相对小众的语法时,可以选更稳的模型来兜底。如果你是刚接触 AI 编程,Trae 的上手成本确实比 Cursor 这类工具低一截,因为它对中文场景做了不少优化。

选 2048 作为开发对象,原因很实在。它看起来简单,但核心算法里包含数组压缩、方向转换、相邻合并、随机生成、终局判断等典型逻辑,复杂度刚好用来检验 AI 写代码的能力。写个 todo 列表试不出模型的边界,反过来开发完整商业项目 AI 又很难一次搞定,中途调起来也费劲。2048 就是“一句话能描述清楚,但实现起来有很多细节”的经典例子,特别适合拿来体验 AI 辅助开发。

Flutter Web 呢?选择它而不是原生小程序或者纯前端,是因为 Flutter 的布局和动画系统太适合这种网格类游戏了。GridView 两行代码就能画出 4x4 棋盘,AnimatedContainer 天然支持平滑过渡动画,这些要是从零手写 CSS 要折腾很久。而且 Flutter Web 编译产物是现代浏览器可直接运行的,一次写好,后续想套壳成桌面版、移动版也方便。

1.2 2048 的核心玩法:先拆清楚再动手

AI 编程和传统编程最大的区别是:你必须先把规则讲清楚,AI 才能写得出来。所以我做的第一件事不是打开 IDE,而是在纸上把 2048 的玩法拆成几条无歧义的规则。

  • 棋盘固定 4x4,每格为空或 2 的 N 次幂。
  • 每次操作选择上下左右四个方向之一。
  • 所有格子朝该方向移动,遇到相同数字则合并一次。
  • 合并最关键的细节:一行里相同数字只能合并一次,且合并优先级由贴近目标方向决定。
  • 每次有效操作后,在空白格随机生成一个数字,90% 概率为 2,10% 概率为 4。
  • 棋盘满且相邻没有可合并数字时游戏结束,出现 2048 即胜利。

几个边界情况要特别注意。比如[2,2,2,2]往左合并后是[4,4,0,0],不是[8,0,0,0][2,2,4,4]往左合并是[4,8,0,0][4,2,2,4]合并之后是[4,4,4,0]。这些细节 AI 第一次写很容易出错,所以后面必须有单元测试兜底。我实际测试的时候发现,模型对“一行只能合并一次”的理解经常有偏差,要么出现连环合并,要么漏掉该合并的相邻对。

1.3 项目文件结构规划

动手前先规划好项目结构,这一步看着简单,但对后续迭代影响很大。我让 Trae 按下面的结构初始化项目,逻辑和界面完全分离,后面调试会轻松很多。

lib/ main.dart // 程序入口 models/ game_model.dart // 核心游戏逻辑,纯 Dart 实现,不依赖 UI screens/ game_page.dart // 主界面,负责布局、手势、动画 widgets/ tile.dart // 单个数字格子的绘制组件

把所有游戏状态和算法都放进game_model.dart,界面层只负责渲染和交互。这种分层的好处是:核心逻辑可以直接用dart test测试,不用启动浏览器;将来如果想把游戏改成其他平台,逻辑代码一行都不用动。

2. 环境准备与 Trae 配置实操

2.1 Flutter Web 开发环境搭建

开发环境是老生常谈,但很多新手在这里就会卡住,我按实际操作的顺序过一遍。

首先是确认本机 Flutter 版本。2048 项目用到的 API 在 Flutter 3.10 及以上才稳定支持 Web,我本地用的是 3.22 版本,实测没遇到兼容问题。安装完 Flutter SDK 后先跑flutter doctor检查依赖,然后执行:

flutter config --enable-web flutter devices

flutter devices能看到 Chrome 设备,说明 Web 支持已经生效。接下来创建项目:

flutter create --platforms web,android,ios 2048_game

创建完项目后,建议先执行一次flutter precache --web,把 Web 相关的编译器工具下载到本地。这一步不做也行,但首次运行会因为现场下载卡很久。

2.2 Trae 安装与模型选择

Trae 的安装没啥好说的,官网下载对应系统的安装包,装完登录就行。真正影响体验的是模型选择。

打开 Trae 的设置界面,在模型配置里能看到它内置的几个模型选项。我的建议是:代码生成尽量选 Claude 系列或者 GPT 系的大模型,速度快、代码质量稳;如果是日常聊需求、梳理思路,可以选轻量级模型,省积分。实际开发中我发现一个规律:用中文描述需求时,不同模型对中文语义的理解差异挺大,有的模型会把“合并一次”误解成“能合并就连环合并”,遇到这种情况就果断切换模型重新生成。

关于积分消耗,可以给大家一个相对合理的预期。Trae 的 Builder 模式一次生成多个文件、大段替换代码时,消耗积分会比普通 Chat 快,这是正常的。日常小改动用 Chat,不要一上来就派 Builder 大改特改。新用户送的免费资源足够完成 2048 这种量级的项目,遇到积分不够的情况,官方渠道有获取方式,保持正常使用节奏就行。

2.3 让 Trae 搭建初始骨架

环境准备好之后,我在 Trae 里新建了一个项目窗口,把之前规划的目录结构写进提示词,让 Builder 模式生成初始骨架。第一版提示词是这么写的:

请用 Flutter 创建一个 2048 小游戏项目,运行目标为 Web 和移动端。 项目结构要求: lib/main.dart 为入口; lib/models/game_model.dart 负责所有游戏逻辑,纯 Dart 实现,不依赖 UI; lib/screens/game_page.dart 负责界面布局和交互; lib/widgets/tile.dart 负责绘制单个数字方块。 第一版先实现:4x4 棋盘、随机生成数字、上下左右滑动合并、分数统计、重新开始。 界面风格参考经典 2048 深色背景,卡片式数字块。 请先生成完整的可运行代码,然后告诉我运行命令。

Trae 跑了一段时间后,直接生成了完整的文件结构和代码。我打开终端执行flutter run -d chrome,第一版居然直接跑起来了。不过运行起来只是开始,接下来才是重头戏——算法对不对,界面好不好看,都要一点点验证和调优。

3. 核心逻辑实现:game_model.dart

3.1 数据结构与状态管理

先看第一版生成的数据结构。我让 Trae 把所有状态封装在一个GameModel类里,界面通过它来读取棋盘和分数,每次操作后由模型返回新的状态。

class GameModel { final int size; List<List<int>> grid; int score; int bestScore; bool gameOver; bool won; GameModel({this.size = 4}) : grid = List.generate(4, (_) => List.filled(4, 0)), score = 0, bestScore = 0, gameOver = false, won = false; factory GameModel.fromState(GameModel old) { final model = GameModel(size: old.size); model.grid = old.grid.map((row) => List<int>.from(row)).toList(); model.score = old.score; model.bestScore = old.bestScore; model.gameOver = old.gameOver; model.won = old.won; return model; } }

这里有个值得注意的点:fromState工厂方法做了深拷贝。如果没有这一层深度复制,界面在使用旧状态做动画对比时,新旧状态会引用同一个数组,动画就会失效。这是 AI 生成代码时容易忽略的细节,Review 时需要特别注意。

为了支持动画,界面需要知道“移动前”和“移动后”两份状态。一开始 Trae 生成的是原地修改数组的版本,我追加了一个要求:“每次移动返回操作前后的棋盘状态”,这样 UI 层可以对比两份状态来做过渡动画。

3.2 移动与合并算法:旋转法为什么最稳

2048 的四个方向移动,最优雅的实现方式是旋转法:把任意方向的移动统一转换成“向左移动”。整个思路很简单:

  1. 如果是向右移动,先把棋盘水平翻转,左移之后再翻转回来。
  2. 如果是向上移动,先把棋盘逆时针旋转 90 度,左移之后再顺时针转回来。
  3. 向下移动同理,旋转两次或者反向旋转。

这样只需要实现一个mergeLeft,四个方向都搞定了。Trae 生成的核心代码如下:

List<int> mergeLine(List<int> line) { final nums = line.where((e) => e != 0).toList(); final result = <int>[]; var i = 0; while (i < nums.length) { if (i + 1 < nums.length && nums[i] == nums[i + 1]) { result.add(nums[i] * 2); i += 2; // 合并后跳过下一个元素,保证一行只合并一次 } else { result.add(nums[i]); i++; } } while (result.length < line.length) { result.add(0); } return result; } List<List<int>> rotate(List<List<int>> grid, {bool clockwise = true}) { final n = grid.length; final result = List.generate(n, (_) => List.filled(n, 0)); for (var i = 0; i < n; i++) { for (var j = 0; j < n; j++) { if (clockwise) { result[j][n - 1 - i] = grid[i][j]; } else { result[n - 1 - j][i] = grid[i][j]; } } } return result; }

mergeLine里有几处细节值得展开讲讲。

第一,先过滤掉所有 0,把非零元素压缩到一起,这是移动的本质。比如[0,2,0,4]压缩后就是[2,4],移动完成后补零变成[2,4,0,0]

第二,合并时用i += 2跳过被合并的元素。这一步保证同一位合并后不会继续参与下一次合并,[2,2,2,2]才能正确得到[4,4,0,0]而不是[8,0,0,0]。这就是“一行只能合并一次”的具体实现。

第三,旋转函数的坐标变换容易写反。我测试时遇到过一个经典问题:向上移动变成了向下移动,根源是旋转方向反了。遇到这种情况快速验证的办法是,准备一个非对称棋盘,比如第一行[2,4,8,16],其余全零,分别测试四个方向,一眼就能看出方向对不对。

四个方向的移动统一封装:

bool moveLeft() { var moved = false; final newGrid = List.generate(size, (_) => List.filled(size, 0)); for (var r = 0; r < size; r++) { final merged = mergeLine(grid[r]); if (merged.toString() != grid[r].toString()) moved = true; newGrid[r] = merged; } if (moved) { grid = newGrid; addRandomTile(); } checkGameState(); return moved; }

moved标记非常重要:只有棋盘真正变化了,才允许生成新数字。如果用户往一个没有空间移动的方向滑动,棋盘不能变化,更不能生成新数字。这个细节决定了游戏手感,Trae 第一版就漏了,导致连续往一个方向按,棋盘会一直新增数字。

3.3 随机生成与胜负判断

随机生成新数字的逻辑很直接:收集所有空格子,从里面随机挑一个,按 90%/10% 的概率放 2 或 4。

void addRandomTile() { final emptyCells = <(int, int)>[]; for (var r = 0; r < size; r++) { for (var c = 0; c < size; c++) { if (grid[r][c] == 0) emptyCells.add((r, c)); } } if (emptyCells.isEmpty) return; final pos = emptyCells[Random().nextInt(emptyCells.length)]; grid[pos.$1][pos.$2] = Random().nextDouble() < 0.9 ? 2 : 4; }

这里有个经验:Random实例最好作为模型的一个字段复用,每次调用都new Random()虽然也能用,但在频繁操作时容易出现重复序列,影响随机性。AI 生成代码时习惯每次现用现 new,我一般会手动改成字段级别。

胜负判断分两部分。胜利很简单,只要棋盘上出现 2048;失败则需要遍历所有格子,检查是否还有空白格,以及上下左右是否存在相邻相同数字:

void checkGameState() { for (var r = 0; r < size; r++) { for (var c = 0; c < size; c++) { if (grid[r][c] == 2048) won = true; if (grid[r][c] == 0) return; if (r + 1 < size && grid[r][c] == grid[r + 1][c]) return; if (c + 1 < size && grid[r][c] == grid[r][c + 1]) return; } } gameOver = true; }

很多新手写失败判断时只检查满盘,结果出现“满盘但还能合并”的情况,游戏提前判定结束。必须同时检查相邻相同数字,这一步不能省。

3.4 用单元测试给算法上保险

算法逻辑是整个游戏的灵魂,光靠肉眼在浏览器里点几下根本测不全。我把核心用例直接写成了 Dart 单测,每次改完代码跑一遍,有问题立刻暴露。

测试场景输入期望结果
基本左移[2,0,0,2][4,0,0,0]
一行只能合并一次[2,2,2,2][4,4,0,0]
交错合并[2,2,4,4][4,8,0,0]
不连续数字[4,2,2,4][4,4,4,0]
无操作允许[2,4,8,16]左移棋盘不变,不生成新数
失败判定满盘且无相邻相同gameOver = true
旋转方向非对称棋盘向上向上合并正确

用表格列出来之后,测试意图一目了然。跑测试的命令:

flutter test

Trae 生成的代码第一次跑测试,通过了大部分用例,但“无操作允许”这个用例挂了——原因就是前面说的moved标记缺失。AI 在“功能实现”上很强,但在“边界行为约束”上需要你主动提需求。把测试用例直接贴给它看,它改起来很快。

4. UI 与交互动效实现

4.1 主界面布局

界面层game_page.dart的布局其实很套路:顶部放标题和分数卡片,中间放棋盘,底部放重新开始按钮。这里我要求 Trae 用LayoutBuilder做响应式适配,让棋盘在手机、平板和浏览器窗口下都能保持正方形并且不溢出。

LayoutBuilder( builder: (context, constraints) { final size = constraints.maxWidth < constraints.maxHeight ? constraints.maxWidth * 0.9 : constraints.maxHeight * 0.7; return Column( children: [ // 顶部分数区 Row( children: [ Text('2048', style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold)), Spacer(), _ScoreCard(label: '分数', value: model.score.toString()), SizedBox(width: 8), _ScoreCard(label: '最高分', value: model.bestScore.toString()), ], ), // 棋盘区 Container( width: size, height: size, child: GridView.builder( padding: EdgeInsets.all(12), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10, crossAxisSpacing: 10, ), itemBuilder: (context, index) => TileView(value: model.grid[index ~/ 4][index % 4]), ), ), // 重新开始按钮 ElevatedButton(onPressed: () => setState(() => model.reset()), child: Text('重新开始')), ], ); }, )

LayoutBuilder是 Flutter Web 做响应式的关键。如果直接写死宽高,窗口一拉伸棋盘就变形或者溢出。我见过很多 AI 生成的 Flutter 小游戏在这个点翻车,原因就是布局没用约束动态计算。

4.2 数字格子的配色与动画

数字格子的经典设计是不同数字对应不同背景色,数字越大颜色越深。这个用 Flutter 的AnimatedContainer可以同时搞定颜色和尺寸的过渡动画。

class TileView extends StatelessWidget { final int value; const TileView({Key? key, required this.value}) : super(key: key); Color get _backgroundColor { return switch (value) { 0 => Color(0xFFCDC1B4), 2 => Color(0xFFEEE4DA), 4 => Color(0xFFEDE0C8), 8 => Color(0xFFF2B179), 16 => Color(0xFFF59563), 32 => Color(0xFFF67C5F), 64 => Color(0xFFF65E3B), 128 => Color(0xFFEDCF72), 256 => Color(0xFFEDCC61), 512 => Color(0xFFEDC850), 1024 => Color(0xFFEDC53F), 2048 => Color(0xFFEDC22E), _ => Color(0xFF3C3A32), }; } @override Widget build(BuildContext context) { if (value == 0) { return Container( decoration: BoxDecoration( color: _backgroundColor, borderRadius: BorderRadius.circular(8), ), ); } return AnimatedContainer( duration: const Duration(milliseconds: 120), curve: Curves.easeInOut, decoration: BoxDecoration( color: _backgroundColor, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text( '$value', style: TextStyle( fontSize: value >= 100 ? 22 : 30, color: value <= 4 ? Color(0xFF776E65) : Colors.white, fontWeight: FontWeight.bold, ), ), ); } }

数字格子的动画,我让 Trae 加了一个新数字弹出效果:刚生成的数字格子用AnimatedScale从 0 放大到 1,模拟“出现”的视觉反馈。这个细节对游戏手感提升非常明显,没有它,新数字就是突然冒出来,很生硬。

TweenAnimationBuilder<double>( tween: Tween(begin: 0, end: 1), duration: Duration(milliseconds: 180), builder: (context, scale, child) { return Transform.scale(scale: scale, child: child); }, child: tileContent, )

4.3 手势与键盘控制

Flutter Web 同时要处理触屏和鼠标拖拽。用GestureDetectoronPanUpdate,比较位移的横纵分量来决定方向,这套逻辑触屏和鼠标拖拽通用。

GestureDetector( onPanUpdate: (details) { final dx = details.delta.dx; final dy = details.delta.dy; if (dx.abs() < 8 && dy.abs() < 8) return; // 忽略微小抖动 if (dx.abs() > dy.abs()) { dx > 0 ? model.moveRight() : model.moveLeft(); } else { dy > 0 ? model.moveDown() : model.moveUp(); } setState(() {}); }, child: boardWidget, )

Web 端还有一个提升体验的小细节:支持键盘方向键。Flutter Web 上用KeyboardListener监听按键事件,把上下左右键映射到对应移动。一开始我没想到这个,是 Trae 生成代码时自动加的,后来发现用键盘玩比鼠标拖拽顺手得多,尤其是在笔记本上。

5. 用 Trae 提升开发效率的几个实用姿势

5.1 三套亲测有效的提示词模板

用 Trae 这段时间我总结出一个核心心得:提示词质量直接决定代码质量。同样的需求,描述方式不同,结果可能是“能跑”和“能直接用”的区别。

第一套是“结构先行”模板,适合项目起步阶段。把目标、技术栈、目录结构、功能清单、界面风格一次说清楚。前面的初始骨架提示词就是这个思路,重点是要给出明确的文件划分。

第二套是“给定方案求实现”模板,适合算法和复杂逻辑。比如在实现合并算法时,我先告诉 Trae 思路是旋转法,再让它写代码。这样不是把控制权交给 AI,而是让 AI 做执行者,效果稳定得多。

请按下面的方案实现 2048 移动逻辑: 1. 所有操作统一转换为向左移动; 2. 向左移动时先压缩非零元素,再相邻合并,合并一次后跳过; 3. 使用旋转函数处理上下右方向; 4. 返回是否发生了移动。请生成 game_model.dart 中相关方法。

第三套是“报错复述”模板,适合排查问题。把 Trae 自己的报错信息原样贴在对话里,再加上一句“请分析根因并修复”,基本都能快速解决。我见过很多人喜欢自己翻译一遍报错,结果把关键信息弄丢了,没必要。

5.2 让 Trae 改 Bug 的正确姿势

AI 改 Bug 最忌讳的是只给一句“这里有问题”。正确的姿势是:把期望行为和实际行为都描述清楚,最好附上复现步骤。

我之前遇到一个触摸滑动方向错乱的问题,按照下面的描述丢给 Trae,它很快就定位到了问题:

游戏运行在 Web 端,使用鼠标拖拽向右滑动,棋盘的格子却向上移动。请检查手势方向和棋盘移动方向的对应关系,并修复。

原因其实是手势的delta.dxdelta.dy的坐标系与旋转函数的坐标系不一致,导致方向映射错位。Trae 迅速给出了修改方案,并顺手加了方向测试。这次体验让我意识到:AI 不是不能用,而是你得给它准确的操作上下文。

5.3 关于积分与模型使用的个人经验

用 Trae 时间长了,对积分消耗也有了一些感受。Builder 模式生成整个项目时消耗较大,但这是必要支出;后续小修小改用 Chat 模式很省。我习惯把大改动集中起来一次让 Builder 执行,而不是改一行就触发一次大范围重写,这样积分消耗可控。

另外,如果某个模型连续两次生成的代码都不对,不要死磕,立刻换模型重试。不同模型对 Dart/Flutter 语法的熟悉程度差异不小,有的模型擅长生成 UI,有的擅长逻辑算法,灵活切换比反复修改高效得多。

6. 常见问题与排查实录

开发过程中踩了不少坑,我把有代表性的问题整理成一个速查表,供大家参考。

问题现象解决方案
Flutter Web 首次运行很慢执行 flutter run 后长时间白屏提前执行flutter precache --web预下载 Web 编译工具
棋盘溢出屏幕窄窗口下棋盘变形或被截断使用 LayoutBuilder 根据窗口尺寸动态计算棋盘边长,保持正方形
滑动方向错乱向左滑动格子却向右移动检查旋转函数的坐标变换,用非对称棋盘分别测试四个方向
连续按同一方向不停出数字棋盘已满但还在生成新数字移动后先判断moved标志,只有棋盘变化才调用addRandomTile
新数字没有入场动画数字瞬间出现很生硬用 TweenAnimationBuilder 包裹格子,做 scale 从 0 到 1 的动画
游戏提前结束满盘但还有可合并数字却提示 Game Over检查失败判定是否包含相邻相同数字的检测
Trae 报错“检测到内容违反社区规范”对话无法继续说明提示词触发了规则,换个表述重新描述需求即可
保存后部分代码被自动删除或格式化错乱文件内容与刚才不一致少数版本存在编辑竞态问题,改完等 1 到 2 秒再切窗口,必要时用版本回滚恢复

有两个问题值得展开说。第一个是 Flutter Web 打包后首屏加载慢的问题,这个几乎每个用 Flutter Web 的人都会遇到。原因是默认构建没有做 gzip 压缩,引擎文件体积较大。部署到服务器时开启静态资源压缩,首屏体验会好很多。

第二个是 Trae 编辑器的自动格式化问题。我遇到过一次保存后大段代码消失的情况,后来查了社区才发现是自动格式化与手动编辑发生了竞态。现在的做法是:涉及大段修改时,先让 Trae 生成到单独文件,人工 Review 后再合并进主项目,避免直接在编辑窗口里反复横跳。

# 构建生产版本 flutter build web --release # 产物目录 build/web/

构建产物就是一个纯静态目录,里面的main.dart.js是编译后的核心文件,assets文件夹里是素材资源。把这个目录丢到任意静态文件服务器上就能运行,不需要额外的后端。

7. 完整代码与运行方式

标题说了附完整代码,这里把最核心的两个文件完整贴出来。game_model.dart是纯逻辑,直接复制就能用;game_page.dart给出关键部分,其余布局代码对照前面小节自行补全。

7.1 核心逻辑 game_model.dart 完整代码

import 'dart:math'; class GameModel { final int size; late List<List<int>> grid; int score = 0; int bestScore = 0; bool gameOver = false; bool won = false; final Random _random = Random(); GameModel({this.size = 4}) { reset(); } void reset() { grid = List.generate(size, (_) => List.filled(size, 0)); score = 0; gameOver = false; won = false; addRandomTile(); addRandomTile(); } GameModel copy() { final model = GameModel(size: size); model.grid = grid.map((row) => List<int>.from(row)).toList(); model.score = score; model.bestScore = bestScore; model.gameOver = gameOver; model.won = won; return model; } static List<int> mergeLine(List<int> line) { final nums = line.where((e) => e != 0).toList(); final result = <int>[]; var i = 0; while (i < nums.length) { if (i + 1 < nums.length && nums[i] == nums[i + 1]) { result.add(nums[i] * 2); i += 2; } else { result.add(nums[i]); i++; } } while (result.length < line.length) { result.add(0); } return result; } static List<List<int>> rotate(List<List<int>> grid, {bool clockwise = true}) { final n = grid.length; final result = List.generate(n, (_) => List.filled(n, 0)); for (var i = 0; i < n; i++) { for (var j = 0; j < n; j++) { if (clockwise) { result[j][n - 1 - i] = grid[i][j]; } else { result[n - 1 - j][i] = grid[i][j]; } } } return result; } bool moveLeft() { var moved = false; final newGrid = List.generate(size, (_) => List.filled(size, 0)); for (var r = 0; r < size; r++) { final merged = mergeLine(grid[r]); if (merged.toString() != grid[r].toString()) moved = true; newGrid[r] = merged; } if (moved) { grid = newGrid; addRandomTile(); } checkGameState(); return moved; } bool moveRight() { return moveWithTransform(clockwise: false, times: 2); } bool moveUp() { return moveWithTransform(clockwise: true, times: 1); } bool moveDown() { return moveWithTransform(clockwise: false, times: 1); } bool moveWithTransform({required bool clockwise, required int times}) { var temp = grid; for (var i = 0; i < times; i++) { temp = rotate(temp, clockwise: clockwise); } grid = temp; final moved = moveLeft(); var reverse = temp; for (var i = 0; i < (clockwise ? 4 - times : times); i++) { reverse = rotate(grid, clockwise: !clockwise); } grid = reverse; bestScore = max(bestScore, score); return moved; } void addRandomTile() { final emptyCells = <(int, int)>[]; for (var r = 0; r < size; r++) { for (var c = 0; c < size; c++) { if (grid[r][c] == 0) emptyCells.add((r, c)); } } if (emptyCells.isEmpty) return; final pos = emptyCells[_random.nextInt(emptyCells.length)]; grid[pos.$1][pos.$2] = _random.nextDouble() < 0.9 ? 2 : 4; } void checkGameState() { for (var r = 0; r < size; r++) { for (var c = 0; c < size; c++) { if (grid[r][c] == 2048) won = true; if (grid[r][c] == 0) return; if (r + 1 < size && grid[r][c] == grid[r + 1][c]) return; if (c + 1 < size && grid[r][c] == grid[r][c + 1]) return; } } gameOver = true; } }

注意moveWithTransform里的方向变换逻辑,这是整个实现里最容易绕晕的地方。如果你直接让 Trae 生成四方向方法,它大概率会复制粘贴四遍moveLeft然后各自改坐标,那样代码又长又容易出错。旋转法虽然理解成本高一点,但代码量小很多,维护起来也省心。

7.2 入口 main.dart 完整代码

import 'package:flutter/material.dart'; import 'screens/game_page.dart'; void main() { runApp(const GameApp()); } class GameApp extends StatelessWidget { const GameApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: '2048', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blueGrey, scaffoldBackgroundColor: const Color(0xFFFAF8EF), ), home: const GamePage(), ); } }

7.3 运行方式

把上述代码放进项目对应目录,执行:

flutter pub get flutter run -d chrome

浏览器打开后即可游玩。如果想打包部署:

flutter build web --release

build/web目录就是最终产物。还可以给 Flutter Web 做额外的配置优化,比如在web/index.html里加 loading 样式,公众号或者博客部署时可以配合 nginx 的 gzip 模块开启压缩,首屏体积能省将近 70%。

8. 后续扩展方向

2048 虽然做完了,但整个项目还能往很多方向扩展,我列几个自己计划尝试的方向,给大家提供参考。

一是加入动画回放和“撤销”功能。每次移动前把GameModel的快照压进栈,撤销时弹出来即可。这个功能对 AI 来说实现成本很低,但游戏体验提升很大,关键时刻按错方向真的能救人一命。

二是接入本地排行榜。Flutter Web 可以用shared_preferences或者localStorage保存最高分。这里有个细节:Flutter Web 的shared_preferences底层就是 localStorage,调await prefs.setInt('bestScore', score)就能持久化,不需要后端。

三是给游戏加“AI 自动演示”模式。理论上可以用搜索算法(比如期望极小化算法)让 AI 自动玩 2048,用来生成教学视频或者单纯的观赏效果。这一块需要单独写一个 AI 决策模块,和游戏逻辑完全解耦。

四是界面主题切换。经典 2048 是深色的,但很多人喜欢浅色主题。把配色方案抽成一个ThemeExtension,切换主题就是改一个类名的事。这几个扩展做完,2048 这个小项目基本上就变成一套完整的 Flutter 学习案例了。

最后再分享一个小技巧:如果你想让 Trae 帮你扩展功能,不要只发一句话“加个主题切换”,而是先告诉它现有的文件结构、关键类名和数据流,再提出需求。AI 对上下文的依赖远比你想象中高,给足上下文,它生成的代码才能跟现有架构无缝衔接。

我个人在实际操作中的体会是:用 Trae 开发小程序,最耗时间的往往不是写代码本身,而是“把需求说清楚”以及“验证生成结果的正确性”。尤其像 2048 这种有明确规则的逻辑,单元测试就是最好的校验工具。AI 可以帮你快速产出初版代码,但能不能稳定运行、边界情况处不处理得好,还是得靠你把关。想用 Trae 提升效率,核心是养成写测试、跑测试、再让 AI 修问题的习惯,这套流程跑顺了,后面开发什么都能快人一步。

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

RS485与Modbus RTU在电动快换模块通信中的工业级协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:54:30

Chrome插件MV3与端侧AI工程化实战指南

1. 这不是“加个弹窗”就能交差的时代&#xff1a;一个真实插件工程师的切肤之感“浏览器插件”这五个字&#xff0c;现在听上去像十年前的“网页小动画”——表面轻巧&#xff0c;内里早已脱胎换骨。我2015年靠写个自动填表脚本入行&#xff0c;那时Chrome扩展商店里90%的插件…

作者头像 李华
网站建设 2026/9/15 7:53:20

腾讯云免费SSL证书申请与Nginx部署HTTPS完整指南

说句实话&#xff0c;现在打开浏览器&#xff0c;要是地址栏没有那把小锁&#xff0c;我第一反应就是这网站不太靠谱。前阵子帮朋友把个人博客从裸HTTP迁到HTTPS&#xff0c;在腾讯云申请免费SSL证书、再配合Nginx做部署&#xff0c;整个过程把常见坑基本踩了一遍。今天就把完整…

作者头像 李华
网站建设 2026/9/15 7:52:22

国微CMS站群系统源码zip:部署、权限与调优实战

简介&#xff1a;基于PHP的国微CMS部队门户站群系统源码&#xff0c;面向部队信息化建设人员及具备一定PHP后端基础的开发者&#xff0c;用于构建和运维多层级部队门户站群&#xff0c;解决内容发布、站点统一管理与权限控制等实际问题&#xff1b;适合希望深入部队信息化项目开…

作者头像 李华
网站建设 2026/9/15 7:50:33

深入解析sun.misc.Unsafe:JVM底层魔法类如何支撑高并发框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:50:08

DataHub调研报告

执行摘要 核心判断&#xff1a; DataHub 是当前开源元数据平台中架构最完整、生态最广的项目之一&#xff0c;其"流式实时 Schema-first 建模 联邦式服务"的设计已在超大规模场景&#xff08;LinkedIn 级&#xff0c;官方称千万级资产、十亿级关系&#xff09;得到…

作者头像 李华