JavaScript/Tutorials/Spiele/Sum-up

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Am Beispiel eines kleinen Zahlenspiels können Sie die Möglichkeiten der DOM-Manipulation nun Schritt für Schritt nachvollziehen.

Vorüberlegungen

Spielidee

  1. Eine Zufallszahl wird als Additionsziel definiert und angezeigt.
  2. In einem Feld aus Zahlen sollen solange Zahlen angeklickt (und damit addiert) werden, bis diese Zielzahl erreicht wird.
    1. Auf jeden Klick hin wird eine (nicht angezeigte) Zwischensumme gebildet.
    2. Wenn das Ziel erreicht oder überschritten wird, wird eine Erfolgs- oder Misserfolgsmeldung ausgegeben
    3. Mit dem Erreichen oder Über-/Unterschreiten der Zielzahl wird die geheime Zwischensumme auf 0 gesetzt und eine neue Zufallszahl definiert und angezeigt.
  3. Das Spiel endet, wenn
    1. alle Zahlen angeklickt worden sind
    2. die restlichen Klick-Zahlen die geforderte Zufallszahl nicht mehr erreichen können.

Programmlogik

Das Spiel wird einzelne Funktionen aufgeteilt:

  1. eine Funktion createNumbers(), die die Arrays mit den (Zufalls)-Zahlen erzeugt
  2. eine Funktion createGameBoard(), die die Buttons mit den enthaltenen Zahlen erzeugt und next() aufruft
  3. eine Funktion next(), die
    1. die Zwischensumme resettet
    2. eine neue Zufallszahl festlegt.
    3. Dann muss sie prüfen, ob die restlichen Klick-Zahlen die neue Zufallszahl überhaupt erreichen können, ansonsten ruft sie end() auf.
  4. eine Funktion click(), die dafür sorgt, dass …
    1. die Klick-Zahl verrechnet wird
    2. das geklickte Element visuell verschwindet.
    3. Im Anschluss bewertet sie, ob die Zufallszahl erreicht/überschritten wurde und aktualisiert eventuell die Anzeige. Nach einer kurzen Verzögerung ruft sie nötigenfalls next() auf.
  5. Eine Funktion end(), die bei Spielende das Resultat ausgibt und evtl. einen Neustart anbietet

JavaScript

Zufallszahlen erzeugen und zerlegen

Im Spiel sollen Sie solange Zahlen addieren, bis die gegebene Summe erreicht ist. Dafür benötigen wir eine Reihe von Summen (numbers) und dementsprechend viele Teiler (factors). Mit einigen Funktionen können Sie sie schnell erzeugen:

Zufallszahlen erzeugen und zerlegen ansehen …
'use strict';
document.addEventListener('DOMContentLoaded', function () {

var size  = 1,       //Anzahl der Zahlen des Arrays kann beliebig verändert werden
    numbers  = [],   //Array der Additionsziele
    factors = [];    //Array der Teiler der Additionsziele 

document.getElementById('button').addEventListener('click', createGame);
		
function createGame(){		
  for (var i = 0; i < size; i++) {
    numbers[i] = (rand(10 , 19));
    var zahl = (numbers[i]),         // Zerlegt die Zufallszahl in drei Teiler (Faktoren)
        factor1 = rand(3,(zahl/2)),
        factor2 = rand(1,4);
    factors[3 * i + 0] = factor1;
    factors[3 * i + 1] = factor2;
    factors[3 * i + 2] = zahl - (factor1 + factor2);	
  }
  //Ausgabe
  createElement('#textblock', 'p', numbers);
  createElement('#textblock', 'p', factors);
}

// erzeugt Zufallszahlen
function rand (min, max) {			
  return Math.floor(Math.random() * (max - min + 1)) + min;
} 

function createElement(parent, elem, content){
...
}
 
});
Zu Beginn werden drei Variablen festgelegt. Ein Array numbers mit unseren zufällig erzeugten Additionszielen; die Größe des Arrays wird durch size festgelegt und ein weiterer Array factors mit den Teilern.

Die Funktion createGame füllt den Array numbers. In einer Schleife wird das Array mit Zufallszahlen gefüllt, die mit der Helferfunktion rand (min, max) erzeugt werden.
Anschließend werden die einzelnen Werte des Arrays in 3 Teiler zerlegt und im Array factors hinterlegt.

