diff --git a/contracts/agent-loop/skeletons/breakout.json b/contracts/agent-loop/skeletons/breakout.json new file mode 100644 index 00000000..524ee847 --- /dev/null +++ b/contracts/agent-loop/skeletons/breakout.json @@ -0,0 +1 @@ +{"entities": [{"id": "world", "transform": {"position": {"x": 0, "y": 0}}, "components": ["bg"]}, {"id": "paddle", "transform": {"position": {"x": 195, "y": 800}}, "components": ["rpaddle"]}, {"id": "ball", "transform": {"position": {"x": 195, "y": 760}}, "vx": 180, "vy": -310, "components": ["rball"]}], "components": [{"id": "bg", "kind": "render", "shape": "fill", "color": "#0D1117"}, {"id": "rpaddle", "kind": "render", "shape": "rect", "color": "#4FC3F7", "w": 90, "h": 16}, {"id": "rball", "kind": "render", "shape": "circle", "color": "#FFEB3B", "r": 9}, {"id": "rbrick1", "kind": "render", "shape": "rect", "color": "#FF5252", "w": 56, "h": 22}, {"id": "rbrick2", "kind": "render", "shape": "rect", "color": "#FFD54F", "w": 56, "h": 22}, {"id": "rbrick3", "kind": "render", "shape": "rect", "color": "#FF7043", "w": 56, "h": 22}, {"id": "rhud", "kind": "render", "shape": "rect", "color": "#222831", "w": 390, "h": 50}], "behaviors": [{"id": "spawnbricks", "trigger": "init", "code": "const cols=6, rows=5; const bw=56, bh=22, gap=4; const totalW = cols*bw + (cols-1)*gap; const startX = (390 - totalW)/2; const startY = 80; for(let r=0;r 381){ b.x = 381; b.vx = -Math.abs(b.vx); rt.fx.beep('wall'); } if(b.y < 9){ b.y = 9; b.vy = Math.abs(b.vy); rt.fx.beep('wall'); }"}, {"id": "ballcollide", "trigger": "update", "code": "const b=rt.getEntity('ball'); const p=rt.getEntity('paddle'); if(!b||!p) return; const bw=18, bh=18; for(const br of rt.query('brick')){ if(rt.overlap({x:b.x-bw/2,y:b.y-bh/2,w:bw,h:bh},{x:br.x-28,y:br.y-11,w:56,h:22})){ br.hp = (br.hp||1) - 1; if(br.hp <= 0){ rt.fx.burst(br.x, br.y, '#FFEB3B'); rt.fx.beep('brick'); rt.addScore(1); rt.destroy(br); } else { br.components = [{kind:'render',shape:'rect',color:'#FF7043',w:56,h:22}]; rt.fx.beep('wall'); } b.vy = -Math.abs(b.vy); b.y = br.y - 11 - 1; break; } } const ph = {x:p.x-45,y:p.y-8,w:90,h:16}; if(rt.overlap({x:b.x-bw/2,y:b.y-bh/2,w:bw,h:bh},ph)){ const rel = (b.x - p.x) / 45; rel = rt.clamp(rel, -1, 1); const speed = Math.sqrt(b.vx*b.vx + b.vy*b.vy); const angle = rel * (Math.PI/3); b.vx = Math.sin(angle) * speed; b.vy = -Math.abs(Math.cos(angle) * speed); b.y = p.y - 8 - bh/2 - 1; rt.fx.beep('paddle'); }"}, {"id": "diffscale", "trigger": "update", "code": "const b=rt.getEntity('ball'); if(!b) return; const target = Math.floor(rt.score/8) * 0.08; if(self.scale===undefined) self.scale = 1; const want = 1 + target; if(Math.abs(self.scale - want) > 0.001){ self.scale = want; const sp = Math.sqrt(b.vx*b.vx + b.vy*b.vy); const cur = sp; const newSp = 360 * want; const k = newSp / cur; b.vx *= k; b.vy *= k; }"}, {"id": "lifelose", "trigger": "update", "code": "const b=rt.getEntity('ball'); if(!b) return; if(self.lives===undefined){ self.lives = 3; } if(b.y > 860){ self.lives = self.lives - 1; rt.fx.burst(195, 800, '#ff4444'); rt.fx.beep('lose'); if(self.lives <= 0){ rt.lose(); } else { b.x = 195; b.y = 760; b.vx = 180; b.vy = -310; } }"}], "scenes": [{"id": "main", "entityRefs": ["world", "paddle", "ball"]}], "rules": [{"id": "win", "condition": "rt.query('brick').length === 0", "outcome": "win"}, {"id": "lose", "condition": "false", "outcome": "lose"}]} \ No newline at end of file diff --git a/contracts/agent-loop/skeletons/simon.json b/contracts/agent-loop/skeletons/simon.json new file mode 100644 index 00000000..f10f9904 --- /dev/null +++ b/contracts/agent-loop/skeletons/simon.json @@ -0,0 +1 @@ +{"components": [{"id": "bg", "kind": "render", "shape": "fill", "color": "#0e1116"}, {"id": "rhud", "kind": "render", "shape": "rect", "color": "#ffffff", "w": 1, "h": 1}, {"id": "rb0", "kind": "render", "shape": "rect", "color": "#801a1a", "w": 130, "h": 130}, {"id": "rb1", "kind": "render", "shape": "rect", "color": "#1a8033", "w": 130, "h": 130}, {"id": "rb2", "kind": "render", "shape": "rect", "color": "#1a3d80", "w": 130, "h": 130}, {"id": "rb3", "kind": "render", "shape": "rect", "color": "#80701a", "w": 130, "h": 130}, {"id": "mb0", "kind": "clickable", "score": 1, "mark": {"shape": "rect", "color": "#ff3344", "w": 130, "h": 130}, "hit": {"x": 65, "y": 65, "w": 130, "h": 130}}, {"id": "mb1", "kind": "clickable", "score": 1, "mark": {"shape": "rect", "color": "#33ff66", "w": 130, "h": 130}, "hit": {"x": 65, "y": 65, "w": 130, "h": 130}}, {"id": "mb2", "kind": "clickable", "score": 1, "mark": {"shape": "rect", "color": "#3366ff", "w": 130, "h": 130}, "hit": {"x": 65, "y": 65, "w": 130, "h": 130}}, {"id": "mb3", "kind": "clickable", "score": 1, "mark": {"shape": "rect", "color": "#ffdd33", "w": 130, "h": 130}, "hit": {"x": 65, "y": 65, "w": 130, "h": 130}}], "entities": [{"id": "world", "transform": {"position": {"x": 0, "y": 0}}, "components": ["bg"]}, {"id": "hud0", "transform": {"position": {"x": 0, "y": 0}}, "components": ["rhud"]}, {"id": "b0", "transform": {"position": {"x": 65, "y": 280}}, "tags": ["target"], "idx": 0, "components": ["rb0", "mb0"], "occupied": false}, {"id": "b1", "transform": {"position": {"x": 215, "y": 280}}, "tags": ["target"], "idx": 1, "components": ["rb1", "mb1"], "occupied": false}, {"id": "b2", "transform": {"position": {"x": 65, "y": 430}}, "tags": ["target"], "idx": 2, "components": ["rb2", "mb2"], "occupied": false}, {"id": "b3", "transform": {"position": {"x": 215, "y": 430}}, "tags": ["target"], "idx": 3, "components": ["rb3", "mb3"], "occupied": false}], "behaviors": [{"id": "init", "trigger": "init", "code": "self.phase='show'; self.round=1; self.progress=0; self.seq=[rt.randInt(0,3)]; self.idx=0; self.lit=-1; self.litT=0; self.gapT=0; self.failT=0; self.score=0; rt.setScore(0); for(const b of rt.query('target')){ b.occupied=false; b._lit=0; } rt.spawn({x:195,y:60,tags:['hud'],components:[{kind:'render',shape:'rect',color:'#ffffff',w:1,h:1}]});"}, {"id": "engine", "trigger": "update", "code": "self.gapT=(self.gapT||0)-dt; self.litT=(self.litT||0)-dt; self.failT=(self.failT||0)-dt; if(self.phase==='show'){ if(self.lit>=0){ const b=rt.getEntity('b'+self.lit); if(b){ b._lit=Math.max(0,self.litT/0.42); const s=1+0.08*Math.sin(Math.min(1,self.litT/0.42)*Math.PI); b._sx=s; b._sy=s; } if(self.litT<=0){ if(b){ b._lit=0; b._sx=1; b._sy=1; } self.lit=-1; self.gapT=0.16; } } else if(self.gapT<=0){ if(self.idx=b.x && p.x<=b.x+130 && p.y>=b.y && p.y<=b.y+130){ hit=b; break; } } if(!hit) return; const expected=self.seq[self.progress]; rt.fx.burst(hit.x+65", "hit.idx===0?'#ff3344'": "hit.idx===1?'#33ff66':hit.idx===2?'#3366ff':'#ffdd33'); rt.fx.beep(hit.idx===0?'red':hit.idx===1?'green':hit.idx===2?'blue':'yellow'); hit._lit=1; hit._sx=1.08; hit._sy=1.08; if(hit.idx===expected){ self.progress++; hit.occupied=true; if(self.progress>=self.seq.length){ self.score++; rt.addScore(1); rt.fx.burst(195,420,'#ffdd33'); self.round++; if(self.round>8){ self.phase='win'; self.failT=1.6; rt.fx.burst(195,422,'#ffdd33'); rt.win(); } else { self.phase='show'; self.idx=0; self.lit=-1; self.gapT=0.8; self.seq.push(rt.randInt(0,3)); for(const b of rt.query('target')){ b.occupied=true; b._lit=0; } } } } else { self.phase='fail'; self.failT=1.6; self.progress=0; rt.fx.burst(195,422,'#ff3344'); rt.fx.burst(195,422,'#ff3344'); for(const b of rt.query('target')){ b.occupied=true; } self.idx=0; }"}], "scenes": [{"id": "main", "entityRefs": ["world", "hud0", "b0", "b1", "b2", "b3"]}], "rules": [{"id": "winrule", "condition": "rt.score >= 1", "outcome": "win"}]} \ No newline at end of file diff --git a/contracts/agent-loop/skeletons/tictactoe.json b/contracts/agent-loop/skeletons/tictactoe.json new file mode 100644 index 00000000..1f3a57a2 --- /dev/null +++ b/contracts/agent-loop/skeletons/tictactoe.json @@ -0,0 +1 @@ +{"entities": [{"id": "world", "transform": {"position": {"x": 0, "y": 0}}, "components": ["bg"]}, {"id": "t0", "transform": {"position": {"x": 85, "y": 310}}, "tags": ["target"], "idx": 0, "components": ["tile", "clk"]}, {"id": "t1", "transform": {"position": {"x": 195, "y": 310}}, "tags": ["target"], "idx": 1, "components": ["tile", "clk"]}, {"id": "t2", "transform": {"position": {"x": 305, "y": 310}}, "tags": ["target"], "idx": 2, "components": ["tile", "clk"]}, {"id": "t3", "transform": {"position": {"x": 85, "y": 420}}, "tags": ["target"], "idx": 3, "components": ["tile", "clk"]}, {"id": "t4", "transform": {"position": {"x": 195, "y": 420}}, "tags": ["target"], "idx": 4, "components": ["tile", "clk"]}, {"id": "t5", "transform": {"position": {"x": 305, "y": 420}}, "tags": ["target"], "idx": 5, "components": ["tile", "clk"]}, {"id": "t6", "transform": {"position": {"x": 85, "y": 530}}, "tags": ["target"], "idx": 6, "components": ["tile", "clk"]}, {"id": "t7", "transform": {"position": {"x": 195, "y": 530}}, "tags": ["target"], "idx": 7, "components": ["tile", "clk"]}, {"id": "t8", "transform": {"position": {"x": 305, "y": 530}}, "tags": ["target"], "idx": 8, "components": ["tile", "clk"]}], "components": [{"id": "bg", "kind": "render", "shape": "fill", "color": "#10101a"}, {"id": "tile", "kind": "render", "shape": "rect", "color": "#33384a", "w": 110, "h": 110}, {"id": "clk", "kind": "clickable", "score": 1, "mark": {"shape": "circle", "color": "#ffcc00", "r": 30}}], "behaviors": [{"id": "init", "trigger": "init", "code": "self.board=[null,null,null,null,null,null,null,null,null]; self.turn='X'; self.result=null; self.moves=0; self.phase='playing'; self.aiPending=0; self.aiIdx=-1;"}, {"id": "tick", "trigger": "update", "code": "if(self.phase==='gameover'){ return; } if(self.turn==='O' && self.aiIdx===-1 && self.moves<9){ self.aiPending += dt; if(self.aiPending >= 0.42){ const empties=[]; for(let i=0;i<9;i++){ if(self.board[i]===null) empties.push(i); } if(empties.length>0){ self.aiIdx = empties[Math.floor(rt.random()*empties.length)]; } else { self.aiIdx=-1; } } } if(self.turn==='O' && self.aiIdx>=0){ const i=self.aiIdx; self.board[i]='O'; self.moves+=1; rt.fx.burst([85,195,305][i%3],[310,420,530][Math.floor(i/3)],'#ff8844'); rt.fx.beep('click'); self.aiIdx=-1; self.aiPending=0; const w=checkWin(self.board); if(w){ self.result=w; self.phase='gameover'; rt.fx.burst(195,420,'#ffaa44'); rt.fx.beep('win'); } else if(self.moves>=9){ self.result='draw'; self.phase='gameover'; rt.fx.beep('lose'); } else { self.turn='X'; } } function checkWin(b){ const L=[[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]; for(const l of L){ if(b[l[0]] && b[l[0]]===b[l[1]] && b[l[0]]===b[l[2]]) return b[l[0]]; } return null; }"}], "scenes": [{"id": "main", "entityRefs": ["world", "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8"]}], "rules": [{"id": "win", "condition": "rt.score >= 9", "outcome": "win"}]} \ No newline at end of file diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml b/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml index 7f5cafbb..4b185cb0 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml +++ b/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml @@ -199,6 +199,8 @@ prompts/01-safety/*.md prompts/04-config/*.md templates/*.schema.json + + agent-loop/skeletons/*.json diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java index 43968ab8..07f5a65b 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java @@ -374,13 +374,29 @@ final class SaaPrompts { if (archetype != null && !archetype.isEmpty() && !"generic".equals(archetype)) { user.append("\n\n(品类提示:").append(archetype).append(")"); } - if (retryFeedback != null && !retryFeedback.isEmpty()) { + boolean firstAttempt = retryFeedback == null || retryFeedback.isEmpty(); + if (firstAttempt) { + appendSkeletonIfAny(user, archetype); // Phase 2:仅首生成注骨架;救场轮不注(已有失败反馈、避免冲淡) + } else { user.append("\n\n———\n上一次生成【未通过】,失败原因如下,请针对性修正后重新产出完整 gameDefinition JSON(不要只给片段):\n") .append(retryFeedback); } return new String[]{GAMEDEF_SYSTEM, user.toString()}; } + /** + * Plan A·Phase 2:若该 archetype 有已验证骨架且 {@code -Dsaa.gen.skeletonFirst} 开,追加骨架段(结构参照、非照抄)。 + * flag 关 / 无映射 / 资源缺失时 {@link SaaSkeletons#forArchetype} 返 null → 不注入(沿用现 hint,生成字节零变)。 + */ + private static void appendSkeletonIfAny(StringBuilder user, String archetype) { + String skeleton = SaaSkeletons.forArchetype(archetype); + if (skeleton != null) { + user.append("\n\n【已验证骨架(同品类、已过门的 gameDefinition 结构参照)——请在此结构上改成上面题面要求的那款游戏:") + .append("沿用其 rt 面用法与结构约定,替换实体/组件/数值/玩法逻辑使之契合题面;若题面与此结构不符,以题面为准、勿照抄】:\n") + .append(skeleton); + } + } + // ============================================================================ // ④.1 连续对话救场(Plan A/U2,仅 anthropic 路)—— system 不变;首轮 user=题面;救场 user=失败反馈增量修。 // 与 ④ buildMessages/buildGameDefMessages(openai 路·每轮全新 [system,user])物理隔离·并存: @@ -415,6 +431,7 @@ final class SaaPrompts { if (archetype != null && !archetype.isEmpty() && !"generic".equals(archetype)) { user.append("\n\n(品类提示:").append(archetype).append(")"); } + appendSkeletonIfAny(user, archetype); // Phase 2:anthropic 路首轮注骨架(救场轮走 historyRepairUser、不注) return user.toString(); } return "请实现下面这款游戏,产出一个合规的 GameHostFactory 模块(只输出一个 ```js 代码块):\n\n" diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaSkeletons.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaSkeletons.java new file mode 100644 index 00000000..f92fbd19 --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaSkeletons.java @@ -0,0 +1,78 @@ +package com.wanxiang.huijing.game.module.aigc.saa; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Plan A·Phase 2(Template-First gamedef 骨架,2026-06-20;OpenGame +10.1 杠杆): + * 给生成的 generate 角色按 archetype 注入一份【已过九门的纯净 gameDefinition 骨架】,让模型「在骨架上改成题面玩法」 + * 而非从零写——降低「rt 面接线 / 运动逻辑」从零写错的概率(哑火静态游戏=跨款最大失败簇)。 + * + *

