从空目录到上线:我全程用 TRAE 做了一款微信小游戏合集(含一次完整架构重构)

本文正在参加 #全新 TRAE 实战体验

赛道:全链路开发实战

这是一篇 "AI 当主力开发" 的实战复盘:整个项目从 0 到 1 的每一行代码都是 TRAE 写的 (包括最初的项目搭建)。

我的角色是产品经理 + 验收者:提想法、定验收标准、试玩给反馈、截屏记录

TRAE 的角色是全栈工程师:技术选型、目录设计、编码、调试、重构、搭建测试,一条龙跑完

项目先后经历两代架构,分别落在两个分支:

  1. main 分支是 Taro + React + TypeScript 微信小程序版
  2. feature/minigame 分支是 重构后的原生微信小游戏版

两代累计 5 万行级代码变更,全程在 TRAE 里完成。

第一代架构(main 分支):TRAE 从空目录搭起 Taro 小程序

项目第一步就是让 TRAE 直接起项目,我给 TRAE 的原始需求只有一句话级别的想法:

帮我做一个微信小游戏,其有多种不同的小游戏,现在暂时做两个:

  1. 做一个二维码迷宫小游戏,二维码就是迷宫,并且扫二维码可以看到正确的路线
  2. 做一个平衡球小游戏,靠手机的重力模块,通过手机倾斜、转动,让小球从一边的起点,滚到另一边的终点,中间有障碍和洞,掉下洞直接失败

它做了完整的技术选型决策:目标是"低成本快速验证玩法",选 Taro(React + TypeScript + SCSS)开发,并主动说明了理由 ------ 组件化开发 UI 页面快,TS 保证游戏逻辑可维护。

随后一次性产出了整个工程骨架:

js 复制代码
    src/
    ├── pages/          # index / qrmaze / balance / records / mine
    ├── utils/          # maze.ts / qr.ts / storage.ts / format.ts / haptic.ts
    ├── data/           # levels.ts 确定性关卡工厂
    └── types/          # game.ts 全局类型

如下图:

重点

TRAE 直接给我搭建项目结构,并引入需要的插件,真的强大!

TRAE 基本上可以理解我说的全部需求,并能生成基本不需要人工审核或者改动的代码,我很多情况都是直接看界面就行,说出界面上的问题,根本没有看代码实现!

过程

后续就是我不断的跟TRAE沟通,去达到我想要的效果!

如图:

到后面菜鸟又让其加了一个游戏,并对老的游戏进行优化:

重点

遇见界面上的问题,也可以直接截图发给TRAE,甚至可以框起来告诉TRAE,TRAE也可以完全理解我描述的问题!

如图:

最终产物

反正通过TRAE优化了很多次,这里就不列举了,最终产出包含三个玩法:

  1. 二维码迷宫:迷宫的墙本身就是一张真实可扫的二维码 ------ 用程序扫码后,会出现最优路线的线,直接照着走就行
  2. 火力全开:竖版射击,带小队机制、算式门二选一、精英怪、BOSS 战特效
  3. 重力平衡球:倾斜手机控制小球避开深渊洞(漩涡)滚进传送门

外加首页、战绩统计、我的(tabBar)三个页面。

不吹不黑

不吹也不黑,功能实现、完成度、代码质量TRAE完成得是真的不错,但是没有画师在场,这个游戏简陋也确实是简陋(其实可以用TRAE Work,即现在的 Agent 模式辅助生成插画,双管齐下,这里菜鸟是企业用户搞不了,咋普通用户还比企业用户先用上了 (T ^ T))

二、小程序发不成功,发现小游戏只能小游戏类目:TRAE 完成 Taro → 原生小游戏迁移

玩法全部跑通、准备发布时,我才知道踩中了一个平台规则的坑:项目本身是小游戏,但微信不允许用"小程序"类目发布游戏。想发出去只有一条路------把账号类目切换为"小游戏",并更换新的小游戏 AppID。

类目一换,麻烦立刻出现:Taro 构建的小程序代码在小游戏运行时里根本跑不起来。

小程序和小游戏是两套完全不同的运行时 ------ 小程序要 app.js + 页面路由 + WXML 组件体系,而小游戏只认 game.js 这一个入口,没有页面、没有组件,一切 UI 都要在 Canvas 上自己画。

