16c562ed1d
- Bikepark: echtes 2x2-Footprint (canPlace/place/demolish/upgrade/Report ankerbasiert, buildingRef auf Teilflaechen), Sprite ohne Schatten neu generiert, zentriert gross gezeichnet. Baukriterium gelockert: Bahn-Naehe ersetzt die Zonen-Regel (wie Hotel im Bahn-Umkreis). - Zeit halbiert: 1 Tag = 4 s bei 1x (Absturz-Rate entsprechend angepasst). - Problem-Sprechblasen alle ~3 min am Gebaeude (Personal gesucht / kein Schnee), erster Hinweis nach 15-35 s. - Heli: sichtbares Abheben/Landen am Pad (aus vorigem Commit) bleibt. - Test-Harnesse (model-test, strategien) wandern in App/sims/tourismustal/tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
4.2 KiB
JavaScript
108 lines
4.2 KiB
JavaScript
// 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());
|