Files
tickte-system/tests/unit/workersLift.test.mjs
T
2026-10-18 02:16:47 +00:00

229 lines
12 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
import { makeCtx } from './helpers/fakeCtx.mjs'
// Aufzug: Warteschlange, Zustandsautomat, Abbruch/Reset und Kabine (Quelle: entwuerfe/scene-turm.js)
const { createLift, LIFT } = await loadModule('src/components/workers/engine/lift.js')
const { buildLayout, cabTop } = await loadModule('src/components/workers/engine/layout.js')
const { PAL } = await loadModule('src/components/workers/engine/sprites.js')
const { FLOORS } = await loadModule('src/lib/workersRoster.js')
const layout = buildLayout()
function until(lift, fn, max = 12) { let t = 0; while (!fn() && t < max) { lift.update(0.05); t += 0.05 } return t }
// Schritt für Schritt bis zur Bedingung, mit Sicherung gegen Endlosschleifen bei einem Fehler im Aufzug
function stepUntil(step, fn, max = 400) { for (let i = 0; i < max && !fn(); i++) step(); assert.ok(fn(), 'Bedingung nicht erreicht') }
test('Fahrt 1 → 8: queued, open, boarded, arrived, out; Fahrgast folgt der Kabine; danach idle', () => {
const lift = createLift(layout), a = { x: 0, y: 0, frame: 1 }, req = lift.request(a, 1, 8)
assert.equal(req.st, 'queued')
let t = until(lift, () => req.st === 'open'); assert.ok(lift.at(1)); assert.equal(lift.state, 'boarding')
req.st = 'boarded'; t += until(lift, () => req.st === 'arrived'); assert.ok(lift.at(8)); assert.equal(a.y, cabTop(layout, 8) + 7); assert.equal(a.x, LIFT.cabWx)
assert.ok(t < 12); req.st = 'out'; lift.update(0.05); assert.equal(lift.state, 'idle')
})
test('FIFO: zweite Anfrage wartet bis die erste ausgestiegen ist', () => {
const lift = createLift(layout), r1 = lift.request({}, 1, 4), r2 = lift.request({}, 5, 2)
until(lift, () => r1.st === 'open'); r1.st = 'boarded'; until(lift, () => r1.st === 'arrived'); assert.equal(r2.st, 'queued')
r1.st = 'out'; until(lift, () => r2.st === 'open'); assert.equal(r2.st, 'open')
})
test('cancel und reset geben die Kabine frei (kein Festhängen)', () => {
const lift = createLift(layout), a = {}, r1 = lift.request(a, 7, 1), r2 = lift.request({}, 2, 3)
lift.update(0.05); lift.cancel(a); assert.equal(r1.st, 'cancelled'); until(lift, () => r2.st === 'open'); assert.equal(r2.st, 'open')
lift.reset(); assert.equal(r2.st, 'cancelled'); assert.equal(lift.state, 'idle')
})
// ---- Ergänzungen: Verhalten, das die drei Tests oben nicht berühren ----
test('LIFT-Werte wie vereinbart und cabH stimmt mit der Kabinenhöhe des Layouts überein', () => {
assert.deepEqual({ ...LIFT }, { cabWx: 22, exitX: 50, slot0: 52, slotStep: 14, speed: 110, cabH: 21 })
layout.floors.forEach((f, i) => assert.equal(f.floorY - cabTop(layout, i), LIFT.cabH))
})
test('Start: Kabine steht auf Etage 2 im Leerlauf; at() gilt bis 1,5 Einheiten Abstand und ist bei ungültigem Index false', () => {
const lift = createLift(layout)
assert.equal(lift.y, cabTop(layout, 2)); assert.equal(lift.state, 'idle')
layout.floors.forEach((f, i) => assert.equal(lift.at(i), i === 2, `at(${i})`))
lift.y = cabTop(layout, 2) + 1.4; assert.ok(lift.at(2))
lift.y = cabTop(layout, 2) - 1.4; assert.ok(lift.at(2))
lift.y = cabTop(layout, 2) + 1.5; assert.ok(!lift.at(2))
for (const i of [-1, 10, 99, 2.5, NaN, '2', undefined, null]) assert.equal(lift.at(i), false)
})
test('Layout mit nur zwei Etagen: Startetage ist die letzte vorhandene (kein Wurf)', () => {
const small = buildLayout([], FLOORS.slice(0, 2)), lift = createLift(small)
assert.equal(lift.y, cabTop(small, 1)); assert.ok(lift.at(1))
})
test('Zustandsfolge einer Fahrt; das Schließen dauert 0,3 s', () => {
const lift = createLift(layout), a = { x: 0, y: 0, frame: 1 }, req = lift.request(a, 4, 6), seen = [lift.state]
const step = () => { lift.update(0.05); if (seen.at(-1) !== lift.state) seen.push(lift.state) }
stepUntil(step, () => req.st === 'open')
assert.equal(lift.state, 'boarding')
req.st = 'boarded'
step(); assert.equal(lift.state, 'closing')
for (let i = 0; i < 5; i++) step()
assert.equal(lift.state, 'closing') // 0,25 s Tür
step(); step(); assert.equal(lift.state, 'riding') // 0,35 s
stepUntil(step, () => req.st === 'arrived')
assert.equal(lift.state, 'unloading')
step(); assert.equal(lift.state, 'unloading') // wartet auf den Fahrgast
req.st = 'out'; step()
assert.deepEqual(seen, ['idle', 'toPick', 'boarding', 'closing', 'riding', 'unloading', 'idle'])
assert.equal(req.st, 'out')
})
test('Kabine fährt mit 110 Einheiten pro Sekunde, nach oben und nach unten', () => {
const lift = createLift(layout), y0 = lift.y
lift.request({}, 8, 1)
for (let i = 0; i < 20; i++) lift.update(0.05) // 1 s
assert.ok(Math.abs(lift.y - (y0 + 110)) < 1e-9, `nach 1 s ${lift.y - y0}`)
const up = createLift(layout)
up.request({}, 0, 1)
for (let i = 0; i < 4; i++) up.update(0.05) // 0,2 s
assert.ok(Math.abs(up.y - (y0 - 22)) < 1e-9, `nach 0,2 s ${up.y - y0}`)
for (let i = 0; i < 6; i++) up.update(0.05) // Etage 0 liegt 46 Einheiten höher: erreicht, kein Überschwingen
assert.equal(up.y, cabTop(layout, 0)); assert.equal(up.state, 'boarding')
})
test('Fahrgast bleibt bis zum Schließen unberührt, folgt dann der Kabine und wird beim Aussteigen losgelassen', () => {
const lift = createLift(layout), a = { x: 12, y: 34, frame: 1 }, req = lift.request(a, 5, 1)
until(lift, () => req.st === 'open')
assert.deepEqual(a, { x: 12, y: 34, frame: 1 }) // Anfahren und Einsteigen: die Regie bewegt ihn
a.x = LIFT.cabWx; a.y = lift.y + 7; a.frame = 0 // die Regie hat ihn in die Kabine gestellt (Schritt 0, Blick nach vorn)
req.st = 'boarded'
let followed = 0
stepUntil(() => {
lift.update(0.05)
if (lift.state === 'closing' || lift.state === 'riding') {
if (a.y !== lift.y + 7 || a.x !== LIFT.cabWx || a.frame !== 0) assert.fail(`Fahrgast ${a.x}/${a.y}/${a.frame} bei Kabine ${lift.y}`)
followed++
}
}, () => req.st === 'arrived')
assert.ok(followed > 10, `folgte nur ${followed}-mal`)
assert.equal(a.y, cabTop(layout, 1) + 7)
a.x = 77; a.y = 66; a.frame = 1 // Aussteigen: die Regie setzt ihn selbst
lift.update(0.05); lift.update(0.05)
assert.deepEqual(a, { x: 77, y: 66, frame: 1 })
})
test('Fahrt auf derselben Etage öffnet sofort und schließt ab', () => {
const lift = createLift(layout), req = lift.request({}, 2, 2)
lift.update(0.05); assert.equal(req.st, 'open')
req.st = 'boarded'; until(lift, () => req.st === 'arrived'); assert.equal(req.st, 'arrived')
req.st = 'out'; lift.update(0.05); assert.equal(lift.state, 'idle')
})
test('cancel während der Fahrt: Kabine frei, Fahrgast wird nicht mehr bewegt, nächste Anfrage kommt dran', () => {
const lift = createLift(layout), a = { x: LIFT.cabWx, y: 0, frame: 0 }, r1 = lift.request(a, 2, 9), r2 = lift.request({}, 3, 4)
until(lift, () => r1.st === 'open'); r1.st = 'boarded'
until(lift, () => lift.state === 'riding'); lift.update(0.05)
lift.cancel(a)
assert.equal(r1.st, 'cancelled'); assert.equal(lift.state, 'idle')
const before = { ...a }
lift.update(0.05); assert.deepEqual(a, before)
until(lift, () => r2.st === 'open'); assert.equal(r2.st, 'open'); assert.equal(lift.state, 'boarding')
assert.deepEqual(a, before)
})
test('cancel einer wartenden Anfrage: nie bedient, die übrigen bleiben in Reihenfolge; mehrere Anfragen eines Agenten fallen alle', () => {
const lift = createLift(layout), a2 = {}
const r1 = lift.request({}, 1, 4), r2 = lift.request(a2, 5, 2), r3 = lift.request({}, 6, 3), r4 = lift.request(a2, 7, 3)
lift.cancel(a2)
assert.deepEqual([r1.st, r2.st, r3.st, r4.st], ['queued', 'cancelled', 'queued', 'cancelled'])
until(lift, () => r1.st === 'open'); r1.st = 'boarded'; until(lift, () => r1.st === 'arrived'); r1.st = 'out'
const seen = new Set()
until(lift, () => { seen.add(r2.st).add(r4.st); return r3.st === 'open' })
assert.equal(r3.st, 'open')
assert.deepEqual([...seen], ['cancelled'])
})
test('cancel eines unbekannten oder bereits ausgestiegenen Agenten ändert nichts', () => {
const lift = createLift(layout), a = {}, r = lift.request(a, 2, 3)
until(lift, () => r.st === 'open'); r.st = 'boarded'; until(lift, () => r.st === 'arrived'); r.st = 'out'
lift.cancel({}); lift.cancel(undefined); assert.equal(r.st, 'out'); assert.equal(lift.state, 'unloading')
lift.cancel(a); assert.equal(r.st, 'out') // ausgestiegen bleibt ausgestiegen
lift.update(0.05); assert.equal(lift.state, 'idle')
})
test('update überspringt von außen abgebrochene Anfragen (wartend und laufend)', () => {
const lift = createLift(layout)
const r1 = lift.request({}, 1, 4), r2 = lift.request({}, 5, 2), r3 = lift.request({}, 6, 3)
r2.st = 'cancelled'
until(lift, () => r1.st === 'open')
r1.st = 'cancelled' // laufende Anfrage von außen abgebrochen: kein Festhängen im Zustand boarding
lift.update(0.05)
assert.equal(lift.state, 'toPick')
until(lift, () => r3.st === 'open'); assert.equal(r3.st, 'open')
assert.equal(r2.st, 'cancelled'); assert.equal(r1.st, 'cancelled')
})
test('reset bricht Wartende und die laufende Anfrage ab, ausgestiegene bleiben; danach fährt der Aufzug wieder', () => {
const lift = createLift(layout), r1 = lift.request({}, 1, 4), r2 = lift.request({}, 5, 2), r3 = lift.request({}, 6, 3)
until(lift, () => r1.st === 'open'); r1.st = 'boarded'; until(lift, () => r1.st === 'arrived'); r1.st = 'out'; lift.update(0.05)
until(lift, () => r2.st === 'open')
lift.reset()
assert.deepEqual([r1.st, r2.st, r3.st], ['out', 'cancelled', 'cancelled']); assert.equal(lift.state, 'idle')
lift.update(0.05); assert.equal(lift.state, 'idle') // Warteschlange ist leer
const r4 = lift.request({}, 3, 5); until(lift, () => r4.st === 'open'); assert.equal(r4.st, 'open')
})
test('ungültige Anfragen (Etage, Agent) kommen abgebrochen zurück und blockieren nichts', () => {
const lift = createLift(layout)
for (const [a, from, to] of [[{}, -1, 3], [{}, 1, 10], [{}, 1.5, 2], [{}, '1', 2], [{}, 1], [{}, NaN, 2], [null, 1, 2], [undefined, 1, 2], ['x', 1, 2]]) {
const r = lift.request(a, from, to)
assert.equal(r.st, 'cancelled', JSON.stringify([from, to]))
assert.equal(r.a, a)
}
lift.update(0.05); assert.equal(lift.state, 'idle')
const ok = lift.request({}, 1, 2); until(lift, () => ok.st === 'open'); assert.equal(ok.st, 'open')
})
test('unbrauchbares dt (0, negativ, NaN, Infinity, kein Zahlentyp) ändert nichts', () => {
const lift = createLift(layout), r = lift.request({}, 8, 1), y0 = lift.y
for (const dt of [0, -1, NaN, Infinity, undefined, '0.05']) lift.update(dt)
assert.equal(lift.state, 'idle'); assert.equal(lift.y, y0); assert.equal(r.st, 'queued')
})
// ---- Kabine zeichnen (Raster 3, kein setGrid: logische Einheit = 3 Geräte-Pixel) ----
const K = 3
const rect = (x, y, w, h, fill) => ({ x: x * K, y: y * K, w: w * K, h: h * K, fill })
test('drawBack: Seil von oben bis zur Kabine, Rückwand mit Handlauf und Fußleiste', () => {
const lift = createLift(layout), { ctx, rects } = makeCtx(), cy = cabTop(layout, 2)
lift.drawBack(ctx)
assert.deepEqual(rects, [
rect(28, 9, 1, cy - 9, PAL.metal2),
rect(18, cy, 20, 21, '#f1f3f8'),
rect(19, cy + 11, 18, 1, '#cfd5df'),
rect(19, cy + 18, 18, 2, '#e1e5ec'),
])
})
test('drawFront: Glasschleier, Rahmen, Lampe und Aufhängung; save/restore gleichen sich aus', () => {
const lift = createLift(layout), { ctx, rects, counts, alphas } = makeCtx(), cy = cabTop(layout, 2)
lift.drawFront(ctx)
assert.deepEqual(rects, [
rect(19, cy + 1, 18, 19, '#ffffff'),
rect(18, cy, 20, 1, PAL.metal2),
rect(18, cy + 20, 20, 1, PAL.metal2),
rect(18, cy, 1, 21, PAL.metal2),
rect(37, cy, 1, 21, PAL.metal2),
rect(24, cy + 1, 8, 1, '#ffe9a8'),
rect(26, cy - 2, 4, 2, PAL.metal2),
])
assert.deepEqual(alphas, [0.18]); assert.equal(counts.save, 1); assert.equal(counts.restore, 1)
})
test('Kabine wird an der gerundeten Fahrposition gezeichnet und bleibt im Schacht (x 18..38)', () => {
const lift = createLift(layout)
lift.request({}, 8, 1)
lift.update(0.05); lift.update(0.05); lift.update(0.05)
assert.ok(!Number.isInteger(lift.y), `Testvoraussetzung: Kabine zwischen Rastern (y ${lift.y})`)
const cy = Math.round(lift.y), back = makeCtx(), front = makeCtx()
lift.drawBack(back.ctx); lift.drawFront(front.ctx)
assert.deepEqual(back.rects[1], rect(18, cy, 20, 21, '#f1f3f8'))
assert.deepEqual(back.rects[0], rect(28, 9, 1, cy - 9, PAL.metal2))
assert.deepEqual(front.rects[1], rect(18, cy, 20, 1, PAL.metal2))
for (const r of [...back.rects.slice(1), ...front.rects]) assert.ok(r.x >= 18 * K && r.x + r.w <= 38 * K, JSON.stringify(r))
})