JavaScript/Tutorials/TicTacToe

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

In diesem Tutorial lernen Sie, wie Sie das Spiel Tic-Tac-Toe als Browsergame selbst programmieren können. Voraussetzungen sind Grundkenntnisse in HTML, CSS und eventuell ein bisschen Syntax in JavaScript.

Inhaltsverzeichnis

[Bearbeiten] Vorüberlegungen

Die Darstellung des Spielfeldes (eine Matrix mit 3x3 Feldern) wird simpel über eine dreispaltige Tabelle über drei Zeilen erfolgen. In den Tabellenfeldern müssen die Spieler abwechselnd klicken oder auf einem Touchdisplay „tappen“, um ihre Spielzüge durchzuführen. Damit ein Spielfeld „markiert“ ist, wird dem zugehörenden HTML-Element (hier werden es wegen der verwendeten Tabelle td-Elemente sein) eine Klasse x für Spieler1 bzw. o für Spieler2 vergeben. Mittels geeigneter Darstellungsregeln in CSS für die jeweilige Klasse sorgt der Browser dann für eine passende Darstellung.

Damit das Spiel zugänglich ist, müssen Nutzer assistiver Technologien eine Möglichkeit haben, ebenfalls das Spiel zu bedienen. Eine reine Darstellungsänderung mittels CSS genügt hier nicht. In den Tabellenzellen muss tatsächlicher Inhalt stehen. Zum einen bedeutet das, dass in der Tabellenzelle ein Button zum Auswählen der Zelle stehen muss (blinde Nutzer bedienen keine Maus zum Anklicken, können aber vorhandene Buttons betätigen), und zum anderen, dass nach dem Auswählen des Feldes dort ein „x“ oder „o“ anstelle des Buttons stehen muss.

Eine passende Anleitung, welcher Spieler gerade am Zug sei, lässt sich bei einer Tabelle bequem in ein caption-Element schreiben, welches als Tabellenüberschrift diesen Zweck gut erfüllt.

[Bearbeiten] Vorbereitungen mit HTML

Stellen wir uns vor, Spieler1 hätte oben links sein „x“ gesetzt und Spieler2 in der Mitte sein „o“. Der aktuelle Spielstand müsste in HTML entsprechend so aussehen:

Beispiel: Grundgerüst des Spielfeldes
<table>
  <caption>Spieler X ist am Zug.</caption>
  <tbody>
    <tr><!-- erste Zeile -->
      <td class="x">x</td>
      <td><button>oben mittig wählen</button></td>
      <td><button>oben rechts wählen</button></td>
    </tr>
    <tr><!-- zweite Zeile -->
      <td><button>Mitte links wählen</button></td>
      <td class="o">o</td>
      <td><button>Mitte rechts wählen</button></td>
    </tr>
    <tr><!-- dritte Zeile -->
      <td><button>unten links wählen</button></td>
      <td><button>unten mittig wählen</button></td>
      <td><button>unten rechts wählen</button></td>
    </tr>
  <tbody>
</table>

Es sieht so aus, als wäre die Doppelung mit dem Tabelleninhalt „x“ und der Klasse x unnötig. Das ist sie nicht, wenn man für sehende Nutzer die Darstellung gefälliger machen möchte. Die Symbole × und ○ sehen sicherlich eher nach Kreuz und Kreis aus, werden aber von einem Screenreader nicht unbedingt genau so vorgelesen. Will man nun auf Nummer sicher gehen und Nutzern assistiver Technologien die Benutzung garantieren, ist ein Tabelleninhalt mit Kreuz und Kreis vielleicht keine gute Idee, weshalb als Mindestvoraussetzung zwingend die Buchstaben „x“ und „o“ in die Tabelle geschrieben werden. Aber man kann im Sinne der schrittweisen Verbesserung (englisch progressive enhancement) die Darstellung für sehende Nutzer dahingehend verbessern, dass mittels CSS tatsächlich Kreuz und Kreis dargestellt werden, und die Buttons visuell vor diesen Nutzern versteckt werden.

Eine solche Tabelle hat natürlich nur dann im Dokument einen Sinn, wenn JavaScript für den Nutzer verfügbar ist. Daher wird dieser HTML-Code nicht im Dokument stehen, sondern erst nach dem Laden von JavaScript erzeugt werden. Damit werden zwar Nutzer ausgeschlossen, für die aus welchen Gründen auch immer JavaScript nicht verfügbar ist, aber welchen Sinn soll eine bedienbare Oberfläche haben, die anschließend keinen Gewinner (oder ein Patt) ermitteln kann? Vor diesem Hintergrund erscheint es vertretbar, diese Nutzergruppe tatsächlich auszuschließen.

Damit nun unser JavaScript „wissen“ kann, wo ein solches Spiel im Dokument eingebaut werden soll, braucht es einen Platzhalter im Dokument. Dieser wird anhand einer Klasse tic-tac-toe erkannt, die einem geeigneten Block-Element (in diesem Tutorial <div>, aber es könnte auch ein fast beliebiges anderes sein) vergeben wird:

Beispiel: Platzhalter für ein Spielfeld
<aside class="tic-tac-toe"></aside>

[Bearbeiten] Vorbereitungen mit CSS

Damit später nicht alle Tabellen in einem Dokument wie ein Tic-Tac-Toe-Spielfeld aussehen, wird der Platzhalter mit der Klasse tic-tac-toe als Selektor in CSS genutzt, um die Darstellung auf solche Tabellen zu beschränken.

Beispiel
.tic-tac-toe table {
  border: 2px solid black;
  border-collapse: collapse;
}
Damit die Rahmen zwischen den Zellen keine doppelte Linien sind, hilft die Eigenschaft border-collapse diese zu einer gemeinsamen Linie zusammenfallen zu lassen.

Aus Programmierersicht ist diese Klasse ein Namensraum. Alles, was zu einem solchen Spiel gehört, trägt sozusagen als Vorsilbe (englisch prefix) diesen Namen. Dadurch lassen sich die Dinge, die nicht zu einem Spiel gehören, klar abgrenzen.

Sehende Benutzer, die eine Maus bedienen, möchten vielleicht von den Buttons visuell nicht gestört werden und bevorzugen eine Darstellung, bei der sie in ein leeres Tabellenfeld klicken. Für diese kann man mit CSS die Buttons visuell verstecken, indem man sie nach links aus dem sichtbaren Bereich des Fensters verschiebt:

Beispiel
.tic-tac-toe button {
  left: -100vw;
  position: absolute;
}
Durch die absolute Positionierung werden die Buttons aus dem Elementfluss genommen. Visuell gehören sie nun nicht mehr zur Tabellenzelle. Durch die Angabe von left: -100vw; wird die linke obere Ecke des Buttons um eine volle Fensterbreite nach links verschoben und für den Nutzer nicht mehr sichtbar. Dieses Vorgehen erzeugt keine unerwünschten Scrollbalken.

Für die veränderte Darstellung eines von einem Spieler ausgewählten Feldes sind die Klassen x und o vorgesehen. Geben wir den so markierten Tabellenzellen also eine gegensätzliche Farbe. Damit die sehenden Nutzer die visuell unerwünschten Buchstaben „x“ und „o“ nicht sehen, bekommen die eingefärbten Tabellenzellen eine dazu passende Schriftfarbe:

