Einführung in die Programmierung mit HTML und PHP
Ablauf des Tic-Tac-Toe - Spieles
Zentrale - Lehrgänge - Einführung HTML+PHP - TicTacToe-Spielablauf

Lehrziel

tictactoe - siebenter Schritt Kurze Erinnerung an den Endzustand der letzten Lektion: Das Spiel an sich läuft schon mal korrekt und gegen Hacking gesichert durch. Folgenden Wünsche sind aber noch offen:
Wir werden diese Punkte in dieser Lektion erstmal vollständig abhandeln, bevor wir das Spiel ab der nächste Lektion auf Multiplayer-Fähigkeit aufbohren.

Wir werden dafür etwas intensiver auf PHP zurückgreifen müssen und neue Konzepte einführen. Und weil ein paar Elemente der Bedienoberfläche hinzukommen, wird CSS natürlich auch wieder gefordert werden. Der HTML-Anteil dagegen wird marginal und trivial bleiben.

Sieg- und Remis-Auswertung

Wir möchten, daß der Rechner (also: Unser PHP-Programm auf Serverseite) den Schiedsrichter spielt, und zwar jetzt nicht mehr nur hinsichtlich der erlaubten Züge, sondern auch hinsichtlich der Feststellung von Sieg oder Remis. Und wir möchten bei der Gelegenheit natürlich eine hübsche Darstellung des Ergebnisses auf der Oberfläche.

Um das zu automatisieren, müssen wir es erstmal formal (also nicht mehr bloß umgangssprachlich, sondern exakt mit Zahlen) ausdrücken...

Formale Beschreibung für "Sieg"
Umgangssprachlich bedeutet "Sieg" im TicTacToe: "3 in einer Reihe".
So wie ja auch einer der alternativen Namen des Spiels lautet.

Wir stellen uns jetzt mal GANZ DOOF an und tun so, als ob wir ein Rechner wären (so absolut komplett ohne jeden Funken von Intelligenz, aber überschäumender Energie)...

  • Wie jetzt: "eine Reihe"?

    Na ja:
    • entweder eine senkrechte Reihe
    • oder eine waagerechte Reihe
    • oder eine diagonal steigende Reihe (links unten -> rechts oben)
    • oder eine diagonal fallende Reihe

  • Ja, wie jetzt: "senkrechte Reihe"?

    Na ja:
    • entweder die ganz links
    • oder die in der Mitte
    • oder die ganz rechts

  • Ja, ähh, wie jetzt: "ganz links"?

    <räusper>... Zu dieser Reihe gehören folgende Felder:
    • das Feld mit Nummer 0
    • das Feld mit Nummer 3
    • das Feld mit Nummer 6
    Verstanden?!

  • Ähmm, ja. DAS war hinreichend konkret. DAMIT kann ich was anfangen.
    Und "in der Mitte"?!?...




Wir stellen fest: Wir müssen ECHT SEHR exakt und ausführlich numerisch ausdrücken, was wir automatisieren wollen, gelle?! Das ist das Schicksal von Programmierern der heutigen Zeit. Das wird auch demnächst nochmal anders kommen, wenn mal endlich echte künstliche Intelligenz entwickelt wird, aber dazu braucht's ein Umdenken im wissenschaftlichen Mainstream. Das wird noch Jahrzehnte brauchen, wenn nicht länger...

Nun ist diese Exaktheit für unser kleines TicTacToe ja nicht SO eine krasse Hürde: Wir haben insgesamt nur 8 Reihen, die wir zur Not alle einzeln mit Feldindizes hinschreiben können. So in der Art eines geschachtelten Arrays zum Beispiel...
$reihen = array
(
    // senkrecht
    array(0,3,6),
    array(1,4,7),
    array(2,5,8),
    // waagerecht
    array(0,1,2),
    array(3,4,5),
    array(6,7,8),
    // diagonal steigend
    array(2,4,6),
    // diagonal fallend
    array(0,4,8),
);

DAMIT könnten wir recht trivial eine Schleife aufstellen, die uns nacheinander jede der Reihen prüft, ob die eventuell vom gerade gesetzten Stein voll gemacht wurde. Dann müßte der Wert jedes einzelnen Feldes einer Reihe gleich dem des gerade dran seienden Spielers (also bei uns: Zeichen '1' oder '2', was gerade an unserer Variablen "$dran" so dran ist) sein. Ganz mechanisch und stupide.

Wer Energie hat und momentan nicht ganz mit meinen Erklärungen ausgelastet ist, darf das bitteschön mal eben probieren. Zum Üben von Schleifen und Verzweigungen und logischen Verknüpfungen (immerhin müssen immer gleichzeitig alle drei Feldbelegungen einer Reihe stimmen - also das erste UND das zweite UND das dritte Feld korrekt belegt sein -, und es reicht, wenn irgendeine der 8 Reihen auf diese Art voll geworden ist - also die erste ODER die zweite ODER ... ODER die achte Reihe).

Ich möchte Sie ja aber ganz sachte in die Ausdrucksmöglichkeiten der Programmiersprache PHP einführen und Ihnen die Techniken näherbringen, mit denen Sie Ihr Spiel auch mal auf ganz große Felder (zum Beispiel für "Gomoku" alias "5-in-einer-Reihe", für das auch richtige Wettkämpfe ausgetragen werden) aufbohren können. Und dazu gehen wir einen etwas anderen Weg - denn wer würde schon freiwillig die rund 600 Reihenvarianten des 15 x 15 - Gomoku-Feldes per Hand notieren wollen?!

Auf solch großen Feldern wäre es eh Humbuk, das gesamte Feld nach Gewinn-Reihen abzugrasen, wenn doch eh nur vom aktuell gesetzten Feld aus in genau vier (nie mehr) Richtungen eine volle Reihe entstanden sein kann. Man prüft also besser nur genau diese vier Richtungen vom gesetzten Feld aus. Wobei sich eine Reihe allerdings in der jeweiligen Richtung, wenn man ihr willkürlich einen Richtungssinn zuordnet, sowohl weiter vorn als auch weiter hinten gebildet haben kann. Bei einem Spiel "k-in-einer-Reihe" - mit "k" variabel betrachtet - kann die Reihe immerhin um k-1 Plätze hinter bis vor dem aktuell gesetzten Stein liegen.
Bei unserem kleinen TicTacToe dürfen wir da aber vorerst noch vereinfachen, weil bei dieser Variante das Spielfeld auf das engstmögliche eingegrenzt ist: Auf genau diese "k" Reihenlänge, welche in diesem Fall 3 ist. Daher haben unsere Gewinnreihen keinerlei Spielraum. Und damit ist es recht einfach, sie zu beschreiben:
  • waagerechte Reihe: die Zeile, in der gesetzt wurde
  • senkrechte Reihe: die Spalte, in der gesetzt wurde
  • diagonal steigende Reihe: die steigende Diagonale - wenn der gesetzte Stein auf dieser lag
  • diagonal fallende Reihe: die fallende Diagonale - wenn der gesetzte Stein auf dieser lag

Wie kriegen wir die "Zeile, in der gesetzt wurde" raus?

Nun, wiederholen wir ein bißchen Schulmathematik... wenn wir uns die Feldnummern anschauen fällt eine Regelmäßigkeit ins Auge: Die Nummern sind immer in aufeinander folgenden Bündeln von je 3 aufeinanderfolgenden Zahlen in den Zeilen angeordnet. Wenn wir diese Feldnummern durch 3 teilen, bleibt bei der Division ein ganzer Teil übrig, der jeweils für alle drei Feldern einer Zeile identisch ist. Und der die Zeilen Null-basiert durchnummeriert (indiziert).

Wie kommen wir an den ganzen Teil der Division?
Da führen mehrere Wege nach Rom. Der einfachste in PHP ist eine Funktion namens "floor" zu benutzen:
$zeile = floor($feld / 3);
Ein identisches Ergebnis liefert ein sogenannter "Typecast" (den hatten wir schon mal kennengelernt bei Sicherungsmaßnahmen gegen Hacking), und zwar auf einen Ganzzahltyp. Den nennt man im Programmierer-Jargon "Integer", gern auch abgekürzt zu "int":
$zeile = ((int)($feld / 3));
Auffällig bei dieser Konstruktion sind die vielen notwendigen Klammern. Unter ausgewählten Umständen kann man die äußere zwar weglassen, aber wenn Sie sich bei Typecasts solch ein Weglassen einmal angewöhnen und DANN mit unserem berühmten "copy & paste" arbeiten, kommen Sie rux-fix in katastrophale Fehlerkonstruktionen, die ihnen schwere Kopfzerbrechen bereiten werden: Weil der exakt selbe Code anderswo perfekt funktioniert, aber nach dem 1:1-kopieren (was ja EIGENTLICH GERADE sicherstellen soll, daß eben NICHTS SCHIEFGEHEN KANN!) an anderer Stelle plötzlich Unsinn macht.
Kurz: Wir nehmen die floor-Variante. In anderen Programmiersprachen (namentlich C++) ist die Typecast-Variante besser, in PHP nicht.

Und wie kriegen wir nun die drei Plätze in einer Zeile adressiert?

Tja: Die Nummern folgen aller einfachst unmittelbar aufeinander. Wenn wir erstmal die Nummer des ersten Feldes haben, brauchen wir anschließend nur noch 2 mal 1 draufzuzählen. Das konnten Sie mit DEN Zahlen schon im Kindergarten. Im Programmier-Jargon nennt man so ein Hochzählen übrigens "inkrementieren", wofür es extra bequeme Operatoren gibt.

Und wie kriegen wir nun die Nummer des ersten Feldes in einer Zeile?

Na ja: Da werfen wir mal einen Blick auf die ersten Nummern in den drei Zeilen und da sollte es uns wie Schuppen aus den Haaren fallen: Das sind Vielfache der Zeilennummern. Nicht ganz unerwartet! Zeilennummer multipliziert mit der Anzahl der Plätze pro Zeile. Das Gegenteil unserer durch 3-Teilung von eben.

Wie läuft das mit den Spalten?

Na ja: - fast - ganz genauso. Nur daß wir in den Spalten den Rest der Division verarbeiten statt ihren ganzzahligen Quotienten. Und für die Bestimmung des Rests gibt es auch eine bequeme Funktion in Form eines Operators, den wir bei unseren ersten PHP-Experimenten in der Lektion 3 schon mal kennengelernt hatten: den "%"-Operator. (Fragen Sie mich bitte nicht, wie der gesprochen wird. Ich kenne keine allgemein anerkannte umgangssprachliche Aussprache für das Ding. Ich rufe es immer "div" im Kontrast zu "durch" bei normaler Division. Wie Sie das selber halten, ist vollkommen Ihr Bier...)

Wird das nicht ein bißchen zu kompliziert, das alles in einem Block von Anweisungen zu beschreiben? Sieht da überhaupt noch jemand durch, wenns fertig ist?

Ja, volle Zustimmung! Wobei: DAS HIER ist ja sogar noch eine denkbar einfache Sache, die man durchaus mit etwas Kommentar ausgestattet verdaulich hinbiegen könnte. Aber Fakt ist, daß wir damit bereits auf dem besten Weg zu unleserlichem Code sind, weil dessen Komplexität sich einem Schwellwert nähert, der durch das typische menschliche Gehirn und dessen Belastbarkeit vorgegeben ist. Da ist stets bei etwa "ein Dutzend" (12) gleichzeitig wirkenden Details Schluß mit Lustig. Mehr wird zunächst anstrengend, viel mehr unverdaulich.

Die Lösung besteht im Zerlegen von komplexen Vorgängen in einfache Häppchen. Das hat einen netten Nebeneffekt (der durchaus auch mal Hauptziel sein kann): Die kleinen Häppchen lassen sich oft genug wiederverwenden. Weil beim Zerlegen komplexer Vorgänge ganz einfach fast immer ein ausreichender Anteil von Elementen anfällt, die halt nun mal immer wieder ähnlich gebraucht werden. Sie werden das selbst schnell genug in ihren eigenen Experimenten mitkriegen...

Diese Zerlegung ist damit auch eine Methode zur Vermeidung von Wiederholungen im Programm. Und DAS hatten wir als eine der 4 Basis-Techniken von restlos sämtlichen Programmiersprachen kennengelernt. Für die Vermeidung von Wiederholungen im Programm kennen wir bereits die Schleifen. Die gestatten es, Wiederholungen unter genau der Bedingung im Programm zu vermeiden, wenn diese an einem Ort im Programm unmittelbar hintereinander auftreten. Unsere Häppchenzerlegung nun gestattet es, Wiederholungen im Programm zu vermeiden, wenn diese beliebig verstreut auftreten.

Die Technik nennt sich "Funktionen", und Sie kennen die bereits: Die sind aus der Schulzeit vertraut ab Klasse 8, auch wenn Sie die dort eher in der Bedeutung als Axiome kennengelernt hatten, die, wenn einmal definiert, über ganze Rechenblätter hinweg ihre Bedeutung beizubehalten hatten. Bei uns in der ablauf-orientierten Programmierung verwenden wir sie als Aktionen, die genau in dem Moment wirken, wo sie ausgeführt werden. Und dann erstmal nur passiv herumhängen, bis sie wieder mal ausgeführt werden.

Außerdem haben wir intuitiv - in Analogie zu dem, was aus der Schule bekannt und gewohnt ist - Funktionen schon in unseren Experimenten verwendet. Allerdings bisher nur welche aufgerufen, die bereits vorkonstruiert waren. "echo" ist eine solche Funktion; "print_r" zum Darstellen von strukturierten Daten ebenfalls. Das "floor" von vorhin auch. Den Test "isset" hatten wir ebenfalls bereits verwendet. Im Kapitel zum Schutz vor Hacking haben wir erste Funktionen zur Umkodierung von Text kennengelernt ("htmlspecialchars" und "urlencode").

Jetzt gehen wir dazu über, diese Geräte selbst zu entwickeln!
PHP-Funktionen selbst bauen
Das Grundprinzip der Funktionen: Sie schnappen sich einen Haufen von Anweisungen, schnüren ihn zu einem Paket und geben diesem einen (von Ihnen vollkommen frei wählbaren) Namen. Natürlich machen Sie das vor allem mit Anweisungshaufen, die sich im Programm verstreut immer wieder mal wiederholen. Sonst lohnt sich der Aufwand nicht. Um einen solchen Haufen von Anweisungen an einer bestimmten Stelle im Programm ausführen zu lassen, lassen Sie ihn unter seinem Namen "aufrufen".

Übrigens ist diese Maßnahme für Sie in keinster Weise neuartig. Sie haben das Denk-Prinzip schon als Kind ab dem zarten Alter von 6 Jahren beherrschen gelernt: Schon bei unserem alltäglichen "dekadischen Zählsystem" tritt es in gleicher Weise auf: Ein Haufen von Objekten wird einfach in Paketen gebündelt und unter etwas anderer Bezeichnung (im Zählsystem: an etwas anderer "Stelle") als Bündel anstelle der einzelnen Objekte weiterverwendet.

