14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project overview
platz (German: "Sitzplatzverteilung" / seating arrangement) is a genetic-algorithm-based
solver that assigns people to tables ("Tische") for an event, given group memberships, table
sizes/neighbor relationships, and a penalty scheme. It reads an XML order/guest list and INI
table layout, runs a small custom GA framework to search for a good seating, and writes the
result to text/CSV output files.
This is Python 3 code, migrated from an original Python 2 implementation (the git history / old comments may still say "Python 2" — that's now stale). Notable migration artifacts to be aware of when touching this code:
Gruppe,Loesung, andSitzplatzverteilunguse@functools.total_orderingwith__eq__/__lt__instead of the old Python 2__cmp__(Python 3 dropped__cmp__andcmp()entirely;max()/.sort()need rich comparisons).sets.Set→ builtinset;ConfigParser→configparser;string.split→str.split;xrange→range;dict.has_key(k)→k in dict;dict.iteritems()→dict.items(); old-styleraise Exception, "msg"→raise Exception("msg").Strukturdaten.LeuteAllein()was fixed to skip people with no entry inself.Gruppen.GvonPid(e.g. VIP-group members who were never added to the regularGruppencontainer) instead of lettingGruppeVonPidraiseKeyError— this was a latent bug that only surfaced once VIP data (work/test2) was actually exercised end-to-end.HandleBestellung()had aG.Anzahlcomparison (G.Anzahl > 0) that used to silently compare a bound method to an int under Python 2's permissive cross-type ordering; Python 3 raisesTypeErrorfor that, which is what surfaced it. Fixed to callG.Anzahl().
There is no build system or linter configured in this repo. A tests/ unittest suite
exists (see below); requirements.txt is still empty/commented — everything used is
standard library — it's there only so bin/install_py.* has something to install into the
.venv.
Running the program
Entry point is libs/platz.py, invoked via the scripts in bin/ (each provided as a
.bat/.sh pair, following the "Standard Programm Template" convention — see
~/.claude/CLAUDE.md for the template rules):
bin/setenv.bat/bin/setenv.sh— derivesPLATZfrom the script's own location (no path editing needed), setsPLATZ_BIN,PLATZ_CFG,PLATZ_LIBS,PLATZ_WORK,PLATZ_IN,PLATZ_OUT, addsPLATZ_LIBStoPYTHONPATH(idempotently), and creates missing folders. Sourced by every other script; not normally invoked directly. The.shvariant must be sourced (source bin/setenv.sh), not executed.bin/install_py.bat/bin/install_py.sh— callssetenv, then creates.venv(viapy -m venv/python3 -m venv) andpip install -r requirements.txtif.venvdoesn't already exist.bin/activate_venv.bat/bin/activate_venv.sh— callssetenv, then activates the existing.venv(errors out ifinstall_pyhasn't been run yet).bin/get_cmd.bat/bin/get_cmd.sh— callssetenv, then opens a new shell with thePLATZ_*environment already set.bin/platz.bat/bin/platz.sh— callssetenv, then runspython "$PLATZ_LIBS/platz.py". This is the actual program entry point (replaces the oldbin/run/bin/run.bat, which hardcoded absolute paths and have been removed).
libs/platz.py requires a command-line switch --indir <dir> (parsed with optparse)
pointing at a directory that holds the per-run input files and
receives the output files — e.g. work/test1/, work/test2/, or any new folder following
the same layout:
tische.ini— table definitions: id,Nummer,Hof(venue/court),Plaetze(seats),Nachbarliste(neighbor table ids),Koordinaten.bestellung.json— the guest order:{ "gruppen": [ ... ] }, each group holding a"personen"list (each withvorname/nachname/optionaltitel), an optional"anzahl"(reservation count — expands every person in the group into that many identical bookings), an optional"name"(display label), and an optional"tisch"to pin a VIP group to a specific table. Replaces the oldbestellung.xml/XMLConfig.- Output (written back into the same
--indirdirectory):TischePersonen.txt(table → seated people) andPersonenTische.csv(person → table, sorted).
The GA cycle and penalty scheme are read directly from $PLATZ_CFG/zyklus.cfg and
$PLATZ_CFG/strafen.cfg (independent of --indir). There is no platz.cfg indirection any
more — it was removed; zyklus.cfg/strafen.cfg are loaded straight from $PLATZ_CFG, the
GA profile is chosen by the --zyklus-art <name> switch (default Adaptiv), and the penalty
profile by the --strafen <name> switch (default default).
cfg/zyklus.cfg— defines the GA profiles. Two flavours: static profiles (Easy,Simple) give an explicit sequence of actions (e=erzeugen/create,s=selektieren/ select-best,S=random-select,m=mutieren/mutate,j=behalten/keep parents,z=neue Generation/advance generation) each with a count (Abfolge/Anzahl). The adaptive profile (Adaptiv, the default) is recognised by aStartfield and has no fixed action list:Zyklus.Modusbecomes'adaptiv'andFarm._LaufAdaptiv()runs a generation loop with a shrinking population (Start,Schrumpfung,MinPopulation) and a convergence stop (Geduldgenerations without an improvement of at leastSchwelle, capped byMaxGenerationen). Static profiles run viaFarm._LaufStatisch().cfg/strafen.cfg— the penalty ("Strafpunkte") profiles. Each section is one complete profile ("Vorgehen") in a single namespace, chosen by--strafen <section>(defaultdefault); this lets several scoring strategies live in one file (the shipped file also has alockerexample with cheaper splits). Keys per profile:Anzahl_Trennungen_N({groupsize:points}for a group split acrossN+1tables, i.e.Nsplits — recursing to smaller sizes when no exact entry),NichtNachbar(no group-mate at a neighbouring table),Allein(a lone person), optionalTischWertigkeiten({freeseats:points}under-utilization, with a nearest-lower-key fallback; absent = free seats not penalised, as in thelockerprofile), and optionalAbstandFaktor(per spatial-distance unit between split-off parts; absent = distance not penalised).Strafliste.laden(FileName, Sektion='default')parses one section; the old[Trennung]/[Globals]/[Abstand]three-section layout is gone.
tische.ini/bestellung.json/TischePersonen.txt/PersonenTische.csv are fixed filenames
resolved as os.path.join(indir, ...) in platz.py.
Pass --tosvg to additionally write <indir>/Sitzplatzverteilung.svg via
Sitzplatzverteilung.alsSVG() (see below). --show-development writes
<indir>/Entwicklung.svg (value curves + pool-size panel), and --show-lineage writes
<indir>/Abstammung.svg — the full mutation tree of every solution, with the final best
solution's ancestral branch highlighted and labelled with its id. Every solution gets a plain
running number (Farm._NeueId, 1..N); the ancestry is not encoded in the id but recorded
via each node's stored predecessor (parent id) and followed back along the highlighted branch.
Pass --seed <int> to make a run fully reproducible: platz.py calls random.seed() with
it before any GA action, so the same seed always yields the same seating (for support
questions like "why is Aunt Erna seated there?" — rerun with the same seed to get the exact
same plan). Reproducibility needs more than seeding the RNG: Person defines an Id-based
__hash__/__eq__ (each booking has a unique running integer Id) so that the set() of
people in a Gruppe iterates/pop()s in a process-stable order — otherwise the order would
hinge on object memory addresses (and, for string-keyed containers, PYTHONHASHSEED), leaving
the run non-reproducible even with --seed. Integer Ids aren't affected by PYTHONHASHSEED,
so no environment tweaking is required.
Farm.TopN(n) returns the n best solutions (descending), clamped to [0, len(Pool)] — the
list generalisation of Farm.Bester() (single best), for surfacing several good alternatives.
Run the test suite with bin/run_tests.bat / bin/run_tests.sh (or directly: python -m unittest discover -s tests -p "test_*.py" -v). A stray global PYTHONPATH entry on this
machine points at an unrelated project that also defines a tests package — this shadows
import tests but does not affect unittest discover, which is what the run_tests
scripts use.
Architecture
Two library modules under libs/, imported via PYTHONPATH=$PLATZ_LIBS:
ga.py — generic genetic-algorithm scaffolding
Domain-agnostic and reusable in principle:
Loesung("Solution") — base class for anything bred/mutated/selected by the GA. Meant to be subclassed;SitzplatzverteilunginStrukturdaten.pyis the concrete solution type used here.Zyklus("Cycle") — parsesAbfolge/Anzahlstrings (fromzyklus.cfg) into the ordered list of GA actions and their counts for one run.Farm— owns the population (Pool/PoolNeu), and executes aZyklusaction-by-action against instances of a given solution class (passed asKlasseplus its constructor args/kwargs).Bester()returns the fittest (max()) solution found; solutions are ordered via__eq__/__lt__(@total_ordering) onself.value. The Farm also tags every solution with a hierarchical lineage id (_absid;_WurzelId/_KindId) and records the mutation tree intoself.Abstammung.Entwicklung("Development") — observer that records one measurement per cycle action (best/avg/worst value + pool size);alsSVG()draws the value curves plus a pool-size panel.Abstammung("Lineage") — observer that records the mutation tree (each solution's id, parent id, generation, value);alsSVG(HervorId=...)draws the tree and highlights/labels the ancestral branch of the given (usually the best) solution.
Strukturdaten.py — domain model for this specific seating problem
Person,Gruppe(group — aSetof people, splittable viateilen()),Tisch(table — aGruppesubclass with a seat capacity and neighbor-table ids).- Container/index classes:
Personen,Gruppen,Tische, andPlaetze("Seats") which indexes tables by how many free seats they currently have, to answer "give me a table with at least N free seats" or "give me two neighboring tables that together fit two groups of given sizes" efficiently during placement/mutation. Strafliste("Penalty list") — loadsstrafen.cfgand computes penalty points for: splitting a group (gibPunkte('Trennung', ...), recursing to smaller group sizes if no exact rule is configured), a person sitting with no group-mate at their table ('allein'), a person having no group-mate at a neighboring table ('nichtNachbar'), and poor seat utilization at a table ('Tischbesetzung'— the penalty rises with the number of free seats, with a nearest- lower-key fallback, so partly-filled tables are penalised most and groups get pulled onto a single table instead of being split next to empty ones).Sitzplatzverteilung("Seating arrangement") — the actualLoesungsubclass bred by the GA. Constructor seats VIPs first at pinned tables, then randomly seats groups either largest-first or smallest-first (coin flip), splitting a group across two neighboring tables when no single table has enough free seats, recursively splitting further if needed, then callsbewerten()to score the arrangement perStrafliste.mutieren()picks the worst-scoring groups (SchlechteGruppenTopX), evicts them, and reseats them largest-first (GruppenSetzen(..., GrosseZuerst=True)) so big groups grab the now-freed whole tables and come back together instead of being re-split; the initial constructor placement still uses the random largest/smallest coin flip (GrosseZuerst=False).speichern()writes the two output files.alsSVG()renders the layout to SVG: tables as white circles sized to fit all their seats' person-circles without touching, people as colored circles (color = group, viaSVGGruppenFarben) placed evenly around the inside of their table's circle. The table-to-table grid spacing is derived from the largest table radius so tables never overlap regardless of theirKoordinaten.JSONConfig— parsesbestellung.jsonintoPersonen/Gruppen/VIP-group/VIP-seat-map/group-names, expanding each person by the group's"anzahl"into that many individual bookings, and treating any group with a"tisch"key as a VIP group pinned to that table rather than going through normal GA placement. Returns a 5th element (GruppenNamen,{Gid: display name}) that the olderXMLConfigdid not;CSVConfig(the web CSV adapter) returns the same 5-tuple.
Data flow through platz.py (__main__)
- Parse
--indir(required) and--zyklus-art(defaultAdaptiv) from the command line viaoptparse. - Resolve
zyklus.cfg/strafen.cfgdirectly asos.path.join($PLATZ_CFG, ...). - Load
Zyklus(GA profile named by--zyklus-art),Strafliste(penalties),Tische(table layout, from<indir>/tische.ini). - Load
<indir>/bestellung.jsonviaJSONConfig→ people, groups, VIP group, VIP seat map, group names. - Build a
Farm(Zyklus, Sitzplatzverteilung, Tische=..., Gruppen=..., Strafen=..., VIPListe=..., VIPs=...), which runs the whole GA cycle in its constructor. - Take
Farm.Bester()and call.speichern(...)to write<indir>/TischePersonen.txtand<indir>/PersonenTische.csv.
Both libs/platz.py and libs/Strukturdaten.py also have self-test code under
if __name__ == '__main__': that exercises the classes directly with hardcoded sample data —
useful as a reference for how the classes are meant to be constructed and used.