Files
geograsim/App/sims/busfahrt/scripts/generate-city-questions.py
T
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

129 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""
Busfahrt — Zwischenfragen-Generator.
Liest cities.json ein, ergänzt pro Stadt ein `question`-Objekt der Form:
"question": {
"prompt": { "de": "In welchem Land liegt Paris?",
"easy": "In welchem Land liegt Paris?" },
"choices": ["Frankreich", "Spanien", "Italien"],
"correctIndex": 0
}
Distraktor-Länder werden deterministisch (mulberry32 auf city.id) aus dem
Pool aller anderen Länder gewählt — damit Re-Runs dieselben Choices liefern,
und damit das Daten-File reproduzierbar bleibt.
Verwendung:
python generate-city-questions.py # in-place überschreiben
python generate-city-questions.py --dry-run # nur ausgeben, was geändert würde
Sensationsstädte können später eine zweite Frage als
"questionExtra": { ... } # hand-kuratiert
bekommen — dieser Generator überschreibt das Feld nicht.
"""
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent / "assets" / "data"
CITIES_PATH = ROOT / "cities.json"
def mulberry32(seed: int):
"""Deterministischer RNG (gleicher wie in der Engine), produziert [0, 1)."""
state = [seed & 0xFFFFFFFF]
def next_float():
state[0] = (state[0] + 0x6D2B79F5) & 0xFFFFFFFF
t = state[0]
r = ((t ^ (t >> 15)) * (1 | t)) & 0xFFFFFFFF
r = ((r + ((r ^ (r >> 7)) * (61 | r))) & 0xFFFFFFFF) ^ r
return ((r ^ (r >> 14)) & 0xFFFFFFFF) / 4294967296.0
return next_float
def slug_hash(s: str) -> int:
"""Einfacher String→u32-Hash für deterministisches Seeding."""
h = 2166136261
for ch in s:
h = (h ^ ord(ch)) & 0xFFFFFFFF
h = (h * 16777619) & 0xFFFFFFFF
return h
def pick_distractors(city, all_countries):
"""Wählt 2 plausible Distraktor-Länder, die nicht das eigene Land sind."""
pool = [c for c in all_countries if c != city["country"]]
rng = mulberry32(slug_hash(city["id"]))
# Fisher-Yates-ähnlich, deterministisch
pool_shuffled = pool.copy()
for i in range(len(pool_shuffled) - 1, 0, -1):
j = int(rng() * (i + 1))
pool_shuffled[i], pool_shuffled[j] = pool_shuffled[j], pool_shuffled[i]
return pool_shuffled[:2]
def build_question(city, all_countries):
distractors = pick_distractors(city, all_countries)
# Position der richtigen Antwort auch deterministisch wählen (0/1/2)
rng = mulberry32(slug_hash(city["id"] + "_pos"))
correct_pos = int(rng() * 3)
choices = distractors[:]
choices.insert(correct_pos, city["country"])
title = city["title"]
return {
"prompt": {
"de": f"In welchem Land liegt {title}?",
"easy": f"In welchem Land liegt {title}?",
},
"choices": choices,
"correctIndex": correct_pos,
}
def main():
dry_run = "--dry-run" in sys.argv
if not CITIES_PATH.exists():
print(f"ERR: {CITIES_PATH} nicht gefunden", file=sys.stderr)
return 1
with CITIES_PATH.open("r", encoding="utf-8") as f:
cities = json.load(f)
all_countries = sorted({c["country"] for c in cities})
print(f"Total cities: {len(cities)}")
print(f"Unique countries: {len(all_countries)}")
added = 0
kept = 0
for city in cities:
if "question" in city:
kept += 1
continue
city["question"] = build_question(city, all_countries)
added += 1
print(f"Neu generiert: {added}")
print(f"Bestehend (nicht überschrieben): {kept}")
if dry_run:
# Sample-Output: erste 3 neuen Fragen
first = [c for c in cities if "question" in c][:3]
for c in first:
print(f"\n{c['id']}:")
print(json.dumps(c["question"], ensure_ascii=False, indent=2))
return 0
with CITIES_PATH.open("w", encoding="utf-8") as f:
json.dump(cities, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"OK — {CITIES_PATH} geschrieben.")
return 0
if __name__ == "__main__":
sys.exit(main())