feat(saa): 零-LLM driver 内置绑定 injectDriverByArchetype + 5 单测

design 只声明 gatespec 不写 driver→amodel 路无人绑 driver→游戏停菜单→九门 E_live/H 名存实亡。按 classify archetype 确定性补内置 driver(经营/反应→tap occupied、三消→placement、跑酷→key-cycle),显式 driver 不覆盖、无合适内置(如 bubble)不强补。WIP:未在 mini mvn 验;archetype→targets 契约假设正于 driver 调查中复核。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-23 19:45:33 -07:00
parent f5918e449e
commit d342de088e
2 changed files with 97 additions and 0 deletions

View File

@ -457,6 +457,11 @@ final class SaaGenNodes {
try {
Path spec = gameRuntimeRoot.resolve("games").resolve("_wg1-gen").resolve(gameId).resolve("play-spec.json");
String playSpec = s.value(SaaStudioNodes.K_PLAY_SPEC, String.class).orElse("{}");
// -LLM driver 内置绑定:design gatespec 规则只声明什么算赢/有进展不写 driver;amodel 路无人绑 driver
// 游戏不被驱动停菜单九门 E_live/H 未驱动 advisory 名存实亡(创始人门要可信指令)此处据 classify
// archetype 确定性补内置 driver,让经营/反应/放置类被真玩通,使门真正测到能不能玩已显式声明 driver 则尊重不覆盖
String archetype = s.value(SaaStudioNodes.K_ARCHETYPE, String.class).orElse("");
playSpec = injectDriverByArchetype(playSpec, archetype);
Files.write(spec, playSpec.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
errs.add("amodel play-spec.json 写入失败:" + e.getMessage());
@ -795,6 +800,54 @@ final class SaaGenNodes {
return lines.isEmpty() ? "真玩未过(原因未知)。" : "真玩未过:\n" + String.join("\n", lines);
}
/**
* -LLM driver 内置绑定: classify archetype 确定性映射到 play.cdp.cjs 内置 driver(harness 自适应读 _forensicsView state 出招)
* <p>design gatespec 规则只声明什么算赢/有进展不写 driver; amodel 路无人绑 driver 游戏不被驱动 停菜单
* 九门 E_live/H 未驱动 advisory 名存实亡本处补齐:让经营/反应/放置等类被真玩通,使门可信(创始人指令)
* <p><b>已含 driver(显式声明)则原样返回不覆盖</b>;无合适内置 driver( narrative)则不补;解析异常原样返回(绝不因补 driver 破坏 play-spec)
*/
static String injectDriverByArchetype(String playSpecJson, String archetype) {
try {
JsonNode node = MAPPER.readTree((playSpecJson == null || playSpecJson.isBlank()) ? "{}" : playSpecJson);
if (!node.isObject()) {
return playSpecJson;
}
com.fasterxml.jackson.databind.node.ObjectNode spec = (com.fasterxml.jackson.databind.node.ObjectNode) node;
if (spec.has("driver") && !spec.get("driver").isNull()) {
return spec.toString(); // 显式 driver 优先,不覆盖
}
com.fasterxml.jackson.databind.node.ObjectNode driver = driverForArchetype(archetype);
if (driver == null) {
return spec.toString(); // 无合适内置 driver( narrative) 不补,保持未驱动(E_live/H advisory)
}
spec.set("driver", driver);
return spec.toString();
} catch (Exception e) {
return playSpecJson; // 解析异常 原样,不破坏 play-spec
}
}
/** archetype → 内置 driver 配置(null=无合适内置 driver)。保守可扩展:经营/反应类点 occupied 目标,放置类点空位,跑酷/躲避走键循环。 */
private static com.fasterxml.jackson.databind.node.ObjectNode driverForArchetype(String archetype) {
String a = (archetype == null) ? "" : archetype.trim().toLowerCase();
com.fasterxml.jackson.databind.node.ObjectNode d = MAPPER.createObjectNode();
switch (a) {
// 经营/放置/合成/点击器/打地鼠/兜底:有内容的离散目标(等待的客/冒头的鼠/可收产出)推进 服务族 tap-occupied
case "tycoon": case "idle": case "clicker": case "merge": case "generic": case "":
return d.put("type", "tap-targets").put("targetsPath", "targets").put("targetMode", "occupied")
.put("steps", 80).put("stepMs", 500);
// 三消/消行:点离散空位/可消组 放置族(默认 occupied!==true)
case "match3": case "line-clear":
return d.put("type", "tap-targets").put("targetsPath", "targets").put("steps", 40).put("stepMs", 360);
// 跑酷/躲避:连续输入 键循环兜底
case "runner": case "dodge":
return d.put("type", "key-cycle").put("steps", 60).put("stepMs", 220);
// 瞄准发射(泡泡龙):无通用内置参数,本期不补 不驱动(E_live/H advisory),留未来 aim-fire 绑定
default:
return null;
}
}
/** 取字符串尾部 n 字(日志摘要用)。 */
private static String tail(String s, int n) {
if (s == null) {

View File

@ -315,4 +315,48 @@ class SaaFullGraphE2eGateLogicTest {
assertTrue(fb.contains("D_render"), "真硬门 D_render 失败应回喂救场");
assertFalse(fb.contains("E_live"), "advisory E_live 不应回喂救场");
}
// driver 内置绑定 archetypedriver(-LLM,2026-06-23 wanglanmei E2E)
//
// design 只声明 gatespec不写 driver amodel 路无人绑 driver 游戏不被驱动 九门 E_live/H 名存实亡
// injectDriverByArchetype classify archetype 确定性补内置 driver,使经营/反应类被真玩通(创始人门要可信)
@Test
@DisplayName("injectDriverByArchetype:tycoon(经营)→ 补 tap-targets occupied(点等待的客→真驱动)")
void injectDriver_tycoon_tapOccupied() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"expectLatch\":true}", "tycoon"));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertEquals("occupied", v.path("driver").path("targetMode").asText(), "经营/服务族点 occupied 目标");
assertTrue(v.path("expectLatch").asBoolean(), "原 gatespec 字段须保留");
}
@Test
@DisplayName("injectDriverByArchetype:match3 → tap-targets 放置族(不带 targetMode)")
void injectDriver_match3_placement() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{}", "match3"));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertTrue(v.path("driver").path("targetMode").isMissingNode(), "放置族不带 occupied 语义");
}
@Test
@DisplayName("injectDriverByArchetype:已显式 driver → 不覆盖(尊重显式声明)")
void injectDriver_respectsExplicit() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"driver\":{\"type\":\"seek-food\"}}", "tycoon"));
assertEquals("seek-food", v.path("driver").path("type").asText(), "显式 driver 优先,不被 archetype 覆盖");
}
@Test
@DisplayName("injectDriverByArchetype:无合适内置(bubble)→ 不补(保持未驱动 advisory)")
void injectDriver_noFit_skips() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"expectLatch\":true}", "bubble"));
assertTrue(v.path("driver").isMissingNode(), "无合适内置 driver 不强补(否则误绑→门误判)");
}
@Test
@DisplayName("injectDriverByArchetype:空/generic → 兜底 tap-targets occupied")
void injectDriver_genericFallback() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{}", ""));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertEquals("occupied", v.path("driver").path("targetMode").asText());
}
}