Tourismusregion: Umwelt beißt jetzt + Schüler-Erklärung (Balance-Pass 1)

Diagnose per headless-Sim-Harness (.dontDeploy/tools/tr-balance-harness.mjs):
„alles zubauen" senkte die Umwelt nur 75→50 (über der 45-Schwelle) und der
Exit-Umwelt-Malus war TOTER Flag-Code → man konnte der Umwelt kaum schaden.

- model.js: (1) Exit-Umwelt-Malus wirklich angewandt — env<60 zieht bei
  umweltbewussten Gästen Zufriedenheit ab (je tiefer, desto mehr). (2) Über-
  tourismus-Drift im 10-s-Takt: dichte Bebauung (>6 Bauwerke) + Verkehr zehren
  die Umwelt aus. (3) Einmalige Lern-Warnkarte, wenn Umwelt < 50 kippt.
- data.js: Naturschutzgebiet-Beschreibung klar (rettet die Umwelt +20, Natur-
  Attraktionen +15 %, sperrt Bauplätze — Naturschutz vs. Ausbau abwägen).
- main.js: Willkommenskarte erklärt die Umwelt-Mechanik für Schüler.

Messung nachher: „alles zubauen" → Umwelt 75→37 (unter 45), Umweltziel
verfehlt, Ø-Sterne fallen. Feintuning Sterne-Kurve/Ansprüche-Eskalation
bleibt für Thomas' Playtest (Game-Feeling headless nicht bewertbar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 18:10:42 +02:00
parent 913f161502
commit 5f603cb193
4 changed files with 110 additions and 2 deletions
+73
View File
@@ -0,0 +1,73 @@
// Headless-Balance-Messung (temporär). node sim-harness.mjs
import { createState, place, upgrade, callNextWave, tick, GOALS } from '../../App/sims/tourismusregion/js/model.js';
import { BUILDINGS, GAME_LEN } from '../../App/sims/tourismusregion/js/data.js';
const ALL_TYPES = Object.keys(BUILDINGS);
function upgradeAllMax(s) {
for (const b of s.buildings) {
const def = BUILDINGS[b.type];
let guard = 0;
while (b.level < def.cost.length && s.budget >= def.cost[b.level] && guard++ < 6) {
const r = upgrade(s, b);
if (!r || !r.ok) break;
}
}
}
function run(strategy, mapId = 'talschleife') {
const s = createState({ mapId, seed: 12345 });
const dt = 0.5;
let t = 0;
function tryBuildAll() {
for (let i = 0; i < s.slots.length; i++) {
const slot = s.slots[i];
if (slot.building || slot.blocked) continue;
for (const type of ALL_TYPES) {
const def = BUILDINGS[type];
if (!def || s.budget < def.cost[0]) continue;
const r = place(s, type, i);
if (r && r.ok) break;
}
}
}
function buildOneCafe() {
// Minimal-Strategie: genau EIN Café (Thomas' „5-Sterne-Café macht alles grün")
if (s.buildings.length) return;
for (let i = 0; i < s.slots.length; i++) {
if (s.slots[i].building || s.slots[i].blocked) continue;
const r = place(s, 'cafe', i);
if (r && r.ok) return;
}
}
let guard = 0;
while (t < GAME_LEN && s.phase < 8 && guard++ < 2000) {
if (strategy === 'buildAll' || strategy === 'maxedAll') tryBuildAll();
if (strategy === 'maxedAll') upgradeAllMax(s);
if (strategy === 'oneCafe') buildOneCafe();
try { callNextWave(s); } catch (_) {}
for (let k = 0; k < 40 && t < GAME_LEN; k++) { tick(s, dt); t += dt; }
}
if (strategy === 'maxedAll') upgradeAllMax(s);
// Restzeit auslaufen lassen
while (t < GAME_LEN && guard++ < 6000) { tick(s, dt); t += dt; }
const stars = s.stats.all.count ? s.stats.all.stars / s.stats.all.count : 0;
return {
strategy,
env: Math.round(s.env),
avgStars: +stars.toFixed(2),
visitorsExited: s.stats.all.count,
budget: Math.round(s.budget),
buildings: s.buildings.length,
traffic: Math.round(s.traffic || 0),
goalsMet: GOALS.filter(g => g.check(s)).length + '/' + GOALS.length,
};
}
console.log('=== Tourismusregion Balance-Messung (talschleife) ===');
for (const strat of ['maxedAll', 'buildAll', 'oneCafe']) {
const r = run(strat);
console.log(`${strat.padEnd(9)} → env ${String(r.env).padStart(3)} · Ø${r.avgStars}★ · ${r.buildings} Geb. · Verkehr ${r.traffic} · Ziele ${r.goalsMet} · Budget ${r.budget}`);
}