Bei der Definition und beim Aufrufen sind gewisse syntaktische Formalismen einzuhalten - wie Sie das schon gewohnt sind. Um uns da heranzutasten, werfen wir erstmal einen Blick auf das Schulwissen der 8. Klasse zurück...

Funktionen im Schulwissen
In der Schule hatten Sie Funktionen definiert in der Form von zum Beispiel:
f(x) = a*x + b
Dieses Beispiel hatten Sie kennengelernt als "Definitions-Gleichung" einer "Funktion in Abhängigkeit von der Variablen x" oder kurz gesprochen: "f von x". Das "x" war in dieser Definition ein "Platzhalter" - etwas, was in einer KONKRETEN ANWENDUNG durch irgendwas zu ersetzen war. Die Elemente "a" und "b" waren Konstanten, die irgendwie irgendwo anders definiert sei mußten.

Dabei stellte die linke Seite dieser "Definitionsgleichung" die prinzipielle Syntax dar, mit der die Funktion in einer konkreten Anwendung "benutzt" alias "aufgerufen" werden konnte, während die rechte Seite darstellte, wie die Funktion in Abhängigkeit der Funktions-"Variablen" alias -"Parameter" funktionieren sollte.

Sie konnten diese Funktion an anderen Stellen aufrufen, indem Sie einen konkreten Wert oder eine beliebigen anderen Term (was sogar selbst wieder eine ganze Formel sein konnte) anstelle der Variablen x einsetzten, zum Beispiel:
y = f(10)
Funktionen in PHP
In formalen Programmiersprachen wird das weitgehend identisch zu dieser grundlegenden Schul-Syntax ausgedrückt. Und zwar aus gutem Grund: Programmierung SOLL sehr wohl schon für Schüler erlernbar sein!

Nur daß in üblichen formalen Programmiersprachen noch ein paar Kleinigkeiten an Formalismen hinzukommen. In PHP sind das:
  • Die Definition wird mit dem Schlüsselwort "function" angekündigt
  • Der Inhalt der Funktion wird nicht als einzelner, simpler Term hinter ein Gleichheitszeichen geschrieben, sondern als Block von beliebig vielen Anweisungen, die in geschweifte Klammern eingefaßt sind. Da wir in PHP eine ablauf-orientierte Programmiersprache (statt einer axiom-orientierten wie in der Schule) haben, werden diese Anweisungen beim Aufruf der Funktion einmalig ausgeführt (statt ab Definition ununterbrochen bis in alle Ewigkeit zu gelten, wie Sie sich das Denken in der Schule angewöhnt hatten).
  • Soll eine Funktion einen Wert berechnen, der an der Stelle des Funktionsaufrufs anstelle dieses Funktionsaufrufs weiterverwendet wird (wie beim y = f(10) in der Schulsyntax), muß dieser Wert am Ende der Funktionsanweisungen mit einer "return"-Anweisung (englisch; zu deutsch: "zurückgeben") zurück an den Aufruf-Punkt übergeben werden.

Die Funktions-Definition aus dem Schul-Beispiel formal in PHP:
function f($x)
{
    return a * $x + b;
}
Da es im Schul-Beispiel nur um eine extrem simple Sache geht, besteht die resultierende PHP-Funktion aus nur einer einzigen Anweisung, und zwar einer return-Anweisung.
Gegenüber der Schul-Syntax sind hinzugekommen: Man kann sich dran gewöhnen: Die Schlüsselworte stören nicht wirklich, weil die meisten Funktionen recht üppige Inhalte haben, wo dieses bißchen extra-Schreiberei in der Masse untergeht. Die geschweiften Klammern und das Semikolon werden Ihnen noch oft genug als ausgesprochen hilfreich zur Strukturierung vorkommen. Und das Dollarzeichen ist historisch bedingt (weil PHP aus einer Shell-Script-Sprache entstanden ist). Nehmen Sie's einfach so hin!

Der Funktions-Aufruf aus dem Schulbeispiel formal in PHP:
$y = f(10);
Hier ist der Unterschied marginal und schon genannt:
  • das Semikolon am Ende jeder Anweisung
  • das Dollarzeichen bei jedem Variablennamen

Man kann Funktionen natürlich auch mit beliebig vielen Parametern definieren und benutzen, zum Beispiel:
function f($x, $y)
{
    return ax * $x + ay * $y + b;
}

$z = f(10,42);
Wo immer man einen Wert benötigt, kann man diesen durch eine Funktion berechnen lassen und den Funktionsaufruf dazu in einen Ausdruck (wo halt der Wert gefordert ist) einsetzen. Dazu können Funktionsaufrufe NATÜRLICH AUCH beliebig geschachtelt werden, zum Beispiel:
$z = f(f(2,3),42);
Wobei es auf der Ebene nix neues mehr zu lernen gibt - solange Sie einfach nur bereit und in der Lage sind, die grenzenlose Allgemeingültigkeit der Funktionen zu akzeptieren.

Übung zu Funktionen
Eine kleine Übung, bevor wir an ernsthaftere Anwendungen gehen...
Sie erinnern sich an die "PHP-Rechenexperimente mit HTML-Struktur"?
Wir hatten dort einen Programmcode-Abschnitt, in dem wir arithmetische Operationen ausprobiert hatten...
<?php
echo '<pre class="code">
Addition:       3 + 2 = '.(3 + 2).'
Subtraktion:    3 - 2 = '.(3 - 2).'
Multiplikation: 3 * 2 = '.(3 * 2).'
Division:       3 / 2 = '.(3 / 2).'
Rest-Division:  3 % 2 = '.(3 % 2).'
</pre>';
?>

Aufgabe: Probieren Sie dasselbe für die beiden Zahlen "4" und "2" aus!

Lösung bisher: Sie müssen mit Ihren bisherigen Fähigkeiten den gesamten Block nochmal schreiben.
OK: Sie HABEN bereits ein Mittel zum Vereinfachen kennengelernt: Copy & Paste .
ABER: Das vereinfacht nur Ihre Handgriffe, nicht den resultierenden Programmcode.
Es entsteht:
<?php
echo '<pre class="code">
Addition:       4 + 2 = '.(4 + 2).'
Subtraktion:    4 - 2 = '.(4 - 2).'
Multiplikation: 4 * 2 = '.(4 * 2).'
Division:       4 / 2 = '.(4 / 2).'
Rest-Division:  4 % 2 = '.(4 % 2).'
</pre>';
?>

Wenn Sie jetzt gar NOCHMAL und dann vielleicht NOCH EIN WEITERES MAL - und wenn's ganz schlimm kommt noch ein Dutzend weitere Male diese Aufgabe bekommen, werden Sie meschugge vor lauter Schreiberei!

Aufgabe: Schreiben Sie eine Funktion, die diesen Block von Anweisungen mit zwei Parametern statt zwei Konstanten ausführt!
Dabei könnte folgendes herauskommen (Ihre Phantasie darf walten):
<?php
function Arithmetik_Test($v1,$v2)
{
    echo "<pre class="code">\r\n"
        .'Addition:       '.$v1.' + '.$v2.' = '.($v1 + $v2)."\r\n"
        .'Subtraktion:    '.$v1.' - '.$v2.' = '.($v1 - $v2)."\r\n"
        .'Multiplikation: '.$v1.' * '.$v2.' = '.($v1 * $v2)."\r\n"
        .'Division:       '.$v1.' / '.$v2.' = '.($v1 / $v2)."\r\n"
        .'Rest-Division:  '.$v1.' % '.$v2.' = '.($v1 % $v2)."\r\n"
        ."</pre>\r\n";
}
?>

JETZT ist es ein Kinderspiel, alle möglichen Zahlenpärchen auszuprobieren:
Arithmetik_Test(2,2);
Arithmetik_Test(3,2);
Arithmetik_Test(4,2);

Aufgabe: Ab damit in Ihren eigenen Code! In eine extra Experimentierdatei!
Legen Sie sich eine neue Datei dafür an (ein weiteres Kapitel unter dem Titel "PHP-Einführung" wäre sinnvoll) und verlinken Sie diese in Ihrem Lehrgangs-Inhaltsverzeichnis!

Sieg-Auswertung mit Funktionen formuliert
Jetzt zur ernsthaften Anwendung im TicTacToe...

Umgangssprachliche Formulierung
Die Sieg- und Remis-Auswertung muß in die Ausführung des Spielzuges eingebaut werden. Unser bisheriger Code sah dort in etwa wie folgt aus:
if ($spielfeld[$zug] == '0')  // Spielzug erlaubt?
{
    $spielfeld[$zug] = $dran;
    $dran = $nächster[$dran]; // Spielerwechsel nur bei korrektem, angenommenem Zug
}
Das müßte für eine Reaktion auf Sieg und Remis ergänzt werden durch etwa folgende umgangssprachlich ausgedrückte Aktionen:
if ($spielfeld[$zug] == '0')  // Spielzug erlaubt?
{
    $spielfeld[$zug] = $dran;
    
    Wenn Sieg für $dran, dann
    {
        Irgendwie auf der Bedienoberfläche den Sieg herausschreien!
        Außerdem ist KEINER mehr dran! Das Spiel ist zuende!
    }
    Sonst: Wenn Remis, dann
    {
        Irgendwie auf der Bedienoberfläche das Remis herausschreien!
        Außerdem ist KEINER mehr dran! Das Spiel ist zuende!
    }
    Sonst:
        $dran = $nächster[$dran]; // Spielerwechsel nur bei korrektem, angenommenem Zug
}
Nun wissen wir ja aus dem Abschnitt Formale Beschreibung für "Sieg", daß allein der Ausdruck "Sieg für $dran" für unsere Anfänger-Sicht schon extrem kompliziert aussieht. Wir brauchten mehrere Seiten Bildschirmfüllung, um dies umgangssprachlich auszudiskutieren. Im Programm wird das zwar deutlich kondensierter herauskommen, aber unsere Spielzugannahme wäre nicht nur schwieriger zu erkennen, sie wäre auch für Profis nur mit viel Aufwand überschaubar.
Formalisierung mit Funktionen
WENN wir aber diese Sieg/Remis-Beurteilung als Funktionen organisieren, die wir hier aus unserem Codeschnipsel heraus nur aufrufen - und zwar sinnvollerweise unter "sprechenden Namen", die genau DAS ausdrücken, was diese Funktionen auch TUN -, dann können wir diesen Code in seiner formalen Darstellung FAST wie Umgangssprache schreiben (und ebenso trivial verstehen):
if ($spielfeld[$zug] == '0')  // Spielzug erlaubt?
{
    $spielfeld[$zug] = $dran;

    if (Sieg($spielfeld,$zug,$dran))
    {
        $status = '<div class="sieg'.$dran.'">'.$namen[$dran].' hat gesiegt!</div>';
        $dran = '';
    }
    elseif (Remis($spielfeld,$zug,$dran))
    {
        $status = '<div class="remis">Remis!</div>';
        $dran = '';
    }
    else
        $dran = $nächster[$dran]; // Spielerwechsel nur bei korrektem, angenommenem Zug
}

Hier wurde vorausgesetzt, daß wir schon irgendwo
  • eine Variable $status für eine Statusmeldung auf der Oberfläche vorbereitet haben
  • ein Array $namen für die Namen der (beiden) Mitspieler vorbereitet haben
  • die Funktionen "Sieg" und "Remis" vorbereitet haben
  • die Darstellung der Remis- und Sieg-Meldung mittels CSS im Detail festlegen (irgendwelche hübschen, brutal ins Spielerauge dreschenden Effekte für die Klassen "remis", "sieg1" und "sieg2")
Die ersten beiden Angelegenheiten sind dermaßen trivial, daß ich die Ihnen zum formulieren überlasse. Als Namen können Sie erstmal IRGENDEINEN Quatsch einsetzen! Wir kümmern uns heute noch um die Übernahme derselben... Die CSS-Auszeichnung kommt ebenfalls später...

Aufgabe: Ab in Ihren Code damit! In eine neue Datei-Variante für's TicTacToe!
Frage: Wozu brauchen wir die drei Parameter in den Parameterlisten dieser Funktionen?
Schrittweiser Top-Down-Entwurf
...Nun funktioniert Ihr Programm allerdings noch nicht: Der PHP-Interpreter schmeißt das Handtuch und liefert Ihnen eine völlig leere Seite (oder eine Fehlermeldung, wenn das in den Server-Optionen so eingestellt wurde). Das ist auch korrekt so, denn die beiden Funktionen haben wir ja noch nicht definiert. Das tun wir aber jetzt. Ich spiele mit Ihnen dazu eine Vorgehensweise durch, die man auch "Top-Down-Entwurfsmethode" nennt...

Legen Sie sich erstmal die Definitionen für beide Funktionen als völlig leere Rümpfe an!
function Sieg($spielfeld,$zug,$dran)
{
}

function Remis($spielfeld,$zug,$dran)
{
}
Aufgabe: Ab in Ihren Code damit!
Damit haben wir das Programm zumindest schon mal wieder syntaktisch funktionsfähig, so daß uns unser TicTacToe wieder angezeigt werden kann. Allerdings ohne daß sich bisher irgendwas am verhalten geändert hätte.

Da unsere Funktionen völlig ratze-kahl sind, liefern sie natürlich auch absolut NIX als Wert an ihre Aufrufstelle zurück.
An ihrer Aufrufstelle wird nun aber eine Bedingung getestet (halt OB Sieg bzw. Remis vorliegt).
Eine Bedingung ist ein boolscher Wahrheitswert (wahr oder falsch), nicht aber GAR NIX!
Das widerspricht sich doch!?!

Tipp: An dieser Stelle (und an jeder anderen auch, wo irgendwelche Werte irgendwie verwendet werden) kommt ein Automatismus des PHP-Script-Interpreters zur Wirkung, der sich um das der Intuition des Programmierers entgegenkommende Zurechtzaubern (typecasten) von Werten kümmert.
In unserem konkreten Fall (GAR NIX returned) bedeutet das: aus dem "NIX" wird implizit ein "false".
Je nachdem, wie strikt die Fehlerberichterstattung des PHP-Interpreters in der Server-Konfiguration eingestellt ist, wird eventuell bei einer solchen impliziten Wandlung aus "GAR NIX" ein Hinweis gemeldet, daß da eventuell ein Irrtum vorliegen können täte. Womit der Interpreter bei uns ja gar nicht soooo weit daneben liegen würde, weil wir ja nicht wirklich vorhaben, in den Funktionen GAR NIX zu berechnen.
Formalisierter Sieges-Test
Ja, also: Wie war das jetzt mit dem "Sieg"? Wir hatten das vorhin schon mal "schöngeistig" (umgangsprachlich) ausdiskutiert: Ein Sieg liegt genau dann vor, wenn durch den gesetzten Stein gebildet wurde (mindestens eines davon):
  • eine waagerechte Reihe: die Zeile, in der gesetzt wurde
  • eine senkrechte Reihe: die Spalte, in der gesetzt wurde
  • eine diagonal steigende Reihe: die steigende Diagonale - wenn der gesetzte Stein auf dieser lag
  • eine diagonal fallende Reihe: die fallende Diagonale - wenn der gesetzte Stein auf dieser lag
