/** * 多文件代码生成器 * * 结合 GDD、Asset Pack 和脚手架模板源码,调用 LLM 生成 * 模块化的多文件游戏代码。 * * 核心约束: * - 模板内容必须注入 system prompt 作为参考 * - LLM 输出必须为 JSON 数组 [{ path, content }] * - 防御性解析:多层回退应对 LLM 转义错误/截断 */ import { callLLM, resolveModel, type UserTier } from './llm-proxy'; import type { GameDesignDocument } from './gdd-generator'; import type { AssetPack } from './asset-generator'; import type { GameArchetype } from './game-classifier'; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 类型定义 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** 生成的代码文件 */ export interface GeneratedCodeFile { path: string; content: string; } /** 代码生成结果 */ export interface GeneratedCode { files: GeneratedCodeFile[]; entryPoint: string; } /** 代码生成参数 */ export interface GenerateCodeParams { gdd: GameDesignDocument; assetPack: AssetPack; templateFiles: Array<{ path: string; content: string }>; archetype: GameArchetype; tier?: UserTier; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 核心逻辑 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * 生成完整的游戏代码 * * @param params - 生成参数(GDD + AssetPack + 模板 + 原型 + 层级) * @returns 生成的代码文件列表和入口点 */ export async function generateGameCode(params: GenerateCodeParams): Promise { const { gdd, assetPack, templateFiles, archetype, tier } = params; const model = resolveModel(tier); const systemPrompt = buildSystemPrompt(archetype, templateFiles); const userPrompt = buildUserPrompt(gdd, assetPack); const raw = await callLLM( [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], model, ); const files = parseCodeOutput(raw); const entryPoint = resolveEntryPoint(files, archetype); return { files, entryPoint }; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Prompt 组装 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ function buildSystemPrompt( archetype: GameArchetype, templateFiles: Array<{ path: string; content: string }>, ): string { const templateSection = templateFiles .map(f => `// === ${f.path} ===\n${f.content}`) .join('\n\n'); return `You are an expert game developer generating modular game code. ARCHETYPE: ${archetype} REFERENCE TEMPLATES (use as structural guide, adapt to GDD requirements): ${templateSection} OUTPUT FORMAT: You MUST output a JSON array of file objects. Each object has: - "path": relative file path (e.g. "src/scenes/MainScene.ts") - "content": complete file content as a string Example: [ { "path": "src/main.ts", "content": "import Phaser from 'phaser';\\n..." }, { "path": "src/scenes/MainScene.ts", "content": "export class MainScene extends Phaser.Scene {\\n..." } ] RULES: 1. Output ONLY the JSON array, no explanations before or after. 2. Do NOT invent new hooks, lifecycle methods, or APIs not in the templates. 3. Do NOT use placeholders like "// TODO" or "..." — every file must be complete. 4. Escape all special characters in JSON strings properly (\\n, \\t, \\", \\\\). 5. Each file must be syntactically valid TypeScript. 6. Use the asset keys from the Asset Pack for loading resources. 7. Match the GDD's entity definitions, game config, and level specifications exactly.`; } function buildUserPrompt(gdd: GameDesignDocument, assetPack: AssetPack): string { return `Generate the complete game code based on this Game Design Document and Asset Pack. GAME DESIGN DOCUMENT: ${JSON.stringify(gdd, null, 2)} ASSET PACK: ${JSON.stringify(assetPack, null, 2)} Generate all files needed to run this game. Output the JSON array now.`; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 防御性代码解析器 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * 解析 LLM 输出的代码文件列表 * * 防御性解析策略: * 1. 直接 JSON.parse * 2. 去除 Markdown 代码块后解析 * 3. 提取 JSON 数组(处理前后额外文本) * 4. 修复常见截断(补全尾部括号) * 5. 全部失败则抛出详细错误 */ export function parseCodeOutput(raw: string): GeneratedCodeFile[] { const trimmed = raw.trim(); if (!trimmed) { throw new Error('[CodeGenerator] LLM returned empty output'); } // 策略 1: 直接解析 const directResult = tryParse(trimmed); if (directResult) return directResult; // 策略 2: 去除 Markdown 代码块 const stripped = stripMarkdown(trimmed); const strippedResult = tryParse(stripped); if (strippedResult) return strippedResult; // 策略 3: 提取 JSON 数组(处理前后额外文本) const extracted = extractJSONArray(stripped); if (extracted) { const extractedResult = tryParse(extracted); if (extractedResult) return extractedResult; // 策略 4: 修复截断 const fixed = fixTruncation(extracted); if (fixed) { const fixedResult = tryParse(fixed); if (fixedResult) return fixedResult; } } // 全部失败 const preview = trimmed.slice(0, 500); throw new Error( `[CodeGenerator] Failed to parse LLM code output. Preview:\n${preview}`, ); } /** 尝试解析 JSON 数组并校验格式 */ function tryParse(text: string): GeneratedCodeFile[] | null { try { const parsed = JSON.parse(text); if (!Array.isArray(parsed)) return null; // 校验每个元素的结构 for (const item of parsed) { if (typeof item !== 'object' || item === null) return null; if (typeof item.path !== 'string' || typeof item.content !== 'string') return null; } return parsed as GeneratedCodeFile[]; } catch { return null; } } /** 去除 Markdown 代码块标记 */ function stripMarkdown(text: string): string { // 匹配 ```json ... ``` 或 ``` ... ``` const match = text.match(/```(?:json|typescript|ts|javascript|js)?\s*\n?([\s\S]*?)```/); if (match) return match[1].trim(); return text; } /** * 从文本中提取第一个 JSON 数组 * 处理 LLM 在 JSON 前后输出额外文本的情况 */ function extractJSONArray(text: string): string | null { const start = text.indexOf('['); if (start === -1) return null; let depth = 0; let inString = false; let escape = false; for (let i = start; i < text.length; i++) { const ch = text[i]; if (escape) { escape = false; continue; } if (ch === '\\' && inString) { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '[') depth++; if (ch === ']') { depth--; if (depth === 0) { return text.slice(start, i + 1); } } } // 未找到匹配的 ] — 返回到末尾(可能被截断) return text.slice(start); } /** * 修复常见截断:尝试补全缺失的尾部括号 * 处理 LLM 输出被 token limit 截断的情况 */ function fixTruncation(text: string): string | null { // 计算未闭合的括号 let braces = 0; let brackets = 0; let inString = false; let escape = false; for (const ch of text) { if (escape) { escape = false; continue; } if (ch === '\\' && inString) { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '{') braces++; if (ch === '}') braces--; if (ch === '[') brackets++; if (ch === ']') brackets--; } if (braces === 0 && brackets === 0) return null; // 尝试补全:先关闭最后一个未完成的对象字符串,再关闭对象和数组 let fix = text; // 如果在字符串中间截断,尝试关闭它 if (inString) { fix += '"}'; } // 补全未闭合的 } for (let i = 0; i < braces; i++) { fix += '}'; } // 补全未闭合的 ] for (let i = 0; i < brackets; i++) { fix += ']'; } return fix; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 入口点解析 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** 从生成的文件列表中确定入口文件 */ function resolveEntryPoint(files: GeneratedCodeFile[], archetype: GameArchetype): string { // 优先查找 main.ts const mainFile = files.find(f => f.path.endsWith('main.ts') || f.path.endsWith('main.js')); if (mainFile) return mainFile.path; // 其次查找 index.ts const indexFile = files.find(f => f.path.endsWith('index.ts') || f.path.endsWith('index.js')); if (indexFile) return indexFile.path; // 回退到第一个文件 return files[0]?.path ?? `src/main.ts`; }