From d342de088e5f06ea18109d3aaa562a8141586fc2 Mon Sep 17 00:00:00 2001 From: lili Date: Tue, 23 Jun 2026 19:45:33 -0700 Subject: [PATCH] =?UTF-8?q?feat(saa):=20=E9=9B=B6-LLM=20driver=20=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E7=BB=91=E5=AE=9A=20injectDriverByArchetype=20+=205?= =?UTF-8?q?=20=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../game/module/aigc/saa/SaaGenNodes.java | 53 +++++++++++++++++++ .../saa/SaaFullGraphE2eGateLogicTest.java | 44 +++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java index 087925f5..4b8cb0d7 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java @@ -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 出招)。 + *

design 按 gatespec 规则③只声明「什么算赢/有进展」、不写 driver;但 amodel 路无人绑 driver → 游戏不被驱动 → 停菜单 → + * 九门 E_live/H 因「未驱动」降 advisory 名存实亡。本处补齐:让经营/反应/放置等类被真玩通,使门可信(创始人指令)。 + *

已含 driver(显式声明)则原样返回不覆盖;无合适内置 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) { diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java index 8ea63cf6..741c3f5a 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java @@ -315,4 +315,48 @@ class SaaFullGraphE2eGateLogicTest { assertTrue(fb.contains("D_render"), "真硬门 D_render 失败应回喂救场"); assertFalse(fb.contains("E_live"), "advisory E_live 不应回喂救场"); } + + // ═══════════════════════ ② driver 内置绑定 archetype→driver(零-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()); + } }