Einführung in die Programmierung mit HTML und PHP
Einrichtung einer Multiplayer-Spielgemeinschaft
Zentrale - Lehrgänge - Einführung HTML+PHP - Multiplayer-Spielgemeinschaft

Lehrziel

tictactoe - achter Schritt Unser heutiges Ziel ist die Vervollständigung des Multiplayer-Modus durch beliebig viele Spielbretter - und zwar gleichzeitig für beliebig viele verschiedene Mitspieler als auch für beliebig umfangreiche Simultanspiele eines einzelnen Spielers.

An der Oberfläche des eigentlichen Spieles wird nicht mehr viel hinzukommen: Nur ein Button für das Eröffnen eines neuen Spielbretts. Zusätzlich werden wir aber eine Datenbank der angemeldeten Spiele einrichten und eine extra Seite, auf der man aus diesen eines auswählen kann, dem man beitreten oder dem man zugucken möchte.

Wir werden das in Etappen realisieren:
  • Jedem sein eigenes Brett: Zuerst werden wir einrichten, daß jeder Besucher zunächst wieder sein EIGENES Brett bekommt, wenn er das Spiel betritt. Wir werden organisieren, daß jedes Spielbrett eigenständig gespeichert wird.
  • Einem Spiel beitreten: Wir richten dann ein Konzept ein, daß ein Spieler einem anderen Spiel beitreten kann.
  • Die Spieldatenbank: Danach werden wir einrichten, daß jeder Spieler sich die augenblicklich eingerichteten Spielbretter angucken und einem beliebigen davon beitreten kann.
  • Simultanspiel mit Sitzungs-Vervielfachung: Zum Schluß richten wir ein, daß jeder Spieler in seinem Browser beliebig viele verschiedene Spiele öffnen kann.

Da unser Programm inzwischen hinreichend komplex geworden ist, daß es nicht mehr als "übersichtlich" zu bezeichnen ist, werden wir zudem eine Technik einführen, mit der wir das Programm in mehrere Teilprogramme zergliedern können:

Aufteilung eines PHP-Programms in mehrere Dateien

Bevor wir an die spieltechnischen Details gehen, bringen wir erstmal wieder Übersichtlichkeit in unser inzwischen doch angeschwollenes Programm. Das wird uns insbesondere die mal wieder notwendige Verlagerung von Programmabschnitten, die untereinander in einer ganz bestimmten Folge abhängig sind, erleichtern.

Um ein PHP-Programm zu zerlegen, sucht mal Programmblöcke, die zunächst mal in sich ein thematisch hinreichend abgeschlossenes (selbständiges) Gebilde darstellen. Das läuft nicht anders als bei anderen Programmiersprachen auch:
  • Sie analysieren Ihr Programm danach, welche atomaren Teile (Funktionen, Daten) welche Abhängigkeiten untereinander haben.
  • Sie zerschneiden es so, daß Blöcke mit hohen inneren Abhängigkeiten, aber minimalen äußeren Abhängigkeiten entstehen.
Wenn Sie sich das Zeug als Spinnennetz vorstellen, in dem einige Kokons hängen, dann zerlegen Sie Ihr Programm so, daß sich einzelne Kokons oder auch Kokongruppen so leicht wie möglich (mit minimalen Fäden) aus dem Gespinnst heraustrennen lassen.

Wenn Sie später mal größere Projekte beginnen, werden Sie dazu übergehen, Ihr Projekt gleich von vornherein in solche Blöcke getrennt anzulegen. Und so eine Zergliederung ist nicht nur für die Übersichtlichkeit sinnvoll, sondern elementare Grundlage für eine Zusammenarbeit mehrerer Programmierer. Außerdem werden Sie viele Funktionen, die Sie im Laufe der Zeit entwickeln, wiederverwenden wollen. Dafür werden Sie diese Funktionen in sogenannte "Bibliotheken" auslagern, wo Sie diese nach Themen und Nutzungshäufigkeit in verschiedenen Dateien ablegen werden.

Zergliederung Ihres TicTacToe-Spiels
Wir haben in unserem Quellcode bisher schon viel mit Kommentaren gearbeitet und alle größeren Blöcke von funktionalen Abschnitten mit deutlichen Überschriften versehen. Anhand derer sollte es sehr leicht fallen, eigenständige Blöcke auszumachen.

Wobei wir nicht auf Teufel-komm-raus alles in kleinstmögliche Teile zergliedern wollen - dann leidet nicht nur die Übersichtlichkeit auch gleich wieder darunter -, sondern in einigermaßen sinnvolle Blöcke. Da fällt ins Auge:
  • Wiederherstellung von Sitzungs-Parametern ("Session-Block")
  • Vorbereitung der und Funktionen für Datenstrukturen ("Datenstrukturblock")
  • Funktionen zur Datenspeicherung ("Cryptoblock")
  • Funktionen zur Spielfeldauswertung ("Spielregelblock")
  • Verarbeitung des Spielzustands (Laden bis Speichern; "Spielzustandsblock")
  • Erzeugung der HTML-Anzeige des Spieles ("HTML-Block")
Nun zwingt Sie keiner, alles auslagern zu müssen. Der Session- und Datenstrukturblock sind hier (noch zumindest) so klein, daß eine Auslagerung eventuell nicht lohnt. Außerdem werden wir eine Reihe von Elementen noch ändern, namentlich solche, die mit der Session, den Spieldaten, deren Verarbeitung und der Anzeige zu tun haben. Da wir hier keine Arbeitsteilung im Kollektiv machen und außerdem ab und zu noch die Programmstruktur ändern, weil wir im Anfängerkurs schrittweise Konzepte erschließen und dann nachrüsten, lassen wir solche veränderlichen Bereiche, wenn sie nicht gerade störend umfangreich sind, besser noch zusammen.

Der Crypto- und der Spielregelblock dagegen sind erstmal (solange Sie nicht darangehen, das Spiel aufzubohren) konstant und ziemlich fertig (wenn Ihnen nicht noch geniale Streiche einfallen). Die lohnen sich für eine Auslagerung.

