feat(adapter): E1 第四游戏族 flappy——flap + flappy-gap,真九门绿(flap-to-gap driver)
flappy/runner 族:重力下落 + 点击振翅(flap input op,vy 上冲)+ 单缝隙左滚穿缝判定(flappy-gap update op:
重力推进 bird、缝隙越过 bird.x → 在缝内计分+穿缝反馈(粒子/音效)否则漏接、回收缝隙、出界=漏+复位)。
- forensics additive 暴露 bird{x,y,vy}/nextGap{centerY}(供 flap-to-gap driver 速度前瞻穿缝)。
- flappy.source.json fixture;单测 +3(forensics/flap 上冲/重力+穿缝机制),共 27 adapter。
- e2e(本机):scaffoldFromSource 落 flappy → build → 真九门 A–I 全过(score 0→2 latch、flap-to-gap 导航穿缝、F 门反馈);瞬时件已清。
- 全 game-runtime 套件 188/188 绿。
证成:E1 适配器覆盖四大游戏族(静态点击/realtime 漂移/控制类 paddle-intercept/flappy 物理)均过真九门
(含 I 门),声明式适配器跨游戏族泛化稳固。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
413f8edce4
commit
5164f28f69
@ -25,6 +25,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'tap-target.source.json'), 'utf8'));
|
||||
const DRIFT_SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'moving-targets.source.json'), 'utf8'));
|
||||
const CATCH_SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'catch.source.json'), 'utf8'));
|
||||
const FLAPPY_SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'flappy.source.json'), 'utf8'));
|
||||
|
||||
/** 录制版 2D 上下文:记 fillRect/fillText 调用,setter 容纳 fillStyle/font/strokeStyle 赋值。 */
|
||||
function recordingCtx2d() {
|
||||
@ -227,6 +228,34 @@ test('控制类:paddle 对齐下落球 → 接住计分', () => {
|
||||
assert.ok(game._forensicsView().state().score >= 1, '对齐后应接住至少 1 次');
|
||||
});
|
||||
|
||||
// ── flappy(flap-to-gap 族:重力 + 振翅 + 穿缝,第 4 游戏族) ──
|
||||
test('flappy:forensics 暴露 bird{y,vy} + nextGap{centerY}(供 flap-to-gap driver)', () => {
|
||||
const game = adaptGameDefinition(FLAPPY_SOURCE)();
|
||||
const { boot } = makeBoot();
|
||||
game.init(boot);
|
||||
const v = game._forensicsView().state();
|
||||
assert.ok(v.bird && typeof v.bird.y === 'number' && typeof v.bird.vy === 'number', '应暴露 bird.y/vy');
|
||||
assert.ok(v.nextGap && typeof v.nextGap.centerY === 'number', '应暴露 nextGap.centerY');
|
||||
});
|
||||
|
||||
test('flappy:flap 给 bird 向上冲量(vy 变负)', () => {
|
||||
const game = adaptGameDefinition(FLAPPY_SOURCE)();
|
||||
const { boot, inputBridge } = makeBoot();
|
||||
game.init(boot);
|
||||
game.update(0.05); // 先因重力 vy>0
|
||||
inputBridge._emit('pointerdown', { x: 195, y: 400 });
|
||||
assert.ok(game._forensicsView().state().bird.vy < 0, 'flap 后 vy 应为负(上冲)');
|
||||
});
|
||||
|
||||
test('flappy:重力下落 + 缝隙滚过 → 穿缝计分或撞漏(机制生效)', () => {
|
||||
const game = adaptGameDefinition(FLAPPY_SOURCE)();
|
||||
const { boot } = makeBoot();
|
||||
game.init(boot);
|
||||
for (let i = 0; i < 60; i++) game.update(0.05); // 推进:bird 落 + 缝隙滚过 bird.x
|
||||
const v = game._forensicsView().state();
|
||||
assert.ok(v.score + v.misses >= 1, '缝隙至少越过一次(穿缝计分或撞=漏接)');
|
||||
});
|
||||
|
||||
// ── 鲁棒性:便宜模型产的 gameDefinition 常残缺/畸形,生产路适配器须优雅降级不崩 ──
|
||||
test('鲁棒:空 gameDefinition → 不崩、phase playing、可渲染', () => {
|
||||
const game = adaptGameDefinition({})();
|
||||
|
||||
@ -19,9 +19,9 @@ const VIEWPORT_W = 390;
|
||||
const VIEWPORT_H = 844;
|
||||
|
||||
/** 最小 behavior 词汇表(声明式路支持的 op;其余 op = 走 code-agent 逻辑码,本切片不解释)。 */
|
||||
const SUPPORTED_INPUT_OPS = new Set(['hit-nearest-target', 'control-x']);
|
||||
/** trigger=update 的 op(drift-targets:目标慢漂移;fall-and-catch:下落物 + 接住判定 = 控制类 archetype)。 */
|
||||
const SUPPORTED_UPDATE_OPS = new Set(['drift-targets', 'fall-and-catch']);
|
||||
const SUPPORTED_INPUT_OPS = new Set(['hit-nearest-target', 'control-x', 'flap']);
|
||||
/** trigger=update 的 op(drift-targets:漂移;fall-and-catch:接住=控制类;flappy-gap:重力+穿缝=flappy 类)。 */
|
||||
const SUPPORTED_UPDATE_OPS = new Set(['drift-targets', 'fall-and-catch', 'flappy-gap']);
|
||||
|
||||
/**
|
||||
* 极小安全条件求值(无 eval):解析 "<field> <op> <rhs>"。
|
||||
@ -111,6 +111,7 @@ export function adaptGameDefinition(source) {
|
||||
id: e.id,
|
||||
x: pos.x, y: pos.y,
|
||||
baseX: pos.x, baseY: pos.y, phase: i * 1.7, // drift-targets 用:漂移基准 + 错相
|
||||
vy: 0, // flappy/重力类用:竖直速度
|
||||
w: typeof rc.w === 'number' ? rc.w : 48,
|
||||
h: typeof rc.h === 'number' ? rc.h : 48,
|
||||
color: typeof rc.color === 'string' ? rc.color : '#3cf',
|
||||
@ -163,6 +164,11 @@ export function adaptGameDefinition(source) {
|
||||
const ent = entById(b.entity);
|
||||
if (ent) ent.x = Math.max(0, Math.min(VIEWPORT_W - ent.w, px - ent.w / 2));
|
||||
}
|
||||
if (b.op === 'flap') {
|
||||
// flappy 类:点击给控制体向上竖直冲量(y 向下为正 → 上冲为负)。
|
||||
const ent = entById(b.entity);
|
||||
if (ent) ent.vy = -(typeof b.impulse === 'number' ? b.impulse : 360);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,6 +209,31 @@ export function adaptGameDefinition(source) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (b.op === 'flappy-gap') {
|
||||
// flappy:bird 重力下落 + 缝隙左滚;缝隙越过 bird.x → 在缝内计分否则漏接,回收缝隙;出界=漏接+复位。
|
||||
const bird = entById(b.bird), gap = entById(b.gap);
|
||||
if (bird && gap) {
|
||||
bird.vy = (bird.vy || 0) + (typeof b.gravity === 'number' ? b.gravity : 1100) * dt;
|
||||
bird.y += bird.vy * dt;
|
||||
const gapH = typeof b.gapHeight === 'number' ? b.gapHeight : 200;
|
||||
const birdCx = bird.x + bird.w / 2, prevCx = gap.x + gap.w / 2;
|
||||
gap.x -= (typeof b.scrollSpeed === 'number' ? b.scrollSpeed : 150) * dt;
|
||||
if (prevCx >= birdCx && gap.x + gap.w / 2 < birdCx) { // 缝隙越过 bird
|
||||
if (Math.abs((bird.y + bird.h / 2) - (gap.y + gap.h / 2)) <= gapH / 2) {
|
||||
state.score += 1; // 穿缝
|
||||
const eng = ctx && ctx.getEngine && ctx.getEngine(); // 穿缝反馈(F 门)
|
||||
if (eng) {
|
||||
if (eng.particles) { try { eng.particles.spawnEmitter({ pos: { x: birdCx, y: bird.y }, count: 16, speed: 100, particleTime: 0.45, colorStart: { r: 0.3, g: 1, b: 0.5, a: 1 }, colorEnd: { r: 0.1, g: 0.4, b: 0.2, a: 0 } }); } catch (_) { /* 不连坐 */ } }
|
||||
if (eng.audio && eng.audio.synth) { try { eng.audio.synth.synthSfx([1, 0.05, 340, , , 0.13, , 1.1]); } catch (_) { /* 同 */ } }
|
||||
}
|
||||
} else {
|
||||
state.misses += 1; // 撞
|
||||
}
|
||||
}
|
||||
if (gap.x + gap.w < 0) { gap.x = VIEWPORT_W; gap.y = (ctx && ctx.random) ? Math.floor(ctx.random.range(180, VIEWPORT_H - 220)) : VIEWPORT_H / 2; } // 回收
|
||||
if (bird.y < 0 || bird.y > VIEWPORT_H) { state.misses += 1; bird.y = VIEWPORT_H / 2; bird.vy = 0; } // 出界=漏+复位
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const r of rules) {
|
||||
if (evalCondition(r.condition, state, config)) {
|
||||
@ -253,10 +284,15 @@ export function adaptGameDefinition(source) {
|
||||
state: () => {
|
||||
const ws = typeof config.winScore === 'number' ? config.winScore : targets.length;
|
||||
// 控制类暴露 paddle/ball(中心逻辑坐标)供 paddle-intercept driver 的 controlCheck.paddlePath/ballPath 读。
|
||||
let paddle = null, ball = null;
|
||||
let paddle = null, ball = null, bird = null, nextGap = null;
|
||||
for (const b of behaviors) {
|
||||
if (b.trigger === 'input' && b.op === 'control-x') { const e = entById(b.entity); if (e) paddle = { x: e.x + e.w / 2, y: e.y, w: e.w }; }
|
||||
if (b.trigger === 'update' && b.op === 'fall-and-catch') { const e = entById(b.faller); if (e) ball = { x: e.x + e.w / 2, y: e.y + e.h / 2 }; }
|
||||
if (b.trigger === 'update' && b.op === 'flappy-gap') {
|
||||
const bd = entById(b.bird), gp = entById(b.gap);
|
||||
if (bd) bird = { x: bd.x + bd.w / 2, y: bd.y + bd.h / 2, vy: bd.vy || 0 };
|
||||
if (gp) nextGap = { centerY: gp.y + gp.h / 2, x: gp.x };
|
||||
}
|
||||
}
|
||||
return {
|
||||
phase: state.phase,
|
||||
@ -273,6 +309,8 @@ export function adaptGameDefinition(source) {
|
||||
})),
|
||||
...(paddle ? { paddle } : {}),
|
||||
...(ball ? { ball } : {}),
|
||||
...(bird ? { bird } : {}),
|
||||
...(nextGap ? { nextGap } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
27
game-runtime/src/adapter/fixtures/flappy.source.json
Normal file
27
game-runtime/src/adapter/fixtures/flappy.source.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"sourceHash": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"profile": { "tickModel": "realtime", "inputModel": "continuous", "progressModel": "metric" },
|
||||
"gameDefinition": {
|
||||
"entities": [
|
||||
{ "id": "bird", "transform": { "position": { "x": 90, "y": 400 } }, "components": ["r-bird"] },
|
||||
{ "id": "gap", "transform": { "position": { "x": 390, "y": 420 } }, "components": ["r-gap"] }
|
||||
],
|
||||
"components": [
|
||||
{ "id": "r-bird", "kind": "render", "shape": "rect", "w": 28, "h": 28, "color": "#fc4" },
|
||||
{ "id": "r-gap", "kind": "render", "shape": "rect", "w": 40, "h": 40, "color": "#4f8" }
|
||||
],
|
||||
"behaviors": [
|
||||
{ "id": "b-flap", "trigger": "input", "op": "flap", "entity": "bird", "impulse": 360 },
|
||||
{ "id": "b-flappy", "trigger": "update", "op": "flappy-gap", "bird": "bird", "gap": "gap", "gravity": 1100, "scrollSpeed": 150, "gapHeight": 200 }
|
||||
],
|
||||
"scenes": [
|
||||
{ "id": "s1", "entityRefs": ["bird", "gap"] }
|
||||
],
|
||||
"rules": [
|
||||
{ "id": "win", "condition": "score >= config.winScore", "outcome": "win" },
|
||||
{ "id": "lose", "condition": "misses >= config.maxMisses", "outcome": "lose" }
|
||||
]
|
||||
},
|
||||
"config": { "winScore": 3, "maxMisses": 8 }
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user