代码一行没改,但就是运行起不来。

我让 TRAE 评估两条路:是继续在 Taro 上找小游戏适配方案,还是整体迁移原生小游戏?

TRAE的结论很直接:Taro 的小游戏支持对这类 "单 Canvas 逐帧循环" 的项目帮助有限,组件模型帮不上忙反而多一层桥接;而这个项目三个玩法全靠 Canvas,原生小游戏 + 手绘 UI 才是正路。

重构决策就这么来的 ------ 不是性能优化,是平台规则导致的重构。

于是整包重构:141 个文件、+24332 / -33181 行,等于把整个项目按小游戏运行时重写了一遍。TRAE 在 Builder 模式下产出了新架构骨架,再逐场景"翻译"旧逻辑:

js 复制代码
minigame/
├── game.js / game.json
├── jsQR.js               # 出口扫码验证
├── js/
│   ├── games/shooter/    # engine / timeline / render / layout / constants
│   ├── scenes/           # home / qrmaze / shooter / balance / records / mine
│   ├── qr.js             # 自实现 QR 编码器(v1~8)
│   ├── maze.js           # 迷宫生成(524 行)
│   ├── levels.js         # 确定性关卡工厂
│   └── ui.js / scroll.js / tabbar.js / haptic.js / icons.js

小游戏架构:

小程序架构:

重点

整个重构非常的丝滑,且完全是等比移植,只有样式有点细节调整,游戏体验基本完全一致!

三、TRAE 在全链路各环节的真实表现

1. 需求 → 方案:会反问、会给依据

"做一个小游戏合集"这种模糊需求,TRAE 没有直接开写,而是先确认玩法范围、离线要求和验收方式,再给技术方案。

选型理由能落到"为什么是这个而不是那个",而不是无脑堆新技术。

2. 编码:跨文件的结构性改动是强项

迁移阶段最典型 ------ 一次对话让 TRAE 把 226 行的 TypeScript 射击时间线(timeline.ts)翻译成 CommonJS 的 timeline.js,波次编排和"减益保底"语义完整保留。

这种跨 20+ 文件的结构性工程,手写至少按天算,TRAE 是按分钟算。

3. 调试:给现象,拿根因

两个印象最深的修复,都是我描述现象、TRAE 定位根因:

  • 边缘弹幕漏怪:怪物从屏幕边缘死角漏进防线,TRAE 定位到弹幕生成的边界采样区间缺陷

  • 加固路线时墙格被误删 (main 分支 eff053c):迷宫"加固"功能破坏已生成的墙,根因是回填顺序与去重逻辑冲突。这类跨函数状态问题,TRAE 通读调用链的效率远高于我加断点。

4. 测试:它自己搭了验证体系

这是我认为 TRAE 最出彩的部分。

因为我做的时候提及了关卡是确定性生成的、逻辑层不依赖微信运行时,TRAE 顺手搭了一套 Node 冒烟测试 (scripts/minigame-smoke.cjs):注入一个 wx stub,就能在 Node 里跑关卡工厂、QR 推导和射击引擎的回归验证。

四、两个最有含金量的技术点(全是 TRAE 产出)

1. 手写 QR 编码器 + jsQR 回环验证

二维码迷宫的墙必须是一张真实可扫 的二维码,而小游戏环境没有现成的 Canvas 二维码库。TRAE 自实现了一个 QR Model 2 编码器(qr.js):Byte 模式 UTF-8、纠错等级 L、支持版本 1 ~ 8(21 ~ 49 模块),v6+ 按规范做 2 个 RS 块交织,底层是 GF(256) 域上基于本原多项式 0x11D 的 RS 纠错。

验证方式很硬核 ------ 用 jsQR 做解码回环:把编码器输出的 0/1 矩阵栅格化成 RGBA,喂给 jsQR 解码,解出的字符串必须等于输入的 8 位关卡码。

2. 确定性关卡工厂 + Node 冒烟测试

所有关卡由 mulberry32(seed) 从关号推导:第 N 关的 QR 版本、迷宫策略、最低步数(100+)、射击波次全部由 N 决定。配合上面的 wx stub:

bash 复制代码
node scripts/minigame-smoke.cjs
# ✓ 关卡工厂 / QR 迷宫推导 + jsQR 扫码回环 / 射击时间线与引擎

