% ---------------------------------------------------------------------------
%  robotkarol.sty — Robot-Karol-Welten und Blockprogramme für Unterrichtsmaterial
%
%  Copyright (C) 2026  Anselm Wagner  <anselm.wagner@posteo.de>
%
%  This work may be distributed and/or modified under the conditions of
%  the LaTeX Project Public License, either version 1.3c of this license
%  or (at your option) any later version.  The latest version of this
%  license is in  https://www.latex-project.org/lppl.txt  and version
%  1.3c or later is part of all distributions of LaTeX version
%  2008-05-04 or later.
%
%  This work has the LPPL maintenance status `maintained'.
%  The Current Maintainer of this work is Anselm Wagner.
%
%  This work consists of the files robotkarol.sty, robotkarol-doc.tex,
%  robotkarol-beispiel.tex, robotkarol.cwl, robotkarol-cheatsheet.md,
%  README.md and the derived files robotkarol-doc.pdf and
%  robotkarol-beispiel.pdf.
% ---------------------------------------------------------------------------
%  Das Paket zeichnet Welten und Blockprogramme im Stil von Robot Karol
%  Online (https://karol.arrrg.de/) für Arbeitsblätter und Prüfungen.
%  Es benötigt LuaLaTeX.
%
%  Bereitgestellt werden:
%    * karolwelt-Umgebung  — volle Welten (breite x laenge), wahlweise
%      3D-Schrägbild oder 2D-Draufsicht; Objekte: \karol, \ziegel,
%      \marke, \quader (= \wand)
%    * \karolzeile / \karolpaar / \karolgitter / \karollegende
%      — kompakte einzeilige Welten über Zeichenketten.  Sie zeichnen
%      mit denselben Bausteinen wie die karolwelt-Umgebung: eine
%      Zeilen-Welt ist optisch eine karolwelt mit laenge=1.
%    * karolbloecke-Umgebung mit \bbefehl, \bwiederholemal,
%      \bwiederholesolange, \bwiederholeimmer, \bwenndann,
%      \bwenndannsonst, \bluecke, \bkommentar, \baufruf, \banweisung,
%      \bhauptprogramm, \bbedingung, \bist, \bnichtist
%      — Blockprogrammierung
%    * Inline-Blöcke für den Fließtext: \ibefehl, \iaufruf,
%      \ianweisung, \ikommentar, \ihauptprogramm, \iwiederholemal,
%      \iwiederholesolange, \iwiederholeimmer, \iwenndann, \isonst,
%      \ibedingung, \iist, \inichtist, \iluecke.  Sie setzen genau
%      einen Block in die
%      laufende Zeile: Textgrundlinie und Schriftgröße wie der umgebende
%      Text, flachere Verzierungen.  Mehrzeilige Blöcke erscheinen
%      inline nur mit ihrem Kopf.
%    * karolcode-Umgebung + \kb, \kw — Karol-Programme als Text
%    * struktex-Wrapper + \leeranweisung — Struktogramme
%    * \karolsetup — globale Einstellungen (Farben, Feldbreite, Figur)
%    * Druckmodus: \usepackage[druck]{robotkarol} bzw.
%      \karolsetup{druck=true} stellt alle Ansichten auf tonersparende
%      Graustufen um (siehe Abschnitt 3b)
%    * \karolfigur{karol|emma} — Aussehen der Figur in den Welten
%      (Vorgabe: karol; emma hat langes Haar und eine Blume)
%    * \karolschrift / \karolcodeschrift — die Schriften der Zeichnungen
%      auch für eigenen Text (siehe Abschnitt 0)
%    * \karolonline{2t7t} — Verweis auf eine Aufgabe in Robot Karol
%      Online (setzt karol.arrrg.de/#2t7t und verlinkt sie; braucht
%      hyperref, abschaltbar mit der Paketoption hyperref=false)
%
%  NOTATION DER ZEILEN-WELTEN (\karolzeile):
%      >  <  ^  v   Karol mit Blickrichtung; . leeres Feld; Z Ziegel;
%      2…9 Ziegelstapel; M Marke; Q Quader;
%      {…} mehrere Objekte auf einem Feld, z. B. {M>}, {2M}, {3>}.
%      Leerzeichen sind erlaubt: \karolzeile{> Z . {2M} . Q}
% ---------------------------------------------------------------------------
%  ÄNDERUNGEN
%    v3.7  Die Marke sitzt im Schrägbild jetzt mittig auf ihrem Feld.
%          Bis v3.6 fehlte in ihren Bildkoordinaten der Tiefenanteil;
%          sie saß rund 0,034 Feldbreiten zu weit links und berührte
%          auf einem Ziegel dessen linke Kante.  Betrifft die
%          karolwelt-Umgebung und \karolzeile gleichermaßen.
%          \karolonline{2t7t} verweist auf eine Aufgabe in Robot Karol
%          Online: gesetzt wird karol.arrrg.de/#2t7t, verlinkt auf
%          https://karol.arrrg.de/#2t7t.  Dafür lädt das Paket hyperref
%          (am Ende der Präambel); mit der Paketoption hyperref=false
%          unterbleibt das und der Text steht ohne Verweis.
%          Druckmodus: \usepackage[druck]{robotkarol} bzw. zur Laufzeit
%          \karolsetup{druck=true} zeichnet Welten, Zeilen-Welten und
%          Blockprogramme in tonersparenden Graustufen — so hell wie
%          möglich, aber so, dass alles funktional Wichtige erkennbar
%          bleibt (Stapelhöhen, Marken, \bbefehl gegen \baufruf).
%    v3.6  Vorgabefigur ist wieder „karol"; „emma" holt man sich mit
%          \karolfigur{emma} bzw. \karolsetup{figur=emma}.
%          Schriften wie in der Oberfläche von Robot Karol Online:
%          Beschriftungen in Noto Sans, Programmtext in Hack (ersatz-
%          weise DejaVu Sans Mono).  Dafür wird fontspec geladen.  Mit
%          \usepackage[schrift=dokument]{robotkarol} bleibt es bei den
%          Schriften des Dokuments (Ausgabe wie bis v3.5); zur Laufzeit
%          umschaltbar mit \karolsetup{schrift=…, textschrift=…,
%          codeschrift=…}.
%    v3.5  Zweite Figur „emma" (langes Haar, Blume), umschaltbar mit
%          \karolfigur bzw. \karolsetup{figur=…}; die bisherige Figur
%          heißt „karol".
%    v3.4  Anschliff physikalisch statt durch Zerlegen der Kontur: auf
%          die Fläche beschneiden, Rand hell, Rand dunkel nach links
%          oben versetzt darüber.  Jede nach oben oder links zeigende
%          Kante wird dadurch hell, jede nach unten oder rechts
%          zeigende dunkel — Ecken folgen der Blockform exakt und die
%          Striche bleiben im Block, sodass Blöcke ineinanderpassen.
%    v3.3  Anschliff der Blöcke aufgeteilt: Oberseite und linke Seite
%          hell, alles übrige dunkel.
%    v3.2  Blockoptik näher an der Oberfläche: jeder Block ist umrandet
%          (Kontur farbe!80!black, helle Oberkante farbe!70!white), damit
%          auch gleichfarbige Blöcke voneinander abgesetzt sind;
%          Beschriftungen normal statt fett, Pillentext reines Schwarz,
%          der Stecker der Bedingungen ist eine Puzzlenase.
%    v3.1  Himmelsrichtungs-Bedingungen \bist / \bnichtist mit
%          Auswahlmenü (acht Blöcke wie in Robot Karol Online).  Der
%          Name in \baufruf steht jetzt wie im Original in einer
%          Auswahlpille mit Dreieck.
%    v3.0  karolwelt-Umgebung (volle Welten, 3D und 2D).  \karolzeile
%          zeichnet im selben Design und teilt sich die Bausteine mit
%          ihr; Feldnummern stehen vor der vordersten Gitterkante,
%          Hervorhebungen sind magenta (neue Schlüssel: rahmen, hoehe,
%          hervorfarbe, gitterfarbe).  Neu: Inline-Blöcke.  Das Kommando
%          \karolwelt aus v2.x heißt jetzt \karolzeile.
% ---------------------------------------------------------------------------
% Der LaTeX-Kern von 2022-06-01 wird für \ProcessKeyOptions gebraucht
% (Schlüssel-Wert-Paketoptionen, siehe Abschnitt 0).
\NeedsTeXFormat{LaTeX2e}[2022-06-01]
\ProvidesPackage{robotkarol}%
	[2026/09/03 v3.7 Robot-Karol-Welten und Blockprogramme (LuaLaTeX)]
\ifdefined\directlua\else
	\PackageWarning{robotkarol}{Dieses Paket ist fuer LuaLaTeX gedacht}%
\fi

\RequirePackage{expl3}
\RequirePackage{listings}
\RequirePackage{struktex}
\RequirePackage{tikz}
\RequirePackage{graphicx}

% ===========================================================================
%  0. Schriften
% ===========================================================================
%  Vorgabe sind die Schriften der Oberfläche von Robot Karol Online:
%  Beschriftungen in Noto Sans, Programmtext in Hack.  Beide sind aus den
%  Referenzaufnahmen bestimmt (Noto Sans deckt sich in
%  `Anweisungen.png` pixelgenau mit dem Blocktext; Hack ist die Schrift
%  des Online-Editors, siehe dessen `public/fonts/hack.css`).
%
%  Paketoption  schrift = karol | dokument
%    karol     (Vorgabe)  Das Paket lädt fontspec und benutzt Noto Sans
%                         (liegt in TeX Live) sowie Hack, falls es im
%                         System installiert ist — sonst DejaVu Sans
%                         Mono, aus dem Hack hervorgegangen ist.
%    dokument             Es wird nichts geladen; das Paket setzt seine
%                         Beschriftungen mit \sffamily und den
%                         Programmtext mit \ttfamily, übernimmt also die
%                         Schriften des Dokuments (Verhalten bis v3.5).
%
%  Zur Laufzeit umschaltbar: \karolsetup{schrift=karol|dokument}.  Eigene
%  Schriften über \karolsetup{textschrift=…} und \karolsetup{codeschrift=…}.
%  Titel und Hinweis einer Welt bleiben immer in der Dokumentschrift —
%  sie sind Text des Dokuments, keine Beschriftung der Zeichnung.
% ---------------------------------------------------------------------------
\ExplSyntaxOn

\bool_new:N \g__karol_druck_bool         % Paketoption: Druckmodus?
\bool_new:N \g__karol_href_bool          % Paketoption: hyperref laden?
\bool_gset_true:N \g__karol_href_bool
\bool_new:N \g__karol_fontladen_bool     % Paketoption: Schriften laden?
\bool_gset_true:N \g__karol_fontladen_bool
\bool_new:N \g__karol_fontda_bool        % Karol-Schriften verfügbar?
\tl_new:N \l__karol_schrift_tl           % Umschaltcode für Beschriftungen
\tl_new:N \l__karol_codeschrift_tl       % Umschaltcode für Programmtext

\keys_define:nn { karol / paket }
{
	schrift .choice: ,
	schrift / karol    .code:n = { \bool_gset_true:N  \g__karol_fontladen_bool },
	schrift / dokument .code:n = { \bool_gset_false:N \g__karol_fontladen_bool },
	% Druckmodus (Abschnitt 3b): \usepackage[druck]{robotkarol}
	druck   .bool_gset:N = \g__karol_druck_bool,
	druck   .default:n   = { true },
	% hyperref für \karolonline (Abschnitt 6) laden?
	hyperref .bool_gset:N = \g__karol_href_bool,
	hyperref .default:n   = { true },
}
\ProcessKeyOptions [ karol / paket ]

% Der eigentliche Schalter ist LOKAL: so wirkt \karolsetup{druck=true} in
% einer Gruppe nur dort, und die Paketoption gibt bloß den Anfangswert vor.
\bool_new:N \l__karol_druck_bool
\bool_set_eq:NN \l__karol_druck_bool \g__karol_druck_bool

% fontspec braucht LuaTeX oder XeTeX und bricht sonst hart ab.  Ohne
% \directlua wird deshalb gar nichts geladen — das Paket setzt dann mit
% den Schriften des Dokuments und bleibt wenigstens übersetzbar.
\cs_if_exist:NF \directlua { \bool_gset_false:N \g__karol_fontladen_bool }

\bool_if:NT \g__karol_fontladen_bool
{
	\RequirePackage { fontspec }
	\newfontfamily \__karol_fonttext:
		[
			Extension      = .ttf ,
			UprightFont    = *-Regular ,
			ItalicFont     = *-Italic ,
			BoldFont       = *-Bold ,
			BoldItalicFont = *-BoldItalic ,
			Ligatures      = TeX ,
			Numbers        = { Monospaced , Lining } ,
		]
		{ NotoSans }
	% Hack liegt nicht in TeX Live.  Ist es im System installiert, wird es
	% benutzt — sonst DejaVu Sans Mono, dessen Vorläufer (Bitstream Vera
	% Sans Mono) die Grundlage von Hack ist.
	\IfFontExistsTF { Hack }
	{
		\newfontfamily \__karol_fontmono: [ Ligatures = TeX ] { Hack }
		\PackageInfo { robotkarol } { Programmtext ~ in ~ Hack }
	}
	{
		\newfontfamily \__karol_fontmono:
			[
				Extension      = .ttf ,
				UprightFont    = * ,
				ItalicFont     = *-Oblique ,
				BoldFont       = *-Bold ,
				BoldItalicFont = *-BoldOblique ,
				Ligatures      = TeX ,
			]
			{ DejaVuSansMono }
		\PackageInfo { robotkarol }
			{ Hack ~ nicht ~ gefunden ~ - ~ Programmtext ~ in ~ DejaVu ~ Sans ~ Mono }
	}
	\bool_gset_true:N \g__karol_fontda_bool
}

% Die beiden Umschalter, die überall im Paket benutzt werden.
\cs_new_protected:Npn \__karol_textfont:  { \tl_use:N \l__karol_schrift_tl }
\cs_new_protected:Npn \__karol_monofont:  { \tl_use:N \l__karol_codeschrift_tl }

% Auswahl setzen (auch von \karolsetup{schrift=…} aus aufgerufen)
\cs_new_protected:Npn \__karol_schrift_dokument:
{
	\tl_set:Nn \l__karol_schrift_tl     { \sffamily }
	\tl_set:Nn \l__karol_codeschrift_tl { \ttfamily }
}
\cs_new_protected:Npn \__karol_schrift_karol:
{
	\bool_if:NTF \g__karol_fontda_bool
	{
		\tl_set:Nn \l__karol_schrift_tl     { \__karol_fonttext: }
		\tl_set:Nn \l__karol_codeschrift_tl { \__karol_fontmono: }
	}
	{
		\PackageWarning { robotkarol }
		{
			Die ~ Schriften ~ von ~ Robot ~ Karol ~ Online ~ sind ~ nicht ~
			geladen \MessageBreak
			(Paketoption ~ schrift=dokument ~ oder ~ keine ~ LuaTeX-Engine).
			\MessageBreak
			Es ~ werden ~ die ~ Schriften ~ des ~ Dokuments ~ benutzt
		}
		\__karol_schrift_dokument:
	}
}
\bool_if:NTF \g__karol_fontda_bool
	{ \__karol_schrift_karol: } { \__karol_schrift_dokument: }

% Öffentliche Umschalter — damit eigener Text zur Zeichnung passt.
\NewDocumentCommand { \karolschrift }     { } { \__karol_textfont: }
\NewDocumentCommand { \karolcodeschrift } { } { \__karol_monofont: }

\ExplSyntaxOff

% ===========================================================================
%  1. Karol-Programme  (Monospace, Schlüsselwörter fett)
% ===========================================================================
\lstdefinelanguage{Karol}{%
	morekeywords={wiederhole,mal,endewiederhole,solange,immer,wenn,dann,sonst,endewenn},
	sensitive=true,
}
\lstdefinestyle{karolstil}{%
	language      = Karol,
	basicstyle    = \karolcodeschrift\small,
	keywordstyle  = \bfseries,
	columns       = fullflexible,
	keepspaces    = true,
	tabsize       = 2,
	xleftmargin   = 1em,
	aboveskip     = .7em,
	belowskip     = .7em,
	extendedchars = true,
	literate      = {ö}{{\"o}}1 {ä}{{\"a}}1 {ü}{{\"u}}1
	                {Ö}{{\"O}}1 {Ä}{{\"A}}1 {Ü}{{\"U}}1 {ß}{{\ss}}1,
}
\lstnewenvironment{karolcode}[1][]{\lstset{style=karolstil,#1}}{}

% Karol-Fragmente im Fließtext: \kb{IstZiegel}, Schlüsselwort: \kw{wenn}
\newcommand*{\kb}[1]{{\karolcodeschrift #1}}
\newcommand*{\kw}[1]{{\karolcodeschrift\bfseries #1}}

% ===========================================================================
%  2. Struktogramme  (struktex)
% ===========================================================================
\newcommand*{\leeranweisung}{\ensuremath{\emptyset}}
\newcommand*{\stklucke}[1][2.4cm]{\rule[-0.4ex]{#1}{0.4pt}}
\setlength{\unitlength}{1mm}

% ===========================================================================
%  3. Farbschema  (zentral, per \karolsetup änderbar)
% ===========================================================================
% --- Welt ---
% Gezeichnet wird mit den karolW…-Farben (in Abschnitt 4, pixelgenau aus
% Robot Karol Online gesampelt).  Die vier Namen hier bleiben als
% öffentliche Kurzformen bestehen: \karolsetup{ziegelfarbe=…} setzt sie
% zusammen mit den zugehörigen karolW…-Farben.
\definecolor{karolBoden}{RGB}{45,45,215}          % Bodengitter
\definecolor{karolZiegel}{RGB}{255,0,0}           % Ziegel
\definecolor{karolMarke}{RGB}{250,205,60}         % Marke
\definecolor{karolQuader}{RGB}{128,128,128}       % Quader / Wand
% --- Blöcke (Voreinstellung; an Karol Online anpassbar) ---
% Blockfarben: exakt aus der Karol-Online-Oberfläche gesampelt
% (systematische 91/165-Palette)
\definecolor{karolBlockBefehl}{RGB}{116,91,165}    % Anweisungen (violett)
\definecolor{karolBlockSchleife}{RGB}{91,165,91}   % Schleifen (grün)
\definecolor{karolBlockWenn}{RGB}{91,165,91}       % Verzweigungen (grün)
\definecolor{karolBlockBedingung}{RGB}{91,128,165} % Bedingungen (stahlblau)
\definecolor{karolBlockAnweisung}{RGB}{165,91,98}  % eigene Anweisungen (rot)
\definecolor{karolBlockKommentar}{RGB}{165,150,91} % Kommentare (khaki)
\definecolor{karolBlockHut}{RGB}{130,123,117}      % Hauptprogramm-Hut

\ExplSyntaxOn

% ---------------------------------------------------------------------------
%  \karolsetup{feldbreite=8mm, ziegelfarbe=red!70!black, …}
% ---------------------------------------------------------------------------
\dim_new:N \g__karol_feld_dim               % Kantenlänge eines Feldes
\dim_gset:Nn \g__karol_feld_dim { 8.4mm }

\bool_new:N \l__karol_wemma_bool
\tl_new:N \l__karol_wfigur_tl   % gewählte Figur (vor keys_define: .initial
                                % wird schon beim Definieren ausgeführt)
\keys_define:nn { karol / setup }
{
	feldbreite       .dim_gset:N = \g__karol_feld_dim,
	% Weltfarben.  Sie wirken auf \karolzeile UND die karolwelt-Umgebung,
	% die dieselben Bausteine zeichnen.
	bodenfarbe       .code:n     = { \colorlet{karolBoden}{#1}
	                                 \colorlet{karolWGitter}{#1} },
	gitterfarbe      .meta:n     = { bodenfarbe = #1 },
	% ziegelfarbe setzt die leuchtende Lagenkante; der flachere Körper wird
	% daraus abgeleitet — dasselbe Verhältnis wie im Original (59 %).
	ziegelfarbe      .code:n     = { \colorlet{karolZiegel}{#1}
	                                 \colorlet{karolWZiegelKante}{#1}
	                                 \colorlet{karolWZiegelDrei}{#1!59!black} },
	markenfarbe      .code:n     = { \colorlet{karolMarke}{#1}
	                                 \colorlet{karolWMarke}{#1}
	                                 \colorlet{karolWZelleMarke}{#1} },
	quaderfarbe      .code:n     = { \colorlet{karolQuader}{#1}
	                                 \colorlet{karolWZelleQuader}{#1} },
	hervorfarbe      .code:n     = { \colorlet{karolWHervor}{#1} },
	% Tonersparender Graustufenmodus (Abschnitt 3b).  Wirkt auf alle
	% Ansichten und gilt nur in der Gruppe, in der er gesetzt wird.
	druck    .bool_set:N = \l__karol_druck_bool,
	druck    .default:n  = { true },
	% Aussehen der Figur in \karolzeile und der karolwelt-Umgebung
	figur    .choices:nn = { karol, emma }
		{ \tl_set:Nx \l__karol_wfigur_tl { \l_keys_choice_tl } },
	figur    .initial:n  = { karol },
	% Schriften: karol = Noto Sans / Hack wie in Robot Karol Online,
	% dokument = \sffamily / \ttfamily des Dokuments (siehe Abschnitt 0).
	schrift  .choices:nn = { karol, dokument }
		{ \use:c { __karol_schrift_ \l_keys_choice_tl : } },
	% Freie Wahl: der Wert ist der Umschaltbefehl selbst,
	% z. B. textschrift = { \fontspec{Fira~Sans} }.
	textschrift .tl_set:N = \l__karol_schrift_tl,
	codeschrift .tl_set:N = \l__karol_codeschrift_tl,
	% Feinsteuerung der Ziegel (nach ziegelfarbe zu setzen)
	ziegelkoerperfarbe .code:n   = { \colorlet{karolWZiegelDrei}{#1} },
	ziegelkantenfarbe  .code:n   = { \colorlet{karolWZiegelKante}{#1} },
	befehlfarbe      .code:n     = { \colorlet{karolBlockBefehl}{#1} },
	schleifenfarbe   .code:n     = { \colorlet{karolBlockSchleife}{#1} },
	verzweigungfarbe .code:n     = { \colorlet{karolBlockWenn}{#1} },
	bedingungfarbe   .code:n     = { \colorlet{karolBlockBedingung}{#1} },
	anweisungfarbe   .code:n     = { \colorlet{karolBlockAnweisung}{#1} },
	kommentarfarbe   .code:n     = { \colorlet{karolBlockKommentar}{#1} },
	hutfarbe         .code:n     = { \colorlet{karolBlockHut}{#1} },
}
\NewDocumentCommand { \karolsetup } { m }
	{ \keys_set:nn { karol / setup } { #1 } }

% \karolfigur{karol} (Vorgabe) bzw. \karolfigur{emma} — Kurzform für
% \karolsetup{figur=…}.  Wirkt auf \karolzeile UND die karolwelt-Umgebung;
% in einer Gruppe gesetzt gilt es nur dort.
\NewDocumentCommand { \karolfigur } { m }
	{ \keys_set:nn { karol / setup } { figur = #1 } }

% ===========================================================================
%  3b. Druckmodus  (tonersparende Graustufen)
% ===========================================================================
%  \usepackage[druck]{robotkarol}   — das ganze Dokument
%  \karolsetup{druck=true}          — ab hier bzw. nur in dieser Gruppe
%  \karolsetup{druck=false}         — zurück zur Farbdarstellung
%
%  Leitgedanke: so wenig Toner wie möglich, so viel wie nötig.  Alles,
%  was eine Aufgabe trägt, muss erkennbar bleiben — dass auf einem Feld
%  eine Marke liegt, wie hoch ein Ziegelstapel ist, ob ein Block ein
%  \bbefehl oder ein \baufruf ist.  Alles andere wird weiß.
%
%  Deshalb tragen im Druckmodus die KONTUREN die Information und die
%  Flächen bleiben so hell wie möglich:
%
%    * Ziegel sind weiß mit schwarz umrandeten Lagen — der Stapel bleibt
%      seitlich abzählbar, kostet aber praktisch keine Farbe.
%    * Der Quader ist die einzige große graue Fläche der Welt (er muss
%      sich vom Ziegel unterscheiden, dessen Form er teilt).
%    * Die Marke ist mittelgrau: sie ist klein, und sie liegt oft auf
%      einem Ziegel, von dem sie sich absetzen muss.
%    * Bei den Blöcken bekommt der häufigste — \bbefehl — die weiße
%      Fläche; die formgleichen \baufruf/\banweisung heben sich durch
%      einen deutlich dunkleren Ton ab.  Alles übrige unterscheidet
%      sich ohnehin schon durch die Form (C-Maul, Hut, Plättchen).
%
%  Technisch ist der Modus eine zweite Palette: \__karol_farbschema: legt
%  sie zu Beginn jeder Zeichnung über die Farbnamen.  Da \colorlet
%  gruppenlokal wirkt und jede Zeichnung in einer Gruppe läuft, sind
%  eigene Farbeinstellungen danach unverändert — die Farbschlüssel von
%  \karolsetup gelten unbesehen weiter und wirken wieder, sobald der
%  Druckmodus aus ist.  Umgekehrt haben sie im Druckmodus keine Wirkung:
%  eigene Farben würden das Graustufenschema gerade aufheben.
% ---------------------------------------------------------------------------
% Rand der Eingabepillen.  Im Farbmodus hebt sich die Pille durch ihre
% Tönung vom Block ab und braucht keinen — im Druck ist sie auf einem
% weißen \bbefehl selbst weiß, dort trägt erst die Kontur die Information,
% dass hier ein Eingabefeld sitzt.
%
% Als STIL, nicht als tl mit Schlüsselliste: pgfkeys zerlegt eine
% Optionsliste, bevor Makros expandiert werden — ein tl mit Komma darin
% wäre ein einziger, unbekannter Schlüsselname.  Und im Farbmodus darf
% wirklich nichts gesetzt werden, auch keine line~width: sie verstellt
% TikZs outer~sep (Vorgabe 0.5\pgflinewidth) und damit den Anker
% base~east, an dem das Auswahldreieck hängt.
\tikzset{ karolpillenrand/.style = { draw = none } }

% Zwei Rollenfarben, die es nur im Druck gibt (per \definecolor änderbar):
\definecolor{karolDruckKontur}{gray}{0}      % Blockkonturen, Pillenrand
\definecolor{karolDruckAkzent}{gray}{0.35}   % Auswahldreieck der Pillen

% Ist die Druckpalette in dieser Gruppe schon gelegt?  Verschachtelte
% Blöcke erben das Register und sparen sich die Wiederholung.
\bool_new:N \l__karol_druckda_bool

\cs_new_protected:Npn \__karol_druckfarben:
{
	% --- Welt ---------------------------------------------------------
	\colorlet { karolBoden }        { black!45 }
	\colorlet { karolWGitter }      { black!45 }
	% Ziegel: weißer Körper, schwarze Lagenkanten.  Genau wie im Original
	% ist damit jede Lage einzeln zu sehen und der Stapel abzählbar.
	\colorlet { karolZiegel }       { black }
	\colorlet { karolWZiegel }      { white }
	\colorlet { karolWZiegelDrei }  { white }
	\colorlet { karolWZiegelKante } { black }
	\colorlet { karolWFuge }        { black!55 }
	% Marke: mittelgrau, damit sie auch auf einem Ziegel auffällt
	\colorlet { karolMarke }        { black!32 }
	\colorlet { karolWMarke }       { black!32 }
	\colorlet { karolWMarkeRand }   { black!60 }
	\colorlet { karolWZelleMarke }  { black!32 }
	% 2D: die Ziegelzelle bekommt nur einen Hauch Grau — die Stapelzahl
	% steht ohnehin darin und sagt schon alles.
	\colorlet { karolWZelleZiegel } { black!10 }
	\colorlet { karolQuader }       { black!22 }
	\colorlet { karolWZelleQuader } { black!22 }
	\colorlet { karolWHervor }      { black }
	% --- Figur --------------------------------------------------------
	% Gestaffelt, damit Mütze (Blickrichtung!), Haar, Hemd und Hose
	% auseinanderzuhalten sind; die Haut bleibt hell, gibt dem Kopf aber
	% noch eine Silhouette vor weißem Grund.
	\colorlet { karolAvatarKappe }  { black!70 }
	\colorlet { karolAvatarShirt }  { black!30 }
	\colorlet { karolAvatarHose }   { black!50 }
	\colorlet { karolAvatarHaut }   { black!10 }
	\colorlet { karolAvatarHaar }   { black!58 }
	\colorlet { karolEmmaShirt }    { black!22 }
	\colorlet { karolEmmaHaar }     { black!58 }
	\colorlet { karolEmmaBluete }   { black!35 }
	\colorlet { karolEmmaStiel }    { black!45 }
	% --- Blöcke -------------------------------------------------------
	\colorlet { karolBlockBefehl }    { white }
	\colorlet { karolBlockAnweisung } { black!22 }
	\colorlet { karolBlockSchleife }  { black!10 }
	\colorlet { karolBlockWenn }      { black!10 }
	\colorlet { karolBlockBedingung } { black!16 }
	\colorlet { karolBlockKommentar } { black!16 }
	\colorlet { karolBlockHut }       { black!28 }
	% Eingabepillen bekommen einen Rand (siehe oben)
	\tikzset{ karolpillenrand/.style =
		{ draw = karolDruckKontur, line~width = \l__karol_bkw_dim } }
}
% Zu Beginn jeder Zeichnung aufzurufen (in deren Gruppe!).
\cs_new_protected:Npn \__karol_farbschema:
{
	\bool_if:NT \l__karol_druck_bool
	{
		\bool_if:NF \l__karol_druckda_bool
		{
			\bool_set_true:N \l__karol_druckda_bool
			\__karol_druckfarben:
		}
	}
}
% Eine beliebige Farbangabe in ihren Grauwert umsetzen (eigene
% Markenfarben: verschiedene Farben bleiben als verschiedene Grauwerte
% unterscheidbar).
\cs_new_protected:Npn \__karol_grau:n #1
	{ \colorlet { karolWMtmp } [ gray ] { #1 } }
\cs_generate_variant:Nn \__karol_grau:n { V }

% ===========================================================================
%  4. Karol-Welten im Schrägbild: Bausteine und karolwelt-Umgebung
% ===========================================================================
%  \begin{karolwelt}[breite=5, laenge=3, ansicht=3d]
%      \karol[x=1, y=2, richtung=osten]
%      \ziegel[x=2, y=2, anzahl=1, bisx=4]
%      \marke[x=5, y=1]
%      \quader[x=4, y=3, bisy=3]     (\wand = Synonym)
%  \end{karolwelt}
%
%  Koordinaten wie in Robot Karol Online: x = Spalte (1 .. breite, von
%  links), y = Zeile (1 .. laenge, von HINTEN); hoehe = Raumhöhe (nur
%  3D-Rahmen). In der 2D-Ansicht ist Zeile y=1 die oberste Zeile.
% ---------------------------------------------------------------------------
% Farben der Online-Oberfläche (gesampelt)
\definecolor{karolWZiegel}{RGB}{224,48,48}       % Ziegel in Zeilen-Welten
\definecolor{karolWZiegelDrei}{RGB}{150,8,8}     % Ziegelkörper 3D-Welt
\definecolor{karolWZiegelKante}{RGB}{255,0,0}    % Ziegel-Lagenkanten 3D
\definecolor{karolWFuge}{RGB}{153,20,40}         % Ziegelfugen (Zeilen-Welten)
\definecolor{karolWMarke}{RGB}{250,205,60}       % Marke 3D
\definecolor{karolWMarkeRand}{RGB}{214,143,0}    % Marke 3D, Rand
\definecolor{karolWGitter}{RGB}{45,45,215}       % Bodengitter
% Hervorhebung: bewusst magenta statt rot — die Ziegelkanten sind reines
% Rot, ein roter Rahmen wäre auf einem Ziegelfeld nicht zu erkennen.
\definecolor{karolWHervor}{RGB}{226,0,122}       % hervorgehobene Felder
\definecolor{karolWZelleZiegel}{RGB}{255,205,205}% 2D: Ziegelzelle
\definecolor{karolWZelleMarke}{RGB}{210,212,39}  % 2D: Markenfläche
\definecolor{karolWZelleQuader}{RGB}{128,128,128}% 2D: Quaderzelle
% Avatar
\definecolor{karolAvatarKappe}{RGB}{198,32,44}
\definecolor{karolAvatarShirt}{RGB}{186,44,54}
\definecolor{karolAvatarHose}{RGB}{62,92,168}
\definecolor{karolAvatarHaut}{RGB}{244,204,158}
\definecolor{karolAvatarHaar}{RGB}{122,74,34}
% Zweite Figur „Emma": dieselbe Figur mit langem Haar und einer Blume.
% Die Farben sind aus der Paketpalette abgeleitet, nicht aus der
% Oberfläche übernommen — nachgebaut, nicht kopiert.
\definecolor{karolEmmaShirt}{RGB}{212,124,130}   % karolAvatarShirt aufgehellt
\definecolor{karolEmmaHaar}{RGB}{138,86,48}      % karolAvatarHaar aufgehellt
\definecolor{karolEmmaBluete}{RGB}{250,205,60}   % wie die Marke
\definecolor{karolEmmaStiel}{RGB}{60,150,70}

% Projektion (aus den Online-Screenshots vermessen), in Feldbreiten:
\fp_const:Nn \c__karol_wdx_fp { 0.47 }   % Tiefenversatz je Reihe, x
\fp_const:Nn \c__karol_wdy_fp { 0.69 }   % Tiefenversatz je Reihe, y
\fp_const:Nn \c__karol_wzh_fp { 0.46 }   % Höhe eines Ziegels
\fp_const:Nn \c__karol_wqh_fp { 0.80 }   % Höhe eines Quaders

\tl_new:N   \l__karol_tmp_tl           % Streuvariable beider Renderer
\int_new:N  \l__karol_wbreite_int
\int_new:N  \l__karol_wlaenge_int
\int_new:N  \l__karol_whoehe_int
\tl_new:N   \l__karol_wansicht_tl
\tl_new:N   \l__karol_wskal_tl
\tl_new:N   \l__karol_wtitel_tl
\tl_new:N   \l__karol_whinweis_tl
\prop_new:N \l__karol_wziegel_prop     % "x,y" -> Stapelhöhe
\prop_new:N \l__karol_wmarke_prop      % "x,y" -> Farbe
\prop_new:N \l__karol_wquader_prop     % "x,y" -> 1
\bool_new:N \l__karol_wkarol_bool
\int_new:N  \l__karol_wkx_int
\int_new:N  \l__karol_wky_int
\tl_new:N   \l__karol_wkdir_tl         % o | w | s | n

\keys_define:nn { karol / weltumg }
{
	breite   .int_set:N   = \l__karol_wbreite_int,
	breite   .initial:n   = { 5 },
	laenge   .int_set:N   = \l__karol_wlaenge_int,
	laenge   .initial:n   = { 5 },
	hoehe    .int_set:N   = \l__karol_whoehe_int,
	hoehe    .initial:n   = { 6 },
	ansicht  .choices:nn  = { 3d, 2d }
		{ \tl_set:Nx \l__karol_wansicht_tl { \l_keys_choice_tl } },
	ansicht  .initial:n   = { 3d },
	skalierung .tl_set:N  = \l__karol_wskal_tl,
	skalierung .initial:n = { 1 },
	titel    .tl_set:N    = \l__karol_wtitel_tl,
	titel    .initial:n   = { },
	hinweis  .tl_set:N    = \l__karol_whinweis_tl,
	hinweis  .initial:n   = { },
}
% Dieselben Vorgaben noch einmal als Liste: die Umgebung setzt sie bei
% jedem Aufruf, denn .initial:n wirkt nur einmal beim Laden des Pakets.
\tl_const:Nn \c__karol_wvorgabe_tl
{
	breite = 5, laenge = 5, hoehe = 6, ansicht = 3d,
	skalierung = 1, titel = { }, hinweis = { },
}

% --- Objekt-Schlüssel ------------------------------------------------------
\int_new:N \l__karol_wox_int
\int_new:N \l__karol_woy_int
\int_new:N \l__karol_wobisx_int
\int_new:N \l__karol_wobisy_int
\int_new:N \l__karol_woanz_int
\tl_new:N  \l__karol_wofarbe_tl

\keys_define:nn { karol / objekt }
{
	x      .int_set:N = \l__karol_wox_int,
	y      .int_set:N = \l__karol_woy_int,
	bisx   .int_set:N = \l__karol_wobisx_int,
	bisy   .int_set:N = \l__karol_wobisy_int,
	bisX   .meta:n    = { bisx = #1 },
	bisY   .meta:n    = { bisy = #1 },
	anzahl .int_set:N = \l__karol_woanz_int,
	farbe  .tl_set:N  = \l__karol_wofarbe_tl,
	richtung .choice: ,
	richtung / osten  .code:n = { \tl_set:Nn \l__karol_wkdir_tl { o } },
	richtung / westen .code:n = { \tl_set:Nn \l__karol_wkdir_tl { w } },
	richtung / sueden .code:n = { \tl_set:Nn \l__karol_wkdir_tl { s } },
	richtung / norden .code:n = { \tl_set:Nn \l__karol_wkdir_tl { n } },
	richtung / rechts .meta:n = { richtung = osten },
	richtung / links  .meta:n = { richtung = westen },
	richtung / vorn   .meta:n = { richtung = sueden },
	richtung / vorne  .meta:n = { richtung = sueden },
	richtung / unten  .meta:n = { richtung = sueden },
	richtung / hinten .meta:n = { richtung = norden },
	richtung / oben   .meta:n = { richtung = norden },
}
\cs_new_protected:Npn \__karol_wobjekt_reset:
{
	\int_set:Nn \l__karol_wox_int    { 1 }
	\int_set:Nn \l__karol_woy_int    { 1 }
	\int_zero:N \l__karol_wobisx_int
	\int_zero:N \l__karol_wobisy_int
	\int_set:Nn \l__karol_woanz_int  { 1 }
	\tl_set:Nn  \l__karol_wofarbe_tl { karolWMarke }
}
% Bereich (x..bisx) x (y..bisy) auf ein Prop-Feld anwenden: #1 Prop, #2 Wert
\cs_new_protected:Npn \__karol_wbereich:Nn #1#2
{
	\int_compare:nT { \l__karol_wobisx_int < \l__karol_wox_int }
		{ \int_set_eq:NN \l__karol_wobisx_int \l__karol_wox_int }
	\int_compare:nT { \l__karol_wobisy_int < \l__karol_woy_int }
		{ \int_set_eq:NN \l__karol_wobisy_int \l__karol_woy_int }
	\int_step_inline:nnn { \l__karol_wox_int } { \l__karol_wobisx_int }
	{
		\int_step_inline:nnn { \l__karol_woy_int } { \l__karol_wobisy_int }
		{ \prop_put:Nnx #1 { ##1 , ####1 } {#2} }
	}
}

% Die Objektkommandos heißen \karol, \ziegel, \marke, \quader und \wand.
% Das sind gewöhnliche deutsche Wörter, die ein Dokument selbst belegt
% haben kann — global definiert würde das Paket dort nicht mehr laden.
% Sie werden deshalb unter privaten Namen angelegt und erst in der
% Umgebung karolwelt sichtbar gemacht (\__karol_wobjekte:).  Da die
% Umgebung eine Gruppe bildet, gilt eine etwaige eigene Bedeutung
% außerhalb unverändert weiter.
\NewDocumentCommand { \__karol_wobj_karol:w } { O{} }
{
	\__karol_wobjekt_reset:
	\keys_set:nn { karol / objekt } { richtung = sueden, #1 }
	\bool_set_true:N \l__karol_wkarol_bool
	\int_set_eq:NN \l__karol_wkx_int \l__karol_wox_int
	\int_set_eq:NN \l__karol_wky_int \l__karol_woy_int
}
\NewDocumentCommand { \__karol_wobj_ziegel:w } { O{} }
{
	\__karol_wobjekt_reset:
	\keys_set:nn { karol / objekt } { #1 }
	\__karol_wbereich:Nn \l__karol_wziegel_prop
		{ \int_use:N \l__karol_woanz_int }
}
\NewDocumentCommand { \__karol_wobj_marke:w } { O{} }
{
	\__karol_wobjekt_reset:
	\keys_set:nn { karol / objekt } { #1 }
	\exp_args:NNx \__karol_wbereich:Nn \l__karol_wmarke_prop
		{ \tl_use:N \l__karol_wofarbe_tl }
}
\NewDocumentCommand { \__karol_wobj_quader:w } { O{} }
{
	\__karol_wobjekt_reset:
	\keys_set:nn { karol / objekt } { #1 }
	\__karol_wbereich:Nn \l__karol_wquader_prop { 1 }
}
\cs_new_protected:Npn \__karol_wobjekte:
{
	\cs_set_eq:NN \karol  \__karol_wobj_karol:w
	\cs_set_eq:NN \ziegel \__karol_wobj_ziegel:w
	\cs_set_eq:NN \marke  \__karol_wobj_marke:w
	\cs_set_eq:NN \quader \__karol_wobj_quader:w
	\cs_set_eq:NN \wand   \__karol_wobj_quader:w
}

% --- 2D-Ansicht (Draufsicht wie in Karol Online) ---------------------------
\cs_new_protected:Npn \__karol_wrender_iid:
{
	\begin{tikzpicture}
		[ x=\dim_use:N \g__karol_feld_dim ,
		  y=-\dim_use:N \g__karol_feld_dim ]
		% Zellfüllungen
		\int_step_inline:nn { \l__karol_wbreite_int }
		{
			\int_step_inline:nn { \l__karol_wlaenge_int }
			{
				\__karol_wzelle_iid:nn { ##1 } { ####1 }
			}
		}
		% Gitter (explizit: „grid" nutzt xstep/ystep als Canvas-Maß und
		% versagt bei negativem y-Einheitsvektor)
		\int_step_inline:nnn { 0 } { \l__karol_wbreite_int }
		{
			\draw [ karolWGitter, line~width=.55pt ]
				( ##1 , 0 ) -- ( ##1 , \int_use:N \l__karol_wlaenge_int );
		}
		\int_step_inline:nnn { 0 } { \l__karol_wlaenge_int }
		{
			\draw [ karolWGitter, line~width=.55pt ]
				( 0 , ##1 ) -- ( \int_use:N \l__karol_wbreite_int , ##1 );
		}
		% Karol
		\bool_if:NT \l__karol_wkarol_bool
		{
			\begin{scope}
				[ shift={ ( { \l__karol_wkx_int - 0.5 },
				            { \l__karol_wky_int - 0.5 } ) },
				  rotate=\str_case:Vn \l__karol_wkdir_tl
					{ { o } { 0 } { n } { 90 } { w } { 180 } { s } { -90 } } ]
				\fill [ black ]
					(-0.34, -0.26) -- (0.38, 0) -- (-0.34, 0.26)
					-- (-0.18, 0) -- cycle;
			\end{scope}
		}
	\end{tikzpicture}
}
\cs_new_protected:Npn \__karol_wzelle_iid:nn #1#2
{
	\prop_if_in:NnT \l__karol_wquader_prop { #1 , #2 }
	{
		\fill [ karolWZelleQuader ]
			( {#1 - 1}, {#2 - 1} ) rectangle ( {#1}, {#2} );
	}
	% Der Ziegelstapel färbt die ganze Zelle — auch unter einer Marke, damit
	% „Marke auf Ziegel“ erkennbar bleibt.
	\prop_if_in:NnT \l__karol_wziegel_prop { #1 , #2 }
	{
		\fill [ karolWZelleZiegel ]
			( {#1 - 1}, {#2 - 1} ) rectangle ( {#1}, {#2} );
	}
	% Marke: einfarbige Fläche (kein Kreuz), kleiner als das Feld, sodass der
	% Ziegel ringsum sichtbar bleibt.
	\prop_get:NnNT \l__karol_wmarke_prop { #1 , #2 } \l__karol_tmp_tl
	{
		\str_if_eq:VnT \l__karol_tmp_tl { karolWMarke }
			{ \tl_set:Nn \l__karol_tmp_tl { karolWZelleMarke } }
		\bool_if:NT \l__karol_druck_bool
		{
			\__karol_grau:V \l__karol_tmp_tl
			\tl_set:Nn \l__karol_tmp_tl { karolWMtmp }
		}
		\fill [ \l__karol_tmp_tl ]
			( {#1 - 0.82}, {#2 - 0.82} ) rectangle ( {#1 - 0.18}, {#2 - 0.18} );
	}
	\prop_get:NnNT \l__karol_wziegel_prop { #1 , #2 } \l__karol_tmp_tl
	{
		\node at ( {#1 - 0.5}, {#2 - 0.5} )
			{ \normalfont \__karol_textfont: \small \l__karol_tmp_tl };
	}
}

% --- 3D-Ansicht (Schrägbild wie in Karol Online) ---------------------------
% Bildkoordinaten einer Bodenecke: x-Spalte #1 (0-basiert), Tiefe #2
% (0 = vorderste Reihe), Höhe #3 (in Ziegellagen ausgedrückt als fp-Wert).
% Reihe y (1 = hinten) hat Tiefe  laenge - y.
\cs_new:Npn \__karol_wpx:nn #1#2
	{ \fp_eval:n { (#1) + \c__karol_wdx_fp * (#2) } }
\cs_new:Npn \__karol_wpy:nnn #1#2#3
	{ \fp_eval:n { \c__karol_wdy_fp * (#2) + \c__karol_wzh_fp * (#3) } }

\cs_new_protected:Npn \__karol_wrender_iiid:
{
	\begin{tikzpicture}
		[ x=\dim_use:N \g__karol_feld_dim ,
		  y=\dim_use:N \g__karol_feld_dim ]
		\__karol_wraum:
		% Objekte, Reihe für Reihe von hinten (y=1) nach vorn
		\int_step_inline:nn { \l__karol_wlaenge_int }
		{
			\int_step_inline:nn { \l__karol_wbreite_int }
			{
				\__karol_wzelle_iiid:nn { ####1 } { ##1 }
			}
		}
	\end{tikzpicture}
}

% Raum: Bodengitter, Wandlinien, oberer Rahmen.
% In zwei Teile zerlegt, weil \karolzeile das Gitter immer, den Rahmen
% aber nur auf Wunsch (Schlüssel rahmen) zeichnet.
\cs_new_protected:Npn \__karol_wraum:
	{ \__karol_wboden_gitter: \__karol_wrahmen: }

% Bodengitter (blau, in beide Richtungen)
\cs_new_protected:Npn \__karol_wboden_gitter:
{
	\int_step_inline:nnn { 0 } { \l__karol_wbreite_int }
	{	% Tiefenlinien
		\draw [ karolWGitter, line~width=.5pt ]
			( \__karol_wpx:nn { ##1 } { 0 },
			  \__karol_wpy:nnn { ##1 } { 0 } { 0 } )
			-- ( \__karol_wpx:nn { ##1 } { \l__karol_wlaenge_int },
			     \__karol_wpy:nnn { ##1 } { \l__karol_wlaenge_int } { 0 } );
	}
	\int_step_inline:nnn { 0 } { \l__karol_wlaenge_int }
	{	% Querlinien
		\draw [ karolWGitter, line~width=.5pt ]
			( \__karol_wpx:nn { 0 } { ##1 },
			  \__karol_wpy:nnn { 0 } { ##1 } { 0 } )
			-- ( \__karol_wpx:nn { \l__karol_wbreite_int } { ##1 },
			     \__karol_wpy:nnn { \l__karol_wbreite_int } { ##1 } { 0 } );
	}
}

% Raumrahmen: Rückwand, linke Wand, oberer Rahmen (Raumhöhe = hoehe Lagen)
\cs_new_protected:Npn \__karol_wrahmen:
{
	% Raumhöhe in Bildeinheiten
	\fp_set:Nn \l__karol_wtmp_fp
		{ \c__karol_wzh_fp * \l__karol_whoehe_int }
	% Rückwand: senkrechte gestrichelte Linien an jeder Gitterlinie
	\int_step_inline:nnn { 0 } { \l__karol_wbreite_int }
	{
		\draw [ black!55, line~width=.4pt, dashed ]
			( \__karol_wpx:nn { ##1 } { \l__karol_wlaenge_int },
			  \__karol_wpy:nnn { ##1 } { \l__karol_wlaenge_int } { 0 } )
			-- ++ ( 0, \fp_use:N \l__karol_wtmp_fp );
	}
	% Linke Wand
	\int_step_inline:nnn { 0 } { \l__karol_wlaenge_int }
	{
		\draw [ karolWGitter!70!white, line~width=.4pt, dashed ]
			( \__karol_wpx:nn { 0 } { ##1 },
			  \__karol_wpy:nnn { 0 } { ##1 } { 0 } )
			-- ++ ( 0, \fp_use:N \l__karol_wtmp_fp );
	}
	% Oberer Rahmen (strichpunktiert): hinten und links
	\draw [ black!70, line~width=.5pt, dash~pattern=on~4pt~off~2pt~on~.6pt~off~2pt ]
		( \__karol_wpx:nn { 0 } { 0 },
		  \fp_eval:n { \l__karol_wtmp_fp } )
		-- ( \__karol_wpx:nn { 0 } { \l__karol_wlaenge_int },
		     \fp_eval:n { \c__karol_wdy_fp * \l__karol_wlaenge_int
		                  + \l__karol_wtmp_fp } )
		-- ( \__karol_wpx:nn { \l__karol_wbreite_int }
		         { \l__karol_wlaenge_int },
		     \fp_eval:n { \c__karol_wdy_fp * \l__karol_wlaenge_int
		                  + \l__karol_wtmp_fp } );
}

% Eine Zelle in 3D: Quader / Ziegelstapel / Marke / Karol
\fp_new:N \l__karol_wtmp_fp
\fp_new:N \l__karol_wz_fp
\cs_new_protected:Npn \__karol_wzelle_iiid:nn #1#2   % #1 = x, #2 = y
{
	\fp_zero:N \l__karol_wz_fp   % Oberkante des Zellinhalts, in Lagen
	\prop_if_in:NnT \l__karol_wquader_prop { #1 , #2 }
		{ \__karol_wquader_iiid:nnn { #1 } { #2 } { 0 } }
	\prop_get:NnNT \l__karol_wziegel_prop { #1 , #2 } \l__karol_tmp_tl
	{
		\int_step_inline:nn { \l__karol_tmp_tl }
			{ \__karol_wziegel_iiid:nnn { #1 } { #2 } { ##1 - 1 } }
		\fp_set:Nn \l__karol_wz_fp { \l__karol_tmp_tl }
	}
	\prop_get:NnNT \l__karol_wmarke_prop { #1 , #2 } \l__karol_tmp_tl
		{ \exp_args:Nnnx \__karol_wmarke_iiid:nnn { #1 } { #2 }
			{ \tl_use:N \l__karol_tmp_tl } }
	\bool_if:NT \l__karol_wkarol_bool
	{
		\int_compare:nT { \l__karol_wkx_int = #1 }
		{
			\int_compare:nT { \l__karol_wky_int = #2 }
				{ \__karol_wavatar_iiid:nn { #1 } { #2 } }
		}
	}
}

% Hilfsgrößen einer Zelle (#1 x, #2 y): Tiefe t = laenge - y
\cs_new:Npn \__karol_wtiefe:n #1
	{ \int_eval:n { \l__karol_wlaenge_int - (#1) } }

% Ziegel: Würfelsegment auf Lage #3 (0-basiert)
\cs_new_protected:Npn \__karol_wziegel_iiid:nnn #1#2#3
{
	% Wie Karol Online: flacher dunkelroter Körper, jede Lage auf allen
	% Sichtflächen leuchtend rot umrandet (Stapelhöhe seitlich ablesbar)
	\__karol_wkubus:nnnnnnnn { #1 } { #2 } { #3 } { 1 }
		{ karolWZiegelDrei } { karolWZiegelDrei } { karolWZiegelDrei }
		{ karolWZiegelKante }
}

% Quader (grau, fester Höhenwert), ab Lage #3
\cs_new_protected:Npn \__karol_wquader_iiid:nnn #1#2#3
{
	\group_begin:
	\fp_set:Nn \l__karol_wtmp_fp
		{ \c__karol_wqh_fp / \c__karol_wzh_fp }
	\__karol_wkubus:nnnnnnnn { #1 } { #2 } { #3 }
		{ \fp_use:N \l__karol_wtmp_fp }
		{ karolWZelleQuader } { karolWZelleQuader!88!black }
		{ karolWZelleQuader!94!white } { black!75 }
	\group_end:
}

% Allgemeiner Kubus: Zelle (#1,#2), ab Lage #3, Höhe #4 Lagen,
% Farben: #5 Front, #6 Seite, #7 Deckel, #8 Kanten
\cs_new_protected:Npn \__karol_wkubus:nnnnnnnn #1#2#3#4#5#6#7#8
{
	\begin{scope}
		[ shift={ ( { \__karol_wpx:nn { #1 - 1 } { \__karol_wtiefe:n {#2} } },
		            { \__karol_wpy:nnn { 0 } { \__karol_wtiefe:n {#2} }
		                                { #3 } } ) } ]
		% rechte Seitenfläche
		\fill [ #6 ]
			( 1, 0 )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp } )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( 1, { (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- cycle;
		% Deckfläche
		\fill [ #7 ]
			( 0, { (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( 1, { (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( { \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- cycle;
		% Frontfläche
		\fill [ #5 ]
			( 0, 0 ) rectangle ( 1, { (#4) * \fp_use:N \c__karol_wzh_fp } );
		% Kanten aller Sichtflächen
		\draw [ #8, line~width=.5pt, line~join=round ]
			( 0, 0 ) rectangle ( 1, { (#4) * \fp_use:N \c__karol_wzh_fp } );
		\draw [ #8, line~width=.5pt, line~join=round ]
			( 1, 0 )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp } )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( 1, { (#4) * \fp_use:N \c__karol_wzh_fp } );
		\draw [ #8, line~width=.5pt, line~join=round ]
			( 0, { (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( { \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } )
			-- ( { 1 + \fp_use:N \c__karol_wdx_fp },
			     { \fp_use:N \c__karol_wdy_fp
			       + (#4) * \fp_use:N \c__karol_wzh_fp } );
	\end{scope}
}

% Marke: flache, einfarbige Platte auf Höhe \l__karol_wz_fp (kein Kreuz)
%
% Die Platte liegt ringsum \c__karol_wmr_fp Feldbreiten INNERHALB der
% Zelle — gerechnet in deren eigenen Koordinaten: u quer, v in die
% Tiefe, beide von 0 bis 1.  Ein Punkt (u,v) der Zellfläche liegt im
% Bild bei
%       x = u + v * wdx ,      y = v * wdy .
% Der Tiefenanteil v*wdx darf dabei NICHT fehlen.  Bis v3.7 stand für
% die vordere Kante schlicht x = 0.07: dadurch saß die ganze Platte um
% v*wdx (rund 0.034 Feldbreiten) zu weit links, berührte auf einem
% Ziegel dessen linke Kante und ließ rechts die doppelte Lücke.
\fp_const:Nn \c__karol_wmr_fp { 0.07 }   % Rand ringsum, in Feldbreiten
\cs_new:Npn \__karol_wmarke_pfad:
{
	\__karol_wmarke_ecke:nn { \c__karol_wmr_fp } { \c__karol_wmr_fp }
	-- \__karol_wmarke_ecke:nn { 1 - \c__karol_wmr_fp } { \c__karol_wmr_fp }
	-- \__karol_wmarke_ecke:nn { 1 - \c__karol_wmr_fp } { 1 - \c__karol_wmr_fp }
	-- \__karol_wmarke_ecke:nn { \c__karol_wmr_fp } { 1 - \c__karol_wmr_fp }
	-- cycle
}
% Zellkoordinate (#1 quer, #2 Tiefe) als Bildkoordinate
\cs_new:Npn \__karol_wmarke_ecke:nn #1#2
{
	( \fp_eval:n { (#1) + (#2) * \c__karol_wdx_fp } ,
	  \fp_eval:n { (#2) * \c__karol_wdy_fp } )
}
\cs_new_protected:Npn \__karol_wmarke_iiid:nnn #1#2#3
{
	% Im Druck wird jede Markenfarbe in ihren Grauwert umgesetzt: eigene
	% Farben bleiben so als verschiedene Grautöne unterscheidbar.
	\bool_if:NTF \l__karol_druck_bool
		{ \__karol_grau:n {#3} } { \colorlet{karolWMtmp}{#3} }
	\begin{scope}
		[ shift={ ( { \__karol_wpx:nn { #1 - 1 } { \__karol_wtiefe:n {#2} } },
		            { \__karol_wpy:nnn { 0 } { \__karol_wtiefe:n {#2} }
		                { \fp_use:N \l__karol_wz_fp } } ) } ]
		\fill [ karolWMtmp ] \__karol_wmarke_pfad: ;
		\draw [ karolWMtmp!62!black, line~width=.6pt ]
			\__karol_wmarke_pfad: ;
	\end{scope}
}

% Avatar (Vektorfassung des Karol-Online-Avatars), Blick per wkdir
\cs_new_protected:Npn \__karol_wavatar_iiid:nn #1#2
{
	\begin{scope}
		[ shift={ ( { \__karol_wpx:nn { #1 - 0.5 } { \__karol_wtiefe:n {#2} }
		              + 0.5 * \fp_use:N \c__karol_wdx_fp },
		            { \__karol_wpy:nnn { 0 } { \__karol_wtiefe:n {#2} }
		                { \fp_use:N \l__karol_wz_fp }
		              + 0.5 * \fp_use:N \c__karol_wdy_fp } ) },
		  scale=1.45,
		  xscale={ \str_if_eq:VnTF \l__karol_wkdir_tl { w } { -1 } { 1 } } ]
		\__karol_wavatar_zeichnen:V \l__karol_wkdir_tl
	\end{scope}
}
% Zeichnet die Figur.  #1 = Blickrichtung (o/w/n/s).
% Beide Figuren teilen sich denselben Körper; „emma" bekommt zusätzlich
% langes Haar, eine Blume in der Hand und ein helleres Hemd.
\cs_new_protected:Npn \__karol_wavatar_zeichnen:n #1
{
	\bool_set_false:N \l__karol_wemma_bool
	\str_if_eq:VnT \l__karol_wfigur_tl { emma }
		{ \bool_set_true:N \l__karol_wemma_bool }
	\bool_if:NTF \l__karol_wemma_bool
		{ \colorlet { karolAvatarHemd } { karolEmmaShirt  } }
		{ \colorlet { karolAvatarHemd } { karolAvatarShirt } }
	% Beine
	\fill [ karolAvatarHose, rounded~corners=.6pt ]
		(-0.17, 0) rectangle (-0.03, 0.34)
		( 0.03, 0) rectangle ( 0.17, 0.34);
	\fill [ black!75 ]
		(-0.19, 0) rectangle (-0.01, 0.055)
		( 0.01, 0) rectangle ( 0.19, 0.055);
	% Rumpf
	\fill [ karolAvatarHemd, rounded~corners=1pt ]
		(-0.21, 0.32) rectangle (0.21, 0.72);
	% Arme
	\fill [ karolAvatarHemd, rounded~corners=.8pt ]
		(-0.29, 0.36) rectangle (-0.19, 0.70)
		( 0.19, 0.36) rectangle ( 0.29, 0.70);
	\fill [ karolAvatarHaut ]
		(-0.285, 0.335) rectangle (-0.195, 0.42)
		( 0.195, 0.335) rectangle ( 0.285, 0.42);
	% Kopf
	\str_if_eq:nnTF {#1} { n }
	{	% Rückenansicht: Haar statt Gesicht
		\fill [ karolAvatarHaar, rounded~corners=1.2pt ]
			(-0.185, 0.70) rectangle (0.185, 0.97);
	}
	{
		\fill [ karolAvatarHaut, rounded~corners=1.2pt ]
			(-0.185, 0.70) rectangle (0.185, 0.97);
		\str_case:nn {#1}
		{
			{ s } {	% zugewandt: zwei Augen, Mund
				\fill [ black!80 ]
					(-0.075, 0.865) circle (0.022)
					( 0.075, 0.865) circle (0.022);
				\draw [ black!60, line~width=.5pt ]
					(-0.05, 0.775) -- (0.05, 0.775);
			}
			{ o } {	% Profil (Osten; Westen via xscale=-1)
				\fill [ black!80 ] (0.10, 0.865) circle (0.022);
				\draw [ black!60, line~width=.5pt ]
					(0.06, 0.775) -- (0.15, 0.775);
				\__karol_wavatar_haar_kurz:
			}
			{ w } {
				\fill [ black!80 ] (0.10, 0.865) circle (0.022);
				\draw [ black!60, line~width=.5pt ]
					(0.06, 0.775) -- (0.15, 0.775);
				\__karol_wavatar_haar_kurz:
			}
		}
	}
	% Emma: langes Haar.  Nach dem Kopf gezeichnet, damit es das Gesicht
	% rahmt statt dahinter zu verschwinden — aber schmal genug, um Augen
	% und Mund frei zu lassen.
	%
	% Alle Strähnen bleiben innerhalb der Mütze (x von -0.20 bis 0.20)
	% und enden oben unter deren Unterkante: die Mütze wird danach
	% gezeichnet und deckt sie ab.  Ragt das Haar seitlich darüber
	% hinaus, sieht es aus, als läge es auf der Mütze.
	\bool_if:NT \l__karol_wemma_bool
	{
		\str_case:nn {#1}
		{
			{ n } {	% abgewandt: das Haar bedeckt Kopf und Rücken
				\fill [ karolEmmaHaar, rounded~corners=1.6pt ]
					(-0.20, 0.38) rectangle (0.20, 1.00);
			}
			{ s } {	% zugewandt: lange Strähne links, kurze rechts
				\fill [ karolEmmaHaar, rounded~corners=1.2pt ]
					(-0.195, 0.44) rectangle (-0.125, 0.98);
				\fill [ karolEmmaHaar, rounded~corners=1pt ]
					( 0.135, 0.73) rectangle ( 0.195, 0.98);
			}
			{ o } {	% Profil: das Haar fällt hinten (links) herab
				\fill [ karolEmmaHaar, rounded~corners=1.2pt ]
					(-0.195, 0.40) rectangle (-0.105, 0.99);
			}
			{ w } {
				\fill [ karolEmmaHaar, rounded~corners=1.2pt ]
					(-0.195, 0.40) rectangle (-0.105, 0.99);
			}
		}
	}
	% Kappe: Deckel + Schirm in Blickrichtung
	\fill [ karolAvatarKappe, rounded~corners=1.4pt ]
		(-0.20, 0.93) rectangle (0.20, 1.08);
	\str_case:nn {#1}
	{
		{ o } { \fill [ karolAvatarKappe, rounded~corners=.5pt ]
			(0.17, 0.93) rectangle (0.40, 0.985); }
		{ w } { \fill [ karolAvatarKappe, rounded~corners=.5pt ]
			(0.17, 0.93) rectangle (0.40, 0.985); }
		{ s } { \fill [ karolAvatarKappe!85!black, rounded~corners=.5pt ]
			(-0.21, 0.905) rectangle (0.21, 0.955); }
		{ n } { }
	}
	% Emma: Blume in der Hand — zuletzt, damit sie vor allem anderen liegt
	\bool_if:NT \l__karol_wemma_bool
	{
		\fill [ karolEmmaStiel, rounded~corners=.3pt ]
			(0.222, 0.38) rectangle (0.258, 0.62);
		\fill [ karolEmmaBluete ] (0.24, 0.655) circle (0.072);
		\fill [ karolEmmaBluete!62!black ] (0.24, 0.655) circle (0.026);
	}
}
% Kurzes Haar am Hinterkopf (nur die Figur „karol"; bei „emma" verdeckt
% es das lange Haar ohnehin)
\cs_new_protected:Npn \__karol_wavatar_haar_kurz:
{
	\bool_if:NF \l__karol_wemma_bool
	{
		\fill [ karolAvatarHaar ]
			(-0.185, 0.70) rectangle (-0.10, 0.95);
	}
}
\cs_generate_variant:Nn \__karol_wavatar_zeichnen:n { V }

% --- Umgebung --------------------------------------------------------------
\NewDocumentEnvironment { karolwelt } { O{} }
{
	\__karol_farbschema:
	\__karol_wobjekte:
	\keys_set:nV { karol / weltumg } \c__karol_wvorgabe_tl
	\keys_set:nn { karol / weltumg } { #1 }
	\prop_clear:N \l__karol_wziegel_prop
	\prop_clear:N \l__karol_wmarke_prop
	\prop_clear:N \l__karol_wquader_prop
	\bool_set_false:N \l__karol_wkarol_bool
	\ignorespaces
}
{
	\par \mode_leave_vertical:
	\begin{tabular} [b] {@{}c@{}}
		\tl_if_empty:NF \l__karol_wtitel_tl
			{ { \footnotesize \l__karol_wtitel_tl } \\ [2pt] }
		\exp_args:NV \scalebox \l__karol_wskal_tl
		{
			\str_if_eq:VnTF \l__karol_wansicht_tl { 2d }
				{ \__karol_wrender_iid: }
				{ \__karol_wrender_iiid: }
		}
		\tl_if_empty:NF \l__karol_whinweis_tl
			{ \\ [3pt] { \footnotesize \itshape \l__karol_whinweis_tl } }
	\end{tabular}
}

% ===========================================================================
%  4b. Zeilen-Welten: \karolzeile
% ===========================================================================
%  \karolzeile{> Z 2 {2M} M Q} zeichnet eine EINZELNE Feldreihe aus einer
%  Zeichenkette.  Sie ist der schnelle Weg für Arbeitsblätter, bei denen
%  die Tiefe der Welt keine Rolle spielt.
%
%  Gezeichnet wird mit denselben Bausteinen wie die karolwelt-Umgebung:
%  gleiche Projektion, gleiches Bodengitter, gleiche Ziegel, Marken,
%  Quader und derselbe Avatar.  Eine Zeilen-Welt ist damit optisch nichts
%  anderes als eine karolwelt mit laenge=1 — nur die Eingabe ist kürzer.
%
%  Zeichen:  .  leeres Feld          >  Karol nach Osten
%            Z  ein Ziegel           <  Karol nach Westen
%            1…9 Ziegelstapel        ^  Karol nach Norden (abgewandt)
%            M  Marke                v  Karol nach Süden (zugewandt)
%            Q  Quader / Wand
%  Mehrere Zeichen auf einem Feld werden geklammert: {2M} = zwei Ziegel
%  mit einer Marke obenauf.  Die Zeichen eines Feldes werden von unten
%  nach oben abgearbeitet.
% ---------------------------------------------------------------------------
\int_new:N   \l__karol_i_int        % aktueller Feldindex (0-basiert)
\int_new:N   \l__karol_n_int        % Feldanzahl
\tl_new:N    \l__karol_skalierung_tl
\tl_new:N    \l__karol_hinweis_tl
\tl_new:N    \l__karol_titel_tl
\bool_new:N  \l__karol_nummern_bool
\bool_new:N  \l__karol_rahmen_bool
\int_new:N   \l__karol_zhoehe_int   % Raumhöhe des optionalen Rahmens
\clist_new:N \l__karol_hervor_clist

\keys_define:nn { karol / welt }
{
	skalierung  .tl_set:N    = \l__karol_skalierung_tl,
	skalierung  .initial:n   = { 1 },
	nummern     .bool_set:N  = \l__karol_nummern_bool,
	nummern     .default:n   = true,
	nummern     .initial:n   = true,
	hinweis     .tl_set:N    = \l__karol_hinweis_tl,
	hinweis     .initial:n   = { },
	titel       .tl_set:N    = \l__karol_titel_tl,
	titel       .initial:n   = { },
	hervorheben .clist_set:N = \l__karol_hervor_clist,
	hervorheben .initial:n   = { },
	% Raumrahmen wie in der karolwelt-Umgebung.  Für eine einzelne Reihe
	% ist er meist zu wuchtig (und in Legenden störend), deshalb aus.
	rahmen      .bool_set:N  = \l__karol_rahmen_bool,
	rahmen      .default:n   = true,
	rahmen      .initial:n   = false,
	hoehe       .int_set:N   = \l__karol_zhoehe_int,
	hoehe       .initial:n   = { 3 },
}

% --- Feldnummern ------------------------------------------------------------
% Die Nummern stehen VOR der vordersten Gitterkante, nicht auf dem Feld:
% so verdeckt sie kein Ziegelstapel und die Zuordnung Zahl <-> Feld bleibt
% auch in vollen Welten eindeutig.
%
% Abstand der Ziffernoberkante von der Gitterkante, in Feldbreiten.  Er
% muss den Hervorhebungsrahmen mit einschließen: der liegt 0.06 unterhalb
% der Zelle (\__karol_zhervor:n) und ist 1.2pt stark, reicht also gut
% 0.066 Feldbreiten unter die Kante.  Sonst berührt die Ziffer ihn.
\fp_const:Nn \c__karol_znummer_fp { 0.15 }
\tl_new:N \l__karol_znfarbe_tl
\cs_new_protected:Npn \__karol_znummer:n #1        % #1 = Feldindex
{
	% Hervorgehobene Felder bekommen auch eine rote Nummer — der rote
	% Bodenrahmen kann von hohen Stapeln verdeckt sein.
	\clist_if_in:NnTF \l__karol_hervor_clist {#1}
		{ \tl_set:Nn \l__karol_znfarbe_tl { karolWHervor } }
		{ \tl_set:Nn \l__karol_znfarbe_tl { karolWGitter } }
	\node [ font=\__karol_textfont:\bfseries\tiny, inner~sep=0pt, anchor=north,
	        text=\l__karol_znfarbe_tl ]
		at ( { (#1) + 0.5 },
		     { -\fp_use:N \c__karol_znummer_fp } ) { \int_eval:n {#1} };
}

% --- Hervorhebung einzelner Felder -----------------------------------------
% Rotes Feldparallelogramm auf dem Boden (wird vor den Objekten gezeichnet,
% die Objekte stehen also darauf).
\cs_new_protected:Npn \__karol_zhervor:n #1
{
	% Der Rahmen liegt 0.06 Feldbreiten außerhalb der Zelle: so bleibt er
	% auch dann sichtbar, wenn ein Ziegelstapel oder Quader auf dem Feld
	% steht (die Objekte füllen die Zelle randlos aus).
	\draw [ karolWHervor, line~width=1.2pt, line~join=round ]
		( \__karol_wpx:nn { #1 - 0.06 }     { -0.06 } ,
		  \__karol_wpy:nnn { 0 } { -0.06 } { 0 } )
		-- ( \__karol_wpx:nn { #1 + 1.06 }  { -0.06 } ,
		     \__karol_wpy:nnn { 0 } { -0.06 } { 0 } )
		-- ( \__karol_wpx:nn { #1 + 1.06 }  { 1.06 } ,
		     \__karol_wpy:nnn { 0 } { 1.06 } { 0 } )
		-- ( \__karol_wpx:nn { #1 - 0.06 }  { 1.06 } ,
		     \__karol_wpy:nnn { 0 } { 1.06 } { 0 } )
		-- cycle;
}

% --- Zellinhalt: ein Element ------------------------------------------------
% Alle Elemente sitzen auf dem Höhencursor \l__karol_wz_fp (in Ziegellagen)
% und schieben ihn — genau wie bisher — um ihre eigene Bauhöhe weiter.
\cs_new_protected:Npn \__karol_zziegel:
{
	\__karol_wziegel_iiid:nnn { \l__karol_i_int + 1 } { 1 }
		{ \fp_use:N \l__karol_wz_fp }
	\fp_add:Nn \l__karol_wz_fp { 1 }
}
\cs_new_protected:Npn \__karol_zstapel:n #1
	{ \prg_replicate:nn {#1} { \__karol_zziegel: } }
\cs_new_protected:Npn \__karol_zquader:
{
	\__karol_wquader_iiid:nnn { \l__karol_i_int + 1 } { 1 }
		{ \fp_use:N \l__karol_wz_fp }
	\fp_add:Nn \l__karol_wz_fp { \c__karol_wqh_fp / \c__karol_wzh_fp }
}
% Die Marke ist — wie in der karolwelt-Umgebung — eine flache Platte ohne
% Bauhöhe; sie schiebt den Cursor daher nicht weiter.  Karol steht auf ihr.
\cs_new_protected:Npn \__karol_zmarke:
	{ \__karol_wmarke_iiid:nnn { \l__karol_i_int + 1 } { 1 } { karolWMarke } }
\cs_new_protected:Npn \__karol_zfigur:n #1
{
	\tl_set:Nn \l__karol_wkdir_tl {#1}
	\__karol_wavatar_iiid:nn { \l__karol_i_int + 1 } { 1 }
}

\cs_new_protected:Npn \__karol_element:n #1
{
	\str_case:nnF {#1}
	{
		{ . } { }
		{ > } { \__karol_zfigur:n { o } }
		{ < } { \__karol_zfigur:n { w } }
		{ ^ } { \__karol_zfigur:n { n } }
		{ v } { \__karol_zfigur:n { s } }
		{ Z } { \__karol_zziegel: }
		{ 1 } { \__karol_zstapel:n { 1 } }
		{ 2 } { \__karol_zstapel:n { 2 } }
		{ 3 } { \__karol_zstapel:n { 3 } }
		{ 4 } { \__karol_zstapel:n { 4 } }
		{ 5 } { \__karol_zstapel:n { 5 } }
		{ 6 } { \__karol_zstapel:n { 6 } }
		{ 7 } { \__karol_zstapel:n { 7 } }
		{ 8 } { \__karol_zstapel:n { 8 } }
		{ 9 } { \__karol_zstapel:n { 9 } }
		{ M } { \__karol_zmarke: }
		{ Q } { \__karol_zquader: }
	}
	{
		\msg_warning:nnn { karol } { unbekanntes-zeichen } {#1}
	}
}
\msg_new:nnn { karol } { unbekanntes-zeichen }
	{ Unbekanntes~Weltzeichen~"#1"~-~wird~ignoriert. }

% --- Zelle: Inhaltsstapel über dem Bodenfeld -------------------------------
\cs_new_protected:Npn \__karol_zelle:n #1
{
	\fp_zero:N \l__karol_wz_fp
	\tl_map_function:nN {#1} \__karol_element:n
	\int_incr:N \l__karol_i_int
}

% ---------------------------------------------------------------------------
%  \karolzeile[keys]{Zeichenkette}
% ---------------------------------------------------------------------------
\NewDocumentCommand { \karolzeile } { O{} m }
{
	\group_begin:
		\keys_set:nn { karol / welt } { #1 }
		\__karol_farbschema:
		\int_set:Nn \l__karol_n_int { \tl_count:n { #2 } }
		\int_zero:N \l__karol_i_int
		% Eine Zeilen-Welt ist eine karolwelt mit genau einer Reihe:
		% damit rechnen die gemeinsamen Bausteine korrekt.
		\int_set_eq:NN \l__karol_wbreite_int \l__karol_n_int
		\int_set:Nn    \l__karol_wlaenge_int { 1 }
		\int_set_eq:NN \l__karol_whoehe_int  \l__karol_zhoehe_int
		\mode_leave_vertical:
		\begin{tabular} [b] {@{}c@{}}
			\tl_if_empty:NF \l__karol_titel_tl
				{ { \footnotesize \l__karol_titel_tl } \\[2pt] }
			\scalebox { \l__karol_skalierung_tl }
			{
				\begin{tikzpicture}
					[ x = \g__karol_feld_dim, y = \g__karol_feld_dim,
					  baseline = 0pt ]
					% unsichtbare Mindesthöhe, damit flache Welten im Paar
					% nicht „springen":
					\path (0, 0) -- ( 0,
						{ \fp_eval:n { \c__karol_wdy_fp
						               + 1.5 * \c__karol_wzh_fp } } );
					\__karol_wboden_gitter:
					\bool_if:NT \l__karol_rahmen_bool { \__karol_wrahmen: }
					\clist_map_function:NN \l__karol_hervor_clist
						\__karol_zhervor:n
					\tl_map_function:nN { #2 } \__karol_zelle:n
					\bool_if:NT \l__karol_nummern_bool
					{
						\int_step_inline:nnn { 0 } { \l__karol_n_int - 1 }
							{ \__karol_znummer:n { ##1 } }
					}
				\end{tikzpicture}
			}
			\tl_if_empty:NF \l__karol_hinweis_tl
				{ \\[3pt] { \footnotesize \itshape \l__karol_hinweis_tl } }
		\end{tabular}
	\group_end:
}

% ---------------------------------------------------------------------------
%  \karolgitter[keys]{Anzahl Felder} — leere Felder zum Eintragen
% ---------------------------------------------------------------------------
\tl_new:N \l__karol_opt_tl
\NewDocumentCommand { \karolgitter } { O{} m }
{
	\group_begin:
		\tl_set:Nn \l__karol_opt_tl { #1 }
		\tl_set:Nx \l__karol_tmp_tl { \prg_replicate:nn { #2 } { . } }
		\use:x
		{
			\exp_not:N \karolzeile [ \exp_not:V \l__karol_opt_tl ]
				{ \exp_not:V \l__karol_tmp_tl }
		}
	\group_end:
}


\ExplSyntaxOff

% ---------------------------------------------------------------------------
%  \karolpaar[gemeinsame keys][keys nur für „nachher"]{vorher}{nachher}
% ---------------------------------------------------------------------------
\providecommand{\faArrowRight}{\ensuremath{\rightarrow}}
\NewDocumentCommand{\karolpaar}{ O{} O{} m m }{%
	\begin{center}
		\begin{tabular}[b]{@{}c@{\hspace{1.1em}}c@{\hspace{1.1em}}c@{}}
			\karolzeile[#1, titel={\textbf{vorher}}]{#3} &
			\raisebox{1.4em}{\large\faArrowRight} &
			\karolzeile[#1, titel={\textbf{nachher}}, #2]{#4} \\
		\end{tabular}
	\end{center}%
}

% ---------------------------------------------------------------------------
%  Legende der Weltsymbole
% ---------------------------------------------------------------------------
% Ein Legendeneintrag: \karollegendenitem{Weltstring}{Beschriftung}
% Die Einträge sind bewusst klein skaliert: im Schrägbild der karolwelt
% braucht ein Feld mehr Fläche als in der früheren flachen Darstellung.
\newcommand{\karollegendenitem}[2]{%
	\mbox{\karolzeile[nummern=false, skalierung=0.42]{#1}\,#2}%
}
% Trenner mit viel Dehnung und leichtem Umbruchanreiz: die Legende ist
% breit und soll notfalls über mehrere Zeilen laufen statt überzustehen.
\newcommand{\karollegendentrenner}{%
	\penalty0\hskip 0.9em plus 2.5em minus .25em\relax}
\newcommand{\karollegende}{%
	\karollegendenitem{>}{Karol (Blickrichtung)}\karollegendentrenner
	\karollegendenitem{Z}{Ziegel}\karollegendentrenner
	\karollegendenitem{3}{Ziegelstapel}\karollegendentrenner
	\karollegendenitem{M}{Marke}\karollegendentrenner
	\karollegendenitem{{M>}}{Karol auf Marke}\karollegendentrenner
	\karollegendenitem{Q}{Quader\,/\,Wand}%
}

% ===========================================================================
%  5. Karol-Blöcke  (Blockprogrammierung wie in Robot Karol Online)
% ===========================================================================
%  Verwendung:
%      \begin{karolbloecke}[skalierung=1]
%          \bwiederholemal{5}{%
%              \bbefehl{Schritt}
%              \bbefehl{MarkeSetzen}}
%      \end{karolbloecke}
%
%      \bwiederholesolange{NichtIstWand}{…}
%      \bwenndann{IstZiegel}{…}
%      \bwenndannsonst{IstZiegel}{…}{…}
%      \bwiederholeimmer{…}
%      \bluecke[7em]              — leerer „Geisterblock" zum Ausfüllen
%      \bbedingung{IstZiegel}     — Sechseck, auch im Fließtext nutzbar
% ---------------------------------------------------------------------------
\ExplSyntaxOn

% --- Maße (bei jedem Rendern aus dem aktuellen em berechnet) ---------------
\dim_new:N \l__karol_bnd_dim    % Nasen-/Kerbentiefe
\dim_new:N \l__karol_bnx_dim    % Kerbe: Beginn
\dim_new:N \l__karol_bns_dim    % Kerbe: Schräge
\dim_new:N \l__karol_bnf_dim    % Kerbe: gerades Stück
\dim_new:N \l__karol_bpx_dim    % Innenabstand links/rechts
\dim_new:N \l__karol_bpt_dim    % Innenabstand oben (unter der Kerbe)
\dim_new:N \l__karol_bpb_dim    % Innenabstand unten
\dim_new:N \l__karol_bmw_dim    % Mindestbreite
\dim_new:N \l__karol_barm_dim   % Armbreite der C-Blöcke
\dim_new:N \l__karol_bfut_dim   % Fußhöhe der C-Blöcke
\dim_new:N \l__karol_bmm_dim    % Mindesthöhe eines Mauls
\dim_new:N \l__karol_bbase_dim  % Absenkung für die Grundlinienausrichtung
\tl_new:N  \l__karol_brc_tl     % Eckenradius (für TikZ-Schlüssel)
\dim_new:N \l__karol_brc_dim   % derselbe Wert zum Rechnen
\dim_new:N \l__karol_bkw_dim    % Strichstärke der Blockumrandung

% Inline-Modus: Blöcke im Fließtext.  Sie werden in der Schriftgröße des
% umgebenden Textes gesetzt, auf dessen Grundlinie ausgerichtet und mit
% kleineren „Verzierungen" (Kerbe, Nase, Innenabstände) versehen, damit
% ihre Höhe der Zeilenhöhe nahekommt.  Mehrzeilige Blöcke (C-/E-Blöcke)
% erscheinen inline nur mit ihrem Kopf.
\bool_new:N \l__karol_binline_bool   % aktuelle Ausgabe ist ein Inline-Block
\bool_new:N \l__karol_bimbau_bool    % wir stecken bereits in einem Block

\cs_new_protected:Npn \__karol_bdims:
{
	\bool_if:NTF \l__karol_binline_bool
	{	% --- Inline: flachere Kerbe, knappe Innenabstände, keine Mindestbreite
		\dim_set:Nn \l__karol_bnd_dim  { 0.12em }
		\dim_set:Nn \l__karol_bnx_dim  { 0.50em }
		\dim_set:Nn \l__karol_bns_dim  { 0.14em }
		\dim_set:Nn \l__karol_bnf_dim  { 0.45em }
		\dim_set:Nn \l__karol_bpx_dim  { 0.40em }
		\dim_set:Nn \l__karol_bpt_dim  { 0.08em }
		\dim_set:Nn \l__karol_bpb_dim  { 0.10em }
		\dim_zero:N \l__karol_bmw_dim
		\dim_set:Nn \l__karol_barm_dim { 0.70em }
		\dim_set:Nn \l__karol_bfut_dim { 0.40em }
		\dim_set:Nn \l__karol_bmm_dim  { 0.90em }
		\dim_set:Nn \l__karol_brc_dim  { 1.6pt }
	}
	{
		\dim_set:Nn \l__karol_bnd_dim  { 0.28em }
		\dim_set:Nn \l__karol_bnx_dim  { 0.75em }
		\dim_set:Nn \l__karol_bns_dim  { 0.26em }
		\dim_set:Nn \l__karol_bnf_dim  { 0.80em }
		\dim_set:Nn \l__karol_bpx_dim  { 0.60em }
		\dim_set:Nn \l__karol_bpt_dim  { 0.26em }
		\dim_set:Nn \l__karol_bpb_dim  { 0.30em }
		\dim_set:Nn \l__karol_bmw_dim  { 5.4em }
		\dim_set:Nn \l__karol_barm_dim { 1.05em }
		\dim_set:Nn \l__karol_bfut_dim { 0.62em }
		\dim_set:Nn \l__karol_bmm_dim  { 1.30em }
		\dim_set:Nn \l__karol_brc_dim  { 2pt }
	}
	\tl_set:Nx \l__karol_brc_tl { \dim_use:N \l__karol_brc_dim }
	% Umrandung: in der Oberfläche 3 px bei 110 px Blockhöhe, also gut
	% 2.7 % — bei den Maßen dieses Pakets rund ein halbes Punkt.  In em,
	% damit sie inline mitschrumpft.
	\dim_set:Nn \l__karol_bkw_dim { 0.045em }
	% Alle Blöcke werden jetzt umrandet, also brauchen ALLE die scharfen
	% Ecken an Kerbe und Nase: dort sind die Segmente kürzer als zwei
	% Eckradien, und pgf macht daraus sonst Spitzen (siehe \bluecke).
	\tl_set:Nn \l__karol_bkerbe_tl { [ sharp~corners ] }
	\tl_set:Nx \l__karol_bkante_tl
		{ [ rounded~corners = \l__karol_brc_tl ] }
}
% Farbe der Blockbeschriftung: weiß auf den kräftigen Flächen der
% Farbfassung, schwarz auf den hellen Graustufen des Druckmodus.
\cs_new_protected:Npn \__karol_bschriftfarbe:
{
	\bool_if:NTF \l__karol_druck_bool
		{ \color{black} } { \color{white} }
}
% Blockschrift: im Stapel \footnotesize, inline die Größe des Fließtextes
% Beschriftungen sind in Robot Karol Online normal gesetzt, nicht fett.
\cs_new_protected:Npn \__karol_blockfont:
{
	\__karol_textfont:
	\bool_if:NF \l__karol_binline_bool { \footnotesize }
}
% Höhenphantom der Beschriftung.  Im Stapel großzügig (Umlaut + Klammer),
% damit alle Blöcke exakt gleich hoch werden; inline knapp (Versalhöhe +
% Unterlänge), damit der Block die Zeilenhöhe nicht unnötig aufbläht.
\cs_new_protected:Npn \__karol_bphantom:
{
	\bool_if:NTF \l__karol_binline_bool
		{ \vphantom { Ag } } { \vphantom { Äg( } }
}
% Flachere Fassung für Bedingungsplättchen und Pillen: sie sitzen INNERHALB
% eines Blocks und dürfen ihn nicht unnötig aufblähen (keine Klammertiefe).
\cs_new_protected:Npn \__karol_bphantom_flach:
{
	\bool_if:NTF \l__karol_binline_bool
		{ \vphantom { Ag } } { \vphantom { Äg } }
}

% Beginn eines Nutzerblocks: Inline-Modus bestimmen und merken, dass wir
% ab jetzt im Blockbau sind (verschachtelte \bbedingung/Pillen erben ihn).
\cs_new_protected:Npn \__karol_bstart:
{
	\bool_if:NF \l__karol_bimbau_bool
	{
		\mode_if_horizontal:TF
			{ \bool_set_true:N  \l__karol_binline_bool }
			{ \bool_set_false:N \l__karol_binline_bool }
	}
	\bool_set_true:N \l__karol_bimbau_bool
}
% Wie \__karol_bstart:, aber erzwingt den Inline-Modus (\i…-Kommandos)
\cs_new_protected:Npn \__karol_bstart_inline:
{
	\bool_if:NF \l__karol_bimbau_bool
		{ \bool_set_true:N \l__karol_binline_bool }
	\bool_set_true:N \l__karol_bimbau_bool
	\mode_leave_vertical:
}

% Block ausgeben: im Stapel als bündige Zeile; inline so abgesenkt, dass
% die Textgrundlinie des Blocks auf der Grundlinie des Fließtextes liegt.
% (Die Bild-Grundlinie liegt an der Blockunterkante, ht = #2, dp = 0; die
% Beschriftung sitzt um Textunterlänge + Fußabstand darüber.)
% #1 = Inhalt, #2 = Gesamthöhen-Variable
\cs_new_protected:Npn \__karol_bemit:nN #1#2
{
	\bool_if:NTF \l__karol_binline_bool
	{
		\mode_leave_vertical:
		\raisebox { -\l__karol_bbase_dim } { \hbox:n { #1 } }
	}
	{ \hbox:n { #1 } }
}

% Arbeitsregister
\box_new:N \l__karol_btxt_box
\box_new:N \l__karol_bbody_box
\box_new:N \l__karol_belse_box
\box_new:N \l__karol_bsonst_box
\dim_new:N \l__karol_bw_dim     % Blockbreite
\dim_new:N \l__karol_bh_dim     % Kopfhöhe
\dim_new:N \l__karol_bm_dim     % Maulhöhe
\dim_new:N \l__karol_bmzwei_dim    % Maulhöhe (sonst-Zweig)
\dim_new:N \l__karol_bs_dim     % Höhe des sonst-Balkens
\dim_new:N \l__karol_bt_dim     % Gesamthöhe
\dim_new:N \l__karol_bbw_dim    % Breite der Gesamt-Boundingbox

% --- Stapel-Umgebung -------------------------------------------------------
% Innerhalb des Stapels: Kästen bündig ohne Zwischenraum übereinander.
\cs_new_protected:Npn \__karol_stapelmodus:
{
	% Im Stapel wird nie inline gesetzt — auch nicht, wenn der umgebende
	% Block inline war.  Damit erben Rümpfe stets die Stapel-Optik.
	\bool_set_false:N \l__karol_binline_bool
	\bool_set_false:N \l__karol_bimbau_bool
	\skip_zero:N \parskip
	\dim_zero:N \parindent
	\skip_set:Nn \baselineskip { 0pt }
	\dim_zero:N \lineskip
	\dim_set:Nn \lineskiplimit { \maxdimen }
}
\tl_new:N \l__karol_bskal_tl
\keys_define:nn { karol / bloecke }
{
	skalierung .tl_set:N  = \l__karol_bskal_tl,
	skalierung .initial:n = { 1 },
}
\box_new:N \l__karol_bstapel_box
\NewDocumentEnvironment { karolbloecke } { O{} }
{
	\keys_set:nn { karol / bloecke } { #1 }
	\vbox_set:Nw \l__karol_bstapel_box
	\__karol_stapelmodus:
}
{
	\vbox_set_end:
	\mode_leave_vertical:
	\hbox:n
	{
		\exp_args:NV \scalebox \l__karol_bskal_tl
			{ \box_use:N \l__karol_bstapel_box }
	}
}

% --- Kerbenpfade -----------------------------------------------------------
% Eckenmodus an Kerbe und Nase.  Gefüllte Blöcke zeichnen die Kontur nicht
% (draw=none), dort bleiben beide Marken leer und der Pfad ist unverändert.
% Der Geisterblock \bluecke STRICHELT die Kontur — und dort sind die
% Kerbensegmente kürzer als zwei Eckradien: inline ist die Schräge rund
% 2.0pt lang, zwei Radien à 1.6pt brauchen 3.2pt.  pgf erzeugt daraus einen
% entarteten Bogen, der als Spitze aus der Kontur schießt.  Deshalb setzt
% \bluecke an Kerbe und Nase auf scharfe Ecken und rundet nur die vier
% Außenecken.
\tl_new:N \l__karol_bkerbe_tl   % vor der Kerbe/Nase einzufügen
\tl_new:N \l__karol_bkante_tl   % danach wieder einzufügen
\dim_new:N \l__karol_bstr_dim   % Geisterblock: Strichlänge
\dim_new:N \l__karol_bluk_dim   % Geisterblock: Lückenlänge
\tl_new:N  \l__karol_bluf_tl    % Geisterblock: Füllfarbe

% Obere Kante mit Kerbe, von (0,0) bis (W,0).  Der Rumpf ohne den
% Startpunkt wird auch von der hellen Kante gebraucht, die schon an der
% linken Seite beginnt.
\cs_new:Npn \__karol_pfad_oben:
	{ (0pt, 0pt) \__karol_pfad_oben_ab: }
\cs_new:Npn \__karol_pfad_oben_ab:
{
	\l__karol_bkerbe_tl
	-- ( \dim_use:N \l__karol_bnx_dim , 0pt )
	-- ( \dim_eval:n { \l__karol_bnx_dim + \l__karol_bns_dim },
	     -\dim_use:N \l__karol_bnd_dim )
	-- ( \dim_eval:n { \l__karol_bnx_dim + \l__karol_bns_dim + \l__karol_bnf_dim },
	     -\dim_use:N \l__karol_bnd_dim )
	-- ( \dim_eval:n { \l__karol_bnx_dim + 2\l__karol_bns_dim + \l__karol_bnf_dim },
	     0pt )
	\l__karol_bkante_tl
	-- ( \dim_use:N \l__karol_bw_dim , 0pt )
}
% Untere Kante mit Nase, von (W,-#1) zurück bis (0,-#1):
\cs_new:Npn \__karol_pfad_unten:n #1
{
	-- ( \dim_use:N \l__karol_bw_dim , \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkerbe_tl
	-- ( \dim_eval:n { \l__karol_bnx_dim + 2\l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) } )
	-- ( \dim_eval:n { \l__karol_bnx_dim + \l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_eval:n { \l__karol_bnx_dim + \l__karol_bns_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_use:N \l__karol_bnx_dim , \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkante_tl
	-- ( 0pt , \dim_eval:n { 0pt-(#1) } )
	-- cycle
}
% Maulöffnung an der Kopf-/Balkenunterkante bei y = -#1
% (von rechts nach links; Nase zeigt ins Maul):
\cs_new:Npn \__karol_pfad_maul_auf:n #1
{
	-- ( \dim_use:N \l__karol_bw_dim , \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkerbe_tl
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + 2\l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + \l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + \l__karol_bns_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim },
	     \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkante_tl
	-- ( \dim_use:N \l__karol_barm_dim , \dim_eval:n { 0pt-(#1) } )
}
% Maulschluss an der Fuß-/Balkenoberkante bei y = -#1
% (von links nach rechts; Kerbe nimmt die Nase des letzten Blocks auf):
\cs_new:Npn \__karol_pfad_maul_zu:n #1
{
	-- ( \dim_use:N \l__karol_barm_dim , \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkerbe_tl
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim },
	     \dim_eval:n { 0pt-(#1) } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + \l__karol_bns_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + \l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim } )
	-- ( \dim_eval:n { \l__karol_barm_dim + \l__karol_bnx_dim
	                   + 2\l__karol_bns_dim + \l__karol_bnf_dim },
	     \dim_eval:n { 0pt-(#1) } )
	\l__karol_bkante_tl
	-- ( \dim_use:N \l__karol_bw_dim , \dim_eval:n { 0pt-(#1) } )
}

% --- Kopftext ausmessen, Breite/Kopfhöhe setzen ----------------------------
\cs_new_protected:Npn \__karol_bmess:n #1
{
	\__karol_bdims:
	\hbox_set:Nn \l__karol_btxt_box
		{ \__karol_blockfont: \__karol_bschriftfarbe: \__karol_bphantom: #1 }
	\dim_set:Nn \l__karol_bw_dim
		{ \box_wd:N \l__karol_btxt_box + 2 \l__karol_bpx_dim }
	\dim_compare:nT { \l__karol_bw_dim < \l__karol_bmw_dim }
		{ \dim_set_eq:NN \l__karol_bw_dim \l__karol_bmw_dim }
	\dim_set:Nn \l__karol_bh_dim
	{
		\l__karol_bnd_dim + \l__karol_bpt_dim
		+ \box_ht:N \l__karol_btxt_box + \box_dp:N \l__karol_btxt_box
		+ \l__karol_bpb_dim
	}
	\__karol_bbase_setzen:
}
% Abstand von der Unterkante der Boundingbox (= Bild-Grundlinie) hinauf zur
% Textgrundlinie im Block.  \raisebox{-\l__karol_bbase_dim} legt beide
% Grundlinien im Fließtext exakt übereinander.  Inline zählt die Nase zur
% Boundingbox (siehe \__karol_bbbox:N) und damit auch hier mit.
\cs_new_protected:Npn \__karol_bbase_setzen:
{
	\dim_set:Nn \l__karol_bbase_dim
		{ \box_dp:N \l__karol_btxt_box + \l__karol_bpb_dim }
	\bool_if:NT \l__karol_binline_bool
		{ \dim_add:Nn \l__karol_bbase_dim { \l__karol_bnd_dim } }
}
% Untere Kante der Boundingbox eines Blocks der Höhe #1.  Im Stapel endet
% sie an der Blockunterkante (die Nase ragt heraus und greift in den
% Folgeblock); inline zählt die Nase mit, damit TeX den Zeilenabstand
% richtig bemisst und nichts in die nächste Zeile ragt.
\cs_new:Npn \__karol_bbbox:N #1
{
	\bool_if:NTF \l__karol_binline_bool
		{ \dim_eval:n { 0pt - #1 - \l__karol_bnd_dim } }
		{ \dim_eval:n { 0pt - #1 } }
}
% Kopftext in den Block setzen (Grundlinie unterhalb der Kerbe)
% Achtung: die Box MUSS vor dem tikzpicture gefüllt werden (nullfont!)
\cs_new_protected:Npn \__karol_btxt_setzen:n #1
	{ \__karol_btext_at:nN {#1} \l__karol_btxt_box }
\cs_new_protected:Npn \__karol_btext_at:nN #1#2  % #1 = y-Offset, #2 = Box
{
	\node [ anchor=base~west, inner~sep=0pt ]
		at ( \dim_use:N \l__karol_bpx_dim ,
		     \dim_eval:n { 0pt-(#1) - \l__karol_bnd_dim - \l__karol_bpt_dim
		                   - \box_ht:N #2 } )
		{ \box_use:N #2 };
}

% --- Blockfläche mit Anschliff ---------------------------------------------
% Robot Karol Online setzt einen gewöhnlichen 3D-Anschliff ein: jede nach
% oben oder links zeigende Kante ist aufgehellt, jede nach unten oder
% rechts zeigende abgedunkelt.  Wichtig daran ist vor allem, dass
% gleichfarbige Blöcke voneinander abgesetzt sind — ein grüner wenn-Block
% in einer grünen Schleife wäre sonst nicht zu erkennen.
%
% Statt die Kontur in helle und dunkle Teilstücke zu zerlegen (fehler-
% anfällig an Ecken, Kerbe, Nase und Maul), wird der Effekt so erzeugt,
% wie er physikalisch entsteht:
%
%   1. Auf die Fläche BESCHNEIDEN.  Dadurch liegt jeder Strich vollständig
%      INNERHALB des Blocks — Blöcke können sich nicht mehr überlagern und
%      passen exakt ineinander.
%   2. Den kompletten Rand hell zeichnen.
%   3. Denselben Rand dunkel darüber, um eine Strichstärke nach links oben
%      versetzt: an nach unten und rechts zeigenden Kanten rutscht er ins
%      Innere und deckt das Helle ab, an nach oben und links zeigenden aus
%      der Fläche heraus, wo der Beschnitt ihn entfernt.  Schräge Kanten
%      bekommen automatisch den passenden Zwischenwert.
%
% Weil beide Striche denselben Pfad benutzen, folgen sie der Blockform in
% jedem Detail — Rundungen, Kerbe, Nase und Maulöffnungen inbegriffen.
% Die doppelte Strichstärke ist Absicht: der Beschnitt halbiert sie.
%   #1 = Blockfarbe, #2 = geschlossener Pfad der Blockform
\cs_new_protected:Npn \__karol_brand:nn #1#2
{
	\bool_if:NTF \l__karol_druck_bool
		{ \__karol_brand_druck:nn {#1} {#2} }
		{ \__karol_brand_farbe:nn {#1} {#2} }
}
% Druckmodus: an die Stelle des Anschliffs tritt eine schwarze Kontur.
% Die Blockfarben sind hier helle Graustufen — aus ihnen ließe sich kein
% sichtbarer Hell-Dunkel-Verlauf mehr gewinnen, wohl aber verlöre ein
% weißer Block ohne Kontur jede Form.  Beschnitten wird wie beim
% Anschliff, damit die Kontur ganz INNERHALB des Blocks liegt und
% gestapelte Blöcke sich nicht gegenseitig überzeichnen; die doppelte
% Strichstärke gleicht die Halbierung durch den Beschnitt aus.
%
% Die Fläche wird hier NICHT nach außen überzeichnet (in der Farbfassung
% 0.1pt gegen den Antialiasing-Saum): eine weiße Blockfläche würde sonst
% die Kontur des darüberliegenden Blocks anknabbern.  Nötig ist die
% Überzeichnung im Druck auch nicht — die Kontur deckt die Fuge ab.
\cs_new_protected:Npn \__karol_brand_druck:nn #1#2
{
	\fill [ #1, line~join = round, rounded~corners = \l__karol_brc_tl ]
		#2 ;
	\begin{scope}
		\clip [ rounded~corners = \l__karol_brc_tl ] #2 ;
		\draw [ draw = karolDruckKontur,
		        line~width = \dim_eval:n { 2.6 \l__karol_bkw_dim },
		        line~join = round, rounded~corners = \l__karol_brc_tl ]
			#2 ;
	\end{scope}
}
\cs_new_protected:Npn \__karol_brand_farbe:nn #1#2
{
	% Die Fläche einen Hauch überzeichnen (0.1pt nach außen).  Zwei exakt
	% aneinanderstoßende Flächen hinterlassen sonst einen hellen
	% Antialiasing-Saum — sichtbar dort, wo die Nase eines Blocks in der
	% Kerbe des nächsten sitzt.  Der Anschliff bleibt davon unberührt, er
	% wird auf den ungewachsenen Pfad beschnitten.
	\draw [ fill = #1, draw = #1, line~width = 0.2pt,
	        line~join = round, rounded~corners = \l__karol_brc_tl ]
		#2 ;
	\begin{scope}
		\clip [ rounded~corners = \l__karol_brc_tl ] #2 ;
		\draw [ fill = #1, draw = #1!70!white,
		        line~width = \dim_eval:n { 2 \l__karol_bkw_dim },
		        line~join = round, rounded~corners = \l__karol_brc_tl ]
			#2 ;
		\draw [ draw = #1!80!black,
		        line~width = \dim_eval:n { 2 \l__karol_bkw_dim },
		        line~join = round, rounded~corners = \l__karol_brc_tl,
		        xshift = -\dim_use:N \l__karol_bkw_dim,
		        yshift = \dim_use:N \l__karol_bkw_dim ]
			#2 ;
	\end{scope}
}

% --- Einfacher Anweisungsblock ---------------------------------------------
% #1 = Farbname   #2 = Beschriftung
\cs_new_protected:Npn \__karol_bblock:nn #1#2
{
	\tl_set:Nn \l__karol_bfarbe_tl {#1}
	\__karol_bmess:n {#2}
	\__karol_bemit:nN
	{
		\begin{tikzpicture}
			\useasboundingbox (0pt, 0pt) rectangle
				( \dim_use:N \l__karol_bw_dim ,
				  \__karol_bbbox:N \l__karol_bh_dim );
			\__karol_brand:nn {#1}
				{ \__karol_pfad_oben:
				  \__karol_pfad_unten:n \l__karol_bh_dim }
			\__karol_btxt_setzen:n { 0pt }
		\end{tikzpicture}
	}
	\l__karol_bh_dim
}

% --- C-Block (eine Maulöffnung) --------------------------------------------
% #1 = Farbname   #2 = Kopfbeschriftung   #3 = Rumpf
\cs_new_protected:Npn \__karol_bcblock:nnn #1#2#3
{
	% Inline gibt es keine Maulöffnung: nur der Kopf wird gesetzt.
	\bool_if:NTF \l__karol_binline_bool
		{ \__karol_bblock:nn      {#1} {#2}      }
		{ \__karol_bcblock_voll:nnn {#1} {#2} {#3} }
}
\cs_new_protected:Npn \__karol_bcblock_voll:nnn #1#2#3
{
	\tl_set:Nn \l__karol_bfarbe_tl {#1}
	\vbox_set:Nn \l__karol_bbody_box { \__karol_stapelmodus: #3 }
	\__karol_bmess:n {#2}
	\dim_set:Nn \l__karol_bm_dim
		{ \box_ht:N \l__karol_bbody_box + \box_dp:N \l__karol_bbody_box }
	\dim_compare:nT { \l__karol_bm_dim < \l__karol_bmm_dim }
		{ \dim_set_eq:NN \l__karol_bm_dim \l__karol_bmm_dim }
	\dim_set:Nn \l__karol_bt_dim
		{ \l__karol_bh_dim + \l__karol_bm_dim + \l__karol_bfut_dim }
	\dim_set:Nn \l__karol_bbw_dim
		{ \l__karol_barm_dim + \box_wd:N \l__karol_bbody_box + 0.4em }
	\dim_compare:nT { \l__karol_bbw_dim < \l__karol_bw_dim }
		{ \dim_set_eq:NN \l__karol_bbw_dim \l__karol_bw_dim }
	\__karol_bemit:nN
	{
		\begin{tikzpicture}
			\useasboundingbox (0pt, 0pt) rectangle
				( \dim_use:N \l__karol_bbw_dim , -\dim_use:N \l__karol_bt_dim );
			\__karol_brand:nn {#1}
			{
				\__karol_pfad_oben:
				\__karol_pfad_maul_auf:n \l__karol_bh_dim
				-- ( \dim_use:N \l__karol_barm_dim ,
				     \dim_eval:n { - \l__karol_bh_dim - \l__karol_bm_dim } )
				\__karol_pfad_maul_zu:n
					{ \dim_eval:n { \l__karol_bh_dim + \l__karol_bm_dim } }
				\__karol_pfad_unten:n \l__karol_bt_dim
			}
			\__karol_btxt_setzen:n { 0pt }
			\node [ anchor=north~west, inner~sep=0pt ]
				at ( \dim_use:N \l__karol_barm_dim ,
				     -\dim_use:N \l__karol_bh_dim )
				{ \box_use:N \l__karol_bbody_box };
		\end{tikzpicture}
	}
	\l__karol_bt_dim
}

% Mittelteil des E-Blocks (beide Mäuler und der sonst-Balken).  Fläche und
% dunkle Kante brauchen ihn gleichermaßen.
\cs_new:Npn \__karol_beblock_rumpf:
{
	\__karol_pfad_maul_auf:n \l__karol_bh_dim
	-- ( \dim_use:N \l__karol_barm_dim ,
	     \dim_eval:n { - \l__karol_bh_dim - \l__karol_bm_dim } )
	\__karol_pfad_maul_zu:n
		{ \dim_eval:n { \l__karol_bh_dim + \l__karol_bm_dim } }
	\__karol_pfad_maul_auf:n
		{ \dim_eval:n { \l__karol_bh_dim + \l__karol_bm_dim
		                + \l__karol_bs_dim } }
	-- ( \dim_use:N \l__karol_barm_dim ,
	     \dim_eval:n { - \l__karol_bh_dim - \l__karol_bm_dim
	                   - \l__karol_bs_dim - \l__karol_bmzwei_dim } )
	\__karol_pfad_maul_zu:n
		{ \dim_eval:n { \l__karol_bh_dim + \l__karol_bm_dim
		                + \l__karol_bs_dim + \l__karol_bmzwei_dim } }
}

% --- E-Block (wenn … dann … sonst …) ---------------------------------------
% #1 = Farbname  #2 = Kopf  #3 = dann-Rumpf  #4 = sonst-Rumpf
\cs_new_protected:Npn \__karol_beblock:nnnn #1#2#3#4
{
	% Inline gibt es keine Maulöffnungen: nur der Kopf wird gesetzt.
	\bool_if:NTF \l__karol_binline_bool
		{ \__karol_bblock:nn        {#1} {#2}           }
		{ \__karol_beblock_voll:nnnn {#1} {#2} {#3} {#4} }
}
\cs_new_protected:Npn \__karol_beblock_voll:nnnn #1#2#3#4
{
	\tl_set:Nn \l__karol_bfarbe_tl {#1}
	\vbox_set:Nn \l__karol_bbody_box { \__karol_stapelmodus: #3 }
	\vbox_set:Nn \l__karol_belse_box { \__karol_stapelmodus: #4 }
	\__karol_bmess:n {#2}
	% Höhe des sonst-Balkens aus seiner Beschriftung
	\hbox_set:Nn \l__karol_bsonst_box
		{ \__karol_blockfont: \__karol_bschriftfarbe: \__karol_bphantom: sonst }
	\dim_set:Nn \l__karol_bs_dim
	{
		\l__karol_bnd_dim + \l__karol_bpt_dim
		+ \box_ht:N \l__karol_bsonst_box + \box_dp:N \l__karol_bsonst_box
		+ \l__karol_bpb_dim
	}
	\dim_set:Nn \l__karol_bm_dim
		{ \box_ht:N \l__karol_bbody_box + \box_dp:N \l__karol_bbody_box }
	\dim_compare:nT { \l__karol_bm_dim < \l__karol_bmm_dim }
		{ \dim_set_eq:NN \l__karol_bm_dim \l__karol_bmm_dim }
	\dim_set:Nn \l__karol_bmzwei_dim
		{ \box_ht:N \l__karol_belse_box + \box_dp:N \l__karol_belse_box }
	\dim_compare:nT { \l__karol_bmzwei_dim < \l__karol_bmm_dim }
		{ \dim_set_eq:NN \l__karol_bmzwei_dim \l__karol_bmm_dim }
	\dim_set:Nn \l__karol_bt_dim
	{
		\l__karol_bh_dim + \l__karol_bm_dim + \l__karol_bs_dim
		+ \l__karol_bmzwei_dim + \l__karol_bfut_dim
	}
	\dim_set:Nn \l__karol_bbw_dim
		{ \l__karol_barm_dim + \box_wd:N \l__karol_bbody_box + 0.4em }
	\dim_compare:nT { \l__karol_bbw_dim < \l__karol_bw_dim }
		{ \dim_set_eq:NN \l__karol_bbw_dim \l__karol_bw_dim }
	\dim_compare:nT
		{ \l__karol_bbw_dim < \l__karol_barm_dim + \box_wd:N \l__karol_belse_box }
		{ \dim_set:Nn \l__karol_bbw_dim
			{ \l__karol_barm_dim + \box_wd:N \l__karol_belse_box + 0.4em } }
	\__karol_bemit:nN
	{
		\begin{tikzpicture}
			\useasboundingbox (0pt, 0pt) rectangle
				( \dim_use:N \l__karol_bbw_dim , -\dim_use:N \l__karol_bt_dim );
			\__karol_brand:nn {#1}
			{
				\__karol_pfad_oben:
				\__karol_beblock_rumpf:
				\__karol_pfad_unten:n \l__karol_bt_dim
			}
			\__karol_btxt_setzen:n { 0pt }
			% sonst-Beschriftung (Box wurde VOR dem Bild gefüllt)
			\__karol_btext_at:nN
				{ \dim_eval:n { \l__karol_bh_dim + \l__karol_bm_dim } }
				\l__karol_bsonst_box
			% Rümpfe
			\node [ anchor=north~west, inner~sep=0pt ]
				at ( \dim_use:N \l__karol_barm_dim ,
				     -\dim_use:N \l__karol_bh_dim )
				{ \box_use:N \l__karol_bbody_box };
			\node [ anchor=north~west, inner~sep=0pt ]
				at ( \dim_use:N \l__karol_barm_dim ,
				     \dim_eval:n { - \l__karol_bh_dim - \l__karol_bm_dim
				                   - \l__karol_bs_dim } )
				{ \box_use:N \l__karol_belse_box };
		\end{tikzpicture}
	}
	\l__karol_bt_dim
}

% --- Eingabefelder: Zahl (Pille) und Bedingung (Sechseck) ------------------
% Aktuelle Blockfarbe (für Pillen-Tönung), von den Buildern gesetzt
\tl_new:N \l__karol_bfarbe_tl
\tl_set:Nn \l__karol_bfarbe_tl { karolBlockBefehl }
% Pille: blasse Tönung der Blockfarbe, dunkle Schrift (wie Karol Online)
\dim_new:N \l__karol_bpxs_dim   % Pille: Innenabstand waagerecht
\dim_new:N \l__karol_bpys_dim   % Pille: Innenabstand senkrecht
\dim_new:N \l__karol_bpmw_dim   % Pille: Mindestbreite
\box_new:N \l__karol_bptxt_box  % Pillenbeschriftung
\box_new:N \l__karol_bpinh_box  % Pilleninhalt (Beschriftung + Platz fürs Dreieck)
\dim_new:N \l__karol_bph_dim    % Pillenhöhe
\dim_new:N \l__karol_bptri_dim  % Auswahldreieck: Seitenlänge
\dim_new:N \l__karol_bpgap_dim  % Auswahldreieck: Lücke zum Text
\dim_new:N \l__karol_bpby_dim   % Auswahldreieck: Spitze über der Grundlinie
\tl_new:N  \l__karol_bpdreieck_tl  % Farbe des Auswahldreiecks

% Verhältnisse des Auswahldreiecks, gemessen an der Oberfläche von Robot
% Karol Online (Pillenhöhe H = 83 px):  Seitenlänge 27 px = 0.325 H,
% Lücke Text->Dreieck 22 px = 0.265 H, und die Spitze steht praktisch auf
% der Textgrundlinie (2 px darüber = 0.024 H).  Das Dreieck hat die VOLLE
% Blockfarbe, nicht die aufgehellte der Pille.
\fp_const:Nn \c__karol_btri_fp     { 0.325 }
% 0.265 ist im Original der Abstand von der TINTE des letzten Buchstabens;
% hier wird ab der Textbox gemessen, deren rechte Kante die Seitenbreite
% des Glyphen schon enthält (~0.09 H).  Deshalb der kleinere Wert.
\fp_const:Nn \c__karol_btrigap_fp  { 0.19 }
\fp_const:Nn \c__karol_btribas_fp  { 0.024 }

% #1 = Auswahldreieck ja/nein (Boolean-Ausdruck), #2 = Beschriftung
\cs_new_protected:Npn \__karol_bpille_aux:nn #1#2
{
	\bool_if:NTF \l__karol_binline_bool
	{
		\dim_set:Nn \l__karol_bpxs_dim { 0.22em }
		\dim_set:Nn \l__karol_bpys_dim { 0.08em }
		\dim_set:Nn \l__karol_bpmw_dim { 0.85em }
	}
	{
		\dim_set:Nn \l__karol_bpxs_dim { 0.26em }
		\dim_set:Nn \l__karol_bpys_dim { 0.14em }
		\dim_set:Nn \l__karol_bpmw_dim { 1.05em }
	}
	% Beschriftung VOR dem Bild setzen (im tikzpicture gilt \nullfont) und
	% daraus die Pillenhöhe bestimmen; em-Maße müssen ohnehin vorher in
	% Register, im Bild sind sie 0pt.
	\hbox_set:Nn \l__karol_bptxt_box
	{
		\normalfont \__karol_textfont: \color{black}
		\bool_if:NF \l__karol_binline_bool { \footnotesize }
		\__karol_bphantom_flach: #2
	}
	\dim_set:Nn \l__karol_bph_dim
	{
		\box_ht:N \l__karol_bptxt_box + \box_dp:N \l__karol_bptxt_box
		+ 2 \l__karol_bpys_dim
	}
	\bool_if:nTF { #1 }
	{
		\dim_set:Nn \l__karol_bptri_dim
			{ \fp_to_dim:n { \c__karol_btri_fp * \l__karol_bph_dim } }
		\dim_set:Nn \l__karol_bpgap_dim
			{ \fp_to_dim:n { \c__karol_btrigap_fp * \l__karol_bph_dim } }
		\dim_set:Nn \l__karol_bpby_dim
			{ \fp_to_dim:n { \c__karol_btribas_fp * \l__karol_bph_dim } }
	}
	{
		\dim_zero:N \l__karol_bptri_dim
		\dim_zero:N \l__karol_bpgap_dim
		\dim_zero:N \l__karol_bpby_dim
	}
	% Im Farbmodus hebt sich die Pille durch ihre Tönung vom Block ab und
	% braucht keinen Rand.  Im Druck ist sie auf einem weißen \bbefehl
	% selbst weiß — dort trägt erst die Kontur die Information, dass hier
	% ein Eingabefeld sitzt.  Das Auswahldreieck hätte aus demselben Grund
	% keine Farbe mehr und bekommt einen festen Grauwert.
	% Das Auswahldreieck hat im Druck keine Blockfarbe mehr (der \bbefehl
	% ist dort weiß) und bekommt einen festen Grauwert.
	\tl_set_eq:NN \l__karol_bpdreieck_tl \l__karol_bfarbe_tl
	\bool_if:NT \l__karol_druck_bool
		{ \tl_set:Nn \l__karol_bpdreieck_tl { karolDruckAkzent } }
	% Platz für Lücke und Dreieck FEST in eine Box backen.  Als Leerraum
	% am Ende des Knotentextes geht er verloren — TikZ verwirft ihn, die
	% Pille wäre dann genau um die Dreieckbreite zu schmal.  Die Breite
	% einer gesetzten Box kann TikZ dagegen nicht mehr ändern.
	\hbox_set:Nn \l__karol_bpinh_box
	{
		\box_use:N \l__karol_bptxt_box
		\skip_horizontal:n
			{ \l__karol_bpgap_dim + \l__karol_bptri_dim }
	}
	\hbox:n
	{
		\begin{tikzpicture} [ baseline=0pt ]
			\node (karolpille) [ fill=\l__karol_bfarbe_tl!40!white,
			        karolpillenrand,
			        rounded~corners=\l__karol_brc_tl,
			        inner~xsep=\l__karol_bpxs_dim,
			        inner~ysep=\l__karol_bpys_dim,
			        minimum~width=\l__karol_bpmw_dim,
			        anchor=base ]
				at (0pt, 0pt)
				{ \box_use:N \l__karol_bpinh_box };
			% Auswahldreieck, rechtsbündig im Innenraum der Pille.
			% Bezugspunkt ist der Anker "base east": rechte Knotenkante auf
			% Höhe der Textgrundlinie — damit stimmt die Lage auch dann,
			% wenn die Mindestbreite die Pille breiter macht als ihr Inhalt.
			\dim_compare:nT { \l__karol_bptri_dim > 0pt }
			{
				\begin{scope} [ shift={ (karolpille.base~east) } ]
					\fill [ \l__karol_bpdreieck_tl ]
						( \dim_eval:n
						    { 0pt - \l__karol_bpxs_dim - \l__karol_bptri_dim },
						  \dim_eval:n
						    { \l__karol_bpby_dim + \l__karol_bptri_dim } )
						-- ( \dim_eval:n { 0pt - \l__karol_bpxs_dim },
						     \dim_eval:n
						       { \l__karol_bpby_dim + \l__karol_bptri_dim } )
						-- ( \dim_eval:n { 0pt - \l__karol_bpxs_dim
						                   - 0.5 \l__karol_bptri_dim },
						     \dim_use:N \l__karol_bpby_dim )
						-- cycle;
				\end{scope}
			}
		\end{tikzpicture}
	}
}
% Pille ohne Auswahl (Zahlen, Namen von Anweisungsdefinitionen, Kommentare)
\cs_new_protected:Npn \__karol_bpille:n #1
	{ \__karol_bpille_aux:nn { \c_false_bool } {#1} }
% Pille mit Auswahldreieck (Aufrufe, Himmelsrichtungen)
\cs_new_protected:Npn \__karol_bpille_menue:n #1
	{ \__karol_bpille_aux:nn { \c_true_bool } {#1} }
\box_new:N \l__karol_bhex_box
\dim_new:N \l__karol_bhw_dim
\dim_new:N \l__karol_bho_dim
\dim_new:N \l__karol_bhu_dim
\dim_new:N \l__karol_bhp_dim
\dim_new:N \l__karol_bhn_dim
\dim_new:N \l__karol_bhm_dim
\dim_new:N \l__karol_bhk_dim
\NewDocumentCommand { \bbedingung } { m }
{
	\group_begin:
	\__karol_farbschema:
	\__karol_bstart:
	\__karol_bdims:
	\tl_if_empty:nTF {#1}
	{	% Leere Fassung: weißer Sockel (für Lückenaufgaben)
		\hbox_set:Nn \l__karol_bhex_box
			{ \__karol_blockfont: \__karol_bphantom_flach: \hspace{1.1em} }
		\__karol_bbedform:n { white }
	}
	{
		\hbox_set:Nn \l__karol_bhex_box
			{ \__karol_blockfont: \__karol_bschriftfarbe: \__karol_bphantom_flach: #1 }
		\__karol_bbedform:n { karolBlockBedingung }
	}
	\group_end:
}
% Form: abgerundetes Rechteck, Stecker an der linken Kante (Karol Online)
\cs_new_protected:Npn \__karol_bbedform:n #1
{
	\bool_if:NTF \l__karol_binline_bool
	{
		\dim_set:Nn \l__karol_bhw_dim
			{ \box_wd:N \l__karol_bhex_box + 0.34em }
		\dim_set:Nn \l__karol_bho_dim
			{ \box_ht:N \l__karol_bhex_box + 0.10em }
		\dim_set:Nn \l__karol_bhu_dim
			{ \box_dp:N \l__karol_bhex_box + 0.10em }
		\dim_set:Nn \l__karol_bhp_dim { 0.20em }   % Steckertiefe
		\dim_set:Nn \l__karol_bhn_dim { 0.17em }   % halbe Steckerhöhe
	}
	{
		\dim_set:Nn \l__karol_bhw_dim
			{ \box_wd:N \l__karol_bhex_box + 0.52em }
		\dim_set:Nn \l__karol_bho_dim
			{ \box_ht:N \l__karol_bhex_box + 0.17em }
		\dim_set:Nn \l__karol_bhu_dim
			{ \box_dp:N \l__karol_bhex_box + 0.17em }
		\dim_set:Nn \l__karol_bhp_dim { 0.30em }   % Steckertiefe
		\dim_set:Nn \l__karol_bhn_dim { 0.26em }   % halbe Steckerhöhe
	}
	% Senkrechte Mitte und Halshöhe des Steckers.  In der Oberfläche ist
	% der Hals schmaler als das Steckerende (Puzzlenase, keine Lasche):
	% Hals ~0.27, Ende ~0.42 der Blockhöhe.
	\dim_set:Nn \l__karol_bhm_dim
		{ 0.5\l__karol_bho_dim - 0.5\l__karol_bhu_dim }
	\dim_set:Nn \l__karol_bhk_dim { 0.62\l__karol_bhn_dim }
	% (em-Werte MÜSSEN vor dem Bild in Register: im tikzpicture ist em=0!)
	\mode_leave_vertical:
	\hbox:n
	{
		\begin{tikzpicture} [ baseline=0pt ]
			% Körper und Stecker in EINEM Pfad, damit die Umrandung außen
			% herumläuft statt quer durch den Stecker.
			\__karol_brand:nn {#1}
			{
				( \dim_use:N \l__karol_bhp_dim , \dim_use:N \l__karol_bho_dim )
				-- ( \dim_eval:n { \l__karol_bhp_dim + \l__karol_bhw_dim },
				     \dim_use:N \l__karol_bho_dim )
				-- ( \dim_eval:n { \l__karol_bhp_dim + \l__karol_bhw_dim },
				     -\dim_use:N \l__karol_bhu_dim )
				-- ( \dim_use:N \l__karol_bhp_dim ,
				     -\dim_use:N \l__karol_bhu_dim )
				\__karol_bbed_links:
				-- cycle
			}
			\node [ anchor=base, inner~sep=0pt ]
				at ( \dim_eval:n { \l__karol_bhp_dim + 0.5\l__karol_bhw_dim },
				     0pt )
				{ \box_use:N \l__karol_bhex_box };
		\end{tikzpicture}
	}
}

% Linke Seite des Bedingungsplättchens: Hals und Puzzlenase, von der
% unteren zur oberen Halskante.  Scharfe Ecken, weil an den Übergängen
% Gerade->Kurve die Segmente kürzer als zwei Eckradien sind — pgf machte
% daraus sonst Häkchen (dieselbe Falle wie bei der Kerbe).
\cs_new:Npn \__karol_bbed_links:
{
	\l__karol_bkerbe_tl
	-- ( \dim_use:N \l__karol_bhp_dim ,
	     \dim_eval:n { \l__karol_bhm_dim - \l__karol_bhk_dim } )
				% Puzzlenase: ausschwingen, runde Außenkante, einschwingen
				.. controls
					( \dim_eval:n { 0.72\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim - \l__karol_bhk_dim } )
					and
					( \dim_eval:n { 0.55\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim - \l__karol_bhn_dim } ) ..
					( \dim_eval:n { 0.22\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim - \l__karol_bhn_dim } )
				.. controls
					( \dim_eval:n { 0.02\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim - \l__karol_bhn_dim } )
					and
					( \dim_eval:n { 0.02\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim + \l__karol_bhn_dim } ) ..
					( \dim_eval:n { 0.22\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim + \l__karol_bhn_dim } )
				.. controls
					( \dim_eval:n { 0.55\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim + \l__karol_bhn_dim } )
					and
					( \dim_eval:n { 0.72\l__karol_bhp_dim },
					  \dim_eval:n { \l__karol_bhm_dim + \l__karol_bhk_dim } ) ..
					( \dim_use:N \l__karol_bhp_dim ,
					  \dim_eval:n { \l__karol_bhm_dim + \l__karol_bhk_dim } )
	\l__karol_bkante_tl
}

% --- Himmelsrichtungen: Ist / NichtIst mit Auswahlmenü ---------------------
% In Robot Karol Online zeigen diese acht Blöcke die Richtung nicht als
% Text, sondern als Auswahlmenü: eine helle Pille mit Auswahldreieck.
%
%     \bist{Norden}        \bnichtist{Süden}
%
% Beide liefern für sich ein vollständiges Bedingungsplättchen (wie im
% Original abgebildet).  Als Argument eines Kontrollblocks steuern sie
% dagegen nur dessen Beschriftung bei — dort baut \bbedingung das
% Plättchen, ein zweites wäre falsch:
%
%     \bwenndann{\bist{Norden}}{…}   ->   wenn <Ist [Norden v]> dann
%
% Unterschieden wird über \l__karol_bimbau_bool: innerhalb eines Blocks
% ist es wahr, auf oberster Ebene falsch.
\cs_new_protected:Npn \__karol_bist_text:nn #1#2
{
	% Die Pille gehört zum Bedingungsplättchen, nicht zum umgebenden
	% Block — sonst bekäme sie in \bwenndann dessen Grün.
	\tl_set:Nn \l__karol_bfarbe_tl { karolBlockBedingung }
	#1 ~ \__karol_bpille_menue:n {#2}
}
\cs_new_protected:Npn \__karol_bist:nn #1#2
{
	\bool_if:NTF \l__karol_bimbau_bool
		{ \__karol_bist_text:nn {#1} {#2} }
		{ \bbedingung { \__karol_bist_text:nn {#1} {#2} } }
}
\NewDocumentCommand { \bist }      { m } { \__karol_bist:nn { Ist }      {#1} }
\NewDocumentCommand { \bnichtist } { m } { \__karol_bist:nn { NichtIst } {#1} }

% --- Benutzerkommandos -----------------------------------------------------
% Jeder Nutzerblock läuft in einer eigenen Gruppe und bestimmt zu Beginn
% über \__karol_bstart:, ob er als Stapelblock oder inline gesetzt wird.
% \__karol_bwrap:n kapselt das für alle \b…-Kommandos.
\cs_new_protected:Npn \__karol_bwrap:n #1
	{ \group_begin: \__karol_farbschema: \__karol_bstart: #1 \group_end: }
% dito, aber mit erzwungenem Inline-Modus (\i…-Kommandos)
\cs_new_protected:Npn \__karol_iwrap:n #1
	{ \group_begin: \__karol_farbschema: \__karol_bstart_inline: #1
	  \group_end: }

% \bbefehl[Anzahl]{Name} — optionale Zahl erscheint als Pille
\NewDocumentCommand { \bbefehl } { O{} m }
{
	\__karol_bwrap:n
	{
		\__karol_bblock:nn { karolBlockBefehl }
			{ #2 \tl_if_empty:nF {#1} { ~ \__karol_bpille:n {#1} } }
	}
}

% \baufruf[Anzahl]{Name} — Aufruf einer eigenen Anweisung (rot).
% Der Name steht wie im Original in einer Auswahlpille mit Dreieck (dort
% wählt man daraus die aufzurufende Anweisung); die Definition
% \banweisung zeigt ihn dagegen in einer Pille OHNE Dreieck.
\NewDocumentCommand { \baufruf } { O{} m }
{
	\__karol_bwrap:n
	{
		\__karol_bblock:nn { karolBlockAnweisung }
			{ \__karol_bpille_menue:n {#2}
			  \tl_if_empty:nF {#1} { ~ \__karol_bpille:n {#1} } }
	}
}

% \banweisung{Name}{Blöcke} — Definition einer eigenen Anweisung (C-Block)
\NewDocumentCommand { \banweisung } { m m }
{
	\__karol_bwrap:n
	{
		\__karol_bcblock:nnn { karolBlockAnweisung }
			{ Anweisung ~ \__karol_bpille:n {#1} } { #2 }
	}
}

% \bkommentar{Text} — Kommentarblock (khaki) mit //-Präfix
\NewDocumentCommand { \bkommentar } { m }
{
	\__karol_bwrap:n
	{
		\__karol_bblock:nn { karolBlockKommentar }
			{ { \normalfont \__karol_textfont: \color{black!70} // } ~
			  \__karol_bpille:n {#1} }
	}
}

% \bhauptprogramm — Hut-Block (ohne obere Kerbe), startet den Stapel
\cs_new_protected:Npn \__karol_bhut:
{
	\__karol_bmess:n { Hauptprogramm }
	\__karol_bemit:nN
	{
		\begin{tikzpicture}
			\useasboundingbox (0pt, 0pt) rectangle
				( \dim_use:N \l__karol_bw_dim ,
				  \__karol_bbbox:N \l__karol_bh_dim );
			\__karol_brand:nn { karolBlockHut }
			{
				(0pt, 0pt)
				-- ( \dim_use:N \l__karol_bw_dim , 0pt )
				\__karol_pfad_unten:n \l__karol_bh_dim
			}
			\__karol_btxt_setzen:n { 0pt }
		\end{tikzpicture}
	}
	\l__karol_bh_dim
}
\NewDocumentCommand { \bhauptprogramm } { }
	{ \__karol_bwrap:n { \__karol_bhut: } }

\NewDocumentCommand { \bwiederholemal } { m m }
{
	\__karol_bwrap:n
	{
		\__karol_bcblock:nnn { karolBlockSchleife }
			{ wiederhole ~ \__karol_bpille:n {#1} ~ mal } { #2 }
	}
}
\NewDocumentCommand { \bwiederholesolange } { m m }
{
	\__karol_bwrap:n
	{
		\__karol_bcblock:nnn { karolBlockSchleife }
			{ wiederhole ~ solange ~ \bbedingung {#1} } { #2 }
	}
}
\NewDocumentCommand { \bwiederholeimmer } { m }
{
	\__karol_bwrap:n
		{ \__karol_bcblock:nnn { karolBlockSchleife }
			{ wiederhole ~ immer } { #1 } }
}

\NewDocumentCommand { \bwenndann } { m m }
{
	\__karol_bwrap:n
	{
		\__karol_bcblock:nnn { karolBlockWenn }
			{ wenn ~ \bbedingung {#1} ~ dann } { #2 }
	}
}
\NewDocumentCommand { \bwenndannsonst } { m m m }
{
	\__karol_bwrap:n
	{
		\__karol_beblock:nnnn { karolBlockWenn }
			{ wenn ~ \bbedingung {#1} ~ dann } { #2 } { #3 }
	}
}

% Leerer „Geisterblock" zum handschriftlichen Ausfüllen
\cs_new_protected:Npn \__karol_bluecke:n #1
{
	\__karol_bdims:
	\dim_set:Nn \l__karol_bw_dim { #1 }
	\dim_set:Nn \l__karol_bh_dim
		{ \bool_if:NTF \l__karol_binline_bool { 1.20em } { 1.70em } }
	% Grundlinie wie bei einem beschrifteten Block: die Lücke sitzt mit
	% ihrer gedachten Textgrundlinie auf der Grundlinie des Fließtextes.
	\hbox_set:Nn \l__karol_btxt_box
		{ \__karol_blockfont: \__karol_bphantom: }
	\__karol_bbase_setzen:
	% Strichmuster in em rechnen, damit es mit der Blockgröße mitwächst —
	% ein festes 3pt-Muster ist für die knapp 2pt langen Kerbensegmente des
	% Inline-Blocks zu grob und zerreißt die Kontur.  em MUSS vorher ins
	% Register: im tikzpicture ist es 0pt.
	\dim_set:Nn \l__karol_bstr_dim { 0.17em }
	\dim_set:Nn \l__karol_bluk_dim { 0.13em }
	% Füllung: im Druck weiß statt hellgrau — der gestrichelte Rand sagt
	% schon alles, die Fläche wird ohnehin von Hand beschrieben.
	% (Der Test gehört NICHT in den TikZ-Schlüsselwert, siehe xcolor.)
	\bool_if:NTF \l__karol_druck_bool
		{ \tl_set:Nn \l__karol_bluf_tl { white } }
		{ \tl_set:Nn \l__karol_bluf_tl { black!6 } }
	\__karol_bemit:nN
	{
		\begin{tikzpicture}
			\useasboundingbox (0pt, 0pt) rectangle
				( \dim_use:N \l__karol_bw_dim ,
				  \__karol_bbbox:N \l__karol_bh_dim );
			\draw [ draw=black!55, line~width=.7pt,
			        dash~pattern=on~\l__karol_bstr_dim~off~\l__karol_bluk_dim,
			        line~join=round, line~cap=round,
			        fill=\l__karol_bluf_tl, rounded~corners=1.6pt ]
				\__karol_pfad_oben: \__karol_pfad_unten:n \l__karol_bh_dim ;
		\end{tikzpicture}
	}
	\l__karol_bh_dim
}
\NewDocumentCommand { \bluecke } { O{7em} }
	{ \__karol_bwrap:n { \__karol_bluecke:n {#1} } }

% ---------------------------------------------------------------------------
%  Inline-Blöcke für den Fließtext
% ---------------------------------------------------------------------------
%  Die \i…-Kommandos setzen genau einen Block in die laufende Zeile:
%  in der Schriftgröße des umgebenden Textes, mit dessen Grundlinie
%  bündig und mit knapperen Verzierungen als im Stapel.  Blöcke, die im
%  Stapel mehrzeilig wären (Schleifen, Verzweigungen, eigene Anweisungen),
%  erscheinen inline nur mit ihrem Kopf — \iwenndann{IstZiegel} ergibt
%  also den Block „wenn ⟨IstZiegel⟩ dann".  Für echte Rümpfe bleibt die
%  Umgebung karolbloecke zuständig.
%
%  Auch die \b…-Kommandos erkennen den Fließtext selbst und setzen dort
%  automatisch inline; die \i…-Fassungen ersparen nur die leeren
%  Rumpf-Argumente und erzwingen den Modus auch im Stapel.
% ---------------------------------------------------------------------------
\NewDocumentCommand { \ibefehl } { O{} m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockBefehl }
			{ #2 \tl_if_empty:nF {#1} { ~ \__karol_bpille:n {#1} } }
	}
}
\NewDocumentCommand { \iaufruf } { O{} m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockAnweisung }
			{ \__karol_bpille_menue:n {#2}
			  \tl_if_empty:nF {#1} { ~ \__karol_bpille:n {#1} } }
	}
}
\NewDocumentCommand { \ianweisung } { m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockAnweisung }
			{ Anweisung ~ \__karol_bpille:n {#1} }
	}
}
\NewDocumentCommand { \ikommentar } { m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockKommentar }
			{ { \normalfont \__karol_textfont: \color{black!70} // } ~
			  \__karol_bpille:n {#1} }
	}
}
\NewDocumentCommand { \ihauptprogramm } { }
	{ \__karol_iwrap:n { \__karol_bhut: } }
\NewDocumentCommand { \iwiederholemal } { m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockSchleife }
			{ wiederhole ~ \__karol_bpille:n {#1} ~ mal }
	}
}
\NewDocumentCommand { \iwiederholesolange } { m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockSchleife }
			{ wiederhole ~ solange ~ \bbedingung {#1} }
	}
}
\NewDocumentCommand { \iwiederholeimmer } { }
{
	\__karol_iwrap:n
		{ \__karol_bblock:nn { karolBlockSchleife } { wiederhole ~ immer } }
}
\NewDocumentCommand { \iwenndann } { m }
{
	\__karol_iwrap:n
	{
		\__karol_bblock:nn { karolBlockWenn }
			{ wenn ~ \bbedingung {#1} ~ dann }
	}
}
% \isonst — der „sonst"-Balken einer Verzweigung als eigener Inline-Block
\NewDocumentCommand { \isonst } { }
	{ \__karol_iwrap:n { \__karol_bblock:nn { karolBlockWenn } { sonst } } }
% \ibedingung{…} — Bedingungsplättchen im Fließtext (erzwungen inline)
\NewDocumentCommand { \ibedingung } { m }
	{ \__karol_iwrap:n { \bbedingung {#1} } }
% Himmelsrichtungen inline
\NewDocumentCommand { \iist } { m }
	{ \__karol_iwrap:n { \bbedingung { \__karol_bist_text:nn { Ist } {#1} } } }
\NewDocumentCommand { \inichtist } { m }
{
	\__karol_iwrap:n
		{ \bbedingung { \__karol_bist_text:nn { NichtIst } {#1} } }
}
\NewDocumentCommand { \iluecke } { O{4em} }
	{ \__karol_iwrap:n { \__karol_bluecke:n {#1} } }

\ExplSyntaxOff

% ===========================================================================
%  6. Verweise auf Aufgaben in Robot Karol Online
% ===========================================================================
%  Eine Aufgabe in Robot Karol Online steckt vollständig in der URL:
%  auf https://karol.arrrg.de/ folgt eine Raute und der Aufgabencode.
%
%      \karolonline{2t7t}   setzt   karol.arrrg.de/#2t7t
%                           und verlinkt auf https://karol.arrrg.de/#2t7t
%
%  Gesetzt wird nur, was man tatsächlich eintippen muss (ohne "https://"),
%  in der Programmschrift des Pakets.  Der Code wird unverändert
%  übernommen — auf Groß- und Kleinschreibung kommt es bei den
%  Aufgabencodes nicht an.
%
%  Der Verweis braucht hyperref.  Ob das Paket es lädt, entscheidet die
%  Paketoption:
%
%      \usepackage{robotkarol}                   lädt hyperref (Vorgabe)
%      \usepackage[hyperref=false]{robotkarol}   lädt es nicht
%
%  Ohne hyperref setzt \karolonline denselben Text, nur ohne Verweis.
%  Geladen wird im Haken "begindocument/before", also am Ende der
%  Präambel: hyperref will als letztes Paket geladen werden, und ein
%  Dokument, das es selbst mit eigenen Optionen lädt, käme sonst zu einem
%  Optionskonflikt.  Geladen wird ohne Optionen: wie Verweise aussehen,
%  ist Sache des Dokuments.  Aus demselben Grund fragt \karolonline nicht die
%  Paketoption ab, sondern ob es \href zum Zeitpunkt der Benutzung
%  wirklich gibt — dann verlinkt es auch in einem Dokument, das hyperref
%  trotz hyperref=false selbst geladen hat.
% ---------------------------------------------------------------------------
\ExplSyntaxOn

% Ohne Optionen geladen: wie Verweise aussehen (Rahmen, Farben), stellt
% das Dokument selbst über hyperref ein — das Paket redet da nicht hinein.
\bool_if:NT \g__karol_href_bool
	{ \AddToHook { begindocument/before } { \RequirePackage { hyperref } } }

% Beides als STRING, nicht als tl: unter \ExplSyntaxOn ist ":" ein
% Buchstabe (Catcode 11).  hyperref zerlegt die URL aber mit einem
% delimitierten Argument (\href@split #1:#2:#3), und das trifft nur
% Doppelpunkte mit Catcode 12 — sonst erscheint der Text, aber es
% entsteht kein Verweis, und zwar ohne jede Fehlermeldung.
\str_const:Nn \c__karol_obasis_str   { https://karol.arrrg.de/ }
\str_const:Nn \c__karol_oanzeige_str { karol.arrrg.de/ }
\str_new:N   \l__karol_ocode_str

% Der sichtbare Text: das, was man eintippen muss.
\cs_new_protected:Npn \__karol_online_text:
{
	{ \__karol_monofont: \str_use:N \c__karol_oanzeige_str \c_hash_str
	  \str_use:N \l__karol_ocode_str }
}
% #1 = fertige URL (x-expandiert, damit die Raute wirklich ankommt)
\cs_new_protected:Npn \__karol_online_href:n #1
	{ \href {#1} { \__karol_online_text: } }

\NewDocumentCommand { \karolonline } { m }
{
	\group_begin:
	% \str_set:Nn statt \tl_set:Nn — der Code muss Catcode 12 haben,
	% damit hyperref die URL zerlegen kann (siehe oben).
	\str_set:Nn \l__karol_ocode_str {#1}
	\cs_if_exist:NTF \href
	{
		\exp_args:Nx \__karol_online_href:n
			{ \c__karol_obasis_str \c_hash_str \l__karol_ocode_str }
	}
	{ \__karol_online_text: }
	\group_end:
}

\ExplSyntaxOff

\endinput