(Den Test, ob der gesetzte Stein auf der Diagonale liegt, kann man sich auch schenken: Falls er nicht liegt, ist es nicht falsch, die Diagonale zu testen, nur überflüssig. Wenn aber durch die Vermeidung einer Überflüssigkeit die Komplexität deutlich stärker ansteigt als die Menge an vermiedener Überflüssigkeit, darf man sich als Programmierer ruhig entschließen, einen einfacheren Quelltext vorzuziehen...)

Und wir hatten auch ausführlichst die genauen Feldindizes ausdiskutiert, aus denen sich diese Reihen zusammensetzen. Das können wir fast wörtlich aus der Umgangssprache in Programmcode übertragen - WENN wir wieder mal unseren Top-Down-Ansatz verfolgen und unser neues Werkzeug "Funktionen" benutzen:
function Sieg($spielfeld,$zug,$dran)
{
    return  Reihe_voll($spielfeld,       $feld % 3     , 3, $dran)  // senkrecht
        ||  Reihe_voll($spielfeld, floor($feld / 3) * 3, 1, $dran)  // waagerecht
        ||  Reihe_voll($spielfeld,                    0, 4, $dran)  // diagonal steigend
        ||  Reihe_voll($spielfeld,                    2, 2, $dran)  // diagonal fallend
        ;
}
Die Funktion "Reihe_voll" legen Sie auch erstmal als Dummy an, damit das Programm weiter ausführbar bleibt, wenn auch noch immer nicht mit funktionierender Sieges-Erkennung:
function Reihe_voll($spielfeld, $startfeld, $inkrement, $dran)
{
}
Aufgabe: Ab in Ihren Code damit!

Die Funktion "Reihe_voll" grast einfach eine Reihe von Plätzen auf dem Spielfeld nach einer vorgegebenen Steinfarbe ($dran) ab, wobei die Menge der Plätze immer gleich ist (vom Spieltyp "3-in-einer-Reihe" vorgegeben), und die Reihe einfach durch ein Startfeld und eine Schrittweite über dem Spielfeld definiert wird, wobei durch das Voranschreiten um diese Schrittweite jede der vier bei uns zu unterscheidenden Reihentypen entstehen kann.

Diese Funktion (auf der untersten Ebene unserer Top-Down-Hierarchie) ist dementsprechend geradezu trivial aufgebaut:
function Reihe_voll($spielfeld, $startfeld, $inkrement, $dran)
{
    for($i = 0; $i < 3; $i++)
    {
        if ($spielfeld[$startfeld+$inkrement*$i] != $dran) return false;
    }
    return true;
}
(Wenn irgendeines der Felder nicht mit der geprüften Steinfarbe belegt ist, dann ist die Reihe nicht voll, sonst sehr wohl.)
Aufgabe: Ab in Ihren Code damit!
Sieger-Ehrung
Ab hier kann Ihr Programm schon einen Sieg feststellen. Behält das aber bislang intern für sich. Auf der Spieloberfläche findet sich noch kein Pixel Hinweis! Das ändern wir jetzt natürlich! Unsere vorbereitete Variable $status wird jetzt zur Anzeige gebracht! Damit können wir auch endlich wieder vorzeigbare Ergebnisse sehen!

Sie ahnen sicherlich, daß wir dazu wieder ein ganz klein bißchen HTML und eine ordentliche Portion CSS benötigen...

Wohin mit der Status-Anzeige?

Ich schlage vor, den Status zwischen den beiden Lämpchen der "Dran"-Anzeige zu platzieren. Und den Start-Schalter unmittelbar darunter. Damit wir so wenig wie möglich Arbeit haben, bietet es sich an, die Status-Anzeige als HTML genau zwischen den Zeilen für die Lämpchen und den Startschalter einzuschieben! Das könnte in etwa folgendermaßen aussehen:
<div class="panel">
    <div class="lamp black<?=$dran1?>"<?=$dran1hinweis?>><img src=...
    <div class="lamp white<?=$dran2?>"<?=$dran2hinweis?>><img src=...
    <div class="status"><?=$status?></div>
    <input type="submit" name="start" value="Start!" title="Spiel (neu) starten!"/>
</div>
Aufgabe: Ab in Ihren Code damit!

Wenn wir das laufen lassen, bekommen wir bereits etwas zu sehen, was aber ein kaltes Grausen hervorrufen dürfte...
(Ich habe hier im Link mal einen Spielstand mit einer soeben entstehenden weißen Reihe vorgegeben, das können Sie sich für Ihr eigenes Experiment übernehmen (oder auch was selbst gebasteltes - wie Sie lustig sind). Noch besser wäre freilich, mal wieder einen neuen Eintrag in Ihr Lehrgangs-Inhaltsverzeichnis zu setzen und die URL dort vorzubelegen...)

Das Spiel stellt zwar den Sieg fest und stellt die Klickbarkeit der Plätze auf dem Spielfeld ab. Aber die Ausschrift ist mickrig und schwarz auf dem dunklen Mahagony-Untergrund, so daß man sie überhaupt nicht erkennen kann! Außerdem ist der Start-Button weiter nach unten gerutscht.

Da müssen wir wieder mal Hand anlegen. Intelligenterweise haben wir die Bereiche der Statusmeldung schon ausführlich mit class-Attributen versehen, so daß es ein leichtes ist, die zugehörigen CSS-Beschreibungen zuzuordnen und genau passend auszuformulieren:
.panel input
{
    ...
    top:             10px;
    ...
}

/* Statusmeldungen */
.status
{
    margin:          13px 90px;
    border:          1px inset #480915;              /* Fallback für veraltete Browser */
    border:          2px inset rgba(72,9,21,0.6);
    border-radius:   5px;
    padding:         5px;
    
    color:           white;
    font-weight:     bold;
    font-size:       20px;
}
.status .remis
{
    color:           #00f;
}
.status .sieg1
{
    color:           #000;
}
.status .sieg2
{
    color:           #fff;
}
Wobei die Schriftfarbe allerdings noch immer sehr Geschmackssache ist. Weder das Blau für Remis noch das Schwarz für den Sieg von Schwarz ist das Gelbe vom Ei. Anders sieht das aus mit gewissen Schrifteffekten, die inzwischen von allen Mainstream-Browsern (außer Internet Explorer) unterstützt werden. Und zwar "text-shadow": Auch das ist weitgehend Geschmackssache, aber auf jeden Fall deutlich besser anzusehen als ohne Effekt. Alternativ könnte der Hintergrund fallabhängig umgefärbt werden oder auch ganz rabiat mit Bildern gearbeitet werden, die man in einem Programm wie Gimp, Photoshop oder Paintshop erzeugt. Oder man gestaltet diese Anzeige als leuchtenden Button (dann wieder mit CSS möglich). Sie dürfen da ihrer Phantasie völlig freien Lauf lassen und sich z.B. Anregungen aus den verlinkten Tutorials holen.

Aufgabe: Ab damit in Ihren Code!
Das Ergebnis könnte zum Beispiel SO aussehen... (weißer Sieg) oder SO... (schwarzer Sieg)

So schwer war das doch gar nicht, oder?! Zum Schluß kamen bloß rund 15 Zeilen PHP und 20 bis 50 Zeilen CSS (je nach Lust zum Basteln auch noch mehr) heraus. Und so, wie wir es formuliert haben (mit sprechenden Namen für Funktionen und CSS-Selektoren), können wir jederzeit auf den ersten Blick erkennen, worum es im PHP- und CSS-Code ging.

Kleine Pause zwischendurch...!

Remis-Auswertung mit Funktionen
So, noch ein letzter Spurt vor der ersten großen Pause...
Dasselbe, was wir eben für die Siegauswertung zusammengebastelt haben, kommt jetzt nochmal für die Remisauswertung!

Wann liegt ein Remis vor?
Wir sehen mal davon ab, daß sich zwei Spielpartner in einem komplexen Spiel, dessen Ende nicht absehbar ist, freiwillig auf den Verzicht des Weiterspielens einigen können. Dafür ist das TicTacToe zu einfach!

Dann liegt ein Remis genau dann vor, wenn keiner der beiden Partner mehr einen Zug machen kann, der zu einem Gewinn führt - selbst wenn der Gegner alles tun würde, um den Partner siegen zu lassen.

Welche Bedingung ist hinreichend dafür, daß wir mit Sicherheit ausschließen können, irgendeine Reihe jemals voll zu kriegen?

Wie ermitteln wir das?
Was halten Sie davon, stur mechanisch alle Plätze des Spielfeldes durchzugucken, falls dort ein Stein gefunden wird, diesen in einer Liste der von ihm beeinflußten Reihen einzutragen, und zum Schluß zu prüfen, ob irgendeine Reihe noch nicht vom Gegner besetzt ist?

Wir bräuchten dafür ein Modell aller Reihen, und zwar einen Zähler pro Reihe (wir hatten die ganz zu Anfang schon mal diskutiert)...
Aber aus Sicht zweier verschiedener Gegner! Also zwei Modelle aller Reihen!
$reihen = array
(
    '1' =>  array
    (
        'senkrecht'         =>  array(0,0,0),
        'waagerecht'        =>  array(0,0,0),
        'diagonal_steigend' =>  0,
        'diagonal_fallend'  =>  0,
    ),                      
    '2' =>  array
    (                       
        'senkrecht'         =>  array(0,0,0),
        'waagerecht'        =>  array(0,0,0),
        'diagonal_steigend' =>  0,
        'diagonal_fallend'  =>  0,
    ),                      
);                          
Aufgabe: Ab damit in Ihren Code! In die Funktion "Remis()", die Sie schon als Dummy angelegt hatten!

Das Zuordnen eines Platzes auf dem Spielfeld zu seinen zugehörigen Reihen beim Durchzählen sieht dem Verfahren beim Test auf Sieg entfernt ähnlich:
for($z = 0; $z < 3; $z++)
{
    for($s = 0; $s < 3; $s++)
    {
        $i = $z * 3 + $s;

        if ($spielfeld[$i] != '0')
        {
            $reihen_ausgewählt = &$reihen[$spielfeld[$i]];
            
            $reihen_ausgewählt['senkrecht' ][$s]++;
            $reihen_ausgewählt['waagerecht'][$z]++;
            if ($z ==   $s) $reihen_ausgewählt['diagonal_steigend']++;
            if ($z == 2-$s) $reihen_ausgewählt['diagonal_fallend' ]++;
        }
    }
}
-> Es wird mit jedem gefundenen Stein in der zu dessen Platz passenden senkrechten, waagerechten, diagonal steigenden und diagonal fallenden Reihe der jeweilige Zähler erhöht (mit dem Operator "++"). In den Diagonalen natürlich nur, wenn der Stein auch auf der Diagonalen lag. Und da wir die Zeilen und Spalten hier eh explizit durchzählen, brauchen wir die nicht so kompliziert wie beim Siegestest aus der linearen Platznummer ermitteln (das darf uns freuen).

Aufgabe: Ab damit in Ihren Code! In die Funktion "Remis()"!

Tipp: Referenzen:
Nebenbei habe ich Ihnen hier mal wieder ein neues Konstrukt vorgestellt, und zwar diesen ominösen "&"-Operator beim Zugriff auf die Reihenzähler:
&$reihen[...]
Hier wird eines der beiden GROSSEN Arrays der Reihenzähler gegriffen. Die waren - das haben Sie hoffentlich im Kasten drüber erkannt - in mehreren geschachtelten Ebenen organisiert:
  1. erst eine Unterscheidung hinsichtlich der Farbe (bei und '1' und '2')
  2. dann eine Unterscheidung hinsichtlich Richtung (senkrecht/waagerecht usw...)
  3. dann noch eine dritte Unterscheidung hinsichtlich des Index in der jeweiligen Richtung (allerdings bei uns nur für senkrechte und waagerechte Richtung nötig)
Wenn wir nun in der ersten Ebene (nach Farbe) auswählen, hätten wir normalerweise eine KOPIE (!) des gesamten Baumes der Zähler, die zur jeweiligen Farbe gehören, "in der Hand" (bzw. in der Variablen, in der wir das Zeug zwischenspeichern - hier $reihen_ausgewählt).

Das hätte gleich zwei für uns schlechte Wirkungen:
  1. Wenn wir in den Daten der Zwischenvariablen etwas manipulieren würden, müßten wir das Ergebnis anschließend noch vor Ende der inneren Schleifenaktion jedesmal wieder in das Gesamtarray zurückschreiben, weil bei jedem Schleifendurchlauf am Beginn der Schleifenaktion immer von ganz oben im Gesamtarray beginnend ausgewählt und die Zwischenvariable dabei überschrieben werden würde.
  2. Bei jeder dieser Kopieroperationen (wovon wir also gleich zwei bei jedem Schleifendurchlauf brauchen würden) würde immer der gesamte Unterbaum (mit vier Elementen, wovon zwei selbst wieder ein Array sind) kopiert werden müssen. Das ist einfach ein komplett sinnloser numerischer Aufwand, der bei etwas größeren Datensammlungen das Programm sehr schnell sehr langsam macht! ;->
Dieses sinnlose Gewurschtel kann unterdrückt werden, indem wir beim Auswählen des Unterbaumes nicht den gesamten Unterbaum per Kopie übernehmen, sondern nur einen Verweis (Referenz, Zeiger, Pointer) auf den Unterbaum setzen. Genau DAS macht dieser Operator "&".

Jetzt müssen noch alle Zähler ausgewertet werden auf der Suche nach IRGENDEINER Reihe, in der noch eine Null steht. Wenn wir KEIN EINZIGES freies Feld mehr finden (für keinen der beiden Spieler), liegt ein Remis vor.
...Wobei es jetzt nicht sooo schwer wäre, das ganze in einer Schleifenkonstruktion zu formulieren. Aber da wir ein geschachteltes Array vorliegen haben, welches auch noch eine gewisse Unregelmäßigkeit in seiner Organisation aufweist, macht sich eine Formulierung mit einer Funktionsauslagerung doch deutlich leichter:
return alle_reihen_besetzt($reihen['1']) && alle_reihen_besetzt($reihen['2']);
Aufgabe: Ab damit in Ihren Code! In die Funktion "Remis()"! Damit sind wir dort erstmal fertig. Für die Funktion "alle_reihen_besetzt" können Sie kurzzeitig ja nochmal ein Dummy anlegen. Sie wissen inzwischen, wie das geht...

Und die Funktion "alle_reihen_besetzt()" soll nun in einem unregelmäßig hierarchisch geschachtelt organisierten Array, dessen Elemente an den äußersten Enden des Baumes jeweils Zahlen sind, testen, ob da keine einzige Null vorkommt.
Dazu ziehen wir uns gleich noch eine Technik rein, die man Rekursion nennt, denn die geht in einem solchen hierarchisch geschachtelten und erst recht in einem unregelmäßigen Fall ganz besonders leicht zu formulieren und zu verstehen:
function alle_reihen_besetzt($reihen)
{
    foreach($reihen as $reihe)
    {
        if (is_array($reihe))
        {
            if (!alle_reihen_besetzt($reihe)) return false;
        }
        else
        {
            if ($reihe == 0) return false;
        }
    }
    return true;
}
Wir gehen hier davon aus, daß das Argument $reihen der Funktion auf jeden Fall ein Array ist. Das wird in seine Einzelteile zerlegt. Falls ein solches Einzelteil selbst noch ein Baum (Ast) ist, wird es weiter zerlegt. Bis nur noch Blätter übrigbleiben.