这套"逻辑层纯函数化 + wx stub + Node 冒烟"的模式,给 TRAE 的每次改动提供了可执行的验收标准 ------ 它改完,我试玩几关就行。

我只验手感,机器校验逻辑!

关卡生成逻辑

js 复制代码
// ============================================
// 关卡数据
// ============================================

/** 二维码迷宫关卡配置 */
export interface QRMazeLevel {
  id: number;
  name: string;
  /** QR 版本(决定迷宫尺寸:v4=33格, v5=37格, v6=41格, v7=45格, v8=49格) */
  version: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
  /** 二维码内容(可被真实扫码识别,含盐值后缀) */
  content: string;
  /** 路线策略:short 简单 / long 困难 */
  strategy: "short" | "long";
  /** 最佳路线步数下限(确定性搜索尽量保证达标) */
  minSteps: number;
}

/** 关卡主题名(前 10 关),之后为「无尽迷阵 N」 */
const MAZE_NAME_POOL = [
  "初识密文",
  "回纹密林",
  "双重迷阵",
  "深渊方阵",
  "时序回廊",
  "校准星图",
  "掩码幻境",
  "光路折返",
  "镜像解码",
  "无尽回响"
];

/** 确定性随机数(mulberry32):同一关卡号永远生成同一套密语 */
export function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

/**
 * 无限关卡工厂:第 N 关完全由 N 确定性生成(可重玩、可离线)
 * - 难度阶梯:迷宫必须是真实可扫的 QR 矩阵,尺寸只能取 21+4×版本(30×30/35×35 等非法尺寸无法扫码),
 *   故按 v5(37×37) → v6(41×41) → v7(45×45) → v8(49×49) 每关 +4 递增,v8 封顶;
 *   全关卡 long 策略(弯绕长廊),并要求最佳路线 ≥ minSteps(100 步)------确定性搜索保证达标
 * - 密语随机:QW{N}#XXXXXXXX(填充区可自由塑形,扫码恒为原文)
 */
export function makeQRMazeLevel(n: number): QRMazeLevel {
  const version = Math.min(4 + n, 8) as QRMazeLevel["version"];
  const strategy: "short" | "long" = "long";
  const minSteps = 100;
  const codeLen = 8;
  const rng = mulberry32(n * 7919 + 13);
  let code = "";
  for (let i = 0; i < codeLen; i++) {
    code += CODE_CHARS[Math.floor(rng() * CODE_CHARS.length)];
  }
  const name = n <= MAZE_NAME_POOL.length ? MAZE_NAME_POOL[n - 1] : `无尽迷阵 ${n}`;
  return { id: n, name, version, content: `QW${n}#${code}`, strategy, minSteps };
}

/** 平衡球障碍墙(左上角 + 宽高,单位坐标 0~100 × 0~137) */
export interface BalanceWall {
  x: number;
  y: number;
  w: number;
  h: number;
}

/** 平衡球深渊洞 */
export interface BalanceHole {
  x: number;
  y: number;
  r: number;
}

/** 平衡球关卡配置 */
export interface BalanceLevel {
  id: number;
  name: string;
  start: { x: number; y: number };
  goal: { x: number; y: number; r: number };
  holes: BalanceHole[];
  walls: BalanceWall[];
  /** 洞口吸引强度倍率:越往后越难(默认 1) */
  suction?: number;
}

/** 逻辑坐标系:宽 100,高 137(与画布 686×940rpx 等比) */
export const BALANCE_FIELD = { width: 100, height: 137 };
export const BALANCE_BALL_R = 3.2;

