// Strategie-Durchspiel: prüft, was das Modell belohnt und bestraft. // Beantwortet: Funktioniert "Panzer-Rush" (alles sofort bauen)? Braucht man // Rücklagen für die einmaligen Entscheidungs-Karten (Skirennen …)? Was // passiert bei Preis-Extremen? Winter-only vs. Ganzjahr in Szene 3? // node tests/strategien.mjs import { createState, simulateDay, canPlace, place, MAP_COLS, MAP_ROWS } from '../js/model.js'; import { BUILDINGS, MILESTONES, DECISIONS } from '../js/data.js'; function fireMilestones(s, fired) { for (const m of MILESTONES) { if (fired.has(m.id)) continue; if (!m.when(s)) continue; fired.add(m.id); const unlocks = Array.isArray(m.unlock) ? m.unlock : [m.unlock]; unlocks.forEach(u => s.unlocked.add(u)); m.apply?.(s); } } // Entscheidungs-Karten wie im Spiel: einmalig, Ja nur wenn leistbar. function handleDecisions(s, st) { for (const d of DECISIONS) { if (s.firedEvents.has(d.id)) continue; if (!d.when(s)) continue; s.firedEvents.add(d.id); st.offered.push(d.id); if (!d.yes.can || d.yes.can(s)) { d.yes.apply(s); st.taken.push(d.id); } else { st.missed.push(`${d.id} (Tag ${s.day}, nur ${s.money} €)`); d.no.apply(s); } } } function findSpot(s, type) { for (let r = MAP_ROWS - 1; r >= 0; r--) { for (let c = 0; c < MAP_COLS; c++) { if (canPlace(s, type, c, r).ok) return { c, r }; } } return null; } function run(name, sceneId, days, buildFn, priceFn) { const s = createState(sceneId); const fired = new Set(); const st = { offered: [], taken: [], missed: [], minMoney: s.money, lossDays: 0 }; for (let d = 1; d <= days; d++) { fireMilestones(s, fired); buildFn(s, d); if (priceFn) s.price = priceFn(s, d); simulateDay(s); handleDecisions(s, st); st.minMoney = Math.min(st.minMoney, s.money); if (s.lastNet < 0) st.lossDays++; if (!Number.isFinite(s.money)) { console.error(name, 'NaN Tag', d); process.exit(1); } } const goals = s.scenario.goals.filter(g => g.check(s)).length; console.log( `${name.padEnd(22)} Geld ${String(s.money).padStart(8)} € · min ${String(st.minMoney).padStart(7)} € ` + `· maxG ${String(s.stats.maxGuests).padStart(3)} · maxSo ${String(s.stats.maxSummerGuests).padStart(3)} ` + `· Zufr ${s.satisfaction} % · Natur ${Math.round(s.nature)} % · Ziele ${goals}/${s.scenario.goals.length} ` + `· Verlusttage ${st.lossDays} · Geb. ${s.buildings.length}`); if (st.missed.length) console.log(` ⚠️ verpasste Chancen: ${st.missed.join(' · ')}`); return { s, st }; } const QUEUE = ['restaurant', 'lift', 'access', 'hotel', 'staffhouse', 'restaurant', 'lift', 'snow']; function queueBuilder(queue, reserve = 0) { const q = [...queue]; return s => { if (!q.length) return; const type = q[0]; if (!BUILDINGS[type] || s.money - BUILDINGS[type].cost < reserve) return; const spot = findSpot(s, type); if (spot) { place(s, type, spot.c, spot.r); q.shift(); } }; } // Panzer-Rush: baut JEDEN Tag alles, was geht (freigeschaltet + leistbar). function rushBuilder() { return s => { let built = true; while (built) { built = false; for (const type of Object.keys(BUILDINGS)) { if (!s.unlocked.has(type)) continue; const spot = findSpot(s, type); if (spot) { place(s, type, spot.c, spot.r); built = true; break; } } } }; } console.log('=== SZENE 1 · Pionierzeit, 600 Tage ==='); run('Referenz (Plan)', 'pionierzeit', 600, queueBuilder(QUEUE)); run('Panzer-Rush', 'pionierzeit', 600, rushBuilder()); run('Rücklage 8000 €', 'pionierzeit', 600, queueBuilder(QUEUE, 8000)); run('Geizhals (2 Geb.)', 'pionierzeit', 600, queueBuilder(['restaurant', 'lift'])); run('Preis 45 € (Abzocke)', 'pionierzeit', 600, queueBuilder(QUEUE), () => 45); run('Preis 15 € (Dumping)', 'pionierzeit', 600, queueBuilder(QUEUE), () => 15); console.log('\n=== SZENE 3 · Schneegrenze 2035, 600 Tage ==='); run('Nur Winter ausbauen', 'klimawandel', 600, queueBuilder(['gondola', 'snow', 'snow', 'staffhouse', 'hotel', 'restaurant'])); run('Ganzjahres-Strategie', 'klimawandel', 600, queueBuilder(['badesee', 'sled', 'trail', 'staffhouse', 'hotel', 'bikepark', 'playground', 'bikerental'])); run('Rush Klimawandel', 'klimawandel', 600, rushBuilder());