Tipp: foreach
Dazu verwenden wir hier eine etwas andere Form der for-Schleife, wo man nicht eine Zählvariable durchlaufen läßt, mit der man dann ein Array-Element greift, sondern diese Operationen zusammenfaßt. Also statt:
for($i = 0, $n = count($reihen); $i < $n; $i++)
{
	$reihe = $reihen[$i];
	...
}
schreibt man:
foreach($reihen as $i => $reihe)
{
	...
}
Um die Zählvariable kümmert sich der PHP-Interpreter automatisch.
Liest sich irgendwie deutlich einfacher, oder?!

Wobei wir den Test abbrechen können, sobald wir IRGENDWO eine Null finden: Dann sind nämlich NICHT "ALLE Reihen besetzt", und wir können GENAU DAS abmelden. Und das gilt sowohl für den Fall, daß unser Element ein einfacher Zähler ist, als auch für den Fall, daß es ein ganzer Unterbaum ist.

Aufgabe: Ab damit in Ihren Code!

Tipp: Es könnte jemand berechtigt darauf hinweisen, daß wir beim Remis-Test doch nebenbei auch noch gleich die Aussage über einen Sieg ermitteln können: Da geht's dann halt nur nicht darum, ob MINDESTENS EIN Stein des Gegners IN JEDER REIHE liegt, sondern ob GENAU DREI Steine der eigenen IN MINDESTENS EINER Reihe liegen. Das kann man aber an den Zählern trivial genauso ablesen.
Stimmt: Und wer sich mal in Schleife und Fallunterscheidungen üben möchte, darf das gern mal eben ausformulieren! Alle, die momentan nicht ganz ausgelastet sind, dürfen das als Aufgabe auffassen!

Netterweise hatten wir vorhin in den CSS-Definitionen zur Siegerehrung auch schon die für eine Remis-Bekanntgabe eingebaut, so daß unser Programm jetzt sofort die Remis-Auswertung drauf haben sollte.
Es sollte jetzt in etwa SO aussehen...

Aufgabe: Testen Sie es! Und verlinken Sie das Ergebnis mal wieder in Ihrem Lehrgangs-Inhaltsverzeichnis!

Große Pause zum Abspannen und eventuell Ausprobieren und Weiterbasteln!

Unterscheidung von Spiel-Vorbereitung, -Ablauf und -Abschluß

Obwohl sich dieses Kapitel vom Titel her recht umfangreich anhört, geht es nur um eine Kleinigkeit. Denn einen Teil der Phasen-Unterscheidung haben wir bereits implementiert: Spielablauf und Spielabschluß.

Der letztere war übrigens geradezu trivial: Wir haben einfach bei Feststellung von Sieg oder Remis das weitere Setzen von Steinen unterdrückt und dafür gesorgt, daß der Endzustand im Status bekanntgegeben wird.

Was fehlt ist also nur noch ein ordentlicher Start. Dabei dürfen wir schon mal Überlegungen zur Multiplayer-Variante (die ja ab der nächsten Lektion kommt) vorwegnehmen...
Der Start sollte konsistent zum Spielabschluß sein: Zu Beginn sollte es nicht möglich sein, sofort ein Feld anzuklicken. Weil sich zu Beginn eventuell Spieler erstmal anmelden können sollen und vielleicht sogar mal besondere Spielregeln einstellbar sein sollen und dergleichen Scherze. Da wäre es höchst unpassend, wenn jemand in dieser Phase durch einen versehentlichen Klick ins Spielfeld schon mal den ersten Zug macht!
Statt dessen sollten zu Anfang Spielzüge genauso wie zum Ende gesperrt sein!

Das ist trivial einzurichten:
$dran = ''; // beim ersten Aufruf ist erstmal noch gar keiner dran
(Bisher stand da was von "erster".)
Aufgabe: Ab in Ihren Code damit! Ausprobieren!

Wenn wir unser Spiel starten, stellen wir fest, daß jetzt zuerst "Weiß" dran ist. Bisher war es so (und in den allgemein anerkannten TicTacToe- alias Gomoku-Regeln ist das auch so festgelegt), daß "Schwarz" als erster dran war. Nun sind zwar Farben Schall und Rauch, aber wir wollen es ja vielleicht doch "exakt" machen. Unser Spielstart benötigt noch eine Anpassung...

Bisher steht dort:
// wirksam beim Erstaufruf, wenn Schwarz dran sein soll...
$erster = '1';
$dran = $erster;

...

if (isset($_REQUEST['start']))
{
    $spielfeld = '000000000';
    $erster = $nächster[$erster];   // Wechsel
    $dran = $erster;
}
Bevor wir beim Spielstart den "dran" seienden Spieler festlegen, lassen wir also erst den Erstziehenden wechseln. Da der aber von vornherein schon für den Spielstart korrekt festgelegt war, ist er NACH diesem Wechsel eben NICHT MEHR für den Spelstart korrekt.

Das muß ja nun nicht so rum sein! Die beiden Zeilen tauschen wir einfach und gut ist! Wir bereiten also ab sofort mit dem Wechsel des Erstziehenden den dran seienden Spieler für die NÄCHSTE Runde statt für die AKTUELLE vor.

Sie merken wieder mal, daß wir es hier mit ablauf-orientierter Programmierung zu tun haben! Speicherplätze können fortlaufend ihre Inhalte geändert bekommen, und die Reihenfolge, in der das geschieht, bestimmt das Ergebnis. An diese Überlegungen müssen Sie sich einfach gewöhnen! Als ablauforientierter Programmierer müssen Sie zwingend ihre Systeme in ihrem ablauforientierten Zusammenwirken betrachten!

Aufgabe: Ab in Ihren Code damit (mit dem Tausch!)
Und testen! JETZT sollte die Erst-Starter-Spielregel unseres TicTacToe wieder den internationalen Gepflogenheiten entsprechen!

Da dieser Abschnitt so kurz war, nachen wir sofort mit dem nächsten weiter...

Sieg- und Remis-Zählung

Zu einem ordentlichen Spiel gehört die Möglichkeit, mehrere Runden zu spielen, damit der Unterlegene eine Chance auf Revanche bekommt. Oder weil es einfach Spaß macht. Und dabei gehört es sich, die Gewinne und Remisen zu zählen und mit Punkten zu bewerten.

Übrigens könnte man allein aus dieser Spielbewertung eine Wissenschaft für sich machen, wie ein Seitenblick in die Remis-Regelungen beim Schach zeigt. Wir müssen es aber auch nicht übertreiben. Wir könnten erstmal folgendes einrichten:
  • Jeder Sieg zählt als ein Siegespunkt.
  • Jedes Remis zählt als ein Remispunkt.
  • Sieges- und Remispunkte werden nebeneinander gezählt (nicht wie beim Schach und Fußball irgendwie miteinander verwurschtelt). Das ermöglicht später die freizügigste und gerechteste Bewertung.


Unsere Aufgabe enthält wieder mal Anteile von PHP, HTML und CSS:
Spielerdaten um Punkte-Zähler erweitern
OK, damit es nicht GANZ so trivial wird, wie zu befürchten (und damit auch mal wieder ein neuer Erkenntnisgewinn dabei herauskommt), werden wird die Sache etwas systematisch und vorausschauend angehen...

Datenstrukturen
Wir wollen mit den Zählern für Gewinne und Remisen die Spielerdaten um weitere Details ergänzen. Bisher haben wir für die Spieler lediglich die Namen notiert - und auch das bisher nur aus formellen Gründen, um in den Siegesmeldungen irgendwas sinnvolles hinschreiben zu können.
Das können wir ändern: Sobald wir zum Multiplayer kommen und dort den Beitritt zu Spielen fremder Leute organisieren, müssen wir die Spielerbeschreibungen eh noch um einige Kleinigkeiten (eine Spieler-ID zum Beispiel) erweitern. Wenn also eh noch weitere Details dazukommen werden, können wir doch unsere Spielerdaten gleich anständig bündeln! Sowas nennt man "Datenstruktur". Wobei ich Sie zu DIESEM Begriff nicht auf die Wikipedia verweisen werde, denn dort wird unter Datenstrukturen alles mögliche aufgefaßt, was zu unterschiedlichsten Komplexitäts- und Anwendungsebenen weit oberhalb dessen gehört, was für uns hier auf der untersten Ebene des Sprachkerns relevant ist. Außerdem ist in der Wikipedia zu diesem Thema momentan keine Systematik zu erkennen. Es wurde dort einfach nur alles Kraut, was sich irgendwie unter dem Begriff einordnen läßt, auf einen großen Haufen geschmissen.

UNSER Zweck, wenn WIR HIER von Datenstrukturen sprechen, ist schlicht
  1. das Zusammenfassen von logisch zusammengehörenden Daten in einer Gruppierung, die ein zusammenhängendes Paket im Arbeitsspeicher bildet, so daß man ein komplettes Datenpaket durch einen einfachen Verweis auf einen Bezugspunkt (üblicherweise die Startadresse dieses Dingens bzw. aus Sicht des Programmierers der Variablenname des Pakets, falls man es in einer Variablen abgelegt hat, oder auch ein anonymes Paket in einem Ausdruck ) "in die Hand nehmen" kann (und mit ihm im Block Dinge anstellen kann wie kopieren, löschen, speichern, laden, an Funktionen übergeben).
  2. der systematisch gleichartige Aufbau solcher Datenpakete, so daß man sich in verarbeitenden Funktionen darauf verlassen kann, daß die vom Prinzip immer gleich aufgebaute Daten vorfinden wenn sie mit unterschiedlichen konkreten Datenpaketen gefüttert werden, solange diese nur vom selben "Typ" sind.
Hierbei handelt es sich wieder mal um einen Mechanismus zur Reduzierung von Redundanz in Programmen bzw. zur Vereinfachung der Organisation von Wiederholungen in Daten. Dieses Organisationsmittel gehört daher zu den allgemeingültigen Grund-Techniken der Programmorganisation, also basisbildenden Werkzeugen des Sprachkerns, wie wir sie bereits in Lektion 4 bei der Fallunterscheidung kennengelernt hatten.

In PHP funktioniert die Datenstrukturierung "dynamisch" auf Basis von Arrays. "Dynamisch" bedeutet hier, daß es keine Möglichkeit gibt, Datenstrukturen "mit Gewalt" in eine einmalig fest definierte Form zu zwingen, so daß im Programm eine Garantie festgeschrieben werden kann, daß Datenpakete eines bestimmten Typs immer und überall bestimmte Elemente in einer vorgegebenen Platzierung in sich enthalten. In PHP (wie den meisten anderen Scriptsprachen auch) darf man jedes Datenpaket jederzeit beliebig ändern, also Datenelemente hinzufügen oder welche rausschmeißen.

Diese Dynamik hat Vor- und Nachteile. Mit denen man leben kann...
  • Nachteil: Man muß durch selbstauferlegte Disziplin und gegebenenfalls selbst programmierte Hilsmittel dafür sorgen, daß Datenpakete an bestimmten Stellen, wo man dies so voraussetzen möchte, bestimmte fest definiert strukturierte Inhalte haben. Wobei: Es ist aber trivial, sowas hinzusetzen. Man muß sich halt einfach nur kümmern!
  • Vorteil: Gerade als Anfänger oder wenn man sich in ein Thema gerade erst "hineinprogrammiert" (also Ideenfindung betreibt), ist diese Dynamik angenehm hilfreich, weil man lokal in einem kleinen Bereich mal eben Daten aufbohren oder ändern kann, um lokal was auszuprobieren, ohne das gesamte Programm reorganisieren zu müssen. Wobei: In "statisch typisierten" (also diesbezüglich starrer funktionierenden) Programmiersprachen umgeht man dieses Problem heutzutage durch Werkzeuge, die einem den gesamten Code auf einen Klick hin überarbeiten. Insofern ist es heutzutage dort auch kein Nachteil/Vorteil mehr - WENN man mit solchen Werkzeugen (sogenannten "integrierten Entwicklungsumgebungen") arbeitet. Für uns, die wir erstmal mit einfachen Texteditoren arbeiten, aber sehr wohl.

Nun haben wir ja ein paar einfache erste Datenstrukturen schon kennengelernt: Jetzt kommt eben die nächste größere Anwendung dazu...

Wir haben in PHP mehrere Möglichkeiten, Datenstrukturen zu definieren. Dabei gehen intern (auf unterster Ebene des Interpreters) eh sämtliche Varianten auf Arrays zurück. Und ich werde mich mit Ihnen jetzt nicht verzetteln in oberflächlichem Anstubsen aller möglichen Variationen auf höheren organisatorischen Ebenen, sondern eben diese Arrays einmal anständig beleuchten und ihre Benutzung üben... Insbesondere dürfen Sie sich bei Interesse selbständig die Organisation von sogenannten "Objekten" reinziehen. Wenn Sie einmal die Arrays verinnerlicht haben, benötigen Sie für die Objekte keine einzige graue Gehirnzelle mehr - weil die 1:1 einfach nur anders formulierte rein syntaktische Variationen von Arrays sind. Funktionell mit NULL Unterschied. Aber etwas langsamer als Arrays, weil der Interpreter eben genau nichts anderes macht, als deren Ausdrücke intern auf Arrays abzubilden, was ein ganz klein bißchen extra Verarbeitungszeit kostet.

Die Syntax der Definition und des Zugriffs auf Arrays hatten wir in unseren bisherigen Anwendungen (bei der Fallunterscheidung) ja schon kennengelernt, und zwar in ihrer allgemeingültigsten Form als "assoziatives Array"...
Wenn man nun Datenstrukturen hat, die sich gleichartig in gigantischen Massen wiederholen, werden sich die Indizes in diesen Daten vollkommen sinnlos in eben diesem gigantischen Ausmaß redundant wiederholen. Das kann unter PHP durchaus recht schnell zu Engpässen im Arbeitsspeicher führen, weil außerdem zu jeder atomaren Dateneinheit auch noch eine Menge Verwaltungsdaten für die interne Organisation der Speicherverwaltung gehören. Für solche Fälle kann man unter Umständen eine Alternative verwenden - falls man nicht zwingend freien assoziativen Zugriff benötigt, sondern diesen auf numerische Indizes reduzieren kann. Und das geht so:
// assoziatives Array:
$a = array
(
    'erster'  => 'Datenelementinhalt 1',
    'zweiter' => 'Datenelementinhalt 2',
    'dritter' => 'Datenelementinhalt 3',
);
// Benutzung eines assoziativen Arrays:
$d1 = $a['erster' ];
$d2 = $a['zweiter'];
$d3 = $a['dritter'];