export const BALANCE_LEVELS: BalanceLevel[] = [
  {
    id: 1,
    name: "热身滚道",
    start: { x: 10, y: 10 },
    goal: { x: 90, y: 126, r: 6 },
    holes: [
      { x: 50, y: 66, r: 5 },
      { x: 80, y: 36, r: 4.5 },
      { x: 60, y: 3.2, r: 4.5 }, // 贴顶边洞:打断沿顶边滑行的捷径
      { x: 96.8, y: 80, r: 4.5 }, // 贴右边洞:打断沿右边滑行的捷径
      { x: 48, y: 133.8, r: 4.5 } // 贴底边洞:打断沿左边+底边绕行的捷径
    ],
    walls: []
  },
  {
    id: 2,
    name: "回旋走廊",
    start: { x: 10, y: 10 },
    goal: { x: 91, y: 126, r: 5.5 },
    holes: [
      { x: 44, y: 112, r: 4.5 },
      { x: 74, y: 24, r: 5 },
      { x: 86, y: 84, r: 4.5 },
      { x: 96.8, y: 64, r: 4.2 } // 贴右边洞:右边走廊的封锁点
    ],
    walls: [
      { x: 26, y: 0, w: 4, h: 88 },
      { x: 58, y: 49, w: 4, h: 88 }
    ]
  },
  {
    id: 3,
    name: "针尖方阵",
    start: { x: 9, y: 9 },
    goal: { x: 91, y: 126, r: 5.5 },
    holes: [
      { x: 34, y: 34, r: 4 },
      { x: 66, y: 34, r: 4 },
      { x: 34, y: 103, r: 4 },
      { x: 66, y: 103, r: 4 },
      { x: 50, y: 68, r: 5.5 },
      { x: 96.8, y: 72, r: 4.2 }, // 贴右边洞
      { x: 70, y: 133.8, r: 4.2 } // 贴底边洞
    ],
    walls: [
      { x: 18, y: 62, w: 24, h: 3 },
      { x: 58, y: 62, w: 24, h: 3 }
    ]
  },
  {
    id: 4,
    name: "深渊回廊",
    start: { x: 9, y: 9 },
    goal: { x: 91, y: 129, r: 5.5 },
    holes: [
      { x: 86, y: 28, r: 5 },
      { x: 14, y: 70, r: 5 },
      { x: 50, y: 123, r: 5 },
      { x: 86, y: 108, r: 4.5 },
      { x: 58, y: 70, r: 4.5 },
      { x: 24, y: 18, r: 4 }
    ],
    walls: [
      { x: 0, y: 44, w: 72, h: 3 },
      { x: 28, y: 97, w: 72, h: 3 }
    ]
  }
];

/** 平衡球关卡主题名(第 5~16 关),之后为「无尽滚道 N」 */
const BALANCE_NAME_POOL = [
  "碎星浅滩",
  "引力回廊",
  "迷雾缓坡",
  "旋涡窄桥",
  "微光裂谷",
  "静默深渊",
  "镜面斜坡",
  "薄冰长阶",
  "乱流航段",
  "无垠坡道",
  "坠星海峡",
  "永夜回环"
];

/** 两点距离(Math.sqrt 为 IEEE-754 精确定义,跨设备结果一致) */
function balanceDist(ax: number, ay: number, bx: number, by: number): number {
  const dx = ax - bx;
  const dy = ay - by;
  return Math.sqrt(dx * dx + dy * dy);
}

/** 点到矩形(墙)的最近距离 */
function pointRectDist(px: number, py: number, w: BalanceWall): number {
  const cx = Math.min(Math.max(px, w.x), w.x + w.w);
  const cy = Math.min(Math.max(py, w.y), w.y + w.h);
  return balanceDist(px, py, cx, cy);
}

/** 矩形(墙)与矩形(墙)的最近距离 */
function rectRectDist(a: BalanceWall, b: BalanceWall): number {
  const dx = Math.max(b.x - (a.x + a.w), a.x - (b.x + b.w), 0);
  const dy = Math.max(b.y - (a.y + a.h), a.y - (b.y + b.h), 0);
  return Math.sqrt(dx * dx + dy * dy);
}

/**
 * 无限平衡球关卡工厂:第 N 关完全由 N 确定性生成
 * - 整数种子 + 可复现随机数,同一关号在任何手机上布局完全一致(可离线重玩)
 * - 前 4 关为手工调校的教学关,第 5 关起程序生成
 * - 难度曲线:深渊洞 5→10 个、障碍墙 2→6 条、传送门半径 5.5→4.2
 * - 可解性保证:漂浮墙距边框/彼此 ≥9(球径 6.4,必有绕行通道)、
 *   洞互不粘连(间距 ≥8)、洞与墙不重叠、洞不压起终点
 * - 防贴边滑行:边缘鳍墙(从边框伸入的短墙)截断边框通道 +
 *   贴边洞(半嵌在边框上的洞),让沿边一路滚到终点不再可行
 */