Mithilfe der im DOM-Tutorial besprochenen Helferfunktion createElement() werden die beiden Arrays in je einem Textabsatz ausgegeben.

Spielfeld

Nachdem die Überprüfung erfolgreich war, können Sie nun das Spielfeld anlegen:

Anlegen des Spielfelds ansehen …
function createGame(){
	
  var container = document.querySelector('#game');
 
  if (container) {			//löscht evtl. vorhandene Elemente
    while (container.firstChild) {
      container.removeChild(container.firstChild);
    }
  }	
	
  for (var i = 0; i < size; i++) {
    numbers[i] = (rand(10 , 19));
    var zahl = (numbers[i]),         // zerlegt die Zufallszahl in drei Teiler (Faktoren)
        factor1 = rand(3,(zahl/2)),
        factor2 = rand(1,4);
    factors[3 * i + 0] = factor1;
    factors[3 * i + 1] = factor2;
    factors[3 * i + 2] = zahl - (factor1 + factor2);	
  }
  
  shuffle(factors);
  
  //Ausgabe
    for (var i = 0; i < factors.length; i++) {	//erzeugt in einer Schleife neue Elemente
    createElement('#game', 'button', factors[i]);
  }  
  var x = document.querySelectorAll('button'),  //Buttons erhalten eine Klasse für eine zufällige Färbung
      i;
  for (i = 0; i < x.length; i++) {
    x[i].className = 'type' + rand(1,9);
  }
  
  createElement('#game', 'output', 'Zahl');
  document.querySelector('output').id = 'display';
}
Die Funktion createGame() löscht zunächst alle evtl. noch vorhandenen Elemente. Hiermit kann verhindert werden, dass noch bestehende Inhalte bzw. Event-Handler auf das aktuelle Spielfeld wirken.

In der for-Schleife werden , wie oben besprochen, die Zufallszahlen erzeugt und in Teilsummen (factors) zerlegt.
Danach wird in einer Schleife für jeden Wert des Arrays factors ein Button erzeugt. Der oben besprochenen HelferFunktion createElement('#game', 'button', factors[i]); werden die id des Elternelements, der gewünschte Elementname und der Inhalt des zu überzeugenden Elements übergeben. Mit dem dritten Parameter factors[i] werden die entsprechenden Werte des Arrays zugewiesen.

Anschließend wird ein output-Element erzeugt, das die Zufallszahl beinhalten soll; es wird mit querySelector angesprochen und erhält eine id display.
DOM vor und nach der Initialisierung ansehen …
  <div id="game">
    <p>Für dieses Spiel benötigen Sie JavaScript.</p>
    <button id="button">Start!</button>
  </div>
Das Spiel besteht aus einem div mit der id game, das einen Hinweis enthält, und einem Button.
Durch einen Klick auf diesen erhalten Sie:
<div id="game">
  <button class="type9">2</button>
  <button class="type7">5</button>
  <button class="type1">4</button>
  <button class="type5">4</button>
  <button class="type3">1</button>
  <button class="type4">12</button>
  <button class="type5">5</button>
  ...
  <output id="display">Zahl</output>
</div>
Das Spiel besteht aus einem div mit der id game, das jetzt die Buttons mit den Teilsummen und ein output-Element für die Berechnungen und Ergebnisse enthält.

Spielfeld mit CSS gestalten

Gut geht anders – die Buttons sehen aus wie Buttons. Deshalb sollte unser Spiel mit CSS gestylt werden. Das output-Element und das Spielbrett können über ihre Selektoren angesprochen werden. Die Buttons haben jedoch (noch) keine Unterscheidungsmerkmale.

Anlegen des Spielfelds ansehen …
function createOutput(parent,elem, content){

  ...

  var x = document.querySelectorAll('button'),  
      i;
  for (i = 0; i < x.length; i++) {
    x[i].className = 'type' + rand(1,12);
  }

  ...

  }