// assoziatives Array mit impliziten numerischen Indizes:
$a = array
(
    'Datenelementinhalt 1',
    'Datenelementinhalt 2',
    'Datenelementinhalt 3',
);
// Benutzung eines implizit numerisch assoziativen Arrays:
$d1 = $a[0]; // Null-basiert
$d2 = $a[1];
$d3 = $a[2];

Die Arrays mit impliziten numerischen Indizes belegen nur etwa die Hälfte des Speicherplatzes. Diese Form sollte man vorziehen, wenn nicht die Benutzung frei wählbarer Zeichenketten als Indizes vom Sinn der Anwendung her vorgegeben ist.

Ungünstig kann sich - insbesondere bei experimentellem Programmieren oder bei sogenanntem "extreme programming" - der Umstand auswirken, daß man bei einer Änderung der Datenstruktur - und zwar wenn man Elemente zwischen den schon vorhandenen einfügt oder aus diesen entfernt - eine geänderte (implizite) Zuordnung von Indizes zu Datenelementen erhält, was dazu führt, daß man das gesamte Programm "refakturieren" müßte.

Dem läßt sich aber durch die Definition von Konstanten und das verbale Ausdrücken des indizierten Zugriffs abhelfen:
// Definition von (numerischen) Konstanten, die als Indizes verwendet werden:
define('erster' ,0);
define('zweiter',1);
define('dritter',2);

$d1 = $a[erster ]; // KEINE Anführungszeichen um Indizes!
$d2 = $a[zweiter]; // Das sind jetzt "Konstanten"!
$d3 = $a[dritter];
Wobei eventuell ein wenig komisch erscheint, daß die Konstanten-Bezeichner bei ihrer Definition als Zeichenkette angegeben werden müssen, bei ihrer Benutzung aber wie Schlüsselwörter (ohne Anführungszeichen)...
Das IST eben so. Die Erfinder der PHP-Sprache WOLLTEN das so. Sie haben das einfach nur hinzunehmen!

Wenn Sie Ihre implizit indizierten Arrays auf diese Weise organisieren, haben Sie gleichzeitig Speicherreduzierung und flexible Indizierung auf Ihrer Seite.

Um in PHP eine gewisse Konstanz der Datenstrukturen zu erzwingen, kann man die Daten durch Kopie aus Muster-Daten...
$neuer_Datensatz = array
(
    'Datenelementinhalt 1',
    'Datenelementinhalt 2',
    'Datenelementinhalt 3',
);

$d = $neuer_Datensatz;

...oder durch Aufruf einer Konstruktor-Funktionen...
function neuer_Datensatz()
{
    return array
    (
        'Datenelementinhalt 1',
        'Datenelementinhalt 2',
        'Datenelementinhalt 3',
    );
}

$d = neuer_Datensatz();

...erzeugen lassen. Dann ist zumindest gewährleistet, daß die Dinger eine standardisierte Mindest-Füllung besitzen. Wenn man die dann nicht gerade bewußt versaubeutelt, behalten die diese einheitliche Struktur auch bei

Tipp: KISS : Übrigens ist diese Gegenüberstellung eben ein hübsches, sofort ins Auge stechendes Beispiel, wie Sie durch krampfhaftes Festkrallen an Methoden, die gemeinhin der "objektorientierten" Ausdrucksweise zugeschrieben werden (hier: Konstruktor-Funktion anstelle von Muster-Daten) Ihr Programm sinnlos verkomplizieren und Ihre Zeit sinnlos verbraten können. Bei mir DÜRFEN Sie durchaus beides, aber EMPFEHLEN werde ich mitnichten die komplizierteren Varianten.
Die Variante mit Konstruktorfunktion wird genau dann eine Notwendigkeit darstellen, wenn Sie Ihre Daten beim Erzeugen mit variablen Elementen füllen wollen. Das und verschiedenes anderes werden wir gleich anwenden...
Spieler - Datenstruktur
Nach dem Theorie-Marathon mal wieder etwas praktisches...
Wir erinnern uns, daß wir EIGENTLICH nur die Trivialität von ein paar Sieges- und Remispunkten notieren wollten. Allerdings unter dem Aspekt der weisen Voraussicht auf noch weitere kommende Daten, die alle zu einem einzelnen Spieler zugeordnet werden müßten. Wir führen deshalb eine Datenstruktur für Spieler ein, wo sämtliche Daten, die einem Spieler zugeordnet sind, zusammengefaßt werden. Für ein einzelnes Spiel brauchen wir erstmal nur genau zwei Spieler. Die unterscheiden sich zunächst mal - vom Start weg - durch ihre Namen (welche wir zunächst an der Bedienoberfläche brauchen, um die "Siegerehrung" für ein gewonnenes Spiel darstellen zu können). Daher ist hier eine Konstruktorfunktion angebracht:
$spieler = array
(
    '1' => neuer_spieler('Schwarz'),
    '2' => neuer_spieler('Weiß'   ),
);

function neuer_spieler($name)
{
    return array
    (
        'name'   => $name,
        'punkte' => array(0,0),
    );
}
...wobei ich zum Zwecke der Übung mal einen Teil (die äußere Ebene) der Datenstruktur als Array mit benannten Indizes ausgeführt habe, während das innere Array mit den beiden Punktständen (Siege/Remisen) als implizit numerisch indiziertes Array daherkommt. Damit beim Benutzen des Punktestand-Arrays deutlicher wird, was da gerade passiert, werden wir die beiden Elemente aber nicht mit nichtssagenden Nummern ansprechen, sondern dafür Konstanten mit sprechenden Namen definieren:
define('pi_siege',0);
define('pi_remis',1);

Wenn wir jetzt von - sagen wir - dem Spieler 1 den Sieges-Punktestand abfragen wollen, sprechen wir den mit dem folgenden Ausdruck an:
$spieler['1']['punkte'][pi_siege]
Das sieht auf den ersten Blick wegen den hintereinander gesetzten Array-Index-Operatoren vielleicht nicht sehr leicht lesbar aus, erweist sich aber unter der Bedingung, daß man mehrere Spieler, mehrere Punktestände (wir haben ja momentan zwei davon) und zudem nicht nur die Punktestände, sondern auch noch andere Eigenschaften der Spieler (zum Beispiel ihre Namen) in einem Rutsch (zum Beispiel einer Schleife) verarbeiten lassen will, als sehr nützlich.

Wenn man in einem bestimmten Code-Abschnitt mehrfach hintereinander in gleicher Weise auf sowas zugreifen muß, kann man sich temporäre Vereinfachungen anlegen...
$siegpunkte =  $spieler['1']['punkte'][pi_siege];  // als Kopie-Zwischenvariable
$siegpunkte = &$spieler['1']['punkte'][pi_siege];  // als Referenz

Wir hatten vorher für die Darstellung der "Siegerehrung" schon ein Array mit Dummy-Namen angelegt. Es wäre jetzt trivial, die bisher genau eine Stelle, wo wir darauf zugreifen, durch einen Term, der auf unser neues Spieler-Array zielt, zu ersetzen. Das Array $namen bräuchten wir dann nicht mehr.
Aber wir können die Gelegenheit auch nutzen, eine Vorgehensweise kennenzulernen, wie man bei inkrementellen Programmänderungen den gesamten restlichen Programmcode unangetastet lassen und trotzdem seine Datenstrukturen ändern kann...

Legen Sie Ihr Namen-Array doch einfach als Referenzen auf die entsprechenden Elemente des Spieler-Arrays an!
$namen = array
(
    '1' => &$spieler['1']['name'],
    '2' => &$spieler['2']['name'],
);

Sie können fortan das Array $namen weiter benutzen als hätten Sie nie irgendwelche Änderungen in ihren Programmcode eingebracht. Dabei werden keine extra Daten gehalten, sondern alle Zugriffe auf dieses Array einfach an die entsprechenden referenzierten Stellen im Array $spieler weitergeleitet.

Aufgabe: Ab in Ihren Code damit!

Jetzt müssen wir nur noch organisieren, daß die Siege und Remisen auch gezählt werden! Das tun wir natürlich dort, wo eben genau diese Auswertung bereits stattfindet!

Also: Wenn Sieg, dann bekommt der Sieger einen Sieg-Punkt:
$spieler[$dran]['punkte'][pi_siege]++;
Und wenn Remis, dann bekommen beide Spieler einen Remis-Punkt:
$spieler['1']['punkte'][pi_remis]++;
$spieler['2']['punkte'][pi_remis]++;

Aufgabe: Ab in Ihren Code damit!
An die richtigen Stellen! (Wir hatten das gerade eben besprochen!)

Punktestand-Anzeige
Jetzt brauchen wir noch etwas HTML-Code und CSS-Verhübschung, damit wir die Punktestände auch zu sehen kriegen! Das erfordert zwar weniger Denkaufwand als die PHP-Programmierung, ist aber mit mehr Experimentieren verbunden, weil es wieder mal sehr um individuellen Geschmack geht. Deshalb ist der Abschnitt komplett als praktische Übung ausgelegt...

Wir könnten erstmal pauschal HTML-Elemente in derselben Art und Weise, wie wir das schon für die Dran-Anzeigen gemacht haben, in das Panel schmeißen und sie dann per CSS zurechtrücken.
Zum Beispiel unter die jeweilige Dran-Lampe!

Das können wir auch im HTML-Code an die entsprechenden Stellen legen (wobei es im Prinzip IRGENDWO hin könnte, weil wir die Dinger ja eh absolut positionieren werden, weil wir unsere Lampen ja auch bereits absolut positioniert haben. Und beim "absolut positionieren" hat man vollkommene Freiheit, und die Elemente wirken sich auf das Layout sämtlicher anderen Elemente sowieso nicht aus, weil die in komplett eigenen Layern liegen.)...
<?php
$p1 = $spieler['1']['punkte'];
$p2 = $spieler['2']['punkte'];
?>
<div class="punkte black"><p><?=$p1[pi_siege]?></p><p><?=$p1[pi_remis]?></p></div>
<div class="punkte white"><p><?=$p2[pi_siege]?></p><p><?=$p2[pi_remis]?></p></div>
Die Zahlen können wir zur Sicherheit mit einem Erläuterungstext (title) versehen, mit dem der Remis-Stand vom Sieg-Stand bei einem Mouse-Over auseinandergehalten werden kann.

Tipp: Wann Copy & Paste, wann Funktion:
...Man könnte natürlich die Ansicht vertreten, daß das ganze nach Auslagerung in eine Funktion ruft, weil es immer wieder Wiederholungen gibt. Ich neige allerdings beim HTML-Code dazu, in Fällen wie hier, wenn die Wiederholungen unmittelbar an Ort und Stelle auftreten und nur ganz wenige sind, die copy & paste - Methode zu bevorzugen, weil das Schreiben von Funktionen für HTML-Abschnitte deutlich mehr Aufwand an Fingertipperei erfordert, länger dauert und die Sache bei so wenig Wiederholung nicht lesbarer macht. (Zur Gegenüberstellung: Bei der $title - Variablen gab es einen guten Grund, trotz nur zweifacher Schreibung eine Variable einzuführen: Die Stellen liegen relativ weit auseinander und schlagen bei Änderungen nicht unmittelbar mit der Faust ins Gesicht des Programmierers. Wenn die synchronen Stellen dagegen auch optisch synchron unmittelbar untereinander liegen wie hier, fällt eine Asymmetrie bei Bearbeitung der Zeilen sofort unmittelbar auf.)

Um die Sache im HTML möglichst einfach zu halten, habe ich hier die beiden Punktestände eines Spielers in <p>-Tags verpackt statt in <span>. Das Zeug wird mir hier sonst einfach zu ausufernd redundant!

Jetzt brauchen wir aber noch dringend CSS, denn wir können uns bereits aus den Erinnerungen an die letzten beiden Lektionen ausmalen, wie unsinnig das Ergebnis aussehen würde, wenn wir noch gar keine Stile definieren würden: Irgendwie in die Bildmitte geklatscht und den Start-Button nach unten rausgeschoben. So ja nun nicht, gelle...?! Die Grundausstattung, damit das Zeug erstmal wenigstens halbwegs zumutbare Positionen einnimmt und erkennbar wird:
.punkte
{
    position:       absolute;
    top:            90px;
    color:          white;
    font-size:      20pt;
}
.punkte.black       {left:  10px;}
.punkte.white       {right: 10px;}
.punkte p
{
    display:        inline;
    padding:        0.25em;
}

Aufgabe: Ab damit in Ihren Code! Anschauen! Kopf machen um's Aussehen!

Und ab jetzt wird's wieder sehr geschmacksabhängig: Da ich als Programmierer zu Faulheit neige, habe ich die Punkte-Anzeige einfach der Status-Anzeige nachgebildet, indem ich bei jener einfach die passenden Selektoren ergänzt habe (mit Komma davor gehängt):
.punkte p:last-child,
.status .remis
{
    ...
}

.punkte p:first-child,
.status .sieg1
{
    ...
}
Ja, auch DAS gehört zu den Stärken von CSS: Förderung der Faulheit der Programmierer! Je cleverer Sie bei der Strukturierung Ihrer Programme sind, desto fauler können Sie bei der Fingerarbeit werden (und umso mehr Zeit bleibt Ihnen für sinnvollere Beschäftigungen)!

Aufgabe: Ab in Ihren Code damit!
Sie dürfen natürlich auch ganz eigene Design-Ideen umsetzen und Ihrer Phantasie freien Lauf lassen!
Das bisherige Ergebnis könnte in etwa SO aussehen...

Punktestand speichern
Jetzt fehlt noch ein winziges letztes Element: Spielen Sie einmal kurz ein Spiel durch, bis sich die Punktestand-Anzeige geändert hat! Das Spiel ist dann erstmal in der Ruhe-Phase. Wenn Sie dann das Spiel neu starten lassen, sind Ihre Punkte wieder futsch! Das ist korrekt so, weil wir die tollen, neu angelegten Spielerdaten bisher noch nicht speichern lassen!

Das Prinzip: Serialisierung
Jetzt testen wir mal, wie gut Sie aufgepaßt haben...
  • WAS für Daten haben wir bisher schon gespeichert?
  • WIE haben wir diese bisher gespeichert?

Nun waren unsere bisherigen Speicher-Werte einfache Zeichenketten. Und nur ganz wenige. Die haben wir bisher einzeln in die Inputs eingetragen.

Aber unsere Spielerdaten sind ja nun bereits strukturiert. Und die sollen noch mehr werden. Beliebig - aus unserem momentanen Blickwinkel betrachtet. Und wir wollen nach Möglichkeit nicht, daß nach jeder Ergänzung erstmal das Spiel wieder nicht funktioniert, weil wir die Speicherung mal wieder noch nicht auf den neuesten Stand angepaßt haben.