只用 3 个金标准过门纯净源(gd-breakout/tictactoe/simon:均 verdict.pass=true + rt 面纯净 + 过 Phase3 坑门), + * 按 (tick/input) profile 相近映射到 archetype;无近似骨架的 archetype(merge/idle/tycoon 经济类)走【无骨架】(沿用现 hint), + * 绝不为凑覆盖喂低质/违禁骨架(Phase2 研究铁律:13 个 gd-* 仅 3 个纯净,其余含违禁 token 或未过门)。 + * + *

flag 旁挂、默认关:{@code -Dsaa.gen.skeletonFirst=1} 开启(A/B 用 thinking-off 衡量能否拉近 67%→91.7%、省 thinking 10x 成本); + * 默认关=生成行为字节不变(零回归)。骨架为静态文件,自身经 {@code validateSourceProject} 校验合法(gameplay-pitfalls.test 旁证)。 + */ +final class SaaSkeletons { + private static final Logger log = LoggerFactory.getLogger(SaaSkeletons.class); + + /** flag 旁挂:默认关(字节零变);{@code -Dsaa.gen.skeletonFirst=1/true} 开启骨架注入。 */ + private static final boolean ENABLED = "1".equals(System.getProperty("saa.gen.skeletonFirst")) + || "true".equalsIgnoreCase(System.getProperty("saa.gen.skeletonFirst")); + + /** archetype → 骨架源名(按 profile 相近映射;仅 3 金标准源,无近似骨架的 archetype 不登记=走无骨架)。 */ + private static final Map ARCHETYPE_SOURCE = new LinkedHashMap<>(); + static { + // 实时动作类 → breakout(realtime + vx/vy 物理 + pointer 控制 + 碰撞 + remaining 下降=进展) + ARCHETYPE_SOURCE.put("generic", "breakout"); + ARCHETYPE_SOURCE.put("dodge", "breakout"); + ARCHETYPE_SOURCE.put("runner", "breakout"); + ARCHETYPE_SOURCE.put("bubble", "breakout"); + // 离散点选类 → tictactoe(clickable 网格 + 离散选 + rt.addScore 进展) + ARCHETYPE_SOURCE.put("clicker", "tictactoe"); + ARCHETYPE_SOURCE.put("match3", "tictactoe"); + // 回合序列类 → simon(clickable + 回合 + 状态实体存 phase/seq=跨 behavior 态正解示范) + ARCHETYPE_SOURCE.put("line-clear", "simon"); + // merge / idle / tycoon(经济类)无近似纯净骨架 → 不登记,走无骨架(沿用现 hint),避免喂错结构。 + } + + /** 骨架内容缓存(源名 → gameDefinition JSON 文本);类加载期一次性装载,缺失只 warn 不抛(对齐 PromptResourceLoader「资源缺失只禁用」精神)。 */ + private static final Map CACHE = new LinkedHashMap<>(); + static { + for (String src : new String[]{"breakout", "tictactoe", "simon"}) { + String path = "wanxiang-contracts/agent-loop/skeletons/" + src + ".json"; + try (InputStream in = SaaSkeletons.class.getClassLoader().getResourceAsStream(path)) { + if (in == null) { + log.warn("[saa-skeleton] 骨架资源缺失,该源禁用:{}", path); + } else { + CACHE.put(src, new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } catch (Exception e) { + log.warn("[saa-skeleton] 骨架装载失败,该源禁用:{}({})", path, e.getMessage()); + } + } + } + + private SaaSkeletons() { + } + + /** + * 取该 archetype 的骨架 gameDefinition JSON(flag 关 / 无映射 / 资源缺失 → 返 null,调用方据此决定是否注入)。 + * + * @param archetype classify 节点产出的 archetype(已 normalize 为合法枚举)。 + * @return 骨架 JSON 文本,或 null(不注入)。 + */ + static String forArchetype(String archetype) { + if (!ENABLED || archetype == null) { + return null; + } + String src = ARCHETYPE_SOURCE.get(archetype); + return src == null ? null : CACHE.get(src); + } +}