Beispiel
.tic-tac-toe .o {
  background: #8f8;
  color: #8f8;
}
 
.tic-tac-toe .x {
  background: #f88;
  color: #f88;
}

Für die gefälligere Darstellung sollen sehende Nutzer ein Kreuz und einen Kreis sehen. Dafür machen wir uns die Möglichkeit zunutze, mittels CSS sogenannten generierten Content zu erzeugen:

Beispiel
.tic-tac-toe .o::after {
  content: "○";
  top: -0.2em;
}
 
.tic-tac-toe .x::after {
  content: "×";
  top: -0.125em;
}
 
.tic-tac-toe .o::after,
.tic-tac-toe .x::after {
  color: black;
  display: block;
  font-size: 5em;
  position: absolute;
  width: 100%;
}
Das Pseudoelement ::after kann mit der Eigenschaft content beliebigen Textinhalt generieren. Dieser wird für eine mittige Darstellung absolut positioniert. Kleine Anpassungen hinsichtlich der vertikalen Position erfolgen mit der Eigenschaft top.

Damit sähe ein Spielstand z.B. so aus:

Beispiel: Tic-Tac-Toe (unfertig, Spielstand) ansehen …
.tic-tac-toe table {
  border: 2px solid black;
  border-collapse: collapse;
}
 
.tic-tac-toe td {
  border: 1px solid black;
  height: 5em;
  position: relative;
  text-align: center;
  width: 5em;
}
 
.tic-tac-toe table:not(.game-over) td {
  cursor: pointer;
}
 
.tic-tac-toe td button {
  position: absolute;
  left: -100vw;
}
 
.tic-tac-toe .o {
  background: #8f8;
  color: #8f8;
}
 
.tic-tac-toe .x {
  background: #f88;
  color: #f88;
}
 
.tic-tac-toe table:not(.game-over) .o,
.tic-tac-toe table:not(.game-over) .x {
  z-index: -1;
}
 
.tic-tac-toe .o::after,
.tic-tac-toe .x::after {
  color: black;
  display: block;
  font-size: 5em;
  position: absolute;
  width: 100%;
}
 
.tic-tac-toe .o::after {
  content: "○";
  top: -0.2em;
}
 
.tic-tac-toe .x::after {
  content: "×";
  top: -0.125em;
}
<div class="tic-tac-toe">
  <p>Zum Spielen bitte abwechselnd in die Spielfelder klicken/tappen!</p>
  <table>
  <caption>Spieler x ist am Zug.</caption>
  <tbody>
    <tr>
    <td class="x">x</td>
    <td><button>oben mittig wählen</button></td>
    <td><button>oben rechts wählen</button></td>
    </tr>
    <tr>
    <td><button>Mitte links wählen</button></td>
    <td class="o">o</td>
    <td><button>Mitte rechts wählen</button></td>
    </tr>
    <tr>
    <td><button>unten links wählen</button></td>
    <td><button>unten mittig wählen</button></td>
    <td><button>unten rechts wählen</button></td>
    </tr>
  </tbody>
  </table>
</div>
Dem Beispiel wurde noch ein Textabsatz mit einer ganz kurzen Spielanleitung spendiert. Betrachten Sie das Dokument in Ihrem Browser und stellen Sie dann ein, dass die Darstellungsregeln deaktiviert werden sollen (z.B. im Firefox unter „Ansicht → Seiten-Stil → kein Stil“), damit Sie einen Eindruck bekommen, was Nutzer assistiver Technologien für ihre Wahrnehmung an Inhalten angeboten bekommen.

Wir können nun loslegen und das JavaScript erstellen.

[Bearbeiten] Das Script

Bevor wir den Programmcode für das Script notieren können, müssen wir überlegen, wo wir ihn hinschreiben.

[Bearbeiten] Einbindung ins Dokument

Üblicherweise notiert man JavaScript-Code in den head des Dokuments, da er keinen Dokumentinhalt, sondern ergänzende Informationen über das Dokument darstellt. Unser JavaScript-Code kommt daher in ein script-Element im head des Dokuments. Alternativ könnte der Code auch in eine separate Textdatei mit der Dateiendung „.js“ ausgelagert werden, welche dann im head als externe Resource referenziert würde.

[Bearbeiten] Zeitversetztes Ausführen von Code mit Events

Der Browser führt das Script aus, noch bevor irgendein Dokumentinhalt vorhanden ist, denn dieser steht ja erst im body des Dokuments, unser Script aber davor im head. Daher beginnt unser Script mit der Anweisung, dass der Hauptteil unseres JavaScripts erst dann vom Browser auszuführen ist, wenn der Dokumentinhalt verfügbar ist. Dazu machen wir uns ein bestimmtes Ereignis im Browser zunutze: DomContentLoaded.

Beispiel: Zeitversetztes Ausführen mit Browser-Event
document.addEventListener(
  "DOMContentLoaded",
  function () {
    // das hier bitte erst im Ereignis-Fall ausführen:
    var a = "Hallo Welt!";
  }
);
Die Methode addEventListener nimmt zwei Parameter entgegen, eine Zeichenkette mit dem Namen des gewünschten Ereignisses und ein Funktionsobjekt, welches den Code enthält, der im Ereignisfall ausgeführt werden soll. Auf diese Art ist es möglich, Code zu ganz bestimmten Momenten ausführen zu lassen.

Hinweis

Das Funktionsobjekt (zweites Argument im Funktionsaufruf) kann auch als Variable übergeben werden und muss nicht wie hier ausnotiert werden.

[Bearbeiten] Geltungsbereich von Variablen (Scope)

Ein schöner Nebeneffekt dieser Schreibweise ist, dass alle in dieser Funktion deklarierten lokalen Variablen nicht im globalen Geltungsbereich (Scope) stehen, sondern für mögliche andere JavaScripte unerreichbar sind.

[Bearbeiten] Platzhalter ermitteln

Wenn unser Script ausgeführt wird, soll es alle Platzhalter in ein Spiel umwandeln. Das geht so:

Beispiel
document.addEventListener("DOMContentLoaded", function () {
  var games, i;
 
  function TicTacToe (element) {
    // Spiel in gefundenem Element erstellen
  }
 
  // finde alle Spielfeld-Tabellen
  games = document.querySelectorAll(".tic-tac-toe");
 
  for (i = 0; i < games.length; i++) {
    TicTacToe(games[i]); // aktuelles Fundstück steht in games[i]
  }
});
Die Methode querySelectorAll nimmt als Argument einen Selektor entgegen, der genau so interpretiert wird, wie er es auch in CSS würde. In diesem Fall werden alle Elemente gefunden, die die Klasse .tic-tac-toe haben. Anschließend wird in der for-Schleife unter Zuhilfenahme der Zählvariablen i jedes der Elemente an die Funktion TicTacToe übermittelt. Der Maximalwert, den i annehmen darf, ist die Anzahl der gefundenen Elemente, welche sich an der length-Eigenschaft der Liste in games ablesen lässt.

[Bearbeiten] Veränderungen am DOM via JavaScript