Es müßte eine Möglichkeit geben, die Daten völlig unabhängig von ihrem Aussehen, ihrem Typ und ihrer Struktur in eine speicherbare Form zu pressen!
Die Möglichkeit gibt's. Und zwar gleich mehrere davon: Allgemein bezeichnet man sowas als "Serialisierung". In PHP werden wir regelrecht von konkurrierenden Varianten bombardiert:
Die Resultate können, wenn Bedarf besteht, mit Funktionen zur Komprimierung... ...und Verschlüsselung weiter verarbeitet werden:
Zum letztendlichen Speichern im Formular benötigen wir noch Funktionen, die die Daten - wie immer die auch aussehen mögen - unempfindlich gegen Fehlinterpretationen im Fall einer Einbettung in die URL oder in den Körper einer Nachricht machen:
Ich habe hier nur Funktionen aufgenommen, die in der Standardinstallation von PHP zugänglich sind (gegebenenfalls nach Konfiguration des Nachladens der Module in "php.ini") und die den einfachstmöglichen Funktionsaufruf (nur Daten und gegebenenfalls einfache Parameter zur Steuerung) gestatten. Es gibt daneben noch eine Fülle weiterer Funktionen und ganzer Bibliotheken, die deutlich komplizierter zu benutzen oder extra zu downloaden und zu installieren sind. Sie müssen es aber auch nicht zwingend übertreiben!

Wir werden aber erstmal nur eine einfache Serialisierung/Deserialisierung verwenden, wobei Ihnen allerdings keine Grenzen auferlegt sind, nach eigenem Gutdünken weitergehend zu experimentieren... (Das Komprimieren zum Beispiel lohnt sich erst bei Daten, die einen gewissen Mindestumfang übersteigen. Das wird bei uns noch nicht der Fall sein.)
HTML-Speicher-Element
Wir bereiten zuerst mal ein neues Datenfeld zum Speichern vor:
<input type="hidden" name="spieler" value="<?=save($spieler)?>"/>
Die Erzeugung des zu speichernden Wertes lagern wir mal wieder in eine Funktion aus, damit der HTML-Code so einfach wie möglich bleibt und die eigentliche Arbeit auch dort stattfindet, wo ihr Ergebnis gebraucht wird: Im PHP-Code-Abschnitt!
PHP-Speicher-Funktionen
Dann folgen auch schon die Funktionen zum Speichern und Laden von Daten. Die werden streng systematisch exakt spiegelsymmetrisch zueinander definiert:
function save(&$data)
{
    return base64_encode(serialize($data));
}

function load(&$data)
{
    return unserialize(base64_decode($data));
}
...weil die Vorgänge des Speicherns beim Laden in genau entgegengesetzter Reihenfolge wieder rückgängig gemacht werden müssen: So wie beim Speichern eine Schale nach der anderen von innen nach außen wachsend um die Daten gelegt wurde, so sind diese Schalen beim Laden eine nach der anderen von außen nach innen wieder zu entfernen!

