"""fresh Match-3 金标生成批次的静态边界测试。""" import sys import hashlib from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import match3_gold_batch as batch # noqa: E402 REPO_ROOT = Path(__file__).resolve().parents[2] def test_candidates_are_five_distinct_fresh_games(): """五款必须独立 gameId,不能把同一 fixture 的多轮冒充多款。""" ids = [candidate["gameId"] for candidate in batch.CANDIDATES] assert len(ids) == 5 assert len(set(ids)) == 5 assert all(game_id.startswith("gold-m3-") for game_id in ids) assert ids == [ "gold-m3-gem-r3", "gold-m3-candy-r3", "gold-m3-porcelain-r3", "gold-m3-fruit-r3", "gold-m3-rune-r3", ] def test_candidates_keep_canonical_match3_requirements(): """每份题面都必须明确 8×8、相邻交换、真实三连、终态和原生事件。""" required = ( "8×8", "上下左右相邻", "无效交换必须换回", "20 步", "gameover", "ctx.event", "puzzle.move-applied", "puzzle.swap-committed", "puzzle.match-formed", "boardRevisionBefore", "boardRevisionAfter", "matchedCells", "matchedCount", "puzzle.goal-reached", "puzzle.level-completed", "1-based row/column", ) for candidate in batch.CANDIDATES: assert all(fragment in candidate["brief"] for fragment in required) assert candidate["brief"].count("_forensicsView") == 1 assert "通过 ctx.log 写原生 game 业务事件" not in candidate["brief"] assert "禁止创建或启动 menu" in candidate["brief"] assert "match3ProducerProfile" in candidate["brief"] assert "drawSelectionOverlay" in candidate["brief"] assert "第一次点选只更新 selected" in candidate["brief"] assert "同一次 handleTap 同步调用栈" in candidate["brief"] assert "全部确定性连锁、重力补新、计分和目标/通关判定" in candidate["brief"] assert "这些业务事实不得延迟" in candidate["brief"] assert "立即调用 match3VisualTimeline.begin(...) 封存视觉事务" in candidate["brief"] assert "update(dt) 必须调用 match3VisualTimeline.update(dt)" in candidate["brief"] assert "render(g) 必须读取 match3VisualTimeline.view().tiles" in candidate["brief"] assert "禁止读取已经完成全部补位的业务终盘伪装动画" in candidate["brief"] assert "业务终态可以同步 latch,但成功/失败场景只能在 timeline settle 后显示" in candidate["brief"] assert "若耗尽最后一步,失败结果仍同步封存但延迟显示" in candidate["brief"] def test_producer_api_exposes_machine_perceivable_theme_contract(): """Writer 被要求先读的 API 必须写清图标感知约束,不能只给安全区坐标。""" source = (REPO_ROOT / "game-runtime/src/plugins/match3-producer-profile/api.d.ts").read_text( encoding="utf-8", ) required = ( "覆盖格心的单个连续实心前景", "禁止空心中心、分离装饰、文字和多块碎片", "70..200 千分比", "同一类别跨棋格逐像素保持同构", "不叠加渐变、半透明柔光、随机纹理或位置相关高光", ) assert all(fragment in source for fragment in required) def test_generation_batch_never_uses_fixture_as_target(): """生成批只允许从黄金 puzzle 起点生成 fresh ID,不得覆盖历史 match3 control。""" assert batch.TEMPLATE == "_template-puzzle" assert all(candidate["gameId"] != "match3" for candidate in batch.CANDIDATES) assert batch.HARD_STOP_RMB == 75.0 def test_batch_manifest_keeps_full_briefs_and_hashes(): """通用 brief 取证即使限长,合成金标批仍必须保留完整可复核原文。""" manifest = batch._batch_manifest() assert manifest["schemaVersion"] == "Match3GoldGenerationBatch/3" assert len(manifest["candidates"]) == 5 for source, recorded in zip(batch.CANDIDATES, manifest["candidates"], strict=True): assert recorded["brief"] == source["brief"] assert len(recorded["brief"]) > 1000 assert recorded["briefSha256"] == hashlib.sha256(source["brief"].encode()).hexdigest()