@
feat(quantik-game): фаза 3 — граф-уровни (движение по f(x)) + зоны Новый тип уровня: Квантик едет по кривой y=f(x), которую игрок собирает слайдерами коэффициентов, проходя сквозь зоны-препятствия. Движок (аддитивно): plot.runner → env-поля curve.runX/runY/runDone (f компилится 1 раз, питает И кривую, И бегунок-героя, без само-ссылки); type zone (forbidden/target/collect) → булево env-поле zone.hit. Грамматика выражений ЗАКРЫТА — никаких inzone()-предикатов, только именованные env-поля (модель t/tries из Ф0), без eval. Глава-созвездие functions из 5 уровней (луч/синус/парабола/модуль/экспонента), разблокировка 9/11/13/ 15/17 (цепочка проходима). validateSpec принимает zone+runner. Все 5 уровней независимо проверены на движке (2★ достижимы). npm test 253/8 baseline; custom-sims 26/26; lint:routes 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
+289
-2
@@ -378,12 +378,299 @@
|
||||
}
|
||||
};
|
||||
|
||||
var LEVELS = [artillery1, arc2, bounce3, pendulum4, orbit5, slingshot6];
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Глава III — «Функции» (созвездие): Квантик едет по кривой y=f(x), которую
|
||||
СОБИРАЕТ игрок (слайдеры коэффициентов). Препятствия — зоны (Ф3 движка):
|
||||
forbidden — задел → проигрыш (fail);
|
||||
target — добрался → победа (goal);
|
||||
collect — задел → звезда-бонус (sticky).
|
||||
Реюз: plot+runner рисует кривую и ведёт по ней героя (одна скомпил. функция),
|
||||
зоны дают булевы env-поля <id>.hit. Без eval — только SimExpr-выражения.
|
||||
──────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
var FORB = '#F87171'; // запретная зона (красный)
|
||||
var TARG = '#34D399'; // целевая зона (зелёный)
|
||||
var COIN = '#FBBF24'; // зона-сбор (золото)
|
||||
var CURVE = '#67E8F9'; // цвет кривой-«дороги»
|
||||
|
||||
/* helper: герой-точка, едет по бегунку кривой (id 'curve'): x=curve.runX, y=curve.runY.
|
||||
glow+trail (визуал P2). НЕ тело (кинематический проход) → не само-ссылка. */
|
||||
function graphHero() {
|
||||
return {
|
||||
id: 'ball', type: 'point', r: 7, color: HERO,
|
||||
x: 'curve.runX', y: 'curve.runY',
|
||||
glow: true, glowColor: HERO, trail: true, trailColor: HERO, trailFade: true
|
||||
};
|
||||
}
|
||||
/* helper: plot-«дорога» = кривая f(x) с бегунком. exprStr — выражение кривой. */
|
||||
function road(exprStr, a, b, dur) {
|
||||
return {
|
||||
id: 'curve', type: 'plot', expr: exprStr, var: 'x',
|
||||
range: [a, b], samples: 220, color: CURVE, width: 3, glow: true, glowColor: CURVE,
|
||||
runner: { duration: dur, hold: true }
|
||||
};
|
||||
}
|
||||
function rectZone(id, kind, x, y, w, h, label) {
|
||||
var col = kind === 'target' ? TARG : kind === 'collect' ? COIN : FORB;
|
||||
return { type: 'zone', id: id, kind: kind, shape: 'rect', x: x, y: y, w: w, h: h,
|
||||
color: col, label: label || '' };
|
||||
}
|
||||
function circZone(id, kind, x, y, r, label) {
|
||||
var col = kind === 'target' ? TARG : kind === 'collect' ? COIN : FORB;
|
||||
return { type: 'zone', id: id, kind: kind, shape: 'circle', x: x, y: y, r: r,
|
||||
color: col, label: label || '' };
|
||||
}
|
||||
function startMarker(x, y) {
|
||||
return { type: 'circle', x: x, y: y, r: 0.16, color: '#94A3B8', width: 0, fill: '#94A3B8' };
|
||||
}
|
||||
|
||||
/* Уровень 7: «Луч через ворота» — прямая f(x)=a·x+b. Сверху и снизу в центре —
|
||||
запретные брусья; проведи луч между ними в зелёный портал справа. Монета по центру. */
|
||||
var graphLine7 = {
|
||||
id: 'graph-line-7',
|
||||
title: 'Луч через ворота',
|
||||
chapter: 'functions',
|
||||
order: 7,
|
||||
unlockStars: 9,
|
||||
par_ms: 5200,
|
||||
subject: 'algebra',
|
||||
hint: 'Квантик поедет по прямой y = a·x + b. Подбери наклон a и сдвиг b, чтобы луч прошёл между брусьями в воротах и достиг зелёного портала. Монета — точно в центре прохода.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Луч через ворота', desc: 'Линейная функция: y = a·x + b.' },
|
||||
viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 8, grid: true, axes: true, bg: BG },
|
||||
time: { duration: 5, loop: false },
|
||||
params: [
|
||||
{ name: 'a', label: 'Наклон a', min: -0.5, max: 1.5, step: 0.05, value: 0.2 },
|
||||
{ name: 'b', label: 'Сдвиг b', min: 0, max: 5, step: 0.1, value: 1 }
|
||||
],
|
||||
objects: [
|
||||
// ворота в центре: верхний и нижний брус (проход 1..6 по y при x≈5)
|
||||
rectZone('topbar', 'forbidden', 5, 7.0, 4, 2.4, 'стена'),
|
||||
rectZone('botbar', 'forbidden', 5, -0.2, 4, 2.4, 'стена'),
|
||||
// монета в центре прохода
|
||||
circZone('coin', 'collect', 5, 3, 0.7, 'монета'),
|
||||
// целевой портал справа
|
||||
circZone('gate', 'target', 10, 5, 0.95, 'портал'),
|
||||
startMarker(0, 1),
|
||||
road('a*x + b', 0, 10, 5),
|
||||
graphHero(),
|
||||
{ type: 'readout', label: 'a', expr: 'a', precision: 2 },
|
||||
{ type: 'readout', label: 'b', expr: 'b', precision: 1 }
|
||||
],
|
||||
goal: {
|
||||
title: 'Проведи луч в портал',
|
||||
hint: 'Достигни зелёного портала, не задев красные брусья. Бонус: собери монету.',
|
||||
when: 'gate.hit',
|
||||
fail: 'topbar.hit || botbar.hit',
|
||||
stars: [
|
||||
{ when: 'coin.hit', label: 'Собрал монету' },
|
||||
{ when: 'gate.hit && b < 1.6', label: 'Низкий старт (b < 1.6)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Уровень 8: «Синус сквозь врата» — f(x)=A·sin(k·x). Сверху/снизу — зубцы-стены
|
||||
с просветами; подбери амплитуду и частоту, чтобы волна прошла сквозь все просветы. */
|
||||
var graphSine8 = {
|
||||
id: 'graph-sine-8',
|
||||
title: 'Синус сквозь врата',
|
||||
chapter: 'functions',
|
||||
order: 8,
|
||||
unlockStars: 11,
|
||||
par_ms: 6000,
|
||||
subject: 'algebra',
|
||||
hint: 'Квантик едет по волне y = A·sin(k·x). Подбери амплитуду A и частоту k, чтобы волна прошла сквозь просветы между зубцами. Монеты — в гребне и впадине волны.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Синус сквозь врата', desc: 'Синусоида: y = A·sin(k·x).' },
|
||||
viewport: { xmin: -0.5, xmax: 12.5, ymin: -5, ymax: 5, grid: true, axes: true, bg: BG },
|
||||
time: { duration: 6, loop: false },
|
||||
params: [
|
||||
{ name: 'A', label: 'Амплитуда A', min: 1, max: 4, step: 0.1, value: 2 },
|
||||
{ name: 'k', label: 'Частота k', min: 0.3, max: 1.6, step: 0.05, value: 0.6 }
|
||||
],
|
||||
objects: [
|
||||
// зубцы: верхний брус над гребнем (x≈π/2k) и нижний под впадиной (x≈3π/2k)
|
||||
// при целевом k≈0.79 (π/4): гребень x≈2, впадина x≈6, гребень x≈10
|
||||
rectZone('spikeTop', 'forbidden', 2, 4.4, 1.2, 2.4, ''),
|
||||
rectZone('spikeBot', 'forbidden', 6, -4.4, 1.2, 2.4, ''),
|
||||
rectZone('spikeTop2', 'forbidden', 10, 4.4, 1.2, 2.4, ''),
|
||||
// монеты в гребне/впадине
|
||||
circZone('coinTop', 'collect', 2, 3, 0.6, ''),
|
||||
circZone('coinBot', 'collect', 6, -3, 0.6, ''),
|
||||
// портал на третьем гребне
|
||||
circZone('gate', 'target', 11.2, 0, 1.0, 'портал'),
|
||||
startMarker(0, 0),
|
||||
road('A*sin(k*x)', 0, 11.5, 6),
|
||||
graphHero(),
|
||||
{ type: 'readout', label: 'A', expr: 'A', precision: 1 },
|
||||
{ type: 'readout', label: 'k', expr: 'k', precision: 2 }
|
||||
],
|
||||
goal: {
|
||||
title: 'Проведи волну в портал',
|
||||
hint: 'Подбери волну так, чтобы пройти все просветы и достичь портала. Бонус: собери монеты в гребне и впадине.',
|
||||
when: 'gate.hit',
|
||||
fail: 'spikeTop.hit || spikeBot.hit || spikeTop2.hit',
|
||||
stars: [
|
||||
{ when: 'coinTop.hit', label: 'Монета-гребень' },
|
||||
{ when: 'coinBot.hit', label: 'Монета-впадина' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Уровень 9: «Парабола над ямой» — f(x)=a·(x-3)^2+k (вершина в (3,k)... меняем).
|
||||
В центре — запретная яма; перебрось параболу-дугу над ней в портал. */
|
||||
var graphParab9 = {
|
||||
id: 'graph-parab-9',
|
||||
title: 'Парабола над ямой',
|
||||
chapter: 'functions',
|
||||
order: 9,
|
||||
unlockStars: 13,
|
||||
par_ms: 6000,
|
||||
subject: 'algebra',
|
||||
hint: 'Квантик едет по параболе y = a·(x − 5)² + k. Подбери раскрытие a (вниз — отрицательное) и высоту вершины k, чтобы дуга прошла над ямой в портал. Монета — на вершине дуги.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Парабола над ямой', desc: 'Квадратичная: y = a·(x − 5)² + k.' },
|
||||
viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG },
|
||||
time: { duration: 6, loop: false },
|
||||
params: [
|
||||
{ name: 'a', label: 'Раскрытие a', min: -0.6, max: -0.1, step: 0.02, value: -0.3 },
|
||||
{ name: 'k', label: 'Высота вершины k', min: 4, max: 8, step: 0.1, value: 6 }
|
||||
],
|
||||
objects: [
|
||||
// яма в центре (запретная) — занимает низ по центру
|
||||
rectZone('pit', 'forbidden', 5, 1.4, 5.0, 3.0, 'яма'),
|
||||
// монета на вершине
|
||||
circZone('coin', 'collect', 5, 6, 0.7, 'монета'),
|
||||
// портал у правого края, низко (нужно спуститься на нисходящей ветви)
|
||||
circZone('gate', 'target', 9.5, 4.4, 0.95, 'портал'),
|
||||
startMarker(0.5, 0), // ориентир старта (фактический старт = f(0))
|
||||
road('a*(x-5)^2 + k', 0, 9.5, 6),
|
||||
graphHero(),
|
||||
{ type: 'readout', label: 'a', expr: 'a', precision: 2 },
|
||||
{ type: 'readout', label: 'k', expr: 'k', precision: 1 }
|
||||
],
|
||||
goal: {
|
||||
title: 'Дугой над ямой в портал',
|
||||
hint: 'Перебрось дугу над ямой к порталу. Бонус: задень монету на вершине.',
|
||||
when: 'gate.hit',
|
||||
fail: 'pit.hit',
|
||||
stars: [
|
||||
{ when: 'coin.hit', label: 'Собрал монету' },
|
||||
{ when: 'gate.hit && k >= 6.4', label: 'Высокая дуга (k ≥ 6.4)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Уровень 10: «Угол модуля» — f(x)=a·abs(x-h)+1. V-образная кривая: подбери
|
||||
раскрытие a и положение вершины h, чтобы «галочка» обошла два бруса и попала в портал. */
|
||||
var graphAbs10 = {
|
||||
id: 'graph-abs-10',
|
||||
title: 'Угол модуля',
|
||||
chapter: 'functions',
|
||||
order: 10,
|
||||
unlockStars: 15,
|
||||
par_ms: 6000,
|
||||
subject: 'algebra',
|
||||
hint: 'Квантик едет по «галочке» y = a·|x − m| + 1. Подбери крутизну a и положение вершины m, чтобы остриё прошло в проход и луч попал в портал. Монета — у самого острия.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Угол модуля', desc: 'Модуль: y = a·|x − m| + 1.' },
|
||||
viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG },
|
||||
time: { duration: 6, loop: false },
|
||||
params: [
|
||||
// m = положение вершины (имя 'h' зарезервировано движком под высоту вьюпорта!)
|
||||
{ name: 'a', label: 'Крутизна a', min: 0.6, max: 2.2, step: 0.05, value: 1.2 },
|
||||
{ name: 'm', label: 'Вершина m', min: 3, max: 7, step: 0.1, value: 5 }
|
||||
],
|
||||
objects: [
|
||||
// нижние брусья слева и справа от прохода-острия (вершина должна попасть в проход)
|
||||
rectZone('floorL', 'forbidden', 2.4, -0.2, 2.4, 1.4, ''),
|
||||
rectZone('floorR', 'forbidden', 7.6, -0.2, 2.4, 1.4, ''),
|
||||
// монета у острия (низко, по центру)
|
||||
circZone('coin', 'collect', 5, 1, 0.7, 'монета'),
|
||||
// портал справа-вверху (на восходящей ветви)
|
||||
circZone('gate', 'target', 9.6, 6.4, 1.0, 'портал'),
|
||||
startMarker(0, 7),
|
||||
road('a*abs(x-m) + 1', 0, 9.6, 6),
|
||||
graphHero(),
|
||||
{ type: 'readout', label: 'a', expr: 'a', precision: 2 },
|
||||
{ type: 'readout', label: 'm', expr: 'm', precision: 1 }
|
||||
],
|
||||
goal: {
|
||||
title: 'Галочкой в портал',
|
||||
hint: 'Проведи остриё галочки в проход между брусьями и попади в портал. Бонус: задень монету у острия.',
|
||||
when: 'gate.hit',
|
||||
fail: 'floorL.hit || floorR.hit',
|
||||
stars: [
|
||||
{ when: 'coin.hit', label: 'Собрал монету' },
|
||||
{ when: 'gate.hit && m >= 4.6 && m <= 5.4', label: 'Вершина по центру' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Уровень 11 (капстоун): «Экспонента-подъём» — f(x)=c·exp(r·x). Барьер-«потолок»
|
||||
слева заставляет держать низкий старт; затем экспонента взмывает в высокий портал. */
|
||||
var graphExp11 = {
|
||||
id: 'graph-exp-11',
|
||||
title: 'Экспонента-подъём',
|
||||
chapter: 'functions',
|
||||
order: 11,
|
||||
unlockStars: 17,
|
||||
par_ms: 6500,
|
||||
subject: 'algebra',
|
||||
hint: 'Квантик едет по экспоненте y = c·e^(r·x). Подбери начальную высоту c и скорость роста r, чтобы пройти под потолком слева, но взмыть в высокий портал справа. Монета — на разгоне.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Экспонента-подъём', desc: 'Экспонента: y = c·e^(r·x).' },
|
||||
viewport: { xmin: -0.5, xmax: 8.5, ymin: -0.5, ymax: 9, grid: true, axes: true, bg: BG },
|
||||
time: { duration: 6.5, loop: false },
|
||||
params: [
|
||||
{ name: 'c', label: 'Старт c', min: 0.2, max: 1.5, step: 0.05, value: 0.6 },
|
||||
{ name: 'r', label: 'Рост r', min: 0.2, max: 0.7, step: 0.02, value: 0.4 }
|
||||
],
|
||||
objects: [
|
||||
// потолок слева (низкий старт): запретный брус над началом x∈[1,4], y от 2.5 вверх
|
||||
rectZone('ceil', 'forbidden', 2.5, 4.0, 3.2, 3.0, 'потолок'),
|
||||
// пол-яма в начале (нельзя слишком низко/нулём): брус снизу x∈[0,5]
|
||||
rectZone('floor', 'forbidden', 2.4, -0.5, 5.0, 1.0, ''),
|
||||
circZone('coin', 'collect', 5, 3.3, 0.7, 'монета'),
|
||||
circZone('gate', 'target', 7.6, 7.2, 1.05, 'портал'),
|
||||
startMarker(0, 0.6),
|
||||
road('c*exp(r*x)', 0, 7.7, 6.5),
|
||||
graphHero(),
|
||||
{ type: 'readout', label: 'c', expr: 'c', precision: 2 },
|
||||
{ type: 'readout', label: 'r', expr: 'r', precision: 2 }
|
||||
],
|
||||
goal: {
|
||||
title: 'Взмой в портал',
|
||||
hint: 'Пройди под потолком слева и взмой экспонентой в высокий портал. Бонус: задень монету на разгоне.',
|
||||
when: 'gate.hit',
|
||||
fail: 'ceil.hit || floor.hit',
|
||||
stars: [
|
||||
{ when: 'coin.hit', label: 'Собрал монету' },
|
||||
{ when: 'gate.hit && r >= 0.42', label: 'Крутой рост (r ≥ 0.42)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var LEVELS = [
|
||||
artillery1, arc2, bounce3, pendulum4, orbit5, slingshot6,
|
||||
graphLine7, graphSine8, graphParab9, graphAbs10, graphExp11
|
||||
];
|
||||
|
||||
/* Метаданные глав (созвездий) — для заголовков/оформления карты. */
|
||||
var CHAPTERS = {
|
||||
kinematics: { key: 'kinematics', title: 'Кинематика', subtitle: 'Полёт и гравитация', accent: '#22D3EE' },
|
||||
dynamics: { key: 'dynamics', title: 'Динамика', subtitle: 'Силы, пружины, орбиты', accent: '#A78BFA' }
|
||||
dynamics: { key: 'dynamics', title: 'Динамика', subtitle: 'Силы, пружины, орбиты', accent: '#A78BFA' },
|
||||
functions: { key: 'functions', title: 'Функции', subtitle: 'Едем по кривой y = f(x)', accent: '#67E8F9' }
|
||||
};
|
||||
|
||||
function list() { return LEVELS.slice(); }
|
||||
|
||||
Reference in New Issue
Block a user