@
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:
@@ -28,6 +28,7 @@ const MAX_POINTS = 1000; // точек в polyline/path/points
|
||||
const OBJECT_TYPES = new Set([
|
||||
'point', 'segment', 'vector', 'circle', 'rect',
|
||||
'polyline', 'path', 'label', 'plot', 'readout',
|
||||
'zone', // Квантик Ф3: зона-препятствие/цель/сбор (граф-уровни)
|
||||
]);
|
||||
|
||||
const STATUSES = new Set(['draft', 'published']);
|
||||
@@ -189,6 +190,14 @@ function validateSpec(spec) {
|
||||
out.drag.param = sanitizeText(o.drag.param, 60);
|
||||
if (o.drag.paramY !== undefined) out.drag.paramY = sanitizeText(o.drag.paramY, 60);
|
||||
}
|
||||
|
||||
// zone{} — track = id отслеживаемой точки (Квантик Ф3): санитизируем как id.
|
||||
if (type === 'zone' && o.track !== undefined) out.track = sanitizeText(o.track, 60);
|
||||
|
||||
// runner{} на plot (Квантик Ф3): duration — число/выражение (длина).
|
||||
if (o.runner && typeof o.runner === 'object' && !Array.isArray(o.runner)) {
|
||||
if (o.runner.duration !== undefined) checkExpr(o.runner.duration, `objects[${i}].runner.duration`, errs);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
|
||||
@@ -203,6 +203,34 @@ describe('/api/custom-sims', () => {
|
||||
assert.ok(txt.includes('<img'), 'escaped form present');
|
||||
});
|
||||
|
||||
it('accepts graph-level spec with zone + runner (Квантик Ф3)', async () => {
|
||||
const spec = {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Граф-уровень' },
|
||||
viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 8 },
|
||||
params: [{ name: 'a', min: -1, max: 2, step: 0.05, value: 0.5 }],
|
||||
objects: [
|
||||
{ id: 'curve', type: 'plot', expr: 'a*x', var: 'x', range: [0, 10], runner: { duration: 5 } },
|
||||
{ id: 'ball', type: 'point', x: 'curve.runX', y: 'curve.runY', r: 7 },
|
||||
{ type: 'zone', id: 'pit', kind: 'forbidden', shape: 'rect', x: 5, y: 0, w: 4, h: 2, track: 'ball', label: 'яма' },
|
||||
{ type: 'zone', id: 'gate', kind: 'target', shape: 'circle', x: 10, y: 5, r: 1, track: 'ball' },
|
||||
],
|
||||
goal: { when: 'gate.hit', fail: 'pit.hit', stars: [{ when: 'gate.hit' }] },
|
||||
};
|
||||
const res = await inject('POST', '/api/custom-sims', { spec }, teacherToken);
|
||||
assert.equal(res.status, 201, `got ${res.status}: ${JSON.stringify(res.body)}`);
|
||||
const get = await inject('GET', `/api/custom-sims/${res.body.id}`, null, teacherToken);
|
||||
const objs = get.body.sim.spec.objects;
|
||||
assert.ok(objs.find(o => o.type === 'zone' && o.id === 'pit'), 'zone object preserved');
|
||||
assert.ok(objs.find(o => o.type === 'plot' && o.runner), 'runner block preserved');
|
||||
});
|
||||
|
||||
it('rejects unknown object type even with zone allowed (400)', async () => {
|
||||
const bad = { ...VALID_SPEC, objects: [{ type: 'zoney_fake', x: 0, y: 0 }] };
|
||||
const res = await inject('POST', '/api/custom-sims', { spec: bad }, teacherToken);
|
||||
assert.equal(res.status, 400, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('owner can DELETE own sim (then 404)', async () => {
|
||||
const del = await inject('DELETE', `/api/custom-sims/${simId}`, null, teacherToken);
|
||||
assert.equal(del.status, 200, `got ${del.status}`);
|
||||
|
||||
Reference in New Issue
Block a user