feat(saa): Phase 2 Template-First gamedef 骨架机制(flag 旁挂默认关;A/B 负面=不迁移到便宜 thinking-off)
机制(OpenGame +10.1 杠杆试落): - SaaSkeletons 加载器:archetype→3 金标准过门纯净骨架(gd-breakout/tictactoe/simon,verdict.pass=true+rt纯净+过 Phase3 坑门),按 (tick/input) profile 映射;经济类(merge/idle/tycoon)无近似骨架不登记=走无骨架,绝不喂低质骨架。 - 注入:SaaPrompts buildGameDefMessages(openai 首生成)/historyFirstUser(anthropic 首轮) 追加骨架段(结构参照、非照抄,题面不符以题面为准);救场轮不注。 - pom 拷 contracts/agent-loop/skeletons/*.json 进 classpath。flag -Dsaa.gen.skeletonFirst=1 开,默认关=生成字节零变、零回归。 A/B 实测(thinking-off + skeleton conc=12 vs A 基线): - 7/12(58.3%) ≤ A 8/12(67%),失败仍 E_live 哑火(#4,5,9,11,12) → 骨架没把基线拉向 91.7%。 - 结论:+10.1 骨架杠杆【不迁移到便宜模型+thinking-off】——哑火是推理缺口(thinking 补)、非结构样例缺口(骨架补不了)。thinking-ON(anthropic 91.7%)仍是唯一真杠杆。 - 处置(创始人定 A):保留机制 flag 关、可逆——换更强模型/配合 thinking 时或有用;默认不启用。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5bc0afb4bb
commit
ad22be9b3a
1
contracts/agent-loop/skeletons/breakout.json
Normal file
1
contracts/agent-loop/skeletons/breakout.json
Normal file
@ -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<rows;r++){ for(let c=0;c<cols;c++){ const x = startX + c*(bw+gap) + bw/2; const y = startY + r*(bh+gap) + bh/2; const hard = (r*cols+c+c)%5===0 || (r*cols+c+c)%5===1; const id = 'b'+r+'_'+c; if(hard){ rt.spawn({id:id,x:x,y:y,hp:2,tags:['brick'],idx:r*cols+c,components:[{kind:'render',shape:'rect',color:'#FFD54F',w:56,h:22}]}); } else { rt.spawn({id:id,x:x,y:y,hp:1,tags:['brick'],idx:r*cols+c,components:[{kind:'render',shape:'rect',color:'#FF5252',w:56,h:22}]}); } } }"}, {"id": "control", "trigger": "update", "code": "const p=rt.getEntity('paddle'); if(!p) return; if(rt.input.pointer.down){ self.tx = rt.input.pointer.x; } if(self.tx===undefined) self.tx = p.x; p.x = p.x + (self.tx - p.x)*0.28; p.x = rt.clamp(p.x, 45, 345);"}, {"id": "hudtext", "trigger": "init", "code": "rt.spawn({id:'hudbg',x:195,y:25,components:[{kind:'render',shape:'rect',color:'#222831',w:390,h:50}]});"}, {"id": "ballupdate", "trigger": "update", "code": "const b=rt.getEntity('ball'); const p=rt.getEntity('paddle'); if(!b||!p) return; b.x += b.vx*dt; b.y += b.vy*dt; if(b.x < 9){ b.x = 9; b.vx = Math.abs(b.vx); rt.fx.beep('wall'); } if(b.x > 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"}]}
|
||||
1
contracts/agent-loop/skeletons/simon.json
Normal file
1
contracts/agent-loop/skeletons/simon.json
Normal file
File diff suppressed because one or more lines are too long
1
contracts/agent-loop/skeletons/tictactoe.json
Normal file
1
contracts/agent-loop/skeletons/tictactoe.json
Normal file
@ -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"}]}
|
||||
@ -199,6 +199,8 @@
|
||||
<include>prompts/01-safety/*.md</include>
|
||||
<include>prompts/04-config/*.md</include>
|
||||
<include>templates/*.schema.json</include>
|
||||
<!-- Plan A·Phase 2:Template-First gamedef 骨架进 classpath(SaaSkeletons 据此装载注入生成 prompt) -->
|
||||
<include>agent-loop/skeletons/*.json</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
@ -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])<b>物理隔离·并存</b>:
|
||||
@ -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"
|
||||
|
||||
@ -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 面接线 / 运动逻辑」从零写错的概率(哑火静态游戏=跨款最大失败簇)。
|
||||
*
|
||||
* <p><b>只用 3 个金标准过门纯净源</b>(gd-breakout/tictactoe/simon:均 verdict.pass=true + rt 面纯净 + 过 Phase3 坑门),
|
||||
* 按 (tick/input) profile 相近映射到 archetype;无近似骨架的 archetype(merge/idle/tycoon 经济类)走【无骨架】(沿用现 hint),
|
||||
* <b>绝不为凑覆盖喂低质/违禁骨架</b>(Phase2 研究铁律:13 个 gd-* 仅 3 个纯净,其余含违禁 token 或未过门)。
|
||||
*
|
||||
* <p><b>flag 旁挂、默认关</b>:{@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<String, String> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user