Zur Technik:
  • Sie kopieren den betreffenden Block in eine extra Datei und geben der einen sinnvollen Namen. Die Dateiendung sollte "php" sein - aus gutem Grund! Die gesamte Datei sollte (aus dem selben Grund) eine gültige PHP-Datei sein. Das heißt also, daß sie von ihrem inneren Aufbau her ZUNÄCHST MAL eine HTML-Datei ist. Und daß Sie aus dem HTML-Modus in den PHP-Modus umschalten müssen, wie Sie das mal in Lektion 3 gelernt hatten: mit den Tags <?php ... ?>!
    Wenn Sie PHP-Codebschnitte auslagern, vergessen Sie also nicht, diese PHP-Tag-Klammer drum herum zu setzen!
  • Sie schmeißen den Block aus dem Originalprogramm raus und hinterlassen statt dessen an dieser Stelle eine "include"-Anweisung.
Die include-Anweisung gibt es in mehreren Variationen: In der verlinkten Referenzdokumentation steht näheres dazu.

Wir werden hier mit "require_once" arbeiten. Es ist ERLAUBT, daß Sie sich eine Grundform angewöhnen, die auf nahezu alle praktischen Belange optimal paßt. Und das ist dieses "require_once".

Aufgabe: Lagern Sie einige in der Entwicklung relativ abgeschlossene Blöcke, die wir nicht mehr ändern brauchen, aus Ihrer Haupt-Datei aus!
Ich empfehle das Auslagern von zumindest:
  • Cryptoblock
  • Spielregelblock
Eine Relativierung zur Zergliederung in Dateien
PHP ist eine "interpretierte" Sprache. Als solche werden die Quelldateien zwar heutzutage auch vor der Ausführung erstmal in optimierten Maschinencode übersetzt, aber dies geschieht bei jedem Ausführen eines PHP-Programms aufs neue. Wenn Sie die gerade kennengelernte Technik mit Bedingungen verknüpfen (ein "include" in einem "if"), dann sogar erst im Moment des erfolgreichen Ausführens der entsprechenden Anweisungen im Programm.

Das bedeutet auch, daß zur Ausführung eines PHP-Programms sämtliche Dateien, in die das Gesamtprogramm zergliedert ist bzw. aus denen es sich zusammensetzt,...
  • im Dateisystem des Rechners gefunden werden müssen (in der Größenordnung von 1000 CPU-Takten pro Datei)
  • eventuell direkt vom Datenträger gelesen werden müssen (in der Größenordnung von zigtausenden CPU-Takten pro Datei)
  • compiliert werden müssen (in der Größenordnung von zigtausenden CPU-Takten pro Datei, proportional zur Codemenge)
Die ersten beiden Punkte davon hängen unmittelbar linear mit der Anzahl der Einzeldateien zusammen, in die man ein PHP-Programm zerlegt: Statt vom Webserver beim Abfragen eines Dokuments dieses eine Dokument als eine einzige Dateioperation zu laden und auszuliefern, wie das bei allen "statischen" Dokumenten ("normalen" HTML-, CSS-, Bilddateien und so weiter) der Fall ist, werden bei dynamisch per PHP erzeugten Dokumenten gegebenenfalls zig bis hunderte Dateioperationen fällig - allein um den Quelltext vollständig zu laden. Diese Dateioperationen haben einen erheblichen Overhead in sich. Das Betriebssystem erledigt eine Vielzahl von Checks und Automatismen im Hintergrund, von denen man bei der Benutzung nichts sieht, die aber Zeit kosten, unter anderem:
  • Überprüfung von Zugriffsrechten
  • Überprüfung von "Hard-Links"
  • Überprüfung und Ausführung von Überwachungsoperationen (Log-Einträgen und dergleichen)

Der dritte Punkt ("compiliert werden") bekommt Relevanz, wenn Sie massiv Bibliotheken benutzen und in diesen Bibliotheken hunderte Funktionen ansammeln, die Sie zwar über Ihre Projekte hinweg gestreut vielfach wiederverwenden, aber in jedem einzelnen Projekt bzw. jedem einzelnen PHP-generierten Dokument immer nur eine kleine Auswahl von allem benötigen. Dann muß der PHP-Interpreter trotzdem den ganzen Salat vor Ausführung kompilieren.

Die Bibliotheken vereinfachen Ihnen einerseits Ihre Arbeit, andererseits erhöhen sie massiv die CPU-Last des Webservers - linear zur Größe der Bibliotheken bzw. zum Quotienten der in denen enthaltenen zu den davon genutzten Funktionen.

Es ist bei PHP (wie bei sämtlichen anderen Scriptsprachen auch) an dieser Stelle also etwas Augenmaß angesagt, wenn man nicht in eine Performancefalle tappen will. Insbesondere kann das massive Nutzen von großen, allgemeinen Bibliotheken aus dem OpenSource-Bereich schnell mal nach hinten losgehen. Sämtliche großen PHP-Projekte (Foren, Wikis, Groupware usw.) haben den Makel, daß sie krass CPU-lastig sind, weil sie mssiv auf Bibliotheken aufsetzen und in sich auch nochmal massiv zergliedert sind.

So toll die Sache also auf den ersten Blick aussehen mag: Üben Sie ein gesundes Maß an Zurückhaltung!

Jedem sein eigenes Brett

Wir geben jetzt erstmal jedem Spieler, der das Spiel erstmalig besucht, ein eigenes Brett vor die Nase...

Nichts leichter als das: Alle notwendigen Techniken haben wir längst kennengelernt. In diesem Fall benötigen wir lediglich für jeden seine eigene Spieldatei. Das könnten wir trivial einrichten, indem wie deren Namen aus der Spielerkennung (ergo der Session-ID) bilden:
$spieldateiname = 'spiele/tictactoe-'.$sid.'.bin';

An der Bedienoberfläche ändert sich dadurch erstmal gar nichts. Allerdings können jetzt auch wieder nicht mehr zwei Spieler zusammenfinden. Damit haben wir letztlich für den Spieler erstmal wieder dieselben Verhältnisse wie am Ende von Lektion 6 - einen Einzelspieler-Modus.
Wir sehen deshalb jetzt gleich im Anschluß vor, daß man sich einem bereits bestehenden anderen Spiel anschließen kann...

