lili 39cc2c09ba feat(cheap-worker): 非遗品类件③few-shot正例+rubric金标锚定回填+骨架import护栏 (W-GENRE)
③ game-runtime/games/_fewshot-feiyi/:few-shot 过门正例(木版年画作坊,源=n=5 小批第 3
款 p11d-print):attempts=1/¥2.65/九门 A-I 全绿/丰富度 9-11;范例签名机器校验门
(tools.mjs check)蒸馏时复跑 PASS;README 带来历/复验配方/漂移纪律(设计档 §3⑥
「必须同改」清单成员)。
④ 锚定回填 heritage.rubric.json:通用 11+品类 5 合评、两分母分开(M3 judge,容差
±1/组)——正例 通用 10/11(L2 6/6·L3 2/3·L4 2/2)+品类 5/5;薄反例(骨架原样)
通用 6/11+品类 3/5(节律张力/技艺成长 false=骨架无 combo/解锁,判罚与轻起点定位
一致)——评分尺判别力坐实。
② 骨架 game-logic.js 头注加 import 完整性护栏:n=5 首款 p11d-porc 失败根因 =
agent 改写时 import 列表漏 FIRST_READY_DELAY_MS、裸标识符 esbuild 当全局放行、
运行时点开工即 ReferenceError 被 L1 隔离→永卡菜单、E/G/H 三挂(check/build 双门
拦不住的盲区;node headless 探针定位)。护栏注释进克隆起点后 3 连过佐证。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:13:11 -07:00