export function makeBalanceLevel(n: number): BalanceLevel {
  if (n <= BALANCE_LEVELS.length) return BALANCE_LEVELS[n - 1];

  const d = n - BALANCE_LEVELS.length; // 生成难度档(第 5 关 = 1)
  const rng = mulberry32(n * 104729 + 7);
  const r1 = (lo: number, hi: number) => Math.round((lo + rng() * (hi - lo)) * 10) / 10;

  const holeCount = Math.min(4 + Math.ceil(d / 2), 12);
  const wallCount = Math.min(2 + Math.floor(d / 2), 7);
  const finCount = Math.min(2 + Math.floor(d / 4), 4);
  const edgeHoleCount = Math.min(1 + Math.floor(d / 3), 4);
  const goalR = Math.max(3.8, 5.5 - d * 0.12);
  // 洞口吸引强度:难度档每 +1 增强 8%,封顶 1.8 倍(后期洞的"引力"明显更凶)
  const suction = 1 + Math.min(0.8, d * 0.08);

  const start = { x: r1(8, 14), y: r1(8, 14) };
  const goal = { x: r1(85, 91), y: r1(122, 129), r: goalR };

  // 墙体集合:主鳍 → 漂浮墙 → 随机鳍,后放者对先放者保持净空
  const walls: BalanceWall[] = [];

  // 边缘鳍墙:从边框伸入场内的短墙,截断"贴边滑行"的捷径。
  // 顶边与右边的主鳍最先落位(此时场地为空,扫描必命中,主贴边路线必被封锁):
  // 沿边取候选点、按种子随机取一;漂浮墙与随机鳍随后避让;洞最后放。
  const scanFin = (edge: number): BalanceWall | null => {
    const len = r1(11, 17 + Math.min(5, d * 0.8));
    const N = 34;
    const cands: BalanceWall[] = [];
    for (let s = 0; s <= N; s++) {
      const t =
        edge === 0 || edge === 2
          ? 16 + ((BALANCE_FIELD.width - 32) * s) / N
          : 16 + ((100 - 16) * s) / N;
      const fin: BalanceWall =
        edge === 0
          ? { x: t, y: 0, w: 3, h: len } // 顶边下垂
          : edge === 1
            ? { x: BALANCE_FIELD.width - len, y: t, w: len, h: 3 } // 右边伸入
            : edge === 2
              ? { x: t, y: BALANCE_FIELD.height - 3, w: 3, h: len } // 底边竖起
              : { x: 0, y: t, w: len, h: 3 }; // 左边伸入
      if (pointRectDist(start.x, start.y, fin) < 13) continue;
      if (pointRectDist(goal.x, goal.y, fin) < goal.r + 10) continue;
      cands.push(fin);
    }
    const clear = (f: BalanceWall) =>
      walls.reduce((m, o) => Math.min(m, rectRectDist(f, o)), Infinity);
    const pool = cands.filter((f) => clear(f) >= 10);
    const fallback = pool.length ? pool : cands.filter((f) => clear(f) >= 9);
    return fallback.length ? fallback[Math.floor(rng() * fallback.length)] : null;
  };
  const finTop = scanFin(0);
  if (finTop) walls.push(finTop);
  const finRight = scanFin(1);
  if (finRight) walls.push(finRight);

  // 再放漂浮横/竖杆:距边框与彼此 ≥9,保证必有绕行通道
  for (let i = 0; i < wallCount; i++) {
    const len = r1(22, Math.min(38, 26 + d * 1.2));
    for (let tries = 0; tries < 50; tries++) {
      const horiz = rng() < 0.5;
      const wall: BalanceWall = horiz
        ? {
            x: r1(9, BALANCE_FIELD.width - 9 - len),
            y: r1(9, BALANCE_FIELD.height - 30),
            w: len,
            h: 3
          }
        : {
            x: r1(9, BALANCE_FIELD.width - 12),
            y: r1(9, BALANCE_FIELD.height - 9 - len),
            w: 3,
            h: len
          };
      if (pointRectDist(start.x, start.y, wall) < 12) continue;
      if (pointRectDist(goal.x, goal.y, wall) < goal.r + 9) continue;
      if (walls.some((o) => rectRectDist(wall, o) < 9)) continue;
      walls.push(wall);
      break;
    }
  }

  // 其余随机鳍(净空不足时允许放弃,不硬塞)
  for (let i = 2; i < finCount; i++) {
    for (let tries = 0; tries < 40; tries++) {
      const edge = Math.floor(rng() * 4);
      const len = r1(11, 17 + Math.min(5, d * 0.8));
      const fin: BalanceWall =
        edge === 0
          ? { x: r1(16, BALANCE_FIELD.width - 16), y: 0, w: 3, h: len }
          : edge === 1
            ? { x: BALANCE_FIELD.width - len, y: r1(16, 100), w: len, h: 3 }
            : edge === 2
              ? { x: r1(16, BALANCE_FIELD.width - 16), y: BALANCE_FIELD.height - 3, w: 3, h: len }
              : { x: 0, y: r1(16, 100), w: len, h: 3 };
      if (pointRectDist(start.x, start.y, fin) < 13) continue;
      if (pointRectDist(goal.x, goal.y, fin) < goal.r + 10) continue;
      if (walls.some((o) => rectRectDist(fin, o) < 10)) continue;
      walls.push(fin);
      break;
    }
  }

  // 再放洞:前若干个为"贴边洞"(半嵌在边框上,贴边走位即有坠洞风险),
  // 其余在场地内部生成;均远离起终点、不压墙、互不粘连
  const holes: BalanceHole[] = [];
  for (let i = 0; i < holeCount; i++) {
    const asEdge = i < edgeHoleCount;
    for (let tries = 0; tries < 60; tries++) {
      const r = r1(4, Math.min(6, 4.4 + d * 0.14));
      let hole: BalanceHole;
      if (asEdge) {
        // 洞心距边框 1.6~3.4:半个洞嵌在边框上;避开四角起终点区
        const edge = Math.floor(rng() * 4);
        const off = r1(1.6, 3.4);
        if (edge === 0) hole = { x: r1(20, BALANCE_FIELD.width - 20), y: off, r };
        else if (edge === 1) hole = { x: BALANCE_FIELD.width - off, y: r1(20, 112), r };
        else if (edge === 2)
          hole = { x: r1(20, BALANCE_FIELD.width - 20), y: BALANCE_FIELD.height - off, r };
        else hole = { x: off, y: r1(20, 112), r };
      } else {
        hole = {
          x: r1(7 + r, BALANCE_FIELD.width - 7 - r),
          y: r1(7 + r, BALANCE_FIELD.height - 7 - r),
          r
        };
      }
      if (
        balanceDist(hole.x, hole.y, start.x, start.y) <
        hole.r + BALANCE_BALL_R + (asEdge ? 12 : 8)
      )
        continue;
      if (balanceDist(hole.x, hole.y, goal.x, goal.y) < hole.r + goal.r + (asEdge ? 8 : 6))
        continue;
      // 洞与墙间距留出球径 + 反吸余量:洞的吸引圈不会贴墙封死通道
      if (walls.some((w) => pointRectDist(hole.x, hole.y, w) < hole.r + 3 + BALANCE_BALL_R))
        continue;
      if (holes.some((o) => balanceDist(hole.x, hole.y, o.x, o.y) < hole.r + o.r + 8)) continue;
      holes.push(hole);
      break;
    }
  }

  const name = d <= BALANCE_NAME_POOL.length ? BALANCE_NAME_POOL[d - 1] : `无尽滚道 ${n}`;
  return { id: n, name, start, goal, holes, walls, suction };
}