Die createGame()-Funktion wird leicht verändert. Über querySelectorAll werden alle neu erzeugten Buttons ausgewählt und erhalten dann mittels className eine nach dem Zufallsprinizp ausgewählte Klasse zugewiesen.
Beachten Sie: Mit einer komfortableren Helferfunktion, bei der Sie auch optionale Attribute übergeben und erzeugen können, wäre diese Schleife nicht nötig, da dann nur z.B. class : 'type' + rand(1,12); als zusätzlicher Parameter übergeben werden müsste.

Da Sie den Buttons Klassen zugewiesen haben, können Sie sie jetzt mit CSS unterschiedlich gestalten:

Formatierung mit CSS ansehen …
output {
  display: block;
  float: right;
  width: 8rem;
  height:8rem;
  background: #333;
  font-size: 4em; 
  text-align: center;
  border: 1px solid #000066;
  border-radius:10%;
}

button[class^="type"]  {
  background-color: #FFFFFF;
  border: 1px solid #000066;
  border-radius: 40%;
  font-size: 120%;
  font-weight: bold;
  margin: 0 5px 5px 0;
  padding: 0.25em 0;
  text-align: center;
  width: 3rem; 
  height: 3rem;
}
button:focus,button:hover {
  border: 2px solid yellow;
  color: yellow;
}

button.type1 {background: white; }
button.type2 {background: yellow;}
button.type3 {background: beige;}
button.type4 {background: orange;}
Alle Buttons, die den String type im Klassennamen enthalten, erhalten ein quadratisches Aussehen und werden größer als die Standardschriftgröße dargestellt.

Per Zufallsprinzip wurden den Buttons Klassen zugewiesen, die unterschiedliche Hintergrundfarben haben.

Die zu erreichende Zahl wird in output mit einer Schriftgröße von 4em angezeigt.
Empfehlung: Achten Sie bei der Festlegung der CSS-Eigenschaften auch auf eine klare Kennzeichnung von mit der Tastatur oder Maus ausgewählten Elementen mittels der Pseudoklassen :focus und :hover

Event-Delegation

Das Spielfeld ist fertig – jetzt können wir uns um das Spiel selbst kümmern: die einzelnen Buttons sollen anklickbar gemacht werden. Was liegt näher als den Buttons beim Erzeugen ein click-Event zuzuweisen?

Klickfunktionalität im herkömmlichen Event-Handling
  for (i = 0; i < factors.length; i++) {
    newElm = document.createElement('button');
    container.appendChild(newElm);
    newElm.appendChild(document.createTextNode(factors[i]));
    newElm.onclick = clickHandler;
  }
Auch wenn das Hinzufügen mithilfe der Punkt-Notation simpel aussieht, birgt es doch ein großes Problem: Da jeder einzelne Button einen Event-Handler hat, gibt es insgesamt 36 einzelne davon.


Die Überwachung zahlreicher Elemente im Dokument ist sehr aufwändig umzusetzen und langsam in der Ausführung, da jedes Element herausgesucht, durchlaufen und bei jedem denselben Event-Handler registriert werden muss.

Bei solchen Aufgabenstellungen können Sie vom Bubbling-Effekt profitieren, das ist das Aufsteigen der Ereignisse im DOM-Baum. Machen Sie sich die Verschachtelung der Elemente im DOM-Baum zunutze und überwachen Sie die Ereignisse von verschiedenen Elementen bei einem gemeinsamen, höherliegenden Element, zu dem die Ereignisse aufsteigen. Diese Technik nennt sich Event-Delegation (englisch delegation für Übertragung von Aufgaben). Dabei wird einem zentralen Element die Aufgabe übertragen, die Ereignisse zu verarbeiten, die bei seinen Nachfahrenelementen passieren.

Beispiel ansehen …
  function createGame(){
    ...
    container.addEventListener('click', click);	
    ...
  } 
}

 function click (e) { 
    var target = e.target;
    var value = target.innerText;
    alert (value + ' wurde geklickt!');
    removeElement(e);
  }
Vorteilhafter ist das dynamische Anbinden des Klick-Events an den Container mittels addEventListener.

Die aufgerufene Funktion click(e) ermittelt mit der Eigenschaft target von welchem Element das Ereignis ausgeht.

Anschließend wird das geklickte Element mit der Helferfunktion removeElement() entfernt.

Das Spiel läuft