Außerdem hätte diese Art der Spiel-Identifizierung einen Haken, den wir uns im nächsten Abschnitt ansehen...

Einem Spiel beitreten

Erinnern Sie sich, wie Sie in der letzten Lektion den Zustand des "Auto-Refreshs" für jeden Spieler einzeln individuell speichern lassen haben? Falls nicht, werfen Sie nochmal einen Blick zurück!
Wir hatten dort das Grundprinzip kennengelernt, wie man Daten spezifisch für eine "Sitzung" speichert (also einen ganz konkreten Besuch eines ganz konkreten Browsers auf einer Seite - wobei ein Besuch das Herumwandern auf der Seite innerhalb eines Zeitfensters umfaßt).

Dieses selbe Prinzip können wir für alles mögliche anwenden, zum Beispiel auch für die Teilnahme an einem bestimmten Spiel:
  • Wir legen mal wieder eine neue Variable an - als Session-Variable genau wie für den Auto-Refresh.
  • Die Variable lassen wir steuern, welches Spiel wir laden lassen. Wir laden also nicht mehr stur - wie eben gerade eingerichtet - ein Spiel, dessen Name von der Spielerkennung abhängt, sondern lassen ein Spiel mit der Kennung eines beliebigen (gegebenenfalls ANDEREN) Spielers laden. Wobei unsere neue Variable genau die Kennung dieses Spieles aufnimmt.

Die Spiel-Kennung
Dabei möchte ich erstmal aus gutem Grund diese Sache noch als Theorie in der Luft hängen lassen und erstmal in aller Ruhe mit Ihnen begutachten...

Wenn wir die Spiel-Kennung genauso heißen lassen wie die Spieler-Kennung, und wenn wir dazu organisieren (was wir ja müssen, wenn man ein Spiel auswählen können soll), daß die Spiel-Kennungen allen Besuchern bekannt sind...
...dann haben wir WASJa: Eine Session-Diebstahl-Einladung! erzeugt?!?

SO einfach also nicht!

Wir können die Spielkennung irgendwie - vollkommen beliebig, auch einfach linear durchgezählt - festlegen lassen. Wobei wir allerdings auf Probleme stoßen, wenn wir den Fall haben sollten, daß eine große Anzahl verschiedener Spieler gleichzeitig zu Besuch ist... Als Alternative dazu können wir die Spielkennung als eine Art Zufallszahl bestimmen lassen, bei der die Wahrscheinlichkeit, daß zwei gleiche Werte herauskommen, dermaßen gering ist, daß die Sonne vorher zur Supernova wird, ehe ein solcher Fall zu erwarten ist.
Wir haben für diesen Zweck ein besonderes Mittel (eventuell ist das den neugierigen oder fortgeschritteneren unter Ihnen über den Weg gelaufen, als Sie mal in die Internas der verschlüsselten Spielzustands-Speicherung in Kapitel 6 geguckt hatten): Hashfunktionen:
  • md5
  • sha1
  • hash (verallgemeinerte Form mit Angebot einer Vielzahl von Hash-Algorithmen)
Sowas wird übrigens auch bei der Bildung von Session-ID's eingesetzt.

So eine Funktion verwenden wir hier jetzt einfach für unsere Spiel-ID. Die ist also etwas, was nach gleichem Prinzip wie die Session-ID alias Spieler-Kennung gebildet wird, aber eben NICHT auf den Browser bezogen (das ging dort mit Hilfe eines Cookies und dessen automatischen Sende-Mechanismus), sondern auf das Spiel (also die Spieldatei bzw. den darin gespeicherten Spielzustand).

Damit eine Hashfunktion tatsächlich in unserem Sinne funktioniert, muß sie allerdings mit "anständigen" Zufallsdaten gefüttert werden. Damit wir uns nicht auch noch um dieses Thema kümmern müssen, lassen wir das einfach das PHP-System selbst erledigen: Die Spieler-Kennung WAR von uns BEREITS (!) dem PHP-System überlassen worden - indem wir die standardmäßige Session-Verwaltung dafür bemüht hatten. DIE HAT BEREITS mit der Session-ID einen Wert erzeugt, der hinreichend anständig mit Zufallsdaten gewürzt ist (sofern die PHP-Hersteller nicht geschlampt haben - aber das ist eine ganz andere Geschichte...).

...Wir füttern unsere Hashfunktion daher einfach mit der Session-ID, womit wir dicke ausreichend Zufälligkeit zur Unterscheidung der Spielkennungen eingebracht haben.

Durch das Abhängigmachen von der Spieler-ID könnte es aber sein, daß wir dennoch eine Angreifbarkeit der Spieler-ID (alias Session-Cookie) einführen, wenn wir entweder eine "zu schwache" Hashfunktion benutzen und/oder diese nicht mit nochmal weiteren Zufallsdaten (wo es in diesem Fall nur drauf ankommt, daß die von einem Hacker nicht erraten werden können, nicht darauf, daß sie sich aktuell ändern - weil letzteres bringt ja schon unsere Session-ID mit rein) angereichert werden. Deshalb modifizieren wir die Spielerkennung zusätzlich mit zum Beispiel unserem Master-Schlüssel für die Spieldaten-Verschlüsselung.

$spiel_id = hash($crypt_datahash,$sid.$crypt_key);

In einem späteren Abschnitt werden wir noch etwas ergänzen, damit wir mit ein und demselben Spieler (damit ein und derselben Session-ID) mehrere verschiedene Spielbretter (also Spiele und deren ID's) aufmachen können...
Die Spielkennung zur Eigenschaft des Spieles machen
Nachdem wir nun grundlegend geklärt haben, wie die Spielkennung erzeugt werden und aussehen soll, muß sie noch zu einer Eigenschaft (einem Attribut) des (jedes) Spieles gemacht werden.

Das ginge auf verschiedenen Wegen:
  • Man könnte ein neues Element in die Spieldaten einfügen:
    $spiel = array
    (
        ...
        'leiter_id' => '',
        'spiel_id'   => $spiel_id,
        ...
    );
    
  • Man könnte aber auch die gesamte Spieldatei nach dieser Kennung benennen:
    $spieldateiname = 'spiele/tictactoe-'.$spiel_id.'.bin';
Die erste Variante wäre eine für Sie bereits bekannte Sache (Ihre Spieldaten haben schon jede Menge Datenelemente, da wäre ein weiteres für Sie nur noch Pillepalle).

Allerdings sollten wir jetzt vielleicht mal einen Blick auf den Sinn und Zweck unserer Spielkennung werfen:
  • Wir wollen sie zum Identifizieren und vor allem Auswählen von Spielen einsetzen!
  • Die Anwendung wird darin bestehen, daß der Spieler eine Spielkennung (per Formular oder so) auswählt und auf Basis dieser Eingabe dann eine Spieldatei zu laden sein wird!
Das letztere legt doch nahe, daß wir besser fahren, wenn wir die Spielkennung unmittelbar zum Dateinamen machen. Wobei uns niemand verbieten würde, die Spielkennung zusätzlich auch als Datenelement in die Spieldaten einzusetzen...
Die Auswahl der Spielkennung sinnvoll notieren
Zu Beginn des Abschnitts hatte ich schon mal an die Session erinnert, aber wir können uns die verfügbaren potentiellen Möglichkeiten ruhig nochmal vor Augen führen...

Wir können die individuelle Session-Auswahl auf zig verschiedenen Wegen umsetzen, aber die naheliegendsten (die Techniken benutzen, die uns schon bekannt sind) wären:
  • Wir könnten dafür extra hidden Inputs bzw. extra URL-Query-Parameter einbauen.
  • Wir könnten uns an einem extra Cookie vergreifen.
  • Wir könnten eine extra Session-Variable (wie die zum Auto-Refreshen) einsetzen.
Die beiden letzten sind die bequemsten Methoden. Die allerletzte ist die, die am wenigsten Datenverkehr benötigt und außerdem auch noch am einfachsten einzurichten ist.
Klar was wir anwenden, oder?!

Nennen Sie dieses Element wieder 'spiel_id' (wie jenes entsprechende Element in den Spieldaten) oder auch 'spiel_auswahl' (wenn Sie den funktionellen Aspekt mehr in den Vordergrund rücken möchten)!
$spiel_auswahl = &$_SESSION['spiel_auswahl'];
Die Auswahl der Spielkennung im richtigen Zusammenhang festlegen
Nachdem wir wissen...
  • wie die Spielkennung aussehen soll
  • wie wir sie als Eigenschaft des Spieles notieren wollen
  • wie wir sie als aktuelle Auswahl in der Sitzung notieren wollen
...bleibt noch zu klären, wie und wo wir die Spielkennung des Spieles, an dem der Spieler aktuell teilnimmt, im Programm AUSWÄHLEN lassen...

Betrachten wir die verschiedenen Fälle, in denen die Spielkennung auszuwählen ist...
  • erstmaliges Eintreffen: Wenn der Spieler die Spielseite das erste Mal besucht, wird für ihn automatisch ein neues Spiel eröffnet. Logischerweise soll er in diesem Fall dieses eigene Spiel sofort vor der Nase haben! Da wird die Kennung frisch neu angelegt.
  • anderes Spiel auswählen: Wenn der Spieler ein anderes Spiel auswählt (auch das richten wir in einem späteren Abschnitt noch ein), soll er jenes sofort vor der Nase haben! Da kommt die Kennung aus einem $_REQUEST-Parameter.
    Nennen Sie auch diesen Parameter konsistent "spiel_id" oder "spiel_auswahl"!
  • jeder andere Fall: In jedem anderen Fall soll er das einmal ausgewählte Spiel beibehalten! Da kommt die Kennung also aus den Session-Daten.

Betrachten wir jetzt, an welcher Stelle im Ablauf der Scriptverarbeitung wir die Spielauswahl einsetzen müssen...
  • Trivial einsehbar ist, daß die Auswahl spätestens VOR dem Laden der Spieldaten abgeschlossen sein muß.
  • Da wir uns gerade eben entschlossen hatten, die tatsächliche aktuelle Auswahl als Datenelement in der Session einzutragen, kann die Auswahl frühestens nach Eröffnung der Session feststehen (falls keine Änderung der Auswahl zu verarbeiten ist)
  • Wenn der Spieler gerade eine Aktion ausgeführt hat, die eine Änderung der Auswahl bewirken soll (zum Beispiel ein neues Spiel eröffnen oder einem anderen Spiel beitreten), muß diese Zustandsänderung zwischen den beiden eben genannten Punkten stattfinden.

Abschließend nochmal eine Betrachtung, wie genau der Wert dieser Variablen nun manipuliert werden muß...
  1. Die Übernahme aus der Session-Variablen sollten wir zuerst machen. Um sie leichter verarbeiten zu können, ist mal wieder eine Referenzbildung sinnvoll. Beides zusammen bewirkt, daß jede Änderung des Wertes über diese Referenz, die wir anschließend einrichten, gleich wieder automatisch in den Session-Daten gespeichert wird.
    $spiel_auswahl = &$_SESSION['spiel_auswahl'];
    
  2. Falls da noch nichts drin steht, liegt der Fall vor, daß ein neues Spiel eröffnet wurde. Dessen Spielkennung wird als Auswahl eingetragen.
    if (!isset($spiel_auswahl)) $spiel_auswahl = $spiel_id;
    
  3. Falls durch eine Benutzeraktion ein anderes Spiel ausgewählt werden soll, liegt ein entsprechender Parameter im $_REQUEST vor. Aus dem übernehmen wir gegebenenfalls die Spiel-ID.

    Wobei wir wieder mal daran denken, diese Übernahme gegen Mißbrauch zu schützen: Wir wollen diese Daten schließlich zum Laden einer Datei benutzen! Deshalb nehmen wir ausschließlich Kennungen an, die auch tatsächlich existierenden Spieldateien zugeordnet sind und keine Pfad-Operanden (bzw. nur die von uns vorgesehenen Zeichen für unsere Hashwerte) enthalten...
    function spieldateiname($id) {return 'spiele/tictactoe-'.$id.'.bin';}
    
    if (isset($_REQUEST['spiel_auswahl']))
    {
        $spiel_auswahl_neu = strtr($_REQUEST['spiel_auswahl'],':/\\?.*','______');
        
        if (file_exists(spieldateiname($spiel_auswahl_neu)))
            $spiel_auswahl = $spiel_auswahl_neu;
    }
    

Übung zum Einbau der Spiel-Kennung
Aufgabe: Übertragen Sie die diskutierten Codeschnipsel in der richtigen Anordnung (und gegebenenfalls mit Anpassungen an Ihre konkrete Version) in Ihr Programm!
Das Ergebnis könnte in etwa so aussehen...

Test der Spiel-Auswahl
Wir haben bisher noch keine Möglichkeit vorgesehen, die Spielauswahl vom Spieler auf der Spieloberfläche vornehmen zu lassen. Das machen wir im nächsten Abschnitt. Dennoch könnten wir schon mal die Funktion der Spielauswahl testen, indem wir wieder mal auf Hacking-Methoden zurückgreifen.

  • Eröffnen Sie ein Spiel, indem Sie das aktuelle TicTacToe aufrufen! Tragen Sie dort einen Spielernamen ein, damit Sie dieses Spiel eindeutig gekennzeichnet haben!
  • Starten Sie ein weiteres Spiel in einem zweiten Browser (der Internet Explorer sollte auf jedem System verfügbar sein)! Vergewissern Sie sich, daß in diesem Spiel nichts von dem steht, was Sie gerade eben im ersten Spiel eingetragen hatten! Tragen Sie auch hier irgendetwas ein, was für Sie dieses Spiel eindeutig (unterscheidbar vom ersten) kennzeichnet!
  • Kontrollieren Sie im Spiel-Verzeichnis auf dem Webserver, daß zwei Spieldateien angelegt wurden!
  • Ergänzen Sie nun in der URL-Zeile eines Browsers eine Query, die die Spiel-Kennung des Spieles im anderen Browser enthält! Zum Beispiel:
    http://localhost/tictactoe-spielfeld-7-2.php?spiel_auswahl=b7e475df20ffcb9d74240ea273f987a2
    
  • Lassen Sie das Dokument in diesem Browser mit dieser Query abrufen! Sie sollten jetzt eine Kopie des Spiels vom anderen Browser sehen! Anschließend können Sie zwischen den beiden Browsern ein Spiel gegen sich selbst spielen. Prüfen Sie auch das!

Falls bei diesem Vorgang etwas nicht stimmen sollte, müssen Sie Ihr Programm nochmal durchsehen und auf Vordermann bringen!

Die Spieldatenbank

Nachdem wir eine Kennung zu einem Spiel haben und das Spiel beim Übergeben einer Kennung wechseln könnten, fehlt noch die Möglichkeit, diesen Wechsel auch tatsächlich auf der Bedienoberfläche auszuführen.

Netterweise sollte es dazu eine Übersicht über die eröffneten Spiele geben. Wenn es viele Spieler werden sollten, darf die auch durchsuchbar gemacht werden - was nichts anderes darstellt als eine Selektion von Datensätzen anstelle der Gesamtmenge anzuzeigen. Kein Gott zwingt einen Webprogrammierer, dazu ein extra Dialogfeld mit extra Ergebnisseite und extra Bedienoberfläche zu erzeugen - wie Sie es sonst im allgemeinen üblicherweise im Netz finden werden. Sie können das besser! Allerdings werden wir uns das Selektieren im Moment noch klemmen, aber ich gebe ein paar Anregungen, wie das jeder nach seinem Geschmack einbauen kann.

Mindestens aber benötigen wir zur Auswahl ein paar Elemente aus den Spieldaten, um den auswählenden Spieler über die Spielteilnehmer (und eventuell später auch mal Spielregeln) zu informieren.
In die Liste sollten folgende Daten sichtbar dargestellt werden:
  • Name des Spielleiters
  • Name des eventuellen Mitspielers
  • Spielzug, falls das Spiel bereits begonnen hat
  • Falls mal später Spielregeln hinzukommen, die aktivierten Spielregeln
Als Vorbild können Sie mal einen Blick in das "große TicTacToe" oder das "Mensch-ärgere-Dich-nicht" werfen! (Jeweils auf den Schalter "einem anderen Spiel beitreten" klicken - gegebenenfalls nach Erzeugung eines Spieles in einem anderen Browser, damit was zu sehen ist...)


Nun haben wir für jedes eröffnete Spiel eine Spieldatei. Solange die Spieldaten nicht riesig werden und/oder die Spieldateien sehr viele, können wir zum Auflisten der Spiele einfach alle Spieldateien durchhangeln. Andernfalls wäre es sowieso anzuraten, auf eine Datenbank umzusteigen. Da machen wir aber erstmal auch noch einen Bogen drum.

Was also brauchen wir zum Durchstöbern der angelegten Spiele?
  • Um eine Liste aller existierenden Spieldateien zu erhalten, benötigen wir eine Funktion, die uns alle Dateien in einem Verzeichnis auflisten kann. Das geht folgendermaßen:
    $files = scandir($path);
    
    Die Funktion liefert ein Array aller Dateinamen im betreffenden Verzeichnis.
  • Das kann man anschließend in einer Schleife durchwandern. Sinnvollerweise in einer "foreach"-Schleife, die uns unmittelbar die einzelnen Elemente - also Dateinamen - liefert. Dazu gleich in der Übung mehr... Schleifen an sich hatten wir bereits in Lektion 4 kennengelernt, "foreach" erstmals in Lektion 6.
  • Wie wir Spieldateien laden und in eine Datenstruktur zurückverwandeln, wissen Sie noch? Das Speichern und Laden auf dem Server hatten wir uns im Kapitel 7 zugelegt - es war ein trivialer Funktionsaufruf.
  • Wie wir auf Elemente der Spieldaten-Struktur zugreifen, haben wir jetzt seit Lektion 4 wahrhaftig hundertfach geübt - kein Problem also...
  • Anzeigen sollten wir die Spiele am besten als Liste mit mehreren Spalten. Sowas nennt man dann "Tabelle" bzw. auf HTMLisch "table".

Spielliste
Nun werden wir auf der bisherigen Spieloberfläche keinen Platz zum Präsentieren der Spielliste haben. Wir legen dazu eine neue, eigenständige PHP-Seite an, die vom Spielbrett aus über einen Schaltknopf aktiviert werden kann und nach Spielauswahl wieder zurück zum Spiel führt.

Der Schaltknopf auf dem Bedienpanel zum Aufruf der Spielliste darf von Ihnen an eine beliebige Stelle völlig frei nach Geschmack gesetzt werden. Wir haben jetzt schon so viele Schaltknöpfe im Panel, daß es für Sie eine Leichtigkeit darstellt, einen davon zum Vorbild zu nehmen und zu kopieren! Ich handle diesen Punkt daher nicht gesondert ab. Ob Sie den Schaltknopf als Formularelement oder als einfach Link schreiben, ist vollkommen Ihr Bier. Wichtig ist nur, daß der Link oder das Formular zur Spielliste verweist.

Verarbeiten einer Liste vom scandir
scandir liefert ein Array von Dateinamen - womit hier sowohl eigentliche Dateien als auch Verzeichnisse gleichermaßen gemeint sind. Wobei wir von uns aus ja nun nichts anderes als normale Dateien in das gescannte Verzeichnis schmeißen werden. Aber scandir liefert auch die "virtuellen Navigations-Verzeichnisse" namens "." und "..". Die müssen "aussortiert" werden, was man einfach durch Abfrage des jeweils behandelten Dateinamens erledigen kann. Das "Aussortieren" geschieht, indem man sie einfach wegläßt. Einen Schleifeninhalt weglassen, um mit dem nächsten Schleifendurchlauf fortzusetzen, geht mit "continue".

Falls wir sowas wie Unterverzeichnisse in dem Verzeichnis hätten, müßten wir die auch irgendwie extra behandeln (je nachdem, was wir wollen täten: aussortieren oder rekursiv durchforsten). Feststellen läßt sich das (OB ein Dateiname ein Verzeichnis bezeichnet), indem man eine Funktion zur Prüfung des Dateityps benutzt: "is_dir" zum Beispiel eignet sich. Im Moment (solange Sie nicht von sich aus mit solchen Schwerzen arbeiten) dürfen Sie diesen Aspekt erstmal vernachlässigen.

Falls Sie Dateien anderer Art ins selbe Verzeichnis schmeißen wollten, könnten Sie diese von den gesuchten Spieldateien anhand des Musters des Dateinamens unterscheiden: Wir haben für unsere Spieldateien ein Prefix und ein Suffix festgelegt. Deren Vorhandensein am Anfang bzw. Ende des Dateinamens kann man testen ("strpos" bzw. "strrpos").

Noch ein paar Details zur Verarbeitung der Liste:
  • Das EIGENE Spiel (wo die Spiel-ID, die auch im Dateinamen zwischen Prefix und Suffix zu finden ist, mit der "aktuell ausgewählten" Spiel-ID übereinstimmt) sollte eventuell nicht in der Liste erscheinen, sondern statt dessen am Rand der Liste (drüber, drunter, daneben - wie immer Sie lustig sind) ein Schalter oder Link, um zum eigenen Spiel zurückzukehren. Dadurch können Mißverständnisse vermieden werden, wenn man tatsächlich nur mal gucken, aber nicht wirklich sein Spiel verlassen wollte.
    Alternativ würde sich auch eine extra Einfärbung des eigenen Spiels in der Liste anbieten.

  • Zudem sollten in der Liste nur Spiele aufgenommen werden, für die es einen bereits eingetragenen Spielleiter gibt. Denn ein leeres Spiel HAT der Spieler ja gegebenenfalls schon vor seiner Nase. Wenn er bei einem anderen Spiel mitspielen möchte, verläßt er sein eigenes Spiel solange und gibt dort die Spielleiter-Eigenschaft ab. Da im Schnitt immer doppelt so viele Spieler in der "Spielhalle" herumhängen werden, wie Spiele eröffnet sind (jedenfalls solange die Leute vorrangig miteinander statt im Solo-Modus spielen), wird im Schnitt immer die Hälfte aller Spiele "verweist" sein. Die wollen wir aber nicht vor die Nase bekommen, wenn wir uns einem anderen Spiel anschließen wollen, oder?!

  • Dateien, die verwaist sind (also hoffnungslos veraltet - zu testen mit "filemtime" gegen die aktuelle "time") sollten eventuell ebenfalls nicht aufgelistet werden. Am besten gleich noch nebenbei gelöscht ("unlink"), damit sie nicht immer wieder stören.

  • Das Beitreten wird mit einem einfachen Link (oder Formular - ganz wie Sie das mögen) realisiert, der wieder auf die eigentliche Spielfeld-Seite zurückverweist, aber die Spiel-ID als Parameter mitgeschickt bekommt.


Aufgabe: Setzen Sie die Seite mit der Spielliste auf!
Ich lasse diesmal bewußt viel Freiraum für eigene Ideenfindung, weil die Formulierungsmittel für die einzelnen Schritte allesamt jetzt mehrfach geübt worden sind und Sie ruhig mal selbst in den eigenen Übungen und gegebenenfalls in den Referenzdokumentationen nachschlagen sollen!

Das Ergebnis könnte in etwa SO aussehen...
Für diese Spielliste wurde das Spielfeld nochmal überarbeitet ...

Aufgabe: Die Funktion dieser Spieleübersicht testen Sie jetzt bitte in aller Ruhe! Sie sollten dazu Spiele eröffnen, sich zu zweit oder zu mehr um einen Spielleiter zusammenfinden und dann kontrollieren, ob ihre Anmeldung am Spiel korrekt über die Bühne geht und Sie dann sowohl Spielteilnehmer werden können als auch einfach daneben sitzen bleiben, um das Spiel von zwei anderen zu beobachten.



Mögliche Weiterentwicklungen für Enthusiasten, die noch nicht ausgelastet sind:
  • Liste sortieren lassen!

    Dazu gehören Buttons in den Tabellenkopf und einer der vielen Wege nach Rom ausgewählt:
    • Nur nach genau einem Kriterium sortieren lassen, während die anderen unsortiert bleiben?
    • Oder mehrere Sortierkriterien gestaffelt zulassen? Wenn ja:
      • Eine extra Suchseite aufmachen, wo der Benutzer von seiner Suche und der Datenansicht fortgerissen wird?
      • Oder die Kriterien-Staffelung unmittelbar in die Verarbeitung der Buttonklicks auf der Tabellenoberfläche integrieren?
  • Liste selektieren lassen!

    Dazu gehören Möglichkeiten zur Eingabe von Suchkriterien, wobei auch hier viele Wege nach Rom führen:
    • Extra Suchseite aufmachen und den Benutzer aus der Tabellenansicht herausreißen?
    • Oder die Suchkriterien-Eingabe unmittelbar in die Tabellenköpfe integrieren - zusammen mit den Sortierbuttons?
Anregungen dazu gibt es unter anderem hier in einem schon etwas angestaubten Material eines PHP-Lehrgangs: Für diesen Einsteigerkurs würde das für die meisten Teilnehmer ein wenig zu viel auf einmal werden. Der Eigeninitiative sind aber keine Grenzen gesetzt.




Eine längere 20 Minuten Pause, um die Bedienoberfläche auszuprobieren und gegebenenfalls nochmal Fehler auszubessern!

Simultanspiel mit Sitzungs-Vervielfachung

Was jetzt noch fehlt, ist die Möglichkeit, gleichzeitig mehrere Spiele am selben Browser spielen zu können. Für unser kleines TicTacToe mit dem 3x3-Feld ist das wahrscheinlich nicht praxisrelevant, aber das TicTacToe ist nur eine Variante aus einer Spielklasse, in die übrigens auf Ebene der abstrakten mathematischen Analyse und im etwas weiter gefaßten Sinn geometrischer Bewegungsregeln sogar das Spiel "Mühle" gehört.

Wir haben noch einen weiteren Grund zur Motivation: Das Prinzip des Einrichtens von Simultanspielen führt zu einem Verfahren der Organisation von untergeordneten Sitzungen, welches von der Standard-Sitzungsverwaltung nicht abgedeckt wird, aber in vielen anderen Bereichen von Webanwendungen gleichermaßen sinnvoll bzw. notwendig sein kann, womit Sie sich fit für solche Anwendungen machen.
In vielen "ernsthaften" Webanwendungen, die mir in den letzten 20 Jahren untergekommen sind, wird der Fall, daß ein Anwender zwei verschiedene Ansichten ein und derselben Webanwendung in verschiedenen Browserfenstern unterhält, frech und rüpelhaft mit Fehlermeldungen quittiert, statt der Intuition des Besuchers entgegenzukommen. SIE können das besser und lernen jetzt, wie das geht!

Wieso Subsessions nicht ganz trivial sind
Eigentlich wollen wir bloß mehrere Spiel-Teilnahmen auswechselbar anlegen. Bisher wird die Spielteilnahme durch gerade mal ein Datenelement gesteuert: Eine Spiel-Kennung ("spiel_id").

Wir hatten die in den Session-Daten abgelegt... Erinnern Sie sich noch an den Zusammenhang, wie diese Session-Daten auf dem Server den Anfragen der Clients zugeordnet werden?
  • Der Server vergibt jedem Browser eine eindeutige Kennung.
  • Diese Kennung wird immer implizit im Hintergrund mit Hilfe sogenannter "Cookies" zwischen dem Server und dem Client hin und her geschickt.
  • EIN (!) Browser wird GENAU EINER (!) Session auf dem Server zugeordnet. Es ist von den Web-Standardisierern so festgelegt worden, daß ein Browser diese Cookies immer GLOBAL (!) für SÄMTLICHE (!) Fenster/Tabs gleich (!) anzusehen hat. Also: Empfängt ein Browser in irgendeinem Fenster, in dem er eine Verbindung zu einem Server unterhält, von diesem irgendein Cookie, so hat er dieses bei der nächsten Anfrage zu diesem selben Server aus JEDEM (!) anderen Fenster heraus genauso mitzuschicken!
Es gibt zwar gewisse Regeln, die eine Bindung der Cookies an Unterverzeichnisse auf dem Server gestatten, aber die sind nur dann nutzbar, wenn verschiedene Dokumente aus verschiedenen Verzeichnissen angefordert werden. Eine Unterscheidung mehrerer Cookies bezogen auf ein und dasselbe Dokument auf dem Server ist durch das Funktionsprinzip des Cookie-Mechanismus verboten worden.

Die Sache hat in der Praxis durchaus einen tieferen Sinn: Ein Besucher SOLL die Möglichkeit haben, zum Beispiel in einem Shop in den "Regalen" zu stöbern und DANEBEN - in einem ANDEREN Browserfenster - sich den Warenkorb anzeigen zu lassen. Wobei BEIDES miteinander synchron bleiben soll: Wenn beim Bummeln durch den Shop etwas in den Warenkorb gelegt wird, soll der Warenkorb (nach Aktualisierung) das anzeigen, was sich der Besucher in DIESER SITZUNG zusammengesammelt hat. Es ist also INTENTIONELL so gewollt, daß eine Sitzung über alle Browserfenster hinweg global wirkt.

Wir wollen aber nun sehr wohl einen Mechanismus installieren, der eine Unterscheidung verschiedener Browserfenster gestattet. Es sollen gewissermaßen verschiedene Sub-Sessions innerhalb einer Standard-Session angelegt und an verschiedene Browserfenster "gebunden" werden. Das entspräche einem Einkaufsbummel in einem Shop mit mehreren verschiedenen Warenkörben gleichzeitig oder eben unserem Simultanspiel an mehreren Brettern gleichzeitig. Der Standard-Session-Mechanismus unterstützt uns darin nicht.

Sub-Session-Kennungen
Was wir benötigen, ist trivial: Irgendeine Kennung, die wir in die ausgelieferten HTML-Seiten einbauen können, und zwar so, daß die mit jeder Anfrage wieder zum Server mitgeschickt werden. Allerdings eben NICHT über den Cookie-Mechanismus, sondern spezifisch für die einzelnen Dokumente, für die diese Kennungen herausgegeben wurden.

Sobald wir ein neues Spielbrett erzeugen wollen, an dem wir parallel neben dem ersten spielen können, klicken wir einen Link an, der die Seite mit der Spielbrett-Anzeige so erzeugt, daß eine um eins erhöhte Spielbrett-Kennung eingebaut ist. Zweckmäßigerweise in einem neuen Browserfenster, damit das alte Spiel noch vor unserer Nase liegen bleibt.

Damit wir da gegebenenfalls mehrere Male auf einen solchen Erzeuger-Link klicken können, lassen wir die Erzeugung neuer Spielbretter und die Verwaltung der zugehörigen Nummern auf dem Server erledigen (sonst müßten wir einen Mechanismus einrichten, der auch den erzeugenden Link auf der erzeugenden Seite irgendwie bei jeder Erzeugung eines neuen Spielbretts austauscht - das wären viel zu viele Kopfstände für zu wenig Nutzen!). Der Server braucht also wieder mal eine neue globale (Session-) Variable, in der die aktuelle "nächste" Spielbrett-Nummer abgelegt ist.

Auf dem Server lassen wir anhand dieser Sub-Session-Kennung die restlichen Sub-Session-Daten aus der "großen" Standard-Sitzung ziehen.
Die Kennung kann dabei eine einfache fortlaufende (Spielbrett-)Nummer sein.

An Alternativen bleibt da nicht viel: Wir KENNEN sehr wohl Möglichkeiten, Parameter in einzelnen HTML-Dokumenten über den Client zu schleifen, und haben die schon angewendet:
  • Entweder hidden fields in Formulare auf der Seite einbauen!
  • Oder Query-Parameter in die URL's auf der Seite einbauen!
Wir brauchen bloß noch eine Entscheidung, welche von den beiden Möglichkeiten wir verwenden wollen. Und die ist trivial:
  • Wo immer wir mit Formularen arbeiten, bauen wir ein hidden field ein.
  • Sonst setzen wir einen extra Query-Parameter in URL's, die der Spielsteuerung dienen.
(Wir dürfen übrigens beide mixen: Die tun sich gegenseitig nicht weh!)

Der Teufel liegt mal wieder im Detail:
Wenn wir verschiedene Links auf andere Seiten desselben Servers auf einer Seite haben, müssen alle die Subsession-Kennung als Parameter eingesetzt kriegen. Dabei kann es Links geben, die noch keine Query in sich tragen, aber auch solche mit Query.

Links ohne Query müssen um sowas wie "?ssid=1" ergänzt werden, Links mit Query dagegen um sowas wie "&ssid=1".

Dumm wird es, wenn man in einem sich entwickelnden Projekt solche Sachen nach und nach einbauen und später nochmal verschiedene Stellen nachbessern bzw. ausbauen möchte, aber nicht jedesmal auch alle möglichen Nebeneffekte wie die, ob jetzt zum Anhämgen eines Parameters in einem dazu gebildeten String gerade ein "?" oder ein "&" notwendig ist, durchzuhecheln gezwungen sein will.

Das ist mal wieder ein perfektes Einsatzgebiet von Funktionen: Wir könnten uns eine Funktion zum Einsetzen von Parametern in URL's basteln, deren Aufruf trivial ist und die sich intern um alle Details kümmert. Nehmen Sie das als Empfehlung. Wie Sie das konkret umsetzen, sei Ihre Freiheit!
Unterscheidung der Spiel-Kennungen
Das war aber noch nicht alles. Die Spielkennungen verschiedener von einer Person eröffneten Spiele sollen unterschiedlich sein. Wir wollen sie schließlich in der Spieleliste auseinanderhalten und anwählen können (sonst wird wohl kein Simultanspiel daraus werden)!

So, wie die Erzeugung der Spiel-Kennungen bisher eingerichtet war, würden alle Spiel-Kennungen in den Subsessions den gleichen Wert haben: Sie sind bisher abhängig von
  • Spieler-ID alias Session-ID, die über eine Sitzung hinweg konstant ist
  • Zufallsdaten, die allerdings über die Zeit konstant sind (alias Master-Passwort)
Die Session-ID ist zwar selber zeitabhängig, aber nachdem sie einmal gebildet wurde nicht mehr veränderlich über die Dauer der Sitzung hinweg. Die Subsessions benötigen also noch ein Element mit veränderlichem Wert. Reines Rauschen wäre OK, aber eine einzelne Bitänderung tut's auch. Wir nehmen einfach die fortlaufende Subsession-Nummer mit rein!

Einbau der Subsession-Kennung
So, jetzt haben wir also noch die Brett-Kennung einzusetzen... Wir hatten in der Diskussion dazu soweit zusammengestellt, daß...
  • die Brett-Kennung nicht an Cookies und Sessions gebunden ist, sondern ein individueller Parameter jeder einzelnen HTML-Seite, der mit jeder Seite ausgeliefert und von Links und Formularen einer jeden Seite wieder zum Server geschickt wird. Der dazu in jeden Link bzw. jedes Formular der Seite einzubauen ist.
  • die Brett-Kennung eine einfache fortlaufende Nummer sein soll, deren Erzeugung auf dem Server verwaltet wird.
  • die Erzeugung eines neuen Brettes durch Klick auf einen Button (als Link oder Formular-Submit - das ist schnurz) ausgelöst werden soll.



Impressum
Email
aktualisiert: 2013-03-28 12:18