180 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* core.js — 【L3 纯逻辑】木版年画作坊 · 工序节拍玩法数值与判定
*
* 本款 = 非遗「工序节拍」品类(template-feiyi)+ 传统技艺「木版年画」。
* 一件年画 = 一条 4 道工序链:刻版 → 上墨 → 铺纸 → 拓印;在火候窗口内点工位
* 完成该道工序;过窗回炉;整条链走完 = 印出一张年画;60s 限时赶工结算。
* 印满阈值解锁新图样(财神/麒麟/童子)——「印得多解锁新的吉祥图样」的核心钩子。
*
* 工序名取自木版年画通识工序词;**不编造具体非遗项目的失实细节**。
*
* 作品字段(公开契约,render / game-logic / 取证共用):
* work = { id, name, bonus, stepIndex, stepState:'crafting'|'ready', stateSinceMs,
* craftMs, windowMs, glyph: '福'|'门神'|... }
*/
'use strict';
/* ═══════════════════════════════════════════════════════════════════════════
* 【多样性参数空间 · 木版年画作坊】
* ═══════════════════════════════════════════════════════════════════════════ */
/** 一局开工时长(ms):年前赶工 60s。 */
export const ROUND_MS = 60000;
/**
* 工序链(木版年画 4 道):名字 + 颜色 + 完成分。
* 颜色用于程序化绘制工位物件(刻刀/墨辊/宣纸/棕刷的视觉锚)。
* 顺序:刻版→上墨→铺纸→拓印,越接近成品越值钱(后道高于前道)。
*/
export const CRAFT_CHAIN = [
{ name: '刻版', color: '#5a3a24', points: 5, tool: '刻刀', desc: '雕梨木板' },
{ name: '上墨', color: '#c8261a', points: 7, tool: '墨辊', desc: '朱砂上墨' },
{ name: '铺纸', color: '#f3e8c2', points: 9, tool: '宣纸', desc: '覆纸上版' },
{ name: '拓印', color: '#1f4e7a', points: 12, tool: '棕刷', desc: '按压转印' },
];
/**
* 作品表(吉祥图样):印出一张年画;bonus = 成品奖励分(≈单道工序分的 3–4 倍)。
* 收集向 = 凑齐一套解锁「图鉴」成就感。
*
* `unlockAtWage` 字段:达成该累计工钱后该图样解锁(传 -1 = 默认解锁)。
* 默认解锁 3 件,工钱达 80 解锁第 4 件「麒麟」,达 160 解锁第 5 件「童子」。
* ——「印得多解锁新的吉祥图样」的核心钩子落在这里。
*/
export const WORKS = [
{ name: '福字', glyph: '福', bgColor: '#c8261a', bonus: 26, unlockAtWage: 0 },
{ name: '门神', glyph: '门', bgColor: '#1f4e7a', bonus: 32, unlockAtWage: 0 },
{ name: '财神', glyph: '财', bgColor: '#caa84a', bonus: 38, unlockAtWage: 0 },
{ name: '麒麟', glyph: '麒', bgColor: '#5a3a24', bonus: 44, unlockAtWage: 80 },
{ name: '童子抱鱼', glyph: '童', bgColor: '#7a9b3e', bonus: 50, unlockAtWage: 160 },
];
/** 解锁的初始默认工资阈值(在本局内)——所有工资 < 阈值的图样在图鉴里置灰挂锁。 */
export const UNLOCK_PROGRESS_TOTAL = 200; // 「印得多解锁新的吉祥图样」封顶参考
/** 工序就绪间隔区间(ms):上一道完成后,下一道多久「就绪」。 */
export const CRAFT_MIN_MS = 650;
export const CRAFT_MAX_MS = 1050;
/** 开局首道工序就绪延迟(ms):留一拍让玩家看清案台。 */
export const FIRST_READY_DELAY_MS = 700;
/** 火候窗口区间(ms):工序就绪后须在窗口内点掉,过窗回炉(不判死)。 */
export const WINDOW_MIN_MS = 5000;
export const WINDOW_MAX_MS = 8000;
/** 正窗暴击(基础分之上加成):窗口前 40% 出手 → 加成倍率(双层奖励之技巧分)。 */
export const CRIT_RATIO = 0.4;
export const CRIT_MULT = 1.6;
/** 回炉冷却(ms):过窗后该道重新加工的耗时(损时间不清进度)。 */
export const REDO_MS = 1100;
/** 工位点按半径(手指友好,别小于 ~44)。 */
export const STATION_TAP_RADIUS = 58;
/** 工位布局比例(读 viewport 算具体像素,别硬编码 390×844):案台一横排。 */
export const STATION_ROW_Y_RATIO = [0.62];
export const STATION_MARGIN_X_RATIO = 0.14;
/* ═══════════════════════════════════════════════════════════════════════════
* 【工序循环判定 · 结构层 · 一般不动】
* ═══════════════════════════════════════════════════════════════════════════ */
/** 受控随机区间 [lo,hi):入参 rng 走 ctx.random,纯确定性。 */
function rngRange(rng, lo, hi) { return lo + rng() * (hi - lo); }
/**
* 算工位中心点(每道工序一个工位;读 viewport,不硬编码)。
* @returns {{x:number,y:number,station:number}[]} 与 CRAFT_CHAIN 等长
*/
export function computeStations(viewW, viewH, count) {
const n = count == null ? CRAFT_CHAIN.length : count;
const rows = STATION_ROW_Y_RATIO.length;
const perRow = Math.ceil(n / rows);
const left = viewW * STATION_MARGIN_X_RATIO;
const right = viewW * (1 - STATION_MARGIN_X_RATIO);
const out = [];
for (let i = 0; i < n; i++) {
const row = Math.floor(i / perRow);
const col = i % perRow;
const inRow = Math.min(perRow, n - row * perRow);
const x = inRow === 1 ? (left + right) / 2 : left + (col * (right - left)) / (inRow - 1);
out.push({ station: i, x: Math.round(x), y: Math.round(viewH * STATION_ROW_Y_RATIO[Math.min(row, rows - 1)]) });
}
return out;
}
/** 取当前工钱下可印的作品表(过滤未解锁的)。 */
export function availableWorks(wage, works) {
const list = works || WORKS;
return list.filter((w) => wage >= (w.unlockAtWage != null ? w.unlockAtWage : 0));
}
/** 开一件新作品(从已解锁图样中轮换)。 */
export function makeWork(rng, nowMs, idSeq, firstDelayMs, wage, works) {
const pool = availableWorks(wage, works);
const list = pool.length > 0 ? pool : (works || WORKS);
const w = list[idSeq % list.length];
return {
id: idSeq,
name: w.name,
glyph: w.glyph,
bgColor: w.bgColor,
bonus: w.bonus,
stepIndex: 0,
stepState: 'crafting',
stateSinceMs: nowMs,
craftMs: firstDelayMs != null ? firstDelayMs : Math.round(rngRange(rng, CRAFT_MIN_MS, CRAFT_MAX_MS)),
windowMs: Math.round(rngRange(rng, WINDOW_MIN_MS, WINDOW_MAX_MS)),
};
}
/** 加工态是否已到就绪时刻(crafting 满 craftMs → 该转 ready)。 */
export function shouldBecomeReady(work, nowMs) {
return !!work && work.stepState === 'crafting' && (nowMs - work.stateSinceMs) >= work.craftMs;
}
/** 就绪态是否已过火候窗口(ready 超 windowMs → 该回炉)。 */
export function isWindowExpired(work, nowMs) {
return !!work && work.stepState === 'ready' && (nowMs - work.stateSinceMs) > work.windowMs;
}
/** 火候窗口剩余比例 [0,1](1=刚就绪,0=已过窗;非 ready 返 0)。 */
export function windowRatio(work, nowMs) {
if (!work || work.stepState !== 'ready' || !work.windowMs) return 0;
const waited = nowMs - work.stateSinceMs;
return Math.max(0, Math.min(1, (work.windowMs - waited) / work.windowMs));
}
/** 当前工序此刻是否可点(= ready 且窗口未过)。 */
export function isStepTappable(work, nowMs) {
return !!work && work.stepState === 'ready' && (nowMs - work.stateSinceMs) <= work.windowMs;
}
/** 点 (px,py) 是否命中工位(圆心 + 半径)。 */
export function hitStation(px, py, st) {
if (!st) return false;
const dx = px - st.x, dy = py - st.y;
return dx * dx + dy * dy <= STATION_TAP_RADIUS * STATION_TAP_RADIUS;
}
/** 本道工序完成分(读 CRAFT_CHAIN;越界容错返 0)。 */
export function stepPoints(stepIndex) {
const s = CRAFT_CHAIN[stepIndex];
return s ? s.points : 0;
}
/** 是否为正窗暴击(在窗口前 CRIT_RATIO 比例内出手)。 */
export function isCritWindow(work, nowMs) {
if (!work || work.stepState !== 'ready' || !work.windowMs) return false;
const waited = nowMs - work.stateSinceMs;
return waited <= work.windowMs * CRIT_RATIO;
}
/** 解锁进度(0..1)——本局累计工资 / UNLOCK_PROGRESS_TOTAL,供图鉴用。 */
export function unlockProgress(wage) {
if (UNLOCK_PROGRESS_TOTAL <= 0) return 1;
return Math.max(0, Math.min(1, wage / UNLOCK_PROGRESS_TOTAL));
}