Mit jedem Klick auf einen Button wird nun überprüft, ob das Additionsziel erreicht wurde; Falls ja wird die Funktion next() aufgerufen, die ein weiteres Additionsziel ausgibt. Gibt es kein Additionsziel oder verfügbare Buttons mehr, wird die Funktion end() aufgerufen;

Beispiel ansehen …
  function createGame(){
    ...
    container.addEventListener('click', click);	
    ...
  } 
}

 function click (e) { 
    var target = e.target;
    var value = target.innerText;
    alert (value + ' wurde geklickt!');
  }

DOM-Manipulation und CSS

Mit JavaScript können Sie das DOM so manipulieren, dass das Aussehen der Seite völlig verändert wird. Dabei sollten aber vorzugsweise die Elemente nicht mit style formatiert werden, sondern bereits im Stylesheet vorhandene Klassenattribute gesetzt, geändert und entfernt werden. Existiert für diese Klasse eine Festlegung im Stylesheet, wird die Darstellung vom Browser sofort geändert.

Klassenattribute hinzufügen und entfernen

Beim Erreichen oder Überschreiten des Additionsziels soll eine Erfolgs- bzw. Misserfolgsmeldung angezeigt werden. Dabei machen wir uns zunutze, dass ein Setzen von Klassen nicht nur das Aussehen des selektierten Elements verändern kann, sondern auch Pseudoklassen wie ::after und ::before.

auf das CSS wirken ansehen …
.exact {
	font-weight: bold;
	color: lime;
}

.exact:after {
	content: "✓";
	display: block;
	margin: -0.5em 0 0 1.5em;
}

.inexact {
	font-weight: bold;
	color:red;
}

.inexact:after {
	content: "✗";
	display: block;
	margin: -0.5em 0 0 1.5em;
}
if (current <= 0) {

  diff.push(current);

  display.className = (
	current === 0
	? "exact"
	: "inexact"
  );
setTimeout(next, 1500);
				}
In einer bedingten Abfrage wird überprüft, ob current kleiner gleich 0 ist. Falls dies zutrifft, wird überprüft, ob der Wert von current 0 ist und mit einem ternären Operator für den true-Fall dem output-Element mit className die Klasse exact, für den false-Fall die Klasse inexact zugewiesen.
Beachten Sie: Da der Klassenname keine Variable ist, muss er in Anführungszeichen gesetzt werden.

Die geänderte Klasse und die Erfolgs- bzw. Fehlermeldungen bleiben für 1500ms sichtbar, da der Aufruf eines neuen Durchlaufs mit setTimeout verzögert wurde.

In der Funktion next() werden evtl. vorhandene Klassenattribute durch className = ''; wieder gelöscht.

Die hier vorgestellte Vorgehensweise ermöglicht es, nicht nur die Textfarbe, sondern eine Vielzahl von CSS-Eigenschaften gleichzeitig zu ändern.

Nachteilig an der Verwendung der className-Eigenschaft ist die Überschreibung eines eventuell schon vorher vorhandenen Klassenattributs. Mit der classList-Eigenschaft können Sie komfortabel eine (auch von mehreren) Klassen ändern, hinzufügen oder löschen.

das fertige Spiel

In der vorherigen Variante verändert sich die Position der Buttons mit jedem Klick, da der Platz der durch Klicks entfernten Buttons von seinem Nachbarn eingenommen wird.

Schöner wäre es, wenn die Buttons nur in einer Richtung (Luftblasen vergleichbar) nach oben steigen würden.


auf das CSS wirken ansehen …
function createGameBoard(arr) {	
  var i, 
      parent = document.querySelector('#gameboard');
 parent.addEventListener('click', clickHandler);
 
  if (parent) {			//löscht evtl. vorhandene Elemente
    while (parent.firstChild) {
      parent.removeChild(container.firstChild);
    }
  }	
 
  for (i = 0; i < arr.length; i++) {	//erzeugt in einer Schleife neue Elemente
    if ((i+6) % 6 == 0){				//Abfrage, ob Zahl durch 6 teilbar ist, erzeugt dann Spalte
	  var div = document.createElement('div');  
      parent.appendChild(div);
	}
	createOutput('#gameboard', 'button', arr[i]);
  } 
}
In der Funktion createGameBoard() werden nun 2 Zeilen eingefügt, sodass immer je sechs Buttons innerhalb eines div angelegt werden.