// ============================================
// 火力全开(射击)
// ============================================

/** 火力全开关卡配置(数值曲线由关号确定性生成) */
export interface ShooterLevel {
  id: number;
  name: string;
  /** 初始队友数量 */
  initCount: number;
  /** 开火间隔(秒) */
  fireGap: number;
  /** 怪兽基础下压速度(单位/秒) */
  monsterSpeed: number;
  /** 怪兽生成间隔(秒) */
  spawnGap: number;
  /** 算式门生成间隔(秒) */
  gateGap: number;
  /** 算式门难度档:0 仅 + ×;1 加入 −;2 全量 + − × ÷ */
  opsMode: 0 | 1 | 2;
}

/** 射击逻辑坐标系:与平衡球一致(宽 100,高 137,画布 686×940rpx) */
export const SHOOTER_FIELD = { width: 100, height: 137 };

/** 火力全开关卡主题名(前 12 关),之后为「无尽战线 N」 */
const SHOOTER_NAME_POOL = [
  "初试枪火",
  "合金弹幕",
  "蜂群来袭",
  "裂口巨兽",
  "钢铁洪流",
  "烈焰走廊",
  "深渊虫巢",
  "雷鸣防线",
  "末日壁垒",
  "星陨要塞",
  "湮灭风暴",
  "无尽硝烟"
];

