cat posts/atlantis-pbem-neu-geschrieben-06-das-regelbuch-byte-fuer-byte.md
Atlantis PbeM neu geschrieben (Teil 6): Das Regelbuch, Byte für Byte
Eine 5.000-Zeilen-Funktion schreibt das Regelbuch von Atlantis PbeM als HTML. Teil 6 portiert sie Kapitel für Kapitel gegen die Referenz – und landet bei 230.407 Bytes, die sich bis auf das Datum nicht vom Original unterscheiden.
In Teil 5 bekam der
Port seine Spieldaten: sechzehn Tabellen, GameDefs mit 167 Einstellungen und den Hook,
der daraus das Regelwerk standard macht. Alles davon ist getestet, aber sichtbar ist
noch nicht viel. Schritt 5 der Roadmap ändert das. Sein Ziel ist das erste echte
Ausgabe-Artefakt des Python-Ports.
Das Regelbuch.
Jedes Atlantis-PbeM-Regelwerk kann seine eigenen Regeln als HTML erzeugen, aus genau den
Tabellen und Einstellungen, die jetzt in Python vorliegen. Für standard sind das
7.960 Zeilen und gut 230 KB: von der Einleitung über Bewegung, Wirtschaft und Kampf bis
zur Befehlsübersicht mit 59 Befehlen.
Das Done-Kriterium der Roadmap ist bewusst weich formuliert: Das erzeugte Regelbuch muss dem der Referenz inhaltlich entsprechen.
Wir haben uns die Latte höher gelegt.
Eine Funktion, 5.000 Zeilen
genrules.cpp besteht im Wesentlichen aus einer einzigen Methode:
Game::generate_rules. Rund 5.000 Zeilen, etwa 1.900 Ausgabeanweisungen, 347 Zugriffe
auf insgesamt 83 Einstellungen aus Globals und 31 Stellen, die direkt in eine
Datentabelle schauen – etwa um herauszufinden, ob eine bestimmte Fertigkeit aktiviert
ist.
Dazwischen steht das eigentliche Regelbuch. Absatz für Absatz, mit Bedingungen überall dort, wo sich ein Regelwerk anders verhalten darf.
Geschrieben wird über eine Handvoll kleiner Helfer. enclose("p", true) öffnet einen
Absatz, url baut einen Link, anchor einen Anker und example_start beginnt einen
Beispielblock.
Ein Abschnitt sieht in C++ so aus:
// reference/genrules.cpp
f << enclose("p", true) << "Ally means that you will fight to defend units of that faction whenever they come "
<< "under attack, if you have non-avoiding units in the region where the attack occurs. ";
if (has_stea) {
f << " You will also attempt to prevent any theft or assassination attempts against units of the faction";
if (has_obse) {
f << ", if you are capable of seeing the unit which is attempting the crime";
}
f << ". ";
}
f << "It also has the implications of the Friendly attitude.\n" << enclose("p", false);
Zwei größere Helfer fallen besonders auf. FactionTypeDescription erklärt die
Punkteverteilung einer Fraktion. unit_tax_description konstruiert den Satz, der
beschreibt, welche Einheiten Steuern eintreiben dürfen.
Über letzterer Funktion steht im Original dieser Kommentar:
This function is an utter abomination capable of summoning eldritch horrors from beyond. This needs to be cleaned up, but that can wait until after this pass.
Die Funktion bleibt zunächst genau so.
Denn die eigentliche Schwierigkeit dieses Schritts ist nicht, schöneren Code zu schreiben. Sie besteht darin, nachher dasselbe Regelbuch zu bekommen.
Der Formatter gehört zum Verhalten
Die Formatierung steckt nicht direkt in genrules.cpp. Dafür gibt es einen eigenen
std::streambuf in indenter.hpp.
Der puffert eine Zeile, setzt die aktuelle Einrückung davor und bricht sie bei 78 Zeichen um. Dabei sucht er bis zu 70 Zeichen zurück nach einem Leerzeichen. Findet er keine geeignete Stelle, trennt er hart an der Umbruchgrenze.
Selbst die DOCTYPE-Zeile des Regelbuchs läuft durch diesen Mechanismus.
Noch interessanter ist, was bei einem Wechsel der Einrückung passiert. Methoden wie
adjust_indent() rufen zuerst pubsync() auf. Liegt zu diesem Zeitpunkt noch Text im
Puffer, wird er sofort geschrieben – ohne den normalen Zeilenumbruch.
Meist sieht man davon nichts, weil die Absätze vorher ohnehin mit \n enden.
Ein Absatz zum Befehl BUILD tut das nicht.
Sein Text bleibt im Puffer, der schließende Tag verringert die Einrückung, pubsync()
schreibt den Absatz sofort heraus und direkt dahinter landet </p>.
Das Ergebnis ist eine 386 Zeichen lange Zeile.
Das ist ziemlich offensichtlich kein beabsichtigtes HTML-Layout.
Und genau deshalb muss der Python-Port es reproduzieren.
Fünf Entscheidungen vor dem Code
Wie in den bisherigen Schritten begann die Arbeit mit einem Vorschlag und offenen Fragen.
Bytegleich oder nur inhaltlich gleich?
Die Roadmap verlangt inhaltliche Gleichheit. Das klingt zunächst vernünftig, verschiebt das Problem aber nur: Dann müsste jemand definieren, welche Unterschiede nur Formatierung sind und welche bereits Inhalt verändern.
Ein exakter Vergleich braucht diese Definition nicht.
Also lautet das Ziel: identische Ausgabe. Eine Ausnahme gibt es zunächst für die Zeile
Last Change, denn dort steht das Datum der Erzeugung. Der Python-Port bekommt dieses
Datum deshalb als Parameter. Ein whitespace-normalisierter Vergleich blieb als
Rückfalloption im Hinterkopf, wurde aber nie gebraucht.
Was gehört überhaupt in diesen Schritt?
Die Portierungskarte aus Teil 2 hatte skillshows.cpp ebenfalls dem Regelbuch
zugeordnet. Beim Lesen stellte sich heraus, dass generate_rules diese Datei gar nicht
benutzt. skillshows.cpp erzeugt Beschreibungen für Berichte und gehört damit in einen
späteren Schritt.
Die Karte wurde korrigiert.
Woher kommt die Einleitung?
Die Referenz liest sie zur Laufzeit aus standard_intro.html. Der Port übernimmt diese
59 Zeilen als rulesets/standard/rulebook_intro.html ins Python-Paket.
Ein Herkunftskommentar steht bewusst nicht in der Datei selbst: Jede Zeile daraus landet später im erzeugten Regelbuch.
Wie wird aus einer 5.000-Zeilen-Funktion Python?
Nicht durch eine weitere 5.000-Zeilen-Funktion.
Das Regelbuch wird nach Kapiteln aufgeteilt. Jedes Modul folgt weiterhin der Reihenfolge des Originals, und innerhalb eines Kapitels bleiben Text und Bedingungen so nah wie möglich an der C++-Version.
Am Ende sind es zwölf Kapitelmodule.
Braucht das Regelbuch schon ein CLI?
Nein. Eine Funktion und Tests reichen an dieser Stelle aus. Die Kommandozeile kommt später, wenn feststeht, welche Unterbefehle der Python-Port überhaupt anbieten soll.
Dazu kam die Aufteilung in vier Pull Requests: zuerst Fixture, Indenter und Helfer, danach zwei größere Blöcke des Regelbuchs und schließlich die letzten Kapitel.
Die Baseline und der Indenter
Der erste Pull Request enthält noch kein Python-Regelbuch. Er schafft zunächst eine verlässliche Referenz.
Der C++-Fork besitzt bereits das CMake-Target standard-rules, das
standard genrules standard_intro.html standard.css standard.html
ausführt.
Unser Docker-Build hatte dieses Target bislang nicht erzeugt. Jetzt wird es nach dem
Binary gebaut und standard-rules.html exportiert.
Das Ergebnis:
230.407 Bytes, 7.960 Zeilen.
Diese Datei liegt als Fixture im Repository. Ein mit reference markierter Test erzeugt
sie frisch aus der gepinnten C++-Referenz und vergleicht sie mit der eingecheckten
Version, wobei die Datumszeile gesondert behandelt wird.
Der lokale Referenz-Build mit Regelbuch braucht 52 Sekunden.
Danach kommt der Formatter.
Aus dem C++-indentbuf wird IndentWriter in core/reports/indent.py. Die
Umbruchlogik bleibt absichtlich dicht am Original:
# src/atlantis_ng/core/reports/indent.py
def _flush_line(self) -> None:
"""``indentbuf::overflow`` at a newline: prefix, wrap, write."""
buffer = self._prefix + self._buffer
while len(buffer) > self._wrap:
# The last space at or before the wrap point, unless it lies further back than the
# look-back allows; then the line breaks hard at the wrap point.
wrap_pos = _find_last_of(buffer, "\n ", self._wrap)
extra = 1
if wrap_pos < 0 or self._wrap - wrap_pos > self._lookback:
wrap_pos = self._wrap
extra = 0
...
_find_last_of bildet dabei die Semantik von std::string::find_last_of nach: Die
angegebene Position ist die Obergrenze der Suche.
Auch das sync() vor Änderungen an Einrückung oder Formatierungsmodus wird übernommen.
Die Tests reproduzieren unter anderem die umgebrochene DOCTYPE-Zeile und die
386-Zeichen-Zeile aus dem BUILD-Abschnitt.
Beim Schreiben dieser Tests hing die Suite einmal komplett.
Ein künstlicher Test konfigurierte lookback genauso groß wie die Umbruchbreite,
aktivierte eine Einrückung und schrieb anschließend ein einzelnes Wort, das länger als
die Zeile war. Die Umbruchposition konnte dadurch innerhalb des Einrückungspräfixes
landen. Nach dem Abschneiden wurde das Präfix wieder vorangestellt – der Puffer wurde
effektiv nicht kürzer.
Endlosschleife.
Die C++-Referenz enthält dieselbe Schleife. Kein echter Aufrufer konfiguriert den Writer auf diese Weise. Der Fall ist deshalb dokumentiert, aber nicht stillschweigend „repariert“.
Auch zwei andere Testerwartungen des Agenten waren zunächst falsch: Auf eine fortgesetzte Zeile passten fünf Wörter statt sechs, und ein Leerzeichen direkt vor dem Zeilenumbruch blieb erhalten.
Die Fixture entschied beide Fragen.
Noch ein paar Voraussetzungen
generate_rules hängt nicht nur an den Tabellen.
Der erste Pull Request portiert deshalb außerdem die Fertigkeitsarithmetik aus
skills.cpp, die Versionskonstanten und die Funktionen, die aus den Fraktionspunkten
erlaubte Mengen für Magier, Handel, Steuern und andere Kategorien berechnen.
Dabei taucht eine kleine Eigenheit der Versionsanzeige auf. Die Engine meldet sich als
Atlantis Engine Version: 5.2.5 (beta)
Der Zusatz entsteht aus der Versionslogik der Referenz. Im Titel des Regelbuchs wird dagegen die Regelwerk-Version ohne diesen Zusatz verwendet:
Wyreth 2.0.0 Rules
Damit sind alle externen Abhängigkeiten von generate_rules vorhanden.
Jetzt kann der eigentliche Port beginnen.
Kapitel für Kapitel gegen die Referenz
Für die Arbeit entstand ein kleines Wegwerfskript. Es erzeugt das Regelbuch mit dem Datum der Fixture und vergleicht beide Strings. Bei einer Abweichung meldet es die erste abweichende Zeichenposition, die betroffene Zeile und den Anteil des gemeinsamen Präfixes.
Die Schleife für die nächsten Pull Requests war dadurch ziemlich mechanisch:
Kapitel portieren, Skript laufen lassen, erste Abweichung ansehen, korrigieren, weiter.
core/gen_rules/ wird dabei zu einem Paket. base.py enthält das zentrale Rules-
Objekt: Writer, Spieldaten, GameDefs, Progressionen und die Plausibilitätsprüfungen,
die generate_rules am Anfang ebenfalls durchführt.
Die kleinen Ausgabehelfer des Originals werden Methoden darauf.
Der C++-Absatz vom Anfang sieht anschließend so aus:
# src/atlantis_ng/core/gen_rules/combat.py
r.open("p")
r.write(
"Ally means that you will fight to defend units of that faction whenever they come "
"under attack, if you have non-avoiding units in the region where the attack occurs. "
)
if r.has_stea:
r.write(" You will also attempt to prevent any theft or assassination attempts against units of the faction")
if r.has_obse:
r.write(", if you are capable of seeing the unit which is attempting the crime")
r.write(". ")
r.write("It also has the implications of the Friendly attitude.\n")
r.close("p")
Ein einfacher Absatz wird zu r.p("…"). Sobald Bedingungen im Text stecken, bleiben
open, write und close sichtbar.
Die Struktur folgt absichtlich dem Original. Wer eine Stelle in genrules.cpp sucht,
soll sie im entsprechenden Python-Kapitel wiedererkennen können.
Zwei Unterschiede sofort gefunden
Kopf und Inhaltsverzeichnis passten direkt.
Die erste echte Abweichung kam in der Tabelle der Fraktionspunkte:
</td align="center" nowrap>
Der Helfer tag() verwendete zum Schließen versehentlich denselben String wie beim
Öffnen – inklusive Attribute.
Die Referenz öffnet dagegen mit
<td align="center" nowrap>
und schließt schlicht mit
</td>
Die Korrektur ist klein:
# src/atlantis_ng/core/gen_rules/base.py
def tag(self, tag: str, text: str) -> None:
"""``enclose(tag, true)``, the text and a newline, ``enclose(tag, false)``."""
self.open(tag)
self.f.write(text + "\n")
self.close(tag.split(" ", maxsplit=1)[0]) # the closing tag carries no attributes
Die zweite Abweichung war interessanter.
faction_point_usage erzeugt Texte wie:
1 point on Magic, 2 points on Trade, 2 points on War
Der Python-Port gab die Einträge zunächst in der Reihenfolge aus, in der sie in das Dictionary eingesetzt worden waren: War, Trade, Magic.
Die Referenz iteriert dagegen über fac.type, einen std::map. Dessen Iteratoren
laufen standardmäßig in aufsteigender Schlüsselordnung. Deshalb entsteht dort Magic,
Trade, War.
Ein Python-dict macht etwas anderes: Es bewahrt Einfügereihenfolge.
Der Port sortiert die Schlüssel deshalb ausdrücklich und dokumentiert an der Stelle, warum.
Danach passten die weiteren Kapitel dieser Hälfte: Welt, Bewegung, Fertigkeiten und Wirtschaft.
Auch das Einlesen der Einleitung ist Verhalten
Selbst bei der 59-zeiligen HTML-Einleitung gibt es eine Eigenheit, die für einen bytegenauen Port wichtig wird.
Die Referenz liest jede Zeile so:
getline(introf >> std::ws, in)
std::ws entfernt vor jedem getline führenden Whitespace. Dadurch verschwinden
Einrückungen und Leerzeilen.
Der Python-Port übernimmt dieses Verhalten bewusst, statt die Datei einfach mit
splitlines() einzulesen und unverändert wieder auszugeben.
Ein semantisch identisches Stück HTML wäre sonst bereits auf Byte-Ebene verschieden.
Das ist genau die Sorte Unterschied, wegen der ein exakter Vergleich hier so praktisch ist: Man muss nicht diskutieren, ob führende Leerzeichen „relevant“ sind. Die Referenz liefert eine eindeutige Antwort.
Die Abscheulichkeit
Dann kommt unit_tax_description.
Die Funktion baut aus mehreren TAX_*-Flags einen englischen Satz. Intern arbeitet sie
mit einer kleinen Zustandsmaschine aus Variablen wie temp, temp2, temp3, prev
und hold.
Der Python-Port übernimmt diese Struktur weitgehend.
Nicht weil sie schön ist, sondern weil ihre seltsamen Randfälle ebenfalls Ausgabe erzeugen.
Ein Test prüft neben der normalen standard-Konfiguration drei andere
Flag-Kombinationen. Bei einer davon – TAX_ANY_MAGE ohne eine konkretere
Magie-Kategorie – erzeugt die C++-Referenz tatsächlich:
is a mage who knows a spell which .
Der Agent hatte im ersten Test den Satz ergänzt, der logisch sinnvoll gewesen wäre.
Der Test schlug fehl.
Die Referenz war eindeutig: Genau dieser kaputte Satz ist das aktuelle Verhalten.
Solange wir nicht bewusst entscheiden, ihn zu ändern, wird er portiert.
Nach diesem Pull Request stimmten 111.877 von 230.407 Zeichen am Anfang des Regelbuchs überein – etwa 48 Prozent. Die Ausgabe reichte exakt bis zum Beginn des Kampfkapitels.
Kampf, Magie und 59 Befehle
Der nächste Pull Request portiert die zweite große Hälfte: Kampf, Tarnung und Beobachtung, Magie, Nichtspieler-Einheiten sowie das Kapitel über Befehle.
Allein die Befehlsübersicht besteht aus 59 Abschnitten, von ADDRESS bis WORK.
Im Python-Port bekommt jeder Befehl eine private Funktion. Die Reihenfolge entspricht
der Referenz. Zwei kleine Helfer vermeiden dabei den schlimmsten Boilerplate:
_heading erzeugt Trennlinie, Anker und Überschrift, _example den Beispielblock.
Eine weitere Abweichung fiel dabei sofort auf: In einer Tabelle heißt ein Feld in der
Referenz abr, im Python-Datenmodell dagegen abbr.
Nach der Korrektur reichte das gemeinsame Präfix bis zum nächsten noch nicht portierten Kapitel:
output 214824 chars, fixture 230407; common prefix 214824 chars = 93 %, first difference at fixture line 7432
Drei Details dieser Hälfte sind besonders gute Beispiele dafür, was „treu portieren“ bedeutet.
Im Absatz über Heilung prüft die Referenz das Kräuter-Item mit
SkillType::DISABLED, obwohl es sich um ein Item handelt. Der numerische Wert dieses
Bits entspricht dort ItemType::NOTRANSPORT.
Der Python-Port prüft deshalb ausdrücklich ItemFlag.NOTRANSPORT und erklärt im
Kommentar, warum.
Der BUILD-Absatz enthält weiterhin den fehlenden Zeilenumbruch und damit die
386-Zeichen-Zeile.
Und die Liste erlaubter Rüstungen beim Attentat verwendet
strings::join(names, ", ", ", or ") – ein Verhalten, das core/text.py bereits seit
Teil 4 reproduziert.
Zwei Zweige bleiben für standard allerdings unsichtbar: die Tabelle für Gebäude, die
Magier beherbergen können, und der Befehl ANNIHILATE.
Beide sind portiert. Die standard-Fixture kann sie aber nicht prüfen, weil ihre
Bedingungen dort nie wahr werden.
Das bleibt eine bekannte Testlücke, bis ein Regelwerk hinzukommt, das diese Pfade tatsächlich ausgibt.
Die letzten 7 Prozent
Der vierte Pull Request übernimmt den Rest: Reihenfolge der Zugphasen, Berichtsformat, Hinweise für neue Spieler, die Sonderbefehle des Mailservers und die Credits.
Am Ende schließen die letzten Funktionen auch body und html.
Der erste vollständige Lauf meldete:
output 230407 chars, fixture 230407; common prefix 230407 chars = 100 %, first difference at fixture line 7961
Zeile 7961 existiert nicht.
Die Fixture hat 7.960 Zeilen.
Aus dem temporären Präfixvergleich wurden anschließend zwei dauerhafte Tests.
Der erste erzeugt das Regelbuch mit genau dem Datum, das in der Fixture steht, und verlangt vollständige Gleichheit:
assert html == reference
Der zweite setzt ein anderes Datum und prüft, dass sich genau die Last Change-Zeile
ändert:
# tests/core/gen_rules/test_generate_rules.py
def test_the_date_is_the_only_line_that_depends_on_today() -> None:
reference, built = reference_rulebook()
html = standard_rulebook(date(2030, 1, 5))
differing = [
(ours.strip(), theirs.strip())
for ours, theirs in zip(html.splitlines(), reference.splitlines(), strict=True)
if ours != theirs
]
assert differing == [("Last Change: January 05, 2030", f"Last Change: {built.strftime('%B %d, %Y')}")]
Die erste Fassung dieses Tests enthielt übrigens wieder eine falsche Annahme: Die Einrückung der Datumszeile war mit sechs Leerzeichen erwartet worden, tatsächlich sind es acht.
Deshalb vergleicht der Test hier bewusst die gestrippten Zeilen. Er soll prüfen, dass nur das Datum variabel ist – nicht zum zweiten Mal die Funktion des Indenters testen.
Über den gesamten Schritt hinweg fand der Präfixvergleich drei tatsächliche
Portierungsabweichungen: den schließenden HTML-Tag mit Attributen, die andere
Iterationsreihenfolge von std::map und dict sowie abr gegen abbr.
Der Rest passte beim Portieren erstaunlich oft direkt.
Das ist weniger Magie, als es klingt. Der C++-Code gibt Text und Bedingungen sehr explizit vor, und die Fixture meldet nach jedem Kapitel sofort, wo die erste Abweichung liegt.
Stolperfallen
- Ein exakter Vergleich kann einfacher sein als ein semantischer. „Inhaltlich gleich“ braucht Regeln dafür, welche Unterschiede ignoriert werden dürfen. Wenn die Ausgabe deterministisch ist, kann vollständige Gleichheit das einfachere Done-Kriterium sein.
- Die erste Abweichung ist oft hilfreicher als ein kompletter Diff. Bei 230 KB HTML reichten Zeichenposition, Zeile und gemeinsames Präfix, um die nächste fehlerhafte Stelle sofort zu finden.
- Container haben unterschiedliche Ordnungsregeln.
std::mapiteriert nach seinem Schlüsselvergleich; ein Python-dictbewahrt Einfügereihenfolge. Wer das eine durch das andere ersetzt, muss entscheiden, welche Reihenfolge zum Verhalten gehört. - Auch der Formatter ist Teil der Ausgabe. Die 386-Zeichen-Zeile stammt nicht aus
einer besonderen Regelbuchlogik, sondern aus einem
pubsync()im Puffer darunter. - Ein hängender Test kann einen echten Randfall sichtbar machen. Die Endlosschleife im Indenter existiert auch in der Referenz. Weil der produktive Code diese Konfiguration nicht erzeugt, wird sie dokumentiert statt nebenbei geändert.
- Falscher Text kann trotzdem Referenzverhalten sein.
is a mage who knows a spell which .ist offensichtlich kaputt. Im treuen Port bleibt es trotzdem erhalten, bis eine bewusste Entscheidung den Fehler ändert. - Tote Zweige sind nicht automatisch getestet. Code kann vollständig portiert sein, obwohl die einzige vorhandene Fixture seine Bedingungen nie aktiviert.
Der Stand nach Teil 6
Vier Pull Requests, Schritt 5 abgeschlossen.
core/gen_rules/ erzeugt das Regelbuch für standard. Zwölf Kapitelmodule, base.py
und __init__.py enthalten zusammen 6.166 Zeilen Python. Dazu kommen IndentWriter, die
benötigte Fertigkeitsarithmetik, Versionsfunktionen und die Berechnung der
Fraktionslimits.
Ein Aufruf reicht:
# aus dem Docstring von src/atlantis_ng/core/gen_rules/__init__.py
html = generate_rules(standard.game_data(), standard.game_defs(), standard.PROGRESSIONS,
standard.RULEBOOK_INTRO.read_text(), "standard.css", date.today())
Mit dem Datum der Fixture ist das Ergebnis vollständig identisch mit der Referenzausgabe: 230.407 Bytes.
Mit einem anderen Datum unterscheidet sich genau eine Zeile.
Das ursprüngliche Done-Kriterium verlangte nur inhaltliche Gleichheit. Der Port liefert für dieses Regelwerk mehr: dieselbe Ausgabe.
Die Portierungsabdeckung steigt von 15,7 auf 26,8 Prozent: 12.834 von 47.949
Zeilen. Den größten Anteil dieses Sprungs macht genrules.cpp mit 5.052 Zeilen aus.
233 schnelle Tests laufen in 2,5 Sekunden. Der Referenz-Job braucht mit warmem Cache gut eine Minute, ohne Cache ungefähr dreieinhalb.
Die Vergleichsquote der Spielszenarien bleibt bei 100 Prozent von drei Szenarien – weiterhin Referenz gegen Referenz. Einen Zug kann die Python-Engine noch immer nicht berechnen.
Aber sie kann inzwischen ein echtes Produkt ihrer Spiellogik erzeugen: das vollständige Regelbuch.
Als Nächstes fehlt das Objektmodell. Regionen, Gebäude, Einheiten und Fraktionen hängen im C++-Code über Zeiger und Besitzbeziehungen zusammen. Teil 7 zeigt, wie daraus Python-Objekte werden, ohne dass dabei die Beziehungen des Originals verloren gehen.
Weiterlesen
- Atlantis PbeM neu geschrieben (Teil 5)
– die Tabellen und
GameDefs, aus denen das Regelbuch seine Zahlen bezieht. - pytest von Null – Fixtures und Marker, wie sie hier den schnellen Lauf vom Referenz-Build trennen.
- f-Strings bis zum Anschlag – die
<<-Ketten der Referenz werden in Python zu String-Literalen und, wo Werte eingesetzt werden, zu f-Strings.
Sources: genrules.cpp,
indenter.hpp,
indenter.cpp
und standard/standard_intro.html
des Forks,
std::basic_streambuf,
std::map,
Python dict,
Python struct.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.