Workers P2: Regie 2 (Schritte, Aufzugfahrten, Rückstau-Kappung, Wachhund)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 03:14:55 +00:00
1 parent e69c4355bd
commit ad8ee9afe2
2 files changed
+848 -27

No files matched your search

+410 -3
View File
@@ -2,12 +2,12 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
import { makeCtx } from './helpers/fakeCtx.mjs'
import { IDS, makeState } from './helpers/workersFixtures.mjs'
import { IDS, makeState, ev } from './helpers/workersFixtures.mjs'
// Regie 1: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Zeichnen (Quelle: entwuerfe/scene-turm.js)
// Regie 1: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Zeichnen; Regie 2: Schritte, Aufzugfahrten, Rückstau, Wachhund (Quelle: entwuerfe/scene-turm.js)
const D = await loadModule('src/components/workers/engine/director.js')
const { buildLayout, homeOf } = await loadModule('src/components/workers/engine/layout.js')
const { createLift } = await loadModule('src/components/workers/engine/lift.js')
const { createLift, LIFT } = await loadModule('src/components/workers/engine/lift.js')
const { createFx } = await loadModule('src/components/workers/engine/fx.js')
const { createLook } = await loadModule('src/components/workers/engine/rooms.js')
const { pickAgent } = await loadModule('src/components/workers/engine/camera.js')
@@ -231,3 +231,410 @@ test('drawAgents: die Blase weicht dem „!“ und dem Auswahlpfeil aus', () =>
assert.ok(textY(m, 'Rita') - textY(m, 'Kim') >= 27) // das „!“ ist 9 Einheiten hoch (Raster 3)
assert.equal(textY(m, 'Rita') - textY(draw(d, 'rita'), 'Rita'), 18) // der Pfeil nimmt 6 Einheiten
})
// ---------- Regie 2: Schritte, Aufzugfahrten, Rückstau, Wachhund (P2-T13) ----------
// Simulationszeit in Sekunden, bis keine Gruppe mehr läuft (höchstens max)
const run = (d, max = 60) => {
let t = 0
while (d.pending() > 0 && t < max) {
d.update(0.05)
t += 0.05
}
return t
}
const at = (layout, id) => homeOf(layout, id)
test('Übergabe über zwei Etagen: Rita bringt Bea ein Dokument, fährt Aufzug, steht danach wieder zuhause', () => {
const { d, lift, layout } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt 2 Funde zu Bea' })])
assert.ok(d.pending() >= 1)
let ride = false
let t = 0
while (d.pending() > 0 && t < 60) {
d.update(0.05)
t += 0.05
if (lift.state !== 'idle') ride = true
}
assert.ok(ride)
assert.ok(t < 40, String(t))
const h = at(layout, 'rita')
assert.deepEqual([Math.round(d.byId.rita.x), Math.round(d.byId.rita.y), d.byId.rita.carrying, lift.state], [h.x, h.y, false, 'idle'])
assert.equal(d.byId.rita.task, 'Rita bringt 2 Funde zu Bea')
})
test('gleiche Etage: kein Aufzug', () => {
const { d, lift } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' })])
let ride = false
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
d.update(0.05)
if (lift.state !== 'idle') ride = true
}
assert.equal(ride, false)
assert.equal(d.pending(), 0)
})
test('Rückstau: 300 Ereignisse ⇒ höchstens 6 Gruppen, Rest still, nach ≤ 60 s ruhig, letzte Aufgaben stehen (Review Focus 2)', () => {
const { d, lift } = setup()
d.applyState(makeState())
d.pushEvents(Array.from({ length: 300 }, (_, i) => ev('uebergabe', IDS[i % 19], { to: IDS[(i + 3) % 19], text: `Aufgabe ${i}` })))
assert.ok(d.pending() <= 6, String(d.pending()))
const t = run(d)
assert.ok(t < 60, String(t))
assert.equal(d.pending(), 0)
assert.equal(lift.state, 'idle')
for (let k = 0; k < 19; k++) {
const last = 299 - ((299 - k) % 19)
assert.equal(d.byId[IDS[k]].task, `Aufgabe ${last}`, IDS[k])
}
d.pushEvents(Array.from({ length: 10 }, (_, i) => ev('fund', 'kim', { text: `K${i}` })))
assert.ok(d.byId.kim.groups <= 4)
})
test('graue, fremde und unbekannte Ereignisse: keine Arbeit, kein Wurf (Review Focus 4)', () => {
const { d } = setup()
d.applyState(makeState({ connected: { kim: false } }))
d.pushEvents([ev('gelesen', 'kim'), ev('fund', 'zed'), ev('neueArt', 'rita'), ev('uebergabe', 'kim', { to: 'bea' })])
assert.equal(d.byId.kim.say, null)
assert.equal(d.byId.kim.pose, 'sleep')
assert.equal(d.byId.kim.groups, 0)
})
test('Blasen: höchstens drei gleichzeitig', () => {
const { d } = setup()
d.applyState(makeState())
d.pushEvents(['kim', 'rita', 'pia', 'finn', 'ali'].map((w) => ev('fund', w)))
d.update(0.05)
assert.ok(d.agents.filter((a) => a.say).length <= 3)
})
test('Pause friert ein, Fortsetzen läuft weiter', () => {
const { d } = setup()
d.applyState(makeState({ paused: true }))
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' })])
const x0 = d.byId.kim.x
for (let i = 0; i < 400; i++) d.update(0.05)
assert.equal(d.byId.kim.x, x0)
d.applyState(makeState({ paused: false }))
for (let i = 0; i < 40; i++) d.update(0.05)
assert.notEqual(d.byId.kim.x, x0)
})
test('Aufzug zurückgesetzt mitten in der Fahrt: Fahrgast erholt sich, Türzähler 0; hängender Aufzug: Wachhund nach 45 s', () => {
const a = setup()
a.d.applyState(makeState())
a.d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' })])
let g = 0
while (a.lift.state !== 'riding' && g++ < 400) a.d.update(0.05)
assert.equal(a.lift.state, 'riding')
a.lift.reset()
assert.ok(run(a.d) < 10)
assert.deepEqual(a.d.debug().door, new Array(10).fill(0))
const layout = buildLayout()
const fx = createFx()
const look = createLook()
const stuck = { y: 0, state: 'idle', request: (ag, f, t) => ({ a: ag, from: f, to: t, st: 'queued' }), update() {}, cancel() {}, reset() {}, at: () => false, drawBack() {}, drawFront() {} }
const d = createDirector({ layout, lift: stuck, fx, look })
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' })])
const t = run(d, 80)
assert.ok(t >= 45 && t < 60, String(t))
assert.deepEqual(d.debug().door, new Array(10).fill(0))
})
test('flush: alles sofort ruhig, alle zuhause; Deal löst Münzen und Jubel aus', () => {
const { d, lift, fx, layout } = setup()
d.applyState(makeState())
d.pushEvents(Array.from({ length: 20 }, (_, i) => ev('uebergabe', IDS[i % 19], { to: IDS[(i + 5) % 19] })))
d.flush()
assert.equal(d.pending(), 0)
assert.equal(lift.state, 'idle')
assert.equal(fx.size(), 0)
for (const a of d.agents) {
const h = at(layout, a.id)
assert.deepEqual([Math.round(a.x), Math.round(a.y)], [h.x, h.y], a.id)
}
d.pushEvents([ev('deal', 'system')])
d.update(0.1)
assert.ok(fx.size() >= 3)
assert.ok(d.byId.otto.cheer > 0)
run(d)
for (let i = 0; i < 100; i++) d.update(0.1)
assert.equal(fx.size(), 0)
})
// Aufzug-Attrappe: request liefert, was der Test vorgibt; alles Übrige tut nichts
const stubLift = (request, extra = {}) => ({ y: 0, state: 'idle', request, update() {}, cancel() {}, reset() {}, at: () => false, drawBack() {}, drawFront() {}, ...extra })
const withLift = (lift) => {
const layout = buildLayout()
const fx = createFx()
const look = createLook()
return { layout, fx, look, d: createDirector({ layout, lift, fx, look, rnd: () => 0.5 }) }
}
// Mittelpunkt aller gezeichneten Rechtecke in Welteinheiten (Raster 3)
const centerOf = (rects) => ({
x: rects.reduce((s, r) => s + r.x + r.w / 2, 0) / rects.length / 3,
y: rects.reduce((s, r) => s + r.y + r.h / 2, 0) / rects.length / 3,
})
const dist = (p, q) => Math.hypot(p.x - q.x, p.y - q.y)
test('Aufzug antwortet cancelled: Fahrt entfällt, der Agent steht sofort wieder zuhause, Türzähler 0', () => {
const calls = []
const lift = stubLift((a, from, to) => {
calls.push([a.id, from, to])
return { a, from, to, st: 'cancelled' }
})
const { d, layout } = withLift(lift)
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt Bea Funde' })])
assert.ok(run(d) < 5)
assert.deepEqual(calls, [['rita', 1, 3]])
const h = at(layout, 'rita')
assert.deepEqual([d.byId.rita.x, d.byId.rita.y, d.byId.rita.floor, d.byId.rita.carrying, d.byId.rita.frame], [h.x, h.y, h.floor, false, 0])
assert.deepEqual(d.debug().door, new Array(10).fill(0))
assert.deepEqual([d.byId.rita.task, d.byId.bea.say, d.pending()], ['Rita bringt Bea Funde', null, 0])
})
test('Wachhund: ruft lift.cancel für den Agenten, stellt ihn heim und beendet die Gruppe', () => {
const cancelled = []
const lift = stubLift((a, from, to) => ({ a, from, to, st: 'queued' }), { cancel: (a) => cancelled.push(a.id) })
const { d, layout } = withLift(lift)
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('fund', 'rita')])
assert.equal(d.byId.rita.groups, 2)
const t = run(d, 200)
assert.deepEqual(cancelled, ['rita'])
assert.ok(t >= 45 && t < 60, String(t))
const h = at(layout, 'rita')
assert.deepEqual([d.byId.rita.x, d.byId.rita.y, d.byId.rita.carrying, d.byId.rita.groups, d.byId.rita.cur], [h.x, h.y, false, 0, null])
})
test('Wartende vor dem Aufzug stehen nebeneinander (Türzähler zählt mit) und werden beim Einsteigen ausgetragen', () => {
const { d } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('uebergabe', 'kim', { to: 'max' })])
let most = 0
const slots = { rita: false, kim: false }
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
d.update(0.05)
most = Math.max(most, d.debug().door[1])
if (d.byId.rita.x === LIFT.slot0) slots.rita = true
if (d.byId.kim.x === LIFT.slot0 + LIFT.slotStep) slots.kim = true
}
assert.equal(most, 2)
assert.deepEqual(slots, { rita: true, kim: true })
assert.deepEqual(d.debug().door, new Array(10).fill(0))
})
test('Snapshot während einer Gruppe: Aufgabe und Pose kommen erst am Gruppenende aus der Tabelle', () => {
const { d } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia', text: 'Kim bringt Pia Funde' })])
for (let i = 0; i < 20; i++) d.update(0.05)
const st = makeState({ statuses: { kim: 'leerlauf' } })
st.workers.find((w) => w.workerId === 'kim').task = 'Snapshot-Aufgabe'
d.applyState(st)
assert.deepEqual([d.byId.kim.status, d.byId.kim.task], ['leerlauf', 'Kim bringt Pia Funde'])
run(d)
assert.deepEqual([d.byId.kim.task, d.byId.kim.pose, d.byId.kim.groups], ['Snapshot-Aufgabe', 'coffee', 0])
})
test('stille Gruppe für einen Worker mit laufender Gruppe: seine Aufgabe kommt am Gruppenende aus der Tabelle', () => {
const { d } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia', text: 'Kim bringt Pia Funde' })])
d.update(0.05)
d.pushEvents([ev('gelesen', 'kim', { text: 'Liest Mails' }), ...Array.from({ length: 6 }, (_, i) => ev('fund', 'rita', { text: `R${i}` }))])
assert.equal(d.byId.kim.task, 'Kim bringt Pia Funde')
run(d)
assert.equal(d.byId.kim.task, 'Liest Mails')
})
test('pro Worker höchstens 4 wartende Gruppen: die neuesten bleiben animiert, die Aufgabe des neuesten gewinnt', () => {
const { d } = setup()
d.applyState(makeState())
d.pushEvents(Array.from({ length: 10 }, (_, i) => ev('fund', 'kim', { text: `K${i}` })))
assert.equal(d.byId.kim.groups, 4)
assert.equal(d.byId.kim.task, 'K5')
d.pushEvents([ev('fund', 'kim', { text: 'K10' })])
assert.equal(d.byId.kim.groups, 4)
run(d)
assert.equal(d.byId.kim.task, 'K10')
})
test('pushEvents vor dem ersten Snapshot und mit kaputten Eingaben: nichts passiert, kein Wurf', () => {
const { d, look } = setup()
d.pushEvents([ev('fund', 'kim')])
assert.deepEqual([d.pending(), d.agents.some((a) => a.grey), look.greyRooms.size, d.byId.kim.status], [0, false, 0, 'wartet'])
d.applyState(makeState())
for (const bad of [null, undefined, 'x', 5, {}, [], [null, 5, 'x', {}, { kind: 'fund' }]]) d.pushEvents(bad)
assert.equal(d.pending(), 0)
})
test('Ereignisse ändern den Status sofort (Fehler-Zeichen, Leerlauf, Räume), die Aufgabe setzt erst der Schritt', () => {
const { d, look } = setup()
d.applyState(makeState())
d.pushEvents([ev('fehler', 'rita', { text: 'Suche kaputt' }), ev('leerlauf', 'kim', { text: 'Pause' })])
assert.deepEqual([d.byId.rita.status, d.byId.rita.alert, d.byId.kim.status], ['fehler', true, 'leerlauf'])
assert.deepEqual([look.busyRooms.has('netz'), look.busyRooms.has('kleinanzeigen'), look.busyRooms.has('portale')], [false, false, true])
assert.equal(d.byId.rita.task, '')
d.update(0.05)
assert.deepEqual([d.byId.rita.task, d.byId.kim.task, d.byId.kim.pose], ['Suche kaputt', 'Pause', 'coffee'])
})
test('wer schläft, wird für seine Gruppe wach; danach gilt wieder der Status', () => {
const { d } = setup()
d.applyState(makeState({ statuses: { kim: 'leerlauf' } }))
for (let i = 0; i < 300; i++) d.update(0.1)
assert.equal(d.byId.kim.pose, 'sleep')
d.pushEvents([ev('doppelt', 'kim')])
d.update(0.05)
assert.deepEqual([d.byId.kim.pose, d.byId.kim.say], ['type', 'Schon bekannt'])
run(d)
assert.equal(d.byId.kim.pose, 'type')
})
test('Übergabe an einen nicht angeschlossenen Worker entfällt: niemand läuft, nur die Aufgabe steht', () => {
const { d, lift } = setup()
d.applyState(makeState({ connected: { kim: false } }))
const x0 = d.byId.pia.x
d.pushEvents([ev('uebergabe', 'pia', { to: 'kim', text: 'Pia an Kim' })])
for (let i = 0; i < 20; i++) d.update(0.05)
assert.equal(d.pending(), 0)
assert.deepEqual([d.byId.pia.x, d.byId.pia.task, d.byId.kim.say, d.byId.kim.pose, lift.state], [x0, 'Pia an Kim', null, 'sleep', 'idle'])
})
test('Übergabe auf einer Etage: Dokument in der Hand, Giver steht neben dem Empfänger (Abstand 12), blickt ihn an, Empfänger dankt', () => {
const { d, layout } = setup()
d.applyState(makeState())
const kim = d.byId.kim
const pia = d.byId.pia
const bea = d.byId.bea
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' }), ev('uebergabe', 'pia', { to: 'kim' })])
let carried = false
const seen = {}
for (let i = 0; i < 800 && d.pending() > 0; i++) {
d.update(0.05)
if (kim.carrying) carried = true
if (!seen.kim && pia.say === 'Danke!') seen.kim = { x: kim.x, facing: kim.facing, carrying: kim.carrying }
if (!seen.pia && kim.say === 'Danke!') seen.pia = { x: pia.x, facing: pia.facing, carrying: pia.carrying }
}
assert.equal(carried, true)
assert.deepEqual(seen.kim, { x: at(layout, 'pia').x - 12, facing: 1, carrying: false })
assert.deepEqual(seen.pia, { x: at(layout, 'kim').x + 12, facing: -1, carrying: false })
assert.equal(bea.say, null)
for (const a of [kim, pia]) assert.deepEqual([a.x, a.y, a.facing], [at(layout, a.id).x, at(layout, a.id).y, 1])
})
test('Effekte: Deal wirft drei Münzen von außen zum Tresor (Versatz 0, 0,4 und 0,8 s), der Tresor blitzt beim Ankommen, Otto jubelt', () => {
const calls = []
const fx = { add: (life, draw, onEnd, delay) => calls.push({ life, draw, onEnd, delay }), update() {}, clear() {}, size: () => calls.length }
const layout = buildLayout()
const look = createLook()
const d = createDirector({ layout, lift: createLift(layout), fx, look, rnd: () => 0.5 })
d.applyState(makeState())
d.pushEvents([ev('deal', 'system')])
d.update(0.05)
assert.deepEqual(calls.map((c) => c.delay), [0, 0.4, 0.8])
assert.deepEqual([look.vaultFlash, d.byId.otto.say], [0, 'Deal! Super!'])
calls[2].onEnd()
assert.equal(look.vaultFlash, 0.7)
const at0 = makeCtx()
calls[0].draw(at0.ctx, 0, { t: 0, life: calls[0].life })
const at1 = makeCtx()
calls[0].draw(at1.ctx, 1, { t: calls[0].life, life: calls[0].life })
assert.ok(dist(centerOf(at0.rects), layout.spots.aussen) < 4)
assert.ok(dist(centerOf(at1.rects), layout.spots.tresor) < 4)
})
test('Effekte: Ablegen ohne Ziel fliegt vom Kopf des Workers zum Archiv', () => {
const calls = []
const fx = { add: (life, draw, onEnd, delay) => calls.push({ life, draw, onEnd, delay }), update() {}, clear() {}, size: () => calls.length }
const layout = buildLayout()
const d = createDirector({ layout, lift: createLift(layout), fx, look: createLook(), rnd: () => 0.5 })
d.applyState(makeState())
d.pushEvents([ev('botlead', 'arne')])
for (let i = 0; i < 60 && calls.length === 0; i++) d.update(0.05)
assert.equal(calls.length, 1)
const arne = d.byId.arne
const start = makeCtx()
calls[0].draw(start.ctx, 0, { t: 0, life: calls[0].life })
const end = makeCtx()
calls[0].draw(end.ctx, 1, { t: calls[0].life, life: calls[0].life })
assert.ok(dist(centerOf(start.rects), { x: arne.x + 6, y: arne.y + arne.lane - 2 }) < 5)
assert.ok(dist(centerOf(end.rects), layout.spots.archiv) < 6)
})
test('flush läuft auch bei Pause durch und lässt alle zuhause', () => {
const { d, layout, look } = setup()
d.applyState(makeState({ paused: true }))
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt Bea Funde' })])
assert.equal(d.pending(), 1)
d.flush()
const h = at(layout, 'rita')
assert.deepEqual([d.pending(), look.paused, d.byId.rita.x, d.byId.rita.y], [0, true, h.x, h.y])
assert.equal(d.byId.rita.task, 'Rita bringt Bea Funde') // die Schritte sind durchgelaufen, nicht nur verworfen
})
test('reset: Warteschlangen leer, Aufzug frei, Türzähler 0, Effekte und Blasen weg, alle zuhause', () => {
const { d, lift, fx, layout } = setup()
d.applyState(makeState())
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('fund', 'kim')])
let g = 0
while (lift.state !== 'riding' && g++ < 400) d.update(0.05)
assert.equal(lift.state, 'riding')
fx.add(5, () => {})
assert.ok(d.pending() > 0)
d.reset()
assert.deepEqual([d.pending(), lift.state, fx.size()], [0, 'idle', 0])
assert.deepEqual(d.debug(), { door: new Array(10).fill(0), groups: 0 })
for (const a of d.agents) {
const h = at(layout, a.id)
assert.deepEqual([a.x, a.y, a.carrying, a.say, a.q.length, a.cur], [h.x, h.y, false, null, 0, null], a.id)
}
const x = d.byId.rita.x
for (let i = 0; i < 100; i++) d.update(0.05)
assert.deepEqual([d.byId.rita.x, d.pending(), lift.state], [x, 0, 'idle'])
})
test('graue Worker bekommen keine vorgespielte Arbeit: weder Aufgabe noch Pose noch Blase noch Gruppe', () => {
const { d } = setup()
d.applyState(makeState({ connected: { kim: false } }))
d.pushEvents([ev('fund', 'kim', { text: 'Fund' }), ev('gelesen', 'kim', { text: 'Gelesen' }), ev('auftrag', 'otto', { to: 'kim', text: 'Kim, los' })])
for (let i = 0; i < 100; i++) d.update(0.05)
assert.deepEqual([d.byId.kim.task, d.byId.kim.pose, d.byId.kim.say, d.byId.kim.groups, d.byId.kim.status], ['', 'sleep', null, 0, 'grau'])
assert.equal(d.byId.otto.task, 'Kim, los')
})
test('Fahrgast steht beim Einsteigen schon in der Kabine, auch der hohe Otto, und kommt danach heim', () => {
const { d, lift, layout } = setup()
d.applyState(makeState())
const otto = d.byId.otto
d.pushEvents([ev('uebergabe', 'otto', { to: 'rita' })])
let boarded = null
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
d.update(0.05)
if (!boarded && lift.state === 'closing') boarded = [otto.x, otto.y, otto.y === lift.y + 7]
}
assert.deepEqual(boarded, [LIFT.cabWx, boarded[1], true])
const h = at(layout, 'otto')
assert.deepEqual([otto.x, otto.y, otto.floor, lift.state, d.pending()], [h.x, h.y, h.floor, 'idle', 0])
})
test('Übergabe vom Aufzug aus: der Giver blickt den Empfänger an, auch wenn er dafür zurücklaufen muss', () => {
const { d, layout } = setup()
d.applyState(makeState())
const bea = d.byId.bea
const rita = d.byId.rita
d.pushEvents([ev('uebergabe', 'bea', { to: 'rita' })])
let seen = null
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
d.update(0.05)
if (!seen && rita.say === 'Danke!') seen = [bea.x, bea.facing]
}
assert.deepEqual(seen, [at(layout, 'rita').x - 12, 1])
assert.ok(seen[0] < LIFT.exitX) // der Weg führt vom Ausgang des Schachts nach links zurück
})