Spaces:
Sleeping
Sleeping
| /** | |
| * 多文件代码生成器 | |
| * | |
| * 结合 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<GeneratedCode> { | |
| 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`; | |
| } | |