"base64" setze ich hier anstelle von "urlencode" ein, weil es wesentlich kürzere Daten liefert (urlencode läßt zwar normale ASCII-Buchstaben und -Zahlen intakt, verschreddert aber alle Sonderzeichen zu dreimal so viel Schrott, während base64 zwar alles verschreddert, aber nur auf 4/3 dehnt (aus 6 bit (2^6 = 64) werden 8 bit).

Dann brauchen wir noch eine Stelle, wo wir die Funktion zum Laden aufrufen, um aus ihr die Spielerdaten zu entnehmen. Das muß sinnvoll in den Rest der Spiel-Initialisierung eingebettet werden. Eine analoge Überlegung hatten wir schon mal im Zusammenhang mit dem Übernehmen des Spielzuges und des dran seienden Spielers angestellt...
  • Die Erst-Initialisierung der Spielerdaten muß bereits abgeschlossen sein!
  • Der Spielzug darf aber noch nicht ausgewertet sein! (Wir wollen schließlich BEI dieser Auswertung den AKTUELLEN Spielstand eventuell um die neuen Punkte erhöhen, gelle?!)
Es bietet sich der Abschnitt an, der in der Beispieldatei mit dem Kommentar "Übernahme weiterer Spielparameter" betitelt ist. Dort kommt hinein:
if (isset($_REQUEST['spieler']))
{
    $spieler = load($_REQUEST['spieler']);
    // to do: Schutz gegen Hacking!
}

Aufgabe: Ab mit all dem Zeug in Ihren eigenen Code! Ausprobieren!
Anti-Hacking-Schutz zum Zweiten
Wie Sie dem Kommentar im letzten Codebeispiel entnehmen können, sind wir noch nicht ganz fertig. Das Spiel funktioniert zwar schon mal samt Speicherung des Spielstands, aber durch diese Speicherung ist natürlich wieder mal eine angreifbare Stelle entstanden, wo wir jetzt eigentlich gründlich testen müßten, ob da nicht irgendeiner der gespeicherten Parameter nicht unseren Kriterien entspricht.

Das würde in der Konsequenz bedeuten, daß wir SÄMTLICHE Parameter, die wir in dieser Form zwischengespeichert hätten, bei jedem Spielzug erneut prüfen müßten auf eventuelle Verletzung der Datenregeln. Dazu müßten sämtliche dieser Prüfungen entweder doppelt eingerichtet werden oder in extra Funktionen ausgelagert werden, die dann einfach doppelt aufzurufen wären (zum einen zur Prüfung der regulären Formulareingaben von Seiten der Nutzer, zum anderen zum Anti-Hacking-Test der durchgeschleiften Spieldaten).
Der Aufwand wäre nicht zu vernachlässigen und er würde bei Änderungen der gespeicherten Daten immer noch inkrementell anzupassen sein, auch wenn es nur noch um Funktionsaufrufe ginge.

Besser wäre ein Verfahren, was die Daten schlicht und ergreifend ein für allemal unantastbar macht für jede Art Hacking und nur ein einziges Mal von uns zu programmieren wäre: Verschlüsselung!
Das Prinzip: Wenn wir die Daten so verschlüsseln, daß sie unknackbar sind, muß zwingend jede Manipulation dazu führen, daß die wieder reinkommenden Daten unbrauchbar sind. Um DAS rauszukriegen, brauchen wir nur ein Element Testdaten mitschicken, das beim Dekodieren wieder genau so wie ursprünglich aussehen muß, dann können wir auf die Integrität der restliche Daten vertrauen.

Warnung: Der folgende Stoff ist für Anfänger nicht unbedingt geeignet!
Sie können versuchen, sich vom Standpunkt des Programmier-Technikers hineinzudenken und alles, was sich nicht unter Programmiertechnik einordnen läßt, einfach mal ausblenden. Leider läßt sich beim Thema Cryptographie ohne ein wenig Tieftaucherei kein Preis holen.

Ihnen andererseits unter diesem Thema etwas hinzulegen, das nicht nach bestem Gewissen dicht ist, halte ich für unverantwortlich, weil es genug fehlerhaften Umgang mit Cryptographiefunktionen auch so schon gibt. Ich muß mit meinem Lehrgang nicht unbedingt noch zu deren Vermehrung beitragen!

Nun ja: Die Funktionen sind ja weiter oben verlinkt... Weil allerdings die Cryptographie weder von den Funktionen noch von der Benutzungs-Philosophie her in dem Grade trivial ist, wie wir das von den anderen bisher benutzten Funktionen gewohnt sind, lagern wir die Verschlüsselung in eine selbst definierte Puffer-Funktion aus (wir kennen das Konzept ja schon zur Genüge: Wir haben die Lizenz zum Erfinden!):
function save(&$data)
{
    return base64_encode(encrypt(serialize($data)));
}

function load(&$data)
{
    return unserialize(decrypt(base64_decode($data)));
}

function encrypt($data)
{
    return $data;   // erstmal dummy
}

function decrypt($data)
{
    return $data;   // erstmal dummy
}
Zum Einbau der Verschlüsselung lesen wir aus der Schnittstellenbeschreibung der Funktion mcrypt_encrypt zunächst mal das grundlegende Prinzip ab und übernehmen es mit ein paar von uns festgelegten Parametern. Die Parameter lagern wir aus den beiden spiegelbildlichen Funktionen aus, da sie in beiden Fällen identisch sein müssen. Falls Sie damit experimentieren möchten, ist auf diese Weise Konsistenz garantiert...

Nebenbei läuft uns bei der Gelegenheit ein neues Sprachelement über den Weg: Die "Sichtbereiche" von Variablen in Funktionen sind eingeschränkt. Man muß Variablen, die außerhalb von Funktionen definiert wurden, innerhalb einer Funktion erst sichtbar machen. Das tun wir hier mit dem Kostrukt "global": Hinter dem Schlüsselwort werden alle Variablen aus dem globalen Bereich aufgezählt, die man sehen können möchte. Wir wollen hier die global definierten gemeinsamen Crypto-Parameter in die Funktionen hineinstopfen...
$crypt_cipher   = MCRYPT_TWOFISH;
$crypt_key      = hash('sha256','Eine wilde Phrase zum Schutz vor Hacking v2',true);
$crypt_iv_size  = mcrypt_get_iv_size($crypt_cipher, MCRYPT_MODE_CBC);
$crypt_iv_base  = str_repeat(' ',$crypt_iv_size);

function encrypt($data)
{
    global $crypt_cipher,$crypt_key,$crypt_iv_size,$crypt_iv_base;

    return mcrypt_encrypt($crypt_cipher,$crypt_key,$data,MCRYPT_MODE_CBC,$crypt_iv_base);
}

function decrypt($data)
{
    global $crypt_cipher,$crypt_key,$crypt_iv_size,$crypt_iv_base;

    return mcrypt_decrypt($crypt_cipher,$crypt_key,$data,MCRYPT_MODE_CBC,$crypt_iv_base);
}
Bleibt ein letzter Feinschliff zu tun: Der CBC-Mode ist minimal (aber nichtsdestotrotz ohne Gegenmaßnahmen definitiv) angreifbar, und zwar in dem Sinn, daß der "Initialvektor" (bei uns "iv" genannt und in der Variablen "crypt_iv_base" notiert) beim Standardverfahren offen übertragen (einfach vorn an den Ciphertext drangeklatscht) wird und von einem Angreifer frei manipulierbar ist, ohne daß dies bemerkt werden könnte. Letzteres kommt dadurch, daß der Inhalt des IV-Vektors standardmäßig nicht als Daten bewertet wird.

Eine genauere Beschreibung des Problems findet sich hier:
Im Beispielcode wurden dafür ein paar Maßnahmen ergriffen, die zu erläutern allerdings unseren momentanen Horizont übersteigt. Für tatsächlich weitergehend Interessierte habe ich im folgenden Quelltext Popups eingerichtet, die jedes einzelne Element erläutern...

Damit wird die Funktion mit etwas Systematisierung erweitert zu:
$crypt_cipher       = MCRYPT_TWOFISH;
$crypt_mode         = MCRYPT_MODE_CBC;
$crypt_keyhash      = 'sha256';
$crypt_datahash     = 'md5';

$crypt_block_size   = mcrypt_get_block_size($crypt_cipher);
$crypt_key_size     = mcrypt_get_key_size($crypt_cipher,$crypt_mode);
$crypt_iv_size      = mcrypt_get_iv_size ($crypt_cipher,$crypt_mode);

$crypt_datahashsize = strlen(hash($crypt_datahash,'',true));
$crypt_keyhashsize  = strlen(hash($crypt_keyhash,'',true));

$crypt_key          = hash($crypt_keyhash,'Eine wilde Phrase zum Schutz vor Hacking v2',true);

function encrypt($data)
{
    global  $crypt_cipher,
            $crypt_mode,
            $crypt_datahash,
            $crypt_key,
            $crypt_block_size,
            $crypt_iv_size,
            $crypt_iv_base;

    $dataxsize  = strlen($data) + 1;
    $padsize    = ($crypt_block_size - ($dataxsize % $crypt_block_size)) % $crypt_block_size;
    
    $iv         = mcrypt_create_iv($crypt_iv_size, MCRYPT_DEV_RANDOM);
    $ps         = chr($padsize);
    $padding    = str_repeat(' ',$padsize);
    $datax      = $iv.$ps.$data.$padding;

    $hash       = hash($crypt_datahash,$datax,true);

    return mcrypt_encrypt
    (
        $crypt_cipher,
        $crypt_key,
        $hash.$datax,
        $crypt_mode,
        $crypt_iv_base
    );
}

function decrypt($data)
{
    global  $crypt_cipher,
            $crypt_mode,
            $crypt_datahash,
            $crypt_key,
            $crypt_datahashsize,
            $crypt_iv_size,
            $crypt_iv_base;

    $temp   = mcrypt_decrypt
            (
                $crypt_cipher,
                $crypt_key,
                $data,
                $crypt_mode,
                $crypt_iv_base
            );

    $hash   = substr($temp,0,$crypt_datahashsize);
    $datax  = substr($temp,$crypt_datahashsize);
    if ($hash != hash($crypt_datahash,$datax,true)) exit;
    
    $padsize= ord(substr($datax,$crypt_iv_size,1));
    return substr($datax,$crypt_iv_size+1,strlen($datax)-$padsize-$crypt_iv_size-1);
}
Wobei man hier die Reaktion auf Hacking sicher auch anders ausfallen lassen könnte als einfach nur den Bildschirm leer zu machen. Aber die Leute sollen ja auch nicht hacken, gelle?!

Aufgabe: Ab in Ihren Code damit! Ausprobieren!
Sie dürfen auch ein wenig experimentieren!

(to do: An sich würde ich die Sache ja gern drinbehalten, aber zum einen ist die Verschlüsselung hier Overkill, zum anderen ist sie doch letztlich komplexer geworden als anfänglich angedacht. Es dürfte besser sein, auf Sicherung allein durch Hashwertbildung zu setzen. Allerdings bräuchte ich dazu hier schon die Session. Die soll aber erst in der nächsten Lektion kommen. Eventuell kann der gesamte Abschnitt verlagert werden...)

Pause zur Erholung von diesem Ausflug in die Welt der Datenstrukturen und des Hackings! Lassen Sie sich alles nochmal in Ruhe durch den Kopf gehen! Und experimentieren Sie eventuell ein wenig oder legen Sie sich Lesezeichen für zukünftiges Kreuzen in den Untiefen der Programmiergewässer an!

Eintragung von Spielernamen

So, endlich wieder mal etwas ruhigere Gewässer...
Es geht jetzt um das Eintragen von Spielernamen, wenn sich Spieler für ein Spiel anmelden. Da wir sämtliche Techniken dafür schon kennengelernt haben und nach dem letzten Kapitel mit allen Wassern gewaschen sind, sollte das jetzt auch keine Hürde mehr darstellen. Es wartet wieder ein wenig HTML (neue Inputs für das Panel), CSS dazu und eine Werteübernahme auf Serverseite. Da die Datenspeicherung schon für unsere Dummy-Namen organisiert war, kommt zur Speicherung nichts neues hinzu (und mit unserer Art der sicheren Speicherung wäre es auch sonst trivial geworden).

Weil das ganze recht wenig Neuigkeiten bietet und kaum Erklärungen fordert, ziehen wir es als Übung durch...

Übung zur Eintragung von Spielernamen
HTML
Wir brauchen also mal wieder HTML-Formular-Elemente. Diesmal Text-Eingabe-Felder. Die nennen sich als HTML-Tag <input>. Man muß ihnen genau wie den Buttons und hidden inputs, die wir schon kennengelernt hatten, ein paar Attribute mitgeben, namentlich einen "name" und einen "type".

Außerdem werden wir wollen, daß die einmal eingegebenen Namen natürlich auch bei allen zukünftigen Abfragen und Darstellungen des Spiels in Zukunft immer in diesen Eingabefeldern dargestellt werden (das ist ja im Moment der einzige Grund, warum wir sie überhaupt eingeben: Wir wollen zu sehen kriegen, wer da mit wem spielt!). Deshalb bekommen die auch noch ein "value"-Atribut mit auf den Weg!

Schließlich setzen wir noch ein class-Attribut ein, um die Felder mit CSS hübsch machen zu können (bzw. wie bisher bei allen Spieler-Elementen zwei davon, um die Seiten schwarz und weiß unterscheiden zu können).

Wo schmeißen wir sie ins Formular? Eigentlich ist das auch wieder egal, weil wir dieselbe Methode der Positionierung verwenden werden, die wir für die anderen beiden Anzeigeelemente für Spieler-Zustände ("dran" und Punktestand) auch schon verwendet hatten: die "absolute" Positionierung, die uns eh volle Freiheit läßt.
Nur aus Sicht der Bedienreihenfolge könnten wir eine bestimmte Reihenfolge vorziehen: Standardmäßig wandert der Tastatur-Fokus (die Stelle, wo Ihr Textcursor blinkt und darauf wartet, daß Sie mit Ihren Fingern in die Tastatur vor Ihrer Nase hauen!) beim Betätigen der Tab-Taste von einem Formularfeld zum nächsten, und zwar in der Reihenfolge, wie die im HTML-Text stehen. Das könnte man zwar auch explizit ändern, aber wir mpssen es ja nun nicht übertreiben, nicht wahr?! Da wäre es doch reichlich sinnvoll, wenn man auf den Startknopf (oder andere Knöpfe, die eine Aktion auslösen) erst NACH den Feldern zur Eingabe irgendwelcher Sachen kommt, oder?!

Wir schmeißen die Zeilen also VOR den Start-Knopf hin!
<input class="namen black" type="text" name="namen[1]" value="<?=$namen['1']?>"/>
<input class="namen white" type="text" name="namen[2]" value="<?=$namen['2']?>"/>

Tipp: Parameter-Arrays:
Und wieder mal läuft uns hier eine (nicht ganz) neue Technik über den Weg: Sie hatten zwar schon mal eine Aufgabe zur Übertragung eines Arrays von Werten beim Hacking erhalten, das aber noch nicht in Formularelementen umgesetzt. Hier sehen Sie beim Attribut "name", wie es geht. Ist auch nicht ganz unlogisch, gelle?!

Es sollte nebenbei deutlich auffallen, daß wir mit der Benennung von PHP-Variablen, HTML-Formularelementen und CSS-Selektoren hochgradig konsistent vorgehen: Alle diese Elemente tragen hier ein und denselben Bezeichner "namen" - damit wir uns nicht künstlich selbst verwirren!

Aufgabe: Ab in Ihren eigenen Code damit!
PHP
Die Werte sind auf Serverseite entgegenzunehmen - im vorderen Teil unseres PHP-Scripts. Das ist jetzt für Sie bereits kalter Kaffee:
if (isset($_REQUEST['namen']))
{
    foreach($_REQUEST['namen'] as $i => $name)
    {
        $namen[$i] = $name;
    }
}

Aufgabe: Ab in Ihren eigenen Code damit!
CSS
Damit das ganze anständig positioniert und zu sehen ist (wo sich wieder Welten für Ihre eigene Phantasie auftun), können wir ein wenig einerseits bei den Spielständen und andererseits beim Status abkupfern (so hab ich es jedenfalls hier gemacht aus lauter Faulheit und ein wenig weil ich Beständigkeit im Layout mag):
/* Namen */
.namen
{
    position:       absolute;
    top:            130px;
    width:          9em;
    border:         2px inset #480915;  /* Fallback für veraltete Browser */
    border:         2px inset rgba(72,9,21,0.6);
    border-radius:  5px;
    background:     none;
    color:          white;
    font-size:      15pt;
    padding:        0.1em 0.25em;
}
.namen:hover,
.namen:focus
{
    background:     rgba(255,255,255,0.1);
}
.namen.black        {left:  10px;}
.namen.white        {right: 10px; text-align: right;}

Aufgabe: Ab in Ihren eigenen Code damit!
Hack-Sicherung
Kurzer Test auf Hacking-Anfälligkeit:
  • Wenn wir im Eingabefeld versuchen, irgendwelchen HTML-Code einzutragen, wird der vom Browser beim Abschicken immer ordentlich "escaped", und zwar in genau jener Art und Weise, die wir in Lektion 5 / "Die Speicherung des Spielfeldes im Browser" schon mal nebenbei angesprochen, aber noch nicht verwendet hatten - wie mit "urlencode":
    Aus zum Beispiel:
    <div></div>
    ...im Eingabefeld macht der Browser im URL-Query nach der bei urlencode beschriebenen Ersetzungeregel:
    namen%5B[1%5D]=%3C<div%3E>%3C<%2F/div%3E>
    Wobei der PHP-Interpreter beim Aufbereiten der $_REQUEST-Daten daraus die originalen Zeichen rekonstruiert, bevor er die Daten weiter verarbeitet und später ans Script übergibt: Er macht intern automatisch ein "urldecode" auf die Query-Elemente, nachdem er die auseinandergepflückt hat. Was dann in dieser originalen Form im HTML-Text landet:
    <input ... value="<div></div>"/>
    Wobei die Browser solche in einen Attributwert eingebetteten HTML-Tags erstmal tolerant behandeln (sie sind intelligent genug - obwohl sie das nach HTML-Standard nicht sein müßten -, die Attribut-Innereien als Attribut-Innereien zu erkennen: Alle HTML-Tag-Interpretation wird abgeschaltet bis zum nächsten Anführungszeichen, welches das Attribut beendet.

    Ja. Genau! BIS dahin...

    Aufgabe: Zerbröseln Sie das Layout mit einem XSS-Angriff! Wählen Sie als Name für eine der beiden Seiten:
    "><div class="status remis">Hallo!</div>"
    Zu welchem Konstrukt führt das im resultierenden HTML-Text bei der nächsten Formular-Auslieferung?

    Für einen Nachweis des Prinzips sollte das reichen, andernfalls gucken und probieren Sie nochmal das Hacking aus Lektion 4!

  • Dasselbe könnten wir natürlich als Hacker auch direkt mit den URL-Parametern anstellen. Was uns in diesem Fall allerdings erstmal keinen Vorteil bringen würde.

Gut: Da muß also wieder eine Absicherung ran, die wir genauso organisieren wie wir das am Ende von Lektion 4 erstmal theoretisch beleuchtet hatten. In diesem Fall geht es wieder darum, die HTML-Funktion des Textes zu unterdrücken. Dazu diente uns schon beim Hacking-Experiment welche Funktion Genau: htmlspecialchars()! (Maßnahme Nr. 3) nochmal?

Aufgabe: Bauen Sie dieses in Ihr Programm ein!
Testen Sie, indem Sie genau dasselbe Experiment wie eben nochmal durchführen! Ihr Spieler sollte jetzt seinen Namen wieder genau so wie eingegeben vor die Nase geklatscht kriegen!

Ganz zum Schluß könnten Sie noch eine Nebensächlichkeit ergänzen, nachdem wir nun die Spieler mit Namen ansprechen können: Es könnte passieren, daß die leuchtenden Lämpchen der "dran"-Anzeige für den einen oder anderen Spieler nicht hinreichend intuitiv anzeigen, daß er jetzt dran ist und ziehen kann. Das könnte durch eine extra Meldung im inzwischen verfügbaren Statusfeld unterstützt werden. Wobei das aber Geschmackssache ist, deshalb nur nebenbei am Rande erwähnt. Werfen Sie einen Blick in den Beispielcode, um eine Anregung zu bekommen!

Speichern und Laden

Ein letzter Feinschliff verbleibt: Sie hatten ja bereits in der letzten Lektion fleißig ausprobiert, Links auf ihre Spielzustände anzulegen und auch in Ihr Lehrgangsinhaltsverzeichnis aufzunehmen. Sie wissen also bereits, wie das vom Prinzip her geht.

Es wäre jetzt noch schön, unsere URL-Zeile ein wenig von dem Streusplit zu reinigen, der sich inzwischen dort angesammelt hat: Spätestens mit dem Übertragen der $spieler-Datenstruktur hat sich dort ein riesiger Klotz breitgemacht, der wahrlich nichts mehr mit Schönheit oder auch nur Zumutbarkeit zu tun hat. Auch wenn der Browser klaglos damit klarkommt.

Die Alternative lautet: POST-Übertragung für's Formular!

Übung zur POST-Übertragung
Ändern Sie in Ihrem HTML-Text folgendes:
<form>
...zu:
<form method="POST">
Probieren Sie aus, was passiert!

Störende Dialogboxen
Eigentlich war hier angedacht, Ihnen einen Vortrag zur Unbrauchbarkeit von Undo/Redo und Sitzungsunterscheidung in Browserfenstern bei Verwendung von Formularen im POST-Modus zu halten. - Im Zusammenhang mit unserem Spiel, wo wir eben für das Undo/Redo und die Sitzungsverwaltung das fortlaufende Mitschicken der Formulardaten benötigen (wo nichts funktionieren könnte, wenn diese Formulardaten nicht mitgeschickt werden würden, da wir noch nichts auf dem Server speichern) und daher die für andere Situationen entwickelten Ausweichmaßnahmen nicht anwendbar sind.

Zu meiner eigenen Überraschung mußte ich beim Erarbeiten dieses Abschnitts feststellen, daß die ehemals beim Firefox berüchtigte Dialogbox...
"To display this page, Firefox must send information that will repeat any action (such as a search or order confirmation) that was performed earlier"
...mit dem derzeitigen Firefox 15 nicht mehr zu reproduzieren ist, wenn man im Spiel Undo/Redo betreibt. Auch die verschiedenen Browserfenster werden sauber auseinandergehalten. Irgendwas muß sich irgendwann in den letzten zwei Jahren in der Verwaltung der Navigation im Zusammenhang mit Formularen getan haben, daß das neuerdings so vollkommen problemlos klappt. Beim Blick in den Mozilla Bug-Tracker sieht das allerdings nach dem genauen Gegenteil aus. Äußerst seltsam...

Die noch vor drei Jahren üblichen Hilfsmaßnahmen (hier zum Beispiel) erweisen sich als überflüssig bzw. nicht mehr funktionsfähig.

Ein Gegentest mit den anderen Mainstreambrowsern (IE, Opera, Chrome und Safari) zeigt, daß diese Konsistenz browserübergreifend gewährleistet wird. Eine Kontrolle des RFC 2616 (HTTP Headers) zeigt keinen Verstoß gegen den Standard. Es gibt nach wie vor (ältere) Seiten im Internet, die von der Verwendung von POST abraten. Offenbar sind diese inzwischen hinfällig.

OK: Thema verkürzt.

Gut, damit haben wir jetzt die URL wieder "sauber", das Spiel aber immer noch funktionierend. Nur mit dem Anlegen von Lesezeichen ist es jetzt nicht mehr so weit her. Vorher hatte jeder einzelne Seitenaufruf für jeden einzelnen Spielzug eine umkehrbar eindeutige URL. Jetzt nicht mehr. Jetzt ist wieder alles Einheitsbrei.
Um dennoch weiter gezielt Lesezeichen zum Speichern eines Spielstandes anlegen zu können, sollten wir auf der Seite einen extra Link mitliefern, der für das Anlegen eines Lesezeichens benutzt werden kann, das genau so aussieht wie die früheren URL's.

Tipp: Begrenzung des Speichers in URL-Queries:
Dabei ist allerdings die Menge der in einem Lesezeichen speicherbaren Daten begrenzt: Es gibt Grenzen für die zulässige Länge der URL, und zwar lustigerweise nicht von einem Standard festgelegt, sondern historisch gewachsen:
Internet Explorer 8: 2048 Byte
Firefox 15: mindestens im zig Mbyte-Bereich - praktisch unbegrenzt
Opera 12: 65535 Byte
Chrome 21: 3254 Byte anzeigefähig, intern offenbar mehr (wird aber unbenutzbar träge durch solche URL's)
Safari 5: 65535 Byte
Webserver Apache: 8190 Byte (standardmäßig, konfigurierbar)
Google Bots: 2048 Byte (Die sollen URL's mit gespeicherten Daten aber in der Regel sowieso nicht anfassen!)
Wer kompatibel (mit dem Schrott vom letzten Jahrtausend namens Internet Explorer-Familie) bleiben will, wird wohl um ein 2K-Limit nicht umhinkommen, wenn die Daten auf dem Browser landen sollen. Was man noch machen könnte, um im 2K-Limit zu bleiben, ist eine Komprimierung der Daten vor Ablage in der URL bzw. im Formular (die Funktionen dazu waren weiter oben verlinkt). Für unsere Zwecke werden wir das aber noch nicht benötigen.

Es kommt wieder mal ein HTML-Element (ein simpler Anchor <a>), ein ganz triviales PHP-Element und ein bißchen CSS hinzu...

Eine Anregung könnte ein Blick auf andere Anwendungen liefern, wo sowas schon umgesetzt ist...
Zum Beispiel im großen Muster-TicTacToe. Dort ist es der gelbe Pfeil links oben. Ganz einfach mit purem Text würde es zur Not auch gehen, wie zum Beispiel im Oblivion-Alchemie-Rechner oben rechts. Allgemein üblich und seit zwei Generationen eingebürgert wird die Speicherfunktion irgendwie oben links erwartet und eine Hilfefunktion oben rechts.

Andererseits: Wenn solche Links oder Buttons auf dem Spielfeld liegen, stören sie irgendwie dessen Ambiente. Sie könnten auf ein extra angedocktes oder ausfahrbares Panel gesetzt werden. Vorläufig könnten wir das Ding aber auch erstmal unten ans Panel dranhängen.

Den Lesezeichen-Link werden wir als normalen, eigenständigen Link auslegen (anstatt als Submit-Button), weil der nicht zum Absenden des Formulars gehört.

Eine Symbolik zur Erinnerung an ein Lesezeichen wäre sicherlich nicht verkehrt. Jedenfalls sollte es nicht als Druckschalter fehlverstanden werden: Wenn man da drauf klickt, landet man eh immer wieder nur beim selben aktuellen Spielstand. Die Benutzer sollen nicht in die Versuchung kommen, das Ding mit all den anderen Schaltern zum draufklicken zu verwechseln. Dennoch muß es funktionieren wie ein Link, damit der Browser das Anlegen eines Lesezeichens anbieten kann. Aus der Situation hilft zum Beispiel ein zusätzlich bei MouseOver (in CSS: "hover") eingeblendeter Hilfetext.

Zur Farbgebung müßten wir uns was ausdenken, was deutlich genug auf der Mahagony-Unterlage zu erkennen ist. Durchaus analog den Druckschaltern, aber mit unterschiedlichem Design.

Sie merken schon, daß das auf CSS-Spielerei hinausläuft...
HTML + PHP
Erstmal zum HTML: Das ist diesmal nicht mehr ganz so trivial, weil wir den Zustand unseres Spieles im Link hinterlegen wollen. Der setzt sich im Moment noch aus einer Reihe einzelner Bausteine zusammen, weshalb das nicht GANZ so einfach aussieht, wie es sein könnte. Wir KÖNNTEN inzwischen dazu übergehen, den GESAMTEN Spielzustand (nicht nur der Spieler, sondern auch des Spielfeldes und des Dranseienden) in einer Datenstruktur zusammenzufassen und im Block in einer Variablen abzulegen, die wir hier einfach per save-Funktion in ein Query gießen könnten. Das dürfen alle ausprobieren, die momentan nicht ausgelastet sind. Ansonsten schieben wir diese Verbesserung mal ein wenig vor uns her...

Aber auch sonst muß der Link sich aus mehreren Komponenten zusammensetzen:
$save =  '?spielfeld='.$spielfeld.
            '&dran='.$dran.
            '&erster='.$erster.
            '&spieler='.urlencode(save($spieler));
            
$sign = ' '.$namen['1'].' - '.$namen['2'].': Zug '.$zugnr;

<a href="<?=$save?>" class="bookmark glas">
    Spielstand<span class="hidden"><?=$sign?></span>
</a>
Schauen wir uns das Konstrukt mal im Detail an (Sie dürfen nebenbei das Ding auch inkrementell nachbauen, damit Sie SEHEN, wozu die einzelnen Teile da sind):
  • Der Anchor (Link) soll ermöglichen, daß der Spieler im Browser einen Rechtsklick machen und "Lesezeichen anlegen" lassen kann:
    <a href="<?=$save?>">
        Spielstand
    </a>
    
    Dazu lassen wir vorher (siehe Kästchen weiter oben) den Inhalt des Links in $save zusammensetzen, damit das nicht ganz so unleserlich wird.

  • Wenn Sie das mal SO in Ihren Code übernehmen, werden Sie merken, daß Ihr Lesezeichen als Bezeichner nur "Spielstand" voreingestellt bekommt:

    Um ein unterscheidbares Lesezeichen anzulegen (wo man aus dessen Text erkennen kann, was damit bezweckt wird), müßte der Spieler selbst den passenden Text tippen.
    Das ist aber nicht wirklich intuitiv und bequem, oder?! NETT wäre, wenn ein sinnvoller Text für das Lesezeichen voreingestellt wäre! Wie Sie möglicherweise aus dem, was Sie gesehen haben, schlußfolgern können, tragen die Browser standardmäßig einfach genau jenen Text, den sie zwischen dem <a> und dem </a>-Tag finden, als Voreinstellung ein. Da wäre es doch trivial, einfach die Beschreibung des aktuellen Spieles mit in den Text von diesem Link zu schreiben, Oder? Das könnte sein:
    • Die Namen der beteiligten Spieler
    • Die Nummer des Spielzugs
    Das lassen wir im Kästchen oben zunächst (damit's nicht ganz so unleserlich wird) in die Variable $sign eintragen (abgekürzter Begriff "Signatur"), die wir hinter dem Wort "Spielstand" in den Link ausgeben lassen

  • Wenn Sie das mal OHNE den "hidden" span in Ihren Code übernehmen, merken Sie allerdings, daß das Ihr Layout zerschießt, weil der Text im Link zu lang geworden ist. Andererseits braucht den auch niemand zu Gesicht bekommen, denn die Namen der Spieler stehen ja im Panel und die Steine des Spielfeldes liegen dem Spieler unmittelbar vor der Nase auf dem Spielfeld. Wenn wir also diesen extra Text einfach verstecken, stört der das Layout nicht mehr, aber der Browser nimmt ihn immer noch als Voreinstellung für das Lesezeichen
Aufgabe: Ab in Ihren Code damit! Ausprobieren!
Das Layout sieht natürlich noch schei..nheilig aus. Das richten wir mit CSS...
CSS
Das CSS für das Lesezeichen habe ich hier gleich mal in zwei Blöcke aufgeteilt: Einen, der sich auch bei anderen Gelegenheiten, wenn Sie mal wieder Lesezeichen anlegen wollen sollten, mit hoher Wahrscheinlichkeit identisch wiederholen wird, und einen anderen, der mit hoher Wahrscheinlichkeit bei anderen Anwendungen anders aussehen wird. Wobei dies natürlich wieder mal - wie alles rund um Layouts - extrem vom persönlichen Geschmack abhängt. Nehmen Sie das hier gezeigte Grundprizip als Anregung für Ihre eigenen Lösungen!
/* Lesezeichen - spezielle Anpassung */
.bookmark
{
    display:        inline;
    position:       relative;
    top:            75px;

    padding:        4px 8px;
    border-radius:  8px;
    font-size:      120%;
    font-weight:    900;
    color:          white;
    text-decoration:none;
}
.bookmark .hidden
{
    display: none;
}

Die Bestandteile dieses CSS dürften mittlerweile intuitiv verständlich sein:
  • "display: inline" bewirkt, daß das Element auf minimale Breite zusammenschrumpft (anstatt auf maximale Breite gedehnt zu werden) und sich an die Zentrierung des übergeordneten Panels hält.
    Wobei alle Anchors (alias Links) normalerweise ohnehin inline gedisplayed werden, aber in diesem Fall nachgeholfen werden muß, weil auf meinem Server eine anderslautende Definition global voreingestellt ist (sorry - historisch bedingt). Bei Ihnen ist diese Definition eventuell überflüssig.
  • "position relative" und "top" bewirken ein Runterziehen des Lesezeichens, damit es nicht mitten auf den Namensfeldern zu liegen kommt, die durch ihr "position absolute" nicht am normalen Layout teilnehmen und daher anderes Zeug überdecken könnten. Dagegen wenden wir halt das vertikale Zurechtrücken an.
  • "padding" bewirkt, daß das Ding nicht so gequetscht aussieht
  • "border-radius" sorgt für weichere und damit augenfreundlichere Ecken
  • die Font-Einstellungen (mitsamt color) sorgen dafür, daß die Schrift ordentlich erkennbar ist
  • "text-decoration none" stellt den hier optisch störenden Unterstrich ab
  • "display none" kürzt das sichtbare Lesezeichen-Label ein

Der allgemeine Teil wurde herausgezogen und im Zusammenhang mit Experimenten zu Schaltknöpfen definiert, welche für experimentierfreudige Teilnehmer unter den HTML-Experimenten in Lektion 2 verlinkt waren.
/* Lesezeichen - allgemeiner Teil */
.bookmark
{
    display:    block;
    text-align: center;
    white-space:nowrap;
}
.bookmark.glas
{
    border:     1px solid rgba(0,0,0,0.5);
    box-shadow:
                -1px -1px 2px       rgba(255,255,255,0.4),
        inset   -1px -1px 2px       rgba(255,255,255,0.4),
                +1px +1px 2px       rgba(  0,  0,  0,0.4),
        inset   +1px +1px 2px       rgba(  0,  0,  0,0.4);
}
.bookmark.glas:focus,
.bookmark.glas:hover
{
    border:     1px solid rgba(255,255,255,0.5);    /* helles Leuchten am Rand */
}
.bookmark.glas:before           {content: "» "; visibility: hidden; }
.bookmark.glas:after            {content: " «"; visibility: hidden; }
.bookmark.glas:hover:before,
.bookmark.glas:hover:after      {visibility: visible; }

Die CSS-Wirkungen sind allgemein am besten zu begreifen, indem Sie sie "life" im Browser manipulieren. Dafür bietet der Firebug eine hervorragende Unterstützung: Schalten Sie den durch einen Rechtsklick auf den Lesezeichen-Link ein...

...klicken Sie gegebenenfalls in der Hierarchie-Ansicht nochmal auf das "richtige" Element und schalten Sie dann auf der rechten Seite durch einfache Klicks Effekte aus und wieder ein, um sofort im Browserfenster drüber zu sehen, wie sich das auswirkt!


Insbesondere Lichteffekte lassen sich durch sehen und spielen leichter verstehen als durch seitenweise Erklärungen. Falls Sie darin kein so gutes Vorstellungsvermögen haben (was allerdings seltener der Fall ist als Schwierigkeiten beim PHP-Programmieren), copy & pasten Sie einfach Vorlagen mit "gläsernen" Effekten (die einen durchscheinenden Hintergrund haben) und können sich für den Rest Ihres Lebens das dauernde Neuschreiben von Button-Effekten schenken, denn Glas-Effekte passen überall! (Lediglich eine leichte Aufhellung oder Abdunklung ist mitunter sinnvoll, was Sie in der hier dargestellten Form als class-Attribute "hell" und "dunkel" wiederfinden.

Was zum Schluß noch fehlt ist eine leichte Verlängerung des Panels, damit der Lesezeichen-Link noch draufpaßt:
.panel
{
    ...
    height:         200px;
    ...
}

Aufgabe: Ab damit in Ihren Code! Ausprobieren!
Ablauf-Korrektur
Wenn Sie das Ergebnis jetzt mal testen, und zwar speziell die Speicherung der Namen in einem als Lesezeichen gespeicherten Spielstand, werden Sie merken, daß die scheinbar nicht im Spielstand enthalten sind. Nach dem Laden eines Spielstands steht dort immer nur "schwarz" und "weiß"!

Das hängt damit zusammen, daß sich die Daten beim Laden eines Spielstands von denen unterscheiden, die beim normalen Abschicken des Formulars ankommen: Beim Lesezeichen haben wir auf redundante Daten verzichtet (wir haben sie nur noch nicht ganz einheitlich organisiert). Im normalen Betrieb des Formulars kommen aber im Moment jede Menge Redundanzen vor. Man könnte die Ansicht vertreten, daß das Formular aufgrund der inkrementellen Weiterentwicklung inzwischen mal eine systematische Überarbeitung vertragen könnte...
  • Im Normalbetrieb lassen wir die Spielernamen momentan doppelt senden:
    • in der Datenstruktur der Spieler, die wir verkryptet in einem hidden Formularfeld hin- und herschicken
    • in den Formularfeldern zur Eingabe der Namen
  • Außerdem haben wir im Programm eine Doppelung in den Variablen: Zum einen werden alle Spielerdaten im Array "$spieler" gespeichert, zum anderen werden die Namen über die Referenzen im Array "$namen" benutzt.

Die konkreten Reihenfolge, in der diese Daten in der Formularbearbeitung auftreten bzw. importiert werden, unterscheidet sich zwischen dem normalen Abschicken des Formulars durch Klick auf Start oder auf ein Feld im Spielfeld und dem Laden eines Lesezeichens:
  • Im Normalbetrieb werden die Namen ZWEIMALIG eingelesen:
    • zuerst mit der verkrypteten Datenstruktur der Spieler
    • dann nochmal einzeln mit den Formularfeldern, die jedesmal mitgeschickt werden
  • beim Laden eines Lesezeichens werden sie nur EINMALIG eingelesen, und zwar mit der verkrypteten Datenstruktur der Spieler

Das Einlesen mit der verkrypteten Datenstruktur der Spieler führt jedoch nicht zum korrekten Benutzen der eingelesenen Namen. Und das hängt mit der Bildung der Referenzen im Array "$namen" zusammen, und zwar mit dem konkreten Zeitpunkt der Referenzbildung. Und damit, wie die Referenzen in PHP funktionieren:
  • Die Referenzen werden NACH dem Anlegen der Datenstruktur der "$spieler", aber VOR dem Einlesen der Daten in diese Datenstruktur durch unsere Ladefunktion "load" und VOR dem Einlesen der Formularfelder gebildet.
  • Beim Einlesen der Formularfelder werden die Namen ÜBER die Referenzen auf ihre Speicherplätze geschoben - was und wo immer die auch sein mögen.
  • Beim Einlesen der Datenstruktur "$spieler" aus den verkrypteten Daten des Formulars oder aus einem Lesezeichen dagegen wird die komplette Datenstruktur "$spieler" AUSGETAUSCHT.

Das letztere führt nun aber NICHT(!) dazu, daß die Referenzen auf die NEUEN (ausgetauschten) Namen zeigen würden. Die zeigen statt dessen weiterhin auf die ALTEN (ursprünglich mit "schwarz" und "weiß" initialisierten) Namen, die jetzt nur nicht mehr unter der Variablen "$spieler" stehen (wo dann die neuen - geladenen - Spielerdaten stehen), sondern anonym im Arbeitsspeicher herumhängen und nur noch am seidenen Faden der Referenz mit dem laufenden Programm verbunden sind. Aber nichtsdestotrotz eben über diese Referenzen auch kräftig verwendet werden. Lediglich wenn die Formularfelder nach der Referenzbildung noch ausgewertet werden (was beim normalen Abschicken des Formulars passiert), werden eben über die Referenzen die neuen Namen den anonym im Arbeitsspeicher herumlungernden Speicherplätzen zugewiesen, bevor sie über die Referenzen in den HTML-Code wandern.

Aus der Situation gäbe es zwei Wege, die Sie beide probieren können:
  • Entweder auf die Referenzen ganz verzichten (das vermeidet derartige Flüchtigkeitsfehler, verlangt aber auch den Verzicht auf den Komfort mit den Referenzen)...
  • ...oder die Behandlung der Datenstrukturen, aus der Teile referenziert werden sollen, VOR der Referenzbildung abschließend fertigstellen und NACH der Referenzbildung nur noch ÜBER die Referenzen auf die betreffenden Daten zugreifen. (Was nicht nur für das konkrete Problem HIER gilt, sondern generell für den Umgang mit Referenzen in PHP!)

Die erste Variante würde eine Art Ausreißen vom Problem darstellen. Das ist in der Praxis durchaus üblich und erlaubt (Probleme zu vermeiden, indem man ihre Ursachen meidet). Aber weil Sie jetzt im Lehrgang durchaus mit Techniken umgehen lernen sollen, zu denen die Referenzen dazugehören, versuchen Sie sich ruhig an einer Verlagerung der Referenzbildung!

Aufgabe: Überlegen Sie nochmal anhand der letzten Erläuterung, wo Sie in Ihrem Programm die Referenzbildung frühestens ausführen dürften und wo Sie sie spätestens ausführen müßten (wo Sie sie verwenden wollen)! Verlagern Sie das Codeschnipsel entsprechend! Testen Sie die Funktion!

Ihr Ergebnis könnte in etwa SO aussehen...

Das war's für diese Lektion.
Probieren Sie ruhig noch etwas die Benutzung Ihres Spieles aus! Testen Sie nochmal die Komfortfunktionen Undo/Redo, Speichern/Laden und Simultanspielen! Es sollte jetzt alles rund laufen.

Zeit, daran zu denken, das Spiel benutzbar für richtiges online-Spielen gegen einen an beliebiger Stelle dieses Erdballs sitzenden Gegner zu machen. Das kommt in der nächsten Lektion...



Impressum
Email
aktualisiert: 2013-02-12 00:39