/**
 * 火力全开关卡工厂:第 N 关完全由 N 确定性生成
 * - 通关条件 = 击杀 BOSS(登场时机与血量由页面时间线估算)
 * - 怪兽生成序列 / 算式门序列由页面用 mulberry32(id*9973+5) 二次展开(同关同布局)
 * - 血量随机分档:小 1-3 / 中 5-15 / 大 20-30 / 超大 40-100(数字越大体型越大,页面按血量分档)
 * - 初始 6 人小队;+−×÷ 双门并排各占半屏、强制二选一(增益比谁加得多、减益比谁减得少)
 * - 前 2 关只出双增益门(教学),第 3 关起混入减益组合,第 6 关起 ÷ 门现身
 */
export function makeShooterLevel(n: number): ShooterLevel {
  return {
    id: n,
    name: n <= SHOOTER_NAME_POOL.length ? SHOOTER_NAME_POOL[n - 1] : `无尽战线 ${n}`,
    initCount: 6,
    fireGap: 0.3,
    // 怪兽整体提速:基础与成长都调高,高关卡封顶 8
    monsterSpeed: Math.min(3.4 + n * 0.3, 8),
    // 出怪更密:同屏怪更多(配合成簇出场)
    spawnGap: Math.max(1.3 - n * 0.06, 0.5),
    // 算式门波间隔:≈ 一对半屏门的穿场时间(GATE_SPEED),保证上一波二选一走完再出下一波
    gateGap: 9.5,
    opsMode: n <= 2 ? 0 : n <= 5 ? 1 : 2
  };
}

五、效率提升

  • 两代架构累计 5.7 万行级变更,Taro 起项目 + 三玩法从零实现 + 全量重构,全程由 TRAE 编码
  • 同等工作量按传统方式(我手写 + 查资料 + 调试)估算要 1 月以上,实际只是断断续续的几天就完成了
  • 其中 TRAE 贡献最大的三件事:从零搭工程骨架、跨文件批量重构、根因定位、代码开发修复
  • 我的实际投入:需求澄清、试玩反馈、验收截图即可

六、写在最后

这次实战我最大的体会是:AI 编码工具的上限,取决于你定的 "验收标准",以及确定的需求!。我设计了确定性关卡和逻辑/渲染分离,让它的产出可以被机器验收;我只需要扮演好产品经理 ------ 把"对"定义清楚,"快"就自然发生。

如果你也想用 TRAE 完成一个从零开始的完整项目,我的建议就一条:别把 AI 当补全工具,把它当一个人来协作------给它完整的需求、明确的验收标准和一个能自动验证的测试体系,它会还你一整个项目。

相关推荐
熊猫钓鱼1 小时前
TRAE开发工具化自检,我用它吐槽两句轻松解决办公电脑内存崩溃问题
ai编程·trae
不爱学英文的码字机器1 小时前
从Q1迁到T1,同一个编号已经换了含义
前端·数据库·microsoft
云和数据.ChenGuang1 小时前
光哥说Python 循环导入(Circular Import)深度解析:成因、排查与解决方案
开发语言·前端·人工智能·python·langchain
avi91111 小时前
【AI教人】在Inspector显示所有Delegate-Odin Inspector之三
java·前端·算法·delegate·inspector·editor扩展·unity editor
fengci.1 小时前
BUUCTF(困难部分)
android·开发语言·前端·学习·php
Ward的AI产品实战1 小时前
开源 AI 网关 Magpie 调优终结篇:顺位排错、跨家族对抗审查与 5 大路由组重构(下篇)
前端·人工智能
Highcharts.js1 小时前
BI可视化图表“维护难”问题的解决方案
开发语言·前端·javascript·编辑器·ecmascript·bi·highcharts
广州华水科技2 小时前
单北斗GNSS在建筑形变监测中的应用与优势分析
前端
hunter_0762 小时前
Swift、Objective-C++ 与 Flutter 如何调用 C++:从混编接入到内存安全
前端