Um nun unser Spielfeld aufzubauen, müssen wir eine komplette Tabelle erstellen. Dazu braucht es zwei Schritte:

  1. Element-Objekt erstellen (mit document.createElement)
  2. Element-Objekt in den DOM-Baum einhängen (mit element.appendChild)

Das bedeutet, dass nun jedes Nachfahrenelement der Tabelle wie <caption>, <tbody>, <tr> und <td> auf diese Weise erstellt und in das table-Element eingefügt werden muss. Alternativ kann man mit der innerHTML-Eigenschaft von Element-Objekten auch den HTML-Code als String zuweisen, was dann vom Browser zu den jeweiligen DOM-Knoten umgewandelt wird. Zweiteres mag einfacher aussehen, ist aber hier weniger flexibel, denn die Beschriftung der Buttons in den Tabellenzellen soll vielleicht konfigurierbar werden, ebenso der Hinweistext.

Beispiel: Aufbau der Tabelle
// element referenziert das HTML-Element, in welches unsere Tabelle soll:
function TicTacToe (element) {
  var field = document.createElement("table"),
      caption = document.createElement("caption"),
      labels = [
        ["oben links", "oben mittig", "oben rechts"],
        ["Mitte links", "Mitte mittig", "Mitte rechts"],
        ["unten links", "unten mittig", "unten rechts"]
      ],
      b, c, r, tr;
 
  // Tabelle ins Dokument einfügen
  element.appendChild(field);
 
  // Tabelle aufbauen
  field.appendChild(caption); // Beschriftung
  field.appendChild(document.createElement("tbody"));
 
  // Hinweis einrichten
  caption.innerHTML = "Spieler x ist am Zug.";
 
  for (r = 0; r < 3; r++) {
    // neue Tabellenzeile
    tr = document.createElement("tr");
 
    field.lastChild.appendChild(tr);
 
    for (c = 0; c < 3; c++) {
      // neue Tabellenzelle
      tr.appendChild(document.createElement("td"));
 
      // Klickbutton
      b = document.createElement("button");
      b.innerHTML = labels[r][c] + " wählen"; // Button-Text
 
      tr.lastChild.appendChild(b);
    }
  }
}
Unsere Tabelle wird in der Variablen field gespeichert. Damit kann man ihr nun das caption-Element, sowie den Tabellenkörper tbody anhängen. In tbody (referenziert durch field.lastChild, den tbody wurde als letztes Element in field eingehängt) werden nun drei Tabellenzeilen (tr) mit jeweils drei Tabellenzellen (td) eingehängt. Diese Tabellenzellen (jeweils referenziert durch tr.lastChild) erhalten jeweils einen Klickbutton. Das Ganze wird über zwei verschachtelte for-Schleifen verkürzt notiert.
Die Beschriftung für den Klickbutton enthält eine Angabe für die Position (wie z.B. „unten rechts“, welche aus einem verschachtelten Array namens labels entnommen wird. Die Verschachtelung des Arrays entspricht der Verschachtelung der beiden for-Schleifen, sodass mit den Zählvariablen r (für row, englisch für Zeile) und c (für column, englisch für Spalte) die entsprechenden Array-Werte adressiert werden können.
Beachten Sie: In einer internationalen Welt, in der Software quelloffen erreichbar ist, sollten Sie sich englisch-sprachige Bezeichner für Variablen und Funktions- bzw. Methodennamen angewöhnen. Auch Kommentare im Code zur Erläuterung sollten auf Englisch sein. Da dieses Tutorial in deutscher Sprache verfasst ist, sind auch die Kommentare auf Deutsch, nicht aber die Bezeichner für Funktionen oder Variablen.

[Bearbeiten] Die Spielmechanik

Damit unser Spiel spielbar wird, braucht es nun die Programmlogik, die den Spielverlauf steuert. Im Grunde soll die Aktion des jeweiligen Spielers in einem Betätigen des Buttons, bzw. in einem Klick oder Tap bestehen. Ob nun der Button, oder das Tabellenfeld direkt angeklickt wird (wir erinnern uns, sehende Spieler sehen später keine Buttons), soll dabei nicht wichtig sein.

[Bearbeiten] Event-Bubbling

Was passiert eigentlich, wenn ein Spieler etwas anklickt? Der Browser löst ein Event aus. Ein click-Event. Dieses wird für ein bestimmtes HTML-Element wahrgenommen, beispielsweise button. Anschließend „blubbert“ das Event über die Elternelemente bis zum Wurzelelement html die Baumstruktur des Dokuments hinauf, damit auch alle Elternelemente, für die ein EventListener für dieses Event eingerichtet wurde, ihrerseits darauf reagieren können.

Wenn wir jetzt unsere gesamte Tabelle auf das click-Event lauschen lassen, um im Ereignisfall das ursprüngliche Element zu bestimmen, ist es egal, ob der Button oder die Tabellenzelle selbst angeklickt wurde:

Beispiel: click-Event für die Tabelle einrichten
function mark (event) {
  // Tabellenzelle bestimmen
  var td = event.target;
 
  // Button oder Zelle?
  while (td.tagName.toLowerCase() != "td"
    && td != field
  ) {
    td = td.parentNode;
  }
 
  // Zelle bei Bedarf markieren
  if (td.tagName.toLowerCase() == "td") {
    // angeklickte Zelle in td
  }
}
 
// field enthält eine Referenz auf unser <table> s.o.
field.addEventListener("click",  mark);
In der letzten Code-Zeile wird dem Tabellenobjekt ein Event-Listener zugewiesen, dessen Handlerfunktion die Funktion mark ist. Diese bekommt bei ihrem Aufruf als Parameter ein Event-Objekt übergeben. Das macht der Browser im Ereignisfall.

In der Handlerfunktion mark wird als erstes mit event.target ermittelt, von welchem Elementobjekt der Klick ausgegangen ist. Da wir die jeweilige Tabellenzelle wissen wollen, wurde als Variablenname gleich td gewählt. Anschließend wird geprüft, ob das auslösende Element auch den Elementnamen (Eigenschaft tagName) „td“ hat, denn es könnte sich ja um ein button-, oder eines der Elternelemente in der Tabelle gehandelt haben. In der while-Schleife wird unser Element so lange durch dessen Elternelement (Eigenschaft parentNode) ersetzt, bis wir ein echtes td haben, oder bis unser Element die Tabelle selbst ist.

Hinweis

Da die Browser aus historischen Gründen die Elementnamen in Großbuchstaben speichern, wird auf die Eigenschaft element.tagName, welche einen String-Wert darstellt, die String-Methode toLowerCase angewandt, um den Elementnamen sicher vergleichen zu können.

[Bearbeiten] Markieren des gewählten Feldes

Ist einmal die gewählte Zelle ermittelt, kann sie einem der beiden Spieler zugeordnet werden. Welcher Spieler gerade am Zug ist, muss in einer Variablen gespeichert werden, auf die in den Eventhandlerfunktionen zugegriffen werden kann. Das kann man mit dem Scope so lösen:

Beispiel: Spielerverwaltung
document.addEventListener("DOMContentLoaded", function () {
  function TicTacToe (element) {
    // ein Spiel einrichten
    var players = [ "x", "o" ];
 
    function mark (event) {
      var td = event.target;
 
      ...
 
      console.log(players); // ["x", "o"]
    }
  }
});
Die Variable players wurde außerhalb der Funktion mark definiert. Da die Funktion mark im selben Scope wie players definiert wurde, liegt sie im gleichen Geltungsbereich und „kennt“ daher diese Variable und ihren Inhalt. Bei der Variable players handelt es sich um ein Array, welches als Array-Literal notiert wurde.

[Bearbeiten] Umschalten zwischen den Spielern

Wenn man zwischen genau zwei Werten hin- und herschalten muss, kann man sich folgende Mathematik zunutze machen:

 x = 1 - x

Wenn x den Wert null hat, dann wird x zu eins. Und umgekehrt. Das wenden wir so an:

Beispiel: Spielerverwaltung
document.addEventListener("DOMContentLoaded", function () {
  function TicTacToe (element) {
    // ein Spiel einrichten
    var current = 0,
        players = [ "x", "o" ];
 
    function mark (event) {
      var td = event.target;
 
      ...
 
      // Zelle bei Bedarf markieren
      if (td.tagName.toLowerCase() == "td") {
        // angeklickte Zelle in td
 
        // aktueller Spieler
        console.log(players[current]); // "x" oder "o"
 
        // Spieler wechseln
        current = 1 - current;
 
        // neuer Spieler
        console.log(players[current]); // "o" oder "x"
      }
    }
  }
});
Will man auf ein Element des Arrays players zugreifen, so notiert man nach dem Variablennamen eckige Klammern, in die man die Nummer des gewünschten Elements notiert, beginnend mit null als erstem Element. In diesem Beispiel ist players[current] in Abhängigkeit vom Wert in current entweder „x“ oder „o“.

[Bearbeiten] Zelle markieren

Wie weiter oben bereits erörtert wollen wir unser Spiel nicht nur für sehende Nutzer, sondern für alle Nutzer zugänglich machen. Daher vergeben wir nun eine passende Klasse an die Tabellenzelle und schreiben zusätzlich noch den passenden Inhalt hinein. Dafür benötigen wir die Objekteigenschaften className und innerHTML:

Beispiel: Auswahl des Spielers umsetzen
function mark (event) {
  var td = event.target;
 
  ...
 
  // Zelle bei Bedarf markieren
  if (td.tagName.toLowerCase() == "td") {
    // angeklickte Zelle in td
    td.className = players[current]; // Klassennamen vergeben
    td.innerHTML = players[current]; // Spielersymbol eintragen
 
    current = 1 - current; // zwischen 0 und 1 hin- und herschalten
 
    // Spiel zuende?
  }
}
Wenn mit innerHTML der Inhalt eines HTML-Elements verändert wird, dann wird vorheriger Inhalt damit ersetzt! Das vorher darin befindliche button-Element ist nach dieser Anweisung verschwunden.

[Bearbeiten] Meldungen an die Spieler

Wir haben doch das caption-Element für Meldungen und Anleitungen eingerichtet. Das sollten wir jetzt nutzen:

Beispiel: Hinweis an die Spieler ausgeben
  // Zelle bei Bedarf markieren
  if (td.tagName.toLowerCase() == "td") {
    // angeklickte Zelle in td
    td.className = players[current]; // Klassennamen vergeben
    td.innerHTML = players[current]; // Spielersymbol eintragen
 
    current = 1 - current; // zwischen 0 und 1 hin- und herschalten
 
    // caption enthält eine Referenz auf <caption>,
    // welches beim Aufbau der Tabelle (s. o.) definiert wurde
    caption.innerHTML = "Spieler " + players[current] + " ist am Zug.";
 
    // Spiel zuende?
  }

Wenn man alle bisherigen Teile zusammensetzt, kann man das Spiel bereits bedienen:

Beispiel: bedienbares Spiel noch ohne Auswertung ansehen …
Wie Sie im Beispiel sehen können, lassen sich die Felder markieren. Auch der passende Hinweis an die Spieler wird korrekt ausgegeben.

Hinweis

Um eine bereits angefangene Partie zu simulieren, um also einen Spielstand wie beim vorherigen Live-Beispiel anzubieten, wurde die Tabelle fest in den HTML-Code geschrieben, anstatt sie mit JavaScript beim Laden der Seite erst zu erstellen.

[Bearbeiten] Prüfung auf Spielende

Wie prüft man nun darauf, ob das Spiel zuende ist? Dazu gibt es zwei Möglichkeiten:

  1. Alle Felder sind verwendet worden.
  2. Ein Spieler hat drei Felder waagrecht, senkrecht oder diagonal und damit gewonnen.
[Bearbeiten] Alle Felder verwendet?

Nehmen wir die erste Prüfung. Um alle Zellen einer Tabelle zu prüfen kann man mit der Methode getElementsByTagName eine Liste aller Zellen erhalten. Im Gegensatz zu querySelectorAll, welches nur für das document-Objekt zur Verfügung steht, kann man getElementsByTagName auf jeden Elementknoten anwenden. Da wir nicht wissen, welche Tabelle im Dokument jetzt genau diejenige ist, die wir in field gespeichert haben, insbesondere dann, wenn es einmal mehrere table-Elemente im Dokument geben wird (vielleicht sogar mehr als ein Tic-Tac-Toe-Spiel?), können wir auch keinen Selektor formulieren, der nur auf diese Tabelle passt. Um aber nur die Tabellenzellen unserer Tabelle zu ermitteln, müssten wir solch einen Selektor formulieren können, damit querySelectorAll nur deren Zellen findet. Eine andere Möglichkeit gibt es nicht um dieser Methode begreiflich zu machen, dass sie sich bitte nur auf unsere Tabelle beschränkt.

Da wir jede von einem Spieler gewählte Zelle mit einer Klasse versehen, können wir jetzt prüfen, ob es eine Zelle gibt, die noch keine Klasse hat. Das bedeutet, dass ihre className-Eigenschaft ein Leerstring sein muss:

Beispiel: Prüfung auf unverwendete Zellen
function check () {
  var tds = field.getElementsByTagName("td"), // field ist unser <table>
      full = true; // wir gehen davon aus, dass alle Zellen benutzt wurden
 
  // alle Felder markiert?
  for (i = 0; i < tds.length; i++) {
    if (tds[i].className == "") {
      full = false;
    }
  }
 
  // wenn full, dann Spiel vorbei, wenn nicht full, dann noch nicht
  if (full) {
    // Spiel zuende weil alle Felder belegt
  }
}
Die Variable tds bekommt von getElementsByTagName eine Liste zurück. Diese Liste kann man ähnlich wie ein Array mit einer for-Schleife der Reihe nach abarbeiten.

Wenn ein Spiel vorbei ist, dann sollte eine weitere Auswahl von noch freien Feldern nicht mehr möglich sein. Daher ist es ratsam eine Variable anzulegen, die diesen Zustand speichert: Nennen wir sie finished und legen wir sie in den äußersten Scope zu players, field und caption.

Hinweis

Eine Variable wie finished (oder wie im vorherigen Code-Beispiel full), die einen Bool'schen Wahrheitswert wie true oder false speichert, nennt man auch „Flag“ (englisch für Flagge), da sie wie eine Flagge entweder gehisst ist, oder eben nicht.
Beispiel: wichtige Variablen im äußersten Scope
document.addEventListener("DOMContentLoaded", function () {
  function TicTacToe (element) {
    var current = 0, // Spieler, der am Zug ist
        players = [ "x", "o" ], // verfügbare Spieler
        field = document.createElement("table"), // Spielfeld
        caption = document.createElement("caption"), // Feld für Hinweistexte
        finished, // Flag für Spiel-ist-zuende
        b, c, r, tr; // Variablen für das Erstellen der Tabelle
 
    function check () { ... }
 
    function mark (event) {
 
      if (!finished) {
        // Zelle bei Bedarf markieren
      }
    }
 
    // Tabelle ins Dokument einfügen
  }
});
In der Eventhandler-Funktion mark kann nun anhand des Flags finished entschieden werden, ob überhaupt noch auf einen Klick hin reagiert werden soll, oder nicht.

Das Ausrufezeichen sorgt dafür, dass der Inhalt der Variable finished ins Gegenteil verkehrt verstanden wird. Ist das Flag negativ, so wird in der Verzweigung notierter Code ausgeführt (if (true) wird ausgeführt). Steht dagegen in unserem Flag der Wert true, so fällt die Prüfung für die Verzweigung negativ aus und der Code darin wird nicht ausgeführt.

Die Verzweigung liest man am besten „wenn (noch) nicht zuende“.
[Bearbeiten] Wer hat gewonnen?

Um einen Gewinner zu ermitteln müssen wir prüfen, ob die drei senkrechten Kombinationen, die drei waagrechten, oder die beiden diagonalen dieselbe Klasse aufweisen. Der Klassenname (entweder x oder o) enthält dann die Identität des gewinnenden Spielers.

0 1 2
3 4 5
6 7 8

Die senkrechten Felder sind immer ein Vielfaches von 3: Entweder 0, 3, 6 oder 1, 4, 7, oder 2, 5, 8. Das lässt sich in einer Schleife so abfragen tds[0 + i], tds[3 + i], tds[6 + i], wobei die Zählvariablen i von null bis einschließlich 2 hochzählt.

Die waagrechten Felder sind immer drei aufeinanderfolgende: Entweder 0, 1, 2, oder 3, 4, 5, oder 6, 7, 8. Das geht auch in einer Schleife: tds[i*3], tds[i*3 + 1], tds[i*3 + 2], wobei die Zählvariablen i wieder von null bis einschließlich 2 hochzählt.

Für die diagonalen Möglichkeiten sind dann nur noch die Kombinationen 0, 4, 8 und 2, 4, 6 zu prüfen.

Beispiel: drei senkrechte, waagrechte oder diagonale Felder?
function check () {
  var tds = field.getElementsByTagName("td"),
      full = true,
      i, winner;
 
  // alle Felder markiert?
  ...
 
  // Gewinner ermitteln
  for (i = 0; i < 3; i++) {
 
    // 3 senkrecht
    if (tds[0 + i].className != ""
      && tds[0 + i].className == tds[3 + i].className
      && tds[3 + i].className == tds[6 + i].className
    ) {
 
      // we have a winner!
      winner = tds[0 + i].className;
    }
 
    // 3 waagrecht
    if (tds[i*3].className != ""
      && tds[i*3].className == tds[i*3 + 1].className
      && tds[i*3 + 1].className == tds[i*3 + 2].className
    ) {
 
      // we have a winner!
      winner = tds[i*3].className;
    }
  }
 
  // diagonal links oben nach rechts unten
  if (tds[0].className != ""
    && tds[0].className == tds[4].className
    && tds[4].className == tds[8].className
  ) {
    winner = tds[0].className;
  }
 
  // diagonal rechts oben nach links unten
  if (tds[2].className != ""
    && tds[2].className == tds[4].className
    && tds[4].className == tds[6].className
  ) {
    winner = tds[2].className;
  }
 
  // Game over?
  if (full || winner) {
    // ja
  }
}
Mit dem logischen UND-Operator (&&) werden die drei notwendigen Bedingungen verknüpft, die wir an die drei zusammengehörigen Felder stellen. Konkret wird verglichen, ob ein Feld eine Klasse hat (className also nicht leer ist), und ob es die selbe Klasse wie die anderen beiden Felder hat (also die className-Eigenschaft aller drei identisch ist). Werden drei solche erkannt, wird in der Variablen winner der Klassenname gespeichert, welcher für den Spieler steht, der nun gewonnen hat. Das Gegenstück zum UND-Operator ist der ODER-Operator (||). Mit ihm wird ermittelt, ob das Spiel zuende ist. Die Verzweigung if (full || winner) liest man am besten als „wenn voll (ausgefüllt) oder Gewinner (ermittelt)“.

[Bearbeiten] Spielende

Wenn nun das Spiel zuende ist, zeigen wir das an, indem wir der Tabelle die Klasse game-over geben. Zudem schreiben wir eine entsprechende Meldung in unsere Tabellenbeschriftung. Außerdem müssen alle verbleibenden Buttons entfernt werden, denn eine Eingabe ist nicht mehr erlaubt. Für das Entfernen der Buttons verwenden wir die element.removeChild-Methode.

Beispiel: drei senkrechte Felder?
function check () {
  var tds = field.getElementsByTagName("td"),
      full = true,
      buttons, i, winner;
 
  // alle Felder markiert?
  ...
 
  // Gewinner ermitteln
  ...
 
 
  // game over?
  if (full || winner) {
 
    finished = true;
 
    field.className = "game-over";
 
    if (winner) {
 
      caption.innerHTML = "Spieler " + players[current] + " hat gewonnen.";
 
    } else {
 
      caption.innerHTML = "Das Spiel endet unentschieden";
    }
 
    // restliche Buttons entfernen
    buttons = field.getElementsByTagName("button");
 
    while (buttons.length) {
      buttons[0].parentNode.removeChild(buttons[0]);
    }
  }
}
Zuerst wird das Flag finished auf true gesetzt. Danach wird der Tabelle die Klasse game-over gegeben.

Die folgende Verzweigung schreibt eine passende Meldung in die Tabellenbeschriftung, basierend darauf, ob es einen Gewinner gibt, oder nicht. Dazu wird ein Trick angewandt: Der String-Wert in winner ist entweder „x“ oder „o“. Sollte es keinen Gewinner geben, hat die Variable keinen Wert, was dem Wert null entspricht (nicht mit der Zahl null verwechseln!). Bei der Prüfung werden die String-Werte wie ein true gewertet, das Nichtvorhandensein eines Wertes als false. Daher kann man ganz kurz if (winner) notieren, ohne einen exakten Wertevergleich mit logischer ODER-Verknüpfung anwenden zu müssen. Dass das möglich ist, liegt an der automatischen Typkonvertierung in JavaScript. Bei Bedarf werden Datentypen (hier entweder String oder null) passend umgeformt.

Das Entfernen der Buttons geschieht über eine Liste, welche die Methode getElementsByTagName zurück liefert. Das Besondere an dieser Liste ist, dass sie eine „lebendige“ Liste ist. Entfernt man ein Element dieser Liste aus dem Dokument, wird die Liste intern von JavaScript aktualisiert. Daher kann man nun nicht einfach eine for-Schleife benutzen, um die Liste der Reihe nach abzuarbeiten, da die Buttons ja aus dem Dokument entfernt werden sollen. Die Lösung besteht darin, eine while-Schleife so oft zu durchlaufen, bis die Liste in buttons leer ist, was sich an ihrer length-Eigenschaft ablesen lässt.
[Bearbeiten] Lösung anzeigen

Wenn das Spiel zuende ist, und wenn es einen Gewinner gibt, dann möchte dieser sicherlich gerne seine drei gewinnenden Felder als solche hervorgehoben haben. Dazu müssen wir wieder an die Nutzer assistiver Technologien denken. Für sie ist es nur dann nachvollziehbar, wenn die Hervorhebung über geeignete HTML-Elemente wie strong oder em erfolgt. Für die Wahrnehmung solcherart hervorgehobener Inhalte sind diese assistiven Technologien ausgerüstet. Für sehende Benutzer können entsprechende visuelle Eigenschaften mit CSS eingerichtet werden.

Wir definieren eine Funktion namens highlight, die als Argument eine Liste (genauer ein Array) an Zellen entgegen nimmt, um diese zu verändern. Der Textinhalt wird in strong-Elemente hinein verschoben und die Zellen selbst erhalten eine zweite Klasse hightlighted, zusätzlich zu der Klasse x bzw. o:

Beispiel: Gewinnfelder anzeigen
function highlightCells (cells) {
  cells.forEach(function (node) {
    var el = document.createElement("strong");
 
    el.innerHTML = node.innerHTML;
 
    node.innerHTML = "";
    node.appendChild(el);
    node.classList.add("highlighted");
  });
}
Da es sich bei dem Parameter cells um ein echtes Array handelt (wir dürfen die Funktion eben nicht anders verwenden), können wir auf die übliche Art auf seine Elemente zugreifen. Es bietet sich aber an, die bei Arrays verfügbare forEach-Methode zu benutzen. Diese benötigt eine Callback-Funktion als Argument, welche auf jedes Array-Element angewendet werden wird. Es liegt nahe, den übergebenen Parameter node zu nennen, da wir Elementknoten (im Englischen node genannt) in unserem Array haben werden.

Die Callback-Funktion erzeugt ein strong-Element, kopiert den Textinhalt unserer Tabellenzelle (hier durch den Parameter node referenziert) hinein, leert den Inhalt der Tabellenzelle um anschließend das neu erzeugte strong-Element dort einzuhängen.

Zu allerletzt bekommt die Tabellenzelle die Klasse highlighted verliehen. Da es sich nicht um die einzige Klasse handelt, die dieser Tabellenzelle zugewiesen wird, lohnt es sich, dieses nicht über die className-Eigenschaft (eine simple Zeichenkette) zu tun, sondern das sehr viel leistungsfähigere und in allen modernen Browsern hinreichend unterstützte classList-Objekt mit seiner add-Methode dafür zu benutzen.

Um diese neue Funktion highlight sinnvoll zu verwenden, müssen wir unsere Funktion check an entsprechender Stelle erweitern:

Beispiel: Gewinnfelder highlighten
function check () {
  var tds = field.getElementsByTagName("td"),
      full = true,
      i, winner;
 
  // alle Felder markiert?
  ...
 
  // Gewinner ermitteln
  for (i = 0; i < 3; i++) {
 
    // 3 senkrecht
    if (tds[0 + i].className != ""
      && tds[0 + i].className == tds[3 + i].className
      && tds[3 + i].className == tds[6 + i].className
    ) {
 
      // we have a winner!
      winner = tds[0 + i].className;
 
      highlightCells([
        tds[i], tds[3 + i], tds[6 + i]
      ]);
    }
 
    // 3 waagrecht
    ...
  }
 
  ...
}
Wenn drei senkrechte Felder mit der identischen Klasse gefunden wurden, konnte ein Gewinner ermittelt werden. Wie bereits oben besprochen wird dieser in der Variablen winner gespeichert.

Um nun die Felder mit der Funktion highlight zu verändern, müssen diese in ein Array verpackt und an die Funktion übermittelt werden. Beim Funktionsaufruf wird das Argument als ein Array-Literal notiert. Damit ist sichergestellt, dass highlight auch tatsächlich ein Array als Parameter erhalten wird. Die im Array-Literal aufgeführten Zellen sind genau die, die in der Prüfung des if-Statements stehen.

Mit den anderen Kombinationsmöglichkeiten muss in der Funktion check natürlich genauso verfahren werden; ihr vollständiger Code kann unten beim fertigen Ergebnis eingesehen werden.

[Bearbeiten] Neustart

Wenn ein neues Spiel gewünscht wird, ist es lästig, wenn die Seite neu geladen werden müsste, nur um das Spiel erneut spielen zu können. Wir wollen daher eine Funktionalität einrichten, mit der das Spiel einen Neustart anbietet.

Wenn ein Spiel zu Ende ist, wird eine entsprechende Meldung in unserem caption-Element angezeigt. Warum sollten wir hier nicht auch einen Button anzeigen lassen, der einen Neustart auslösen kann?

Um das zu erreichen, müssen wir unsere check-Funktion erweitern. Momentan sieht sie verkürzt so aus:

Beispiel: verkürzte check-Funktion
function check () {
  ...
  // alle Felder markiert?
  ...
  // Gewinner ermitteln
  ...
  // game over?
  if (full || winner) {
 
    ...
    // restliche Buttons entfernen
    buttons = field.getElementsByTagName("button");
 
    while (buttons.length) {
      buttons[0].parentNode.removeChild(buttons[0]);
    }
 
    // new game?
  }
}
Nachdem die möglicherweise verbliebenen Buttons zum Auswählen eines Spielfeldes entfernt wurden, das Spiel also nicht mehr spielbar ist, kann an dieser Stelle (siehe Kommentarzeile // new game?) die Funktionalität zum Zurücksetzen unseres Spiels eingebracht werden.

Um in JavaScript etwas zeitversetzt ausführen zu können, benötigen wir wieder ein Event. Dieses Mal benötigen wir wieder ein click-Event, welches beim Betätigen unseres Neustart-Buttons eine Funktionalität auslöst, die unser Spiel in den Ursprungszustand zurück setzt. Das könnte wiederum verkürzt in etwa so aussehen:

Beispiel: Neustart-Button
var button = document.createElement("button");
 
caption.appendChild(button);
 
button.innerHTML = "Neues Spiel?";
button.addEventListener("click", function () {
  // hier passiert etwas
});
In der Variable button wird ein neu erzeugtes button-ElementObjekt gespeichert, welches als Kindelement in das caption-Element unserer Tabelle eingehängt wird. Dadurch erscheint nach der bereits sichtbaren Meldung zusätzlich ein Klickbutton.

Der auf dem Button sichtbare Text wird durch die innerHTML-Eigenschaft zugewiesen.

Anschließend wird auf diesen Button ein Eventlistener für ein click-Event eingerichtet. In dessen zugewiesener Funktion können wir nun unseren Reset schreiben.

Wenn man sich anschaut, was wir alles tun müssen, um das Spiel in den Ursprungszustand zu versetzen, so gibt es diese fünf Dinge zu tun:

  • Die Variable current muss wieder den Wert 0 für Spieler X bekommen
  • Die Variable finished muss wieder den Wert false bekommen, damit Felder ausgewählt werden können.
  • Unser table-Element darf die Klasse game-over nicht mehr haben.
  • Alle Tabellenzellen benötigen wieder passend beschriftete Buttons zum Auswählen.
  • Die Spielanweisung im caption-Element muss wieder passend lauten.

Damit ergibt sich folgender Code für das Zurücksetzen des Spielfeldes:

Beispiel
// new game?
buttons = document.createElement("button");
buttons.innerHTML = "Neues Spiel?";
 
caption.appendChild(buttons);
 
buttons.addEventListener("click", function () {
  var cells = field.getElementsByTagName("td"),
    button, cell;
 
  // reset game
  current = 0;
  finished = false;
  field.removeAttribute("class");
 
  for (r = 0; r < 3; r++) {
    for (c = 0; c < 3; c++) {
      // reset cell
      cell = cells[r * 3 + c];
      cell.removeAttribute("class");
      cell.innerHTML = "";
 
      // re-insert button
      button = document.createElement("button");
      button.innerHTML = labels[r][c] + " wählen";
 
      cell.appendChild(button);
    }
  }
 
  // Hinweis einrichten
  caption.innerHTML = "Spieler " + players[current] + " ist am Zug.";
});
Aus Bequemlichkeit wurde für das neu erzeugte button-Element keine neue Variable angelegt, sondern die bereits vorhandene Variable buttons wiederverwendet.

[Bearbeiten] Konfigurierbarkeit

Die Meldungen, die im Spielverlauf angezeigt werden, sollten frei konfigurierbar sein. Das macht es leicht, eine Version in einer anderen Landessprache zu erstellen. Daher legen wir ein Objekt an, das unsere Sprachausgaben enthält. Wir notieren im äußersten Scope folgendes Literal:

Beispiel: konfigurierbare Meldungen
document.addEventListener("DOMContentLoaded", function () {
  function TicTacToe (element) {
    var current = 0, // Spieler, der am Zug ist
        players = [ "x", "o" ], // verfügbare Spieler
        field = document.createElement("table"), // Spielfeld
        caption = document.createElement("caption"), // Feld für Hinweistexte
        labels = [
          ["oben links", "oben mittig", "oben rechts"],
          ["Mitte links", "Mitte mittig", "Mitte rechts"],
          ["unten links", "unten mittig", "unten rechts"]
        ],
        messages = {
          "o's-turn": "Spieler O ist am Zug.",
          "x's-turn": "Spieler X ist am Zug.",
          "o-wins": "Spieler O gewinnt.",
          "x-wins": "Spieler X gewinnt.",
          "draw": "Das Spiel endet unentschieden.",
          "instructions": "Zum Spielen bitte abwechselnd in die Spielfelder klicken/tappen!",
          "select": "wählen"
        },
        finished, // Flag für Spiel-ist-zuende
        b, c, r, tr; // Variablen für das Erstellen der Tabelle
 
    function check () { ... }
 
    function mark (event) { ... }
 
    // Spielanleitung ins Dokument einfügen
    b = document.createElement("p");
    b.innerHTML = messages["instructions"];
    element.appendChild(b);
 
    // Tabelle ins Dokument einfügen
    ...
  }
});
Die Eigenschaftsnamen des Objektes messages sind die Schlüssel, mit denen wir später unsere Sprachausgaben bestimmen. Die Spielanleitung erreicht man z.B. mit messages["instructions"]. Alternativ konnte man in diesem speziellen Fall auch messages.instructions notieren. Für die anderen Schlüssel klappt das aber nicht, wenn sie Apostrophe oder Bindestriche enthalten, dort ist man auf die erstere Schreibweise angewiesen.

[Bearbeiten] Das fertige Browser-Game

Nun haben wir alle Zutaten zusammen, um ein spielbares Browser-Game zu erstellen:

Beispiel: Tic-Tac-Toe als fertiges Browser-Game ansehen …
.tic-tac-toe table {
  border: 2px solid black;
  border-collapse: collapse;
}
 
.tic-tac-toe td {
  border: 1px solid black;
  height: 5em;
  position: relative;
  text-align: center;
  width: 5em;
}
 
.tic-tac-toe table:not(.game-over) td {
  cursor: pointer;
}
 
.tic-tac-toe td button {
  position: absolute;
  left: -100vw;
}
 
.tic-tac-toe .o {
  background: #8f8;
  color: #8f8;
}
 
.tic-tac-toe .x {
  background: #f88;
  color: #f88;
}
 
.tic-tac-toe .o,
.tic-tac-toe .x {
  z-index: -1;
}
 
.tic-tac-toe .o::after,
.tic-tac-toe .x::after {
  color: black;
  display: block;
  font-size: 5em;
  position: absolute;
  width: 100%;
}
 
.tic-tac-toe .o::after {
  content: "○";
  top: -0.2em;
}
 
.tic-tac-toe .x::after {
  content: "×";
  top: -0.125em;
}
 
.tic-tac-toe .o.highlighted::after,
.tic-tac-toe .x.highlighted::after {
  text-shadow:
    0 0 1px #fff,
    0 0 2px #fff,
    0 0 3px #fff,
    0 0 4px white,
    0 0 5px white,
    0 0 6px white,
    0 0 7px white;
}
 
.tic-tac-toe .game-over {
  border-color: red;
}
document.addEventListener("DOMContentLoaded", function () {
 
  function TicTacToe (element) {
    var current = 0,
      players = [ "x", "o" ],
      field = document.createElement("table"),
      caption = document.createElement("caption"),
      labels = [
        ["oben links", "oben mittig", "oben rechts"],
        ["Mitte links", "Mitte mittig", "Mitte rechts"],
        ["unten links", "unten mittig", "unten rechts"]
      ],
      messages = {
        "o's-turn": "Spieler O ist am Zug.",
        "x's-turn": "Spieler X ist am Zug.",
        "o-wins": "Spieler O gewinnt.",
        "x-wins": "Spieler X gewinnt.",
        "draw": "Das Spiel endet unentschieden.",
        "instructions": "Zum Spielen bitte abwechselnd in die Spielfelder klicken/tappen!",
        "select": "wählen",
        "new game?": "Neues Spiel?"
      },
      finished, games, b, c, i, r, tr;
 
    function check () {
      var tds = field.getElementsByTagName("td"),
        full = true,
        buttons, i, winner;
 
      tds = field.getElementsByTagName("td");
 
      // alle Felder markiert?
      for (i = 0; i < tds.length; i++) {
 
        if (tds[i].className == "") {
          full = false;
        }
      }
 
      // Gewinner ermitteln
      for (i = 0; i < 3; i++) {
 
        // senkrecht
        if (tds[0 + i].className != ""
          && tds[0 + i].className == tds[3 + i].className
          && tds[3 + i].className == tds[6 + i].className
        ) {
          // we have a winner!
          winner = tds[0 + i].className;
 
          highlightCells([
            tds[i], tds[3 + i], tds[6 + i]
          ]);
        }
 
        // waagrecht
        if (tds[i*3 + 0].className != ""
          && tds[i*3 + 0].className == tds[i*3 + 1].className
          && tds[i*3 + 1].className == tds[i*3 + 2].className
        ) {
          // we have a winner!
          winner = tds[i*3].className;
 
          highlightCells([
            tds[i*3], tds[i*3 + 1], tds[i*3 + 2]
          ]);
        }
      }
 
      // diagonal links oben nach rechts unten
      if (tds[0].className != ""
        && tds[0].className == tds[4].className
        && tds[4].className == tds[8].className
      ) {
        winner = tds[0].className;
 
        highlightCells([
          tds[0], tds[4], tds[8]
        ]);
      }
 
      // diagonal rechts oben nach links unten
      if (tds[2].className != ""
        && tds[2].className == tds[4].className
        && tds[4].className == tds[6].className
      ) {
        winner = tds[2].className;
 
        highlightCells([
          tds[2], tds[4], tds[6]
        ]);
      }
 
      // game over?
      if (full || winner) {
 
        finished = true;
 
        field.className = "game-over";
 
        if (winner) {
 
          caption.innerHTML = messages[
            players[current] + "-wins"
          ];
 
        } else {
 
          caption.innerHTML = messages["draw"];
        }
 
        // restliche Buttons entfernen
        buttons = field.getElementsByTagName("button");
 
        while (buttons.length) {
          buttons[0].parentNode.removeChild(buttons[0]);
        }
 
        // new game?
        buttons = document.createElement("button");
        buttons.innerHTML = messages["new game?"];
 
        caption.appendChild(document.createTextNode(" "));
        caption.appendChild(buttons);
 
        buttons.addEventListener("click", function (event) {
          var cells = field.getElementsByTagName("td"),
            button, cell;
 
          // reset game
          current = 0;
          finished = false;
          field.removeAttribute("class");
 
          for (r = 0; r < 3; r++) {
            for (c = 0; c < 3; c++) {
              // reset cell
              cell = cells[r * 3 + c];
              cell.removeAttribute("class");
              cell.innerHTML = "";
 
              // re-insert button
              button = document.createElement("button");
              button.innerHTML = labels[r][c] + " " + messages["select"];
 
              cell.appendChild(button);
            }
          }
 
          // Hinweis einrichten
          caption.innerHTML = messages[
            players[current] + "'s-turn"
          ];
        });
      }
    }
 
    function highlightCells (cells) {
      cells.forEach(function (node) {
        var el = document.createElement("strong");
 
        el.innerHTML = node.innerHTML;
 
        node.innerHTML = "";
        node.appendChild(el);
        node.classList.add("highlighted");
      });
    }
 
    // click / tap verarbeiten
    function mark (event) {
      // Tabellenzelle bestimmen
      var td = event.target;
 
      // Button oder Zelle?
      while (td.tagName.toLowerCase() != "td"
        && td != field
      ) {
        td = td.parentNode;
      }
 
      // Zelle bei Bedarf markieren
      if (!finished
        && td.tagName.toLowerCase() == "td"
        && td.className.length < 1
      ) {
 
        td.className = players[current]; // Klassennamen vergeben
        td.innerHTML = players[current];
 
        check(); // Spiel zuende?
 
        if (!finished) {
 
          current = 1 - current; // zwischen 0 und 1 hin- und herschalten
 
          // Hinweis aktualisieren
          caption.innerHTML = messages[
            players[current] + "'s-turn"
          ];
        }
      }
    }
 
    // Spielanleitung ins Dokument einfügen
    b = document.createElement("p");
    b.innerHTML = messages["instructions"];
    element.appendChild(b);
 
    // Tabelle ins Dokument einfügen
    element.appendChild(field);
 
    // Tabelle aufbauen
    field.appendChild(caption); // Beschriftung
    field.appendChild(document.createElement("tbody"));
 
    // Hinweis einrichten
    caption.innerHTML = messages[
      players[current] + "'s-turn"
    ];
 
    for (r = 0; r < 3; r++) {
      // neue Tabellenzeile
      tr = document.createElement("tr");
 
      field.lastChild.appendChild(tr);
 
      for (c = 0; c < 3; c++) {
        // neue Tabellenzelle
        tr.appendChild(document.createElement("td"));
 
        // Klickbutton
        b = document.createElement("button");
        b.innerHTML = labels[r][c] + " " + messages["select"];
 
        tr.lastChild.appendChild(b);
      }
    }
 
    // Ereignis bei Tabelle überwachen
    field.addEventListener("click", mark);
  }
 
  // finde alle Spiel-Platzhalter
  games = document.querySelectorAll(".tic-tac-toe");
 
  for (i = 0; i < games.length; i++) {
    TicTacToe(games[i]); // aktuelles Fundstück steht in games[i]
  }
});
<h1>Tic-Tac-Toe</h1>
<div class="tic-tac-toe"></div>
Testen Sie das Dokument in Ihrem Browser immer wieder daraufhin, welche Inhalte für Nutzer assistiver Technologien angeboten werden, indem Sie die Darstellungsregeln im Browser deaktivieren sollen (z.B. im Firefox unter „Ansicht → Seiten-Stil → kein Stil“), damit Sie prüfen können, ob das Spiel für Nutzer assistiver Technologien benutzbar ist.

[Bearbeiten] Was fehlt noch?

Da es sich hier um ein Anfänger-Tutorial handelt, werden gewisse Dinge einfach „unter den Tisch gekehrt“, über die sich fortgeschrittene Script-Autoren aber Gedanken machen sollten. Hier nur ein paar solcher Ideen:

[Bearbeiten] Erweiterte Bedienung des Spiels

Eine Verbesserung wäre die Möglichkeit, ein beendetes Spiel neu starten zu können. Dieses würde aber entweder ein zusätzlichen Bedienelement (z.B. einen Button), oder wenigstens einen Hinweis erfordern, dass ein erneuter Klick (oder Tap) das Spiel erneut starten könnte. Eine entsprechende Modifikation des Codes bei der Handlerfunktion wäre ebenfalls nötig, denn im gegenwärtigen Zustand kann unser Spiel nicht neu gestartet werden.

[Bearbeiten] Feature-Detection

Unser Script ist nicht genügend gegen Fehler gehärtet. Wir verwenden wie selbstverständlich die Methode document.querySelectorAll, ohne zu prüfen, ob es diese auch wirklich gibt. Das kann in einem älteren Browser zu einer Fehlermeldung mit Programmabbruch führen. Eine Lösung wäre der Einsatz von Feature-Detection und einer Alternative zu querySelectorAll, um im einen Programmabbruch zu vermeiden

Beispiel: Feature-Detection
// kennt der Browser querySelectorAll?
if (document.querySelectorAll) {
  // finde alle Spielfeld-Tabellen
  var games = document.querySelectorAll("table.tic-tac-toe");
}

[Bearbeiten] Auslagern von Code

Um auf beliebigen eigenen Seiten ein Tic-Tac-Toe-Spiel anbieten zu können, wäre ein Auslagern von Code sinnvoll, denn dann muss man nicht alles in die jeweiligen HTML-Dokumente erneut schreiben, sondern kann die betreffenden Dateien (JavaScript und CSS) einfach einbinden. Wie das im Detail gelingen kann, können Sie hier vertiefen:

Im Idealfall kann die CSS-Datei vom Script selbständig eingebunden werden.

Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Index
Mitmachen
Werkzeuge
Spenden
SELFHTML