JavaScript/Tutorials/Spiele/Zahlenspiele
In diesem Kapitel lernen Sie die Grundzüge von Lernspielen kennen. Der Partner, hier der Computer, stellt eine Aufgabe, die dann zu bearbeiten ist. Die Eingabe des Benutzers wird nach dem EVA-Prinzip ausgelesen, verarbeitet und dann eine Rückmeldung ausgegeben.
Dabei stehen nicht die (zugegebenermaßen) einfachen Spiele im Vordergrund, sondern …
- die Planung,
- die Übertragung der Spielidee von einer Skizze in ein Script
(siehe auch: JavaScript/Tutorials/Einstieg/Fehlervermeidung)
und - die Auswahl der richtigen HTML-Elemente, die uns viel Programmierarbeit ersparen kann.
Inhaltsverzeichnis
Zahlen-Raten
Spielidee
- Der Computer „denkt“ sich eine Zufallszahl aus.
- Der Spieler versucht sie in möglichst wenigen Schritten zu erraten.
HTML-Markup
Anders als im Einstieg im JavaScript machen wir hier von allen Möglichkeiten von HTML5 Gebrauch:
<p>Wir haben eine Zahl zwischen 1-10 ausgesucht. Kannst Du sie erraten?</p>
<form>
<label for="guessField">Gib Deine Zahl ein:</label>
<input type="number" id="guessField">
<button type="button" id="submitguess">Prüfe!</button>
</form>
Das Formular besteht aus einem Eingabefeld von type="number". So können Fehleingaben von nichtnumerischen Zeichen von vorneherein ausgeschlossen werden. Dem Eingabefeld ist über seine id ein label zugeordnet, das eine erklärende Beschriftung gibt. Ein Button kann dann die Überprüfung des eingegebenen Werts auslösen.
JavaScript
Einbindung
Wie im Event-Handling-Tutorial näher beschrieben, wird das JavaScript im head des HTML-Dokuments notiert und erst nach dem Laden der Seite ausgeführt:
<script>
'use strict';
document.addEventListener('DOMContentLoaded', function () {
// Hier kommt unser Script hin!
});
</script>
Zufallszahlen erzeugen
In JavaScript können mit Math.random() Zufallszahlen erzeugt werden. Da diese aber nur zwischen 0 und 1 liegen, benötigen wir eine kleine Helferfunktion:
document.querySelector('#submit').addEventListener('click', ausgabeZufallszahl);
let randomNumberMin = 1,
randomNumberMax = 10;
function rand (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function ausgabeZufallszahl () {
document.querySelector('output').innerText = rand(randomNumberMin,randomNumberMax);
}
min
und max
kennzeichnen, mit randomNumberMin
ist es aber auch später in komplexeren Skripten klar, was gemeint ist.Verarbeitung von Benutzereingaben
Nachdem die Zufallszahl gebildet wurde, warten wir nun auf eine Benutzereingabe, die dann „verarbeitet“ – nämlich mit der Zufallszahl verglichen – wird:
document.querySelector('#submitguess').addEventListener('click', checkInput);
let input = document.getElementById('guessField'),
output = document.querySelector('output'),
randomNumberMin = 1,
randomNumberMax = 10,
randomNumber = rand(randomNumberMin,randomNumberMax),
guess = 1;
function rand (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function checkInput() {
console.log('Zufallszahl: ' + randomNumber + ' input: ' + input.value + ' Versuche(guess): ' + guess);
if (input.value == randomNumber) {
output.innerText = 'Glückwunsch! Sie haben es in ' + guess + ' Versuch(en) erraten!';
}
else if(input.value > randomNumber) {
guess++;
output.innerText = 'Schade! Versuchen Sie eine kleinere Zahl.';
}
else {
guess++;
output.innerText = 'Schade! Versuchen Sie eine größere Zahl.';
}
input.value = '';
}
An den submit-Button wird mit addEventListener ein EventListener angehängt, der beim Auslösen des Klickereignisses die Funktion checkInput()
aufruft.
Diese Funktion besteht aus einer bedingten Anweisung if(). Innerhalb der Klammern werden zwei Werte (randomNumber
und input.value
) miteinander verglichen. Dafür benötigen wir den Vergleichsoperator ==
.
Falls die Bedingung zutrifft gibt es eine Erfolgsmeldung; falls nicht wird eine Fehlermeldung ausgegeben und der Zähler guess
um eins erhöht.
Anschließend wird das Eingabefeld geleert und auf eine neue Eingabe gewartet.
Ist ihnen diese Zeile aufgefallen?
console.log('Zufallszahl:' + randomNumber + ' input: ' + input.value + 'Versuche(guess): ' + guess);
- Öffnen Sie das Beispiel mit Vorschau und Rechtsklick in einem neuen Tab.
- Mit einem Klick auf F12 können Sie die Konsole öffnen und die aktuellen Werte der drei verwendeten Variablen überprüfen.
Mathe-Quiz
Spielidee
- Der Benutzer wählt eine Rechenart, bzw. den Operator aus.
- Der Computer „denkt“ sich eine Rechnung mit 2 Zufallszahlen aus.
- Der Spieler versucht die Rechnung zu lösen und gibt das Ergebnis ein.
- Die eingegebene Zahl wird mit dem Ergebnis verglichen.
- Es wird eine Erfolgs- oder Fehlermeldung ausgegeben.
- Auf dem Rechner des Nutzers wird eine Statistik gespeichert.
HTML-Markup
Auswahlmenü
Der Nutzer soll zwischen mehreren Operatoren und einem Zufallsmodus, bei dem alle Rechenarten zufällig ausgewählt werden, auswählen können. Auch wenn dies wie eine Navigation aussieht, ist dies semantisch eine Eingabe, die dann vom Skript ausgewertet wird.
Vorteil dieser Vorgehensweise ist, dass Sie das Standardverhalten der Radio-Buttons ausnutzen können.
Radio-Buttons sind bereits interaktiv: Wenn ein Button ausgewählt wird, wird der zuvor ausgewählte Button deaktiviert.
<form class="toggle-buttons" id="rechenarten">
<input type="radio" name="op" data-operator="+" id="add" checked><label for="add">+</label>
<input type="radio" name="op" data-operator="-" id="subtract"> <label for="subtract">-</label>
<input type="radio" name="op" data-operator="÷" id="divide"> <label for="divide">/</label>
<input type="radio" name="op" data-operator="×" id="multiply"> <label for="multiply">x</label>
<input type="radio" name="op" data-operator="any" id="any"> <label for="any">?</label>
</form>
Das Toggle-Menü besteht aus Radio-Buttons, die über das name-Attribut miteinander verknüpft sind. Danach folgen label mit einer Beschriftung.
Das Menü wird nun mit CSS gestylt:
.toggle-buttons input[type="radio"] {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.toggle-buttons label {
display: inline-block;
border: 1px solid #333;
border-radius: 0.5em;
width: 2rem;
height: 2rem;
margin: 0.5rem;
font: 0/0;
color: transparent;
}
.toggle-buttons label[for=add] {
background-image: url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20...");
}
.toggle-buttons input:checked + label {
background-color: #ebf5d7;
}
Die Radio-Buttons selbst werden mit Image-Replacement versteckt und dafür die label-Elemente als Buttons gestylt: Da es schwierig wäre, die vier Symbole der Zufallsauswahl passend zu formatieren, wird die Beschriftung ausgeblendet und durch SVG-Hintergrundgrafiken ersetzt. Um einen HTTP-Request zu sparen, wird das SVG-Markup direkt in die url()-Funktion notiert.
Der letzte Regelsatz färbt das Label des angeklickten Radio-Buttons grün. Dafür wird für das input-Element die Pseudoklasse :checked und dann der Nachbarkombinator + verwendet.
Aufgabenfeld
<form id="task">
<span id="var1">a</span>
<span id="operator">+</span>
<span id="var2">b</span>
=
<input id="input" type="number" step="1" size="3">
<button type="submit">↵</button>
</form>
Die beiden Zufallszahlen und der Operator werden innerhalb von inhaltsleeren span-Elementen notiert. Danach findet sich ein input-Eingabefeld. Der type="number" sorgt dafür, dass nur Zahlen eingegeben werden können, sodass eine Überprüfung der Eingabe mit JavaScript nicht mehr nötig ist. Ein Button ermöglicht das Absenden des Formulars.
Das Eingabefeld enthält normalerweise eine Scrollleiste am rechten Rand - sie wird nun mit CSS entfernt:
#task input[type="number"]{
width: 2em;
-moz-appearance: textfield;
appearance: textfield;
margin: 0;
font-size: 1em;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
Die appearance-Eigenschaft ermöglicht es, Formularelementen ihr betriebssystemspezifisches Aussehen zu nehmen. Dabei gibt es je nach Browser aber verschiedene Umsetzungen.
JavaScript
Das JavaScript dieses Spiels ist nun etwas komplexer. Umso wichtiger wird es, die einzelnen Schritte in Funktionen zu gliedern und jeden Schritt so zu kommentieren, dass wir das Skript auch nach ein paar Monaten wieder nachvollziehen und evtl. verbessern / ausbauen können.
Aufgabenerstellung
Für die Erstellung der Aufgaben benötigen Sie nun den gewählten Operator und zwei Zufallszahlen. Dabei muss aber darauf geachtet werden, dass bei einer Subtraktion keine negativen und bei einer Division keine Dezimalzahlen herauskommen.
Im Internet gibt es ältere Skripte, die die Aufgabenrechnung nun aus den drei Variablen zu einem String zusammenbauen und dann mit eval() auswerten. Allerdings ist eval() im Strengen Modus verboten.
Alternativ könnte man mit einer Verzweigung jeweils eine Anweisung mit der passenden Rechnung erstellen:
if (rechenArt == '2') {
operator = "-";
b = rand(1, randomNumberMax);
a = rand(b, randomNumberMax);
result = a - b;
}
In diesem Skript wurden die einzelnen Rechenarten durch ein Select-Menü ausgewählt. In jeder If-Verzweigung wird nun eine Variable operator
zugewiesen und die beiden Zufallszahlen ermittelt. (Damit bei einer Subtraktion eine positive ganze Zahl als Differenz herauskommt, muss der Minuend größer als der Subtrahend sein. Dies wird erreicht, indem zuerst der Subtrahend b
und dann erst eine Zufallszahl mit dem Wert von b
als randomNumberMin
ermittelt wird.)
In unserem Beispiel verwenden wir jedoch ein im SELF-Forum vorgeschlagenes Objekt, das die Operatoren als Eigenschaften des opMap-Objekts ...[1]
function createTask () {
let a,
b;
let opMap = {
"+": function() {
a = rand( 1, randomNumberMax * 3);
b = rand( 1, randomNumberMax * 3);
result = a+b;
return "+";
},
"-": function() {
// So kann sichergestellt werden, dass das Ergebnis immer positiv ist
b = rand( 1, randomNumberMax * 3);
a = rand( b, randomNumberMax * 3);
result = a-b;
return "-";
},
"×": function() {
a = rand( 1, randomNumberMax);
b = rand( 1, randomNumberMax);
result = a*b;
return "×";
},
"÷": function() {
// So kann sichergestellt werden, dass das Ergebnis immer eine ganze Zahl ist
b = rand( 1, randomNumberMax);
a = b * rand( 0, randomNumberMax);
result = a/b;
return "÷";
},
"any": function() {
let p, props = [];
for (p in opMap) {
if (p != "any") {
props.push(p);
}
}
p = rand(0, props.length);
return opMap[props[p]]();
}
};
In diesem Objekt sind für jeden Operator sowohl die Symbole als auch die Rechenoperation gespeichert.
Für den Zufallsoperator werden die Operatoren dann mit der Zufallsfunktion ausgesucht. Die Zufallszahlen je nach Operator so ermitteln, dass als Ergebnis nur positive ganze Zahlen herauskommen.
Verarbeitung der Benutzereingabe
Um das Spiel zu starten, wird nun zuerst die Funktion createGame()
aufgerufen, die alles Nötige einrichtet:
let input = document.getElementById('input'),
output = document.querySelector('output'),
randomNumberMax = 10,
result = 0,
stat = {
correct: 0,
incorrect: 0
};
function createGame () {
document.querySelector('#rechenarten').addEventListener('change', createTask);
// submit-Button einrichten
document.querySelector('#task').addEventListener('submit', evaluateResponse);
createTask();
}
Die Funktion createGame()
hängt nun sowohl an das Toggle-Menü als auch an den submit-Button EventListener, die bei Benutzereingaben im Toggle-Menü das Spiel starten. Danach wird mit createTask()
eine Rechenaufgabe gestellt.
Nach einer Benutzereingabe, die mit einem Klick auf den submit-Button bestätigt werden muss, wird die Funktion evaluateResponse()
aufgerufen.
function evaluateResponse (evt) {
// Ergebnis mit Eingabe vergleichen
evt.preventDefault();
if (result == input.value){
output.textContent = 'Super!';
output.className = 'exact';
stat.correct++;
} else {
output.textContent = 'Leider falsch!';
output.className = 'inexact';
stat.incorrect++;
}
setTimeout(createTask, 1000);
}
Nach einer Benutzereingabe, die mit einem Klick auf den Submit-Button bestätigt werden muss, wird die Funktion evaluateResponse()
aufgerufen.
evt.preventDefault()
verhindert das Standardverhalten des Submit eines Formulars, bei dem die Seite neu geladen würde.
Anschließend werden …
- das erwartete Ergebnis
result
und die Eingabeinput.value
verglichen - eine Fehlermeldung in das output-Element ausgegeben
- diese Fehlermeldung mit
output.className
mit einer Klasse versehen, die dann mit CSS formatiert werden kann. - und die Eigenschaften
correct
oderincorrect
des Stat-Objekts um eins erhöht.
Damit die Erfolgs-/Fehlermeldung sichtbar bleibt, wird erst nach einer Verzögerung von 1sec eine neue Aufgabe gestartet. Hierfür wird setTimeout() verwendet.
Ergebnissicherung
Schon in den Variablen zum Spielstart und in der vorherigen Funktion evaluateResponse()
begegnete uns der Term stat.correct
.
let stat = {
correct: 0,
incorrect: 0
};
stat
ist ein Objekt mit den zwei Eigenschaften correct
und incorrect
. Mit ihnen kann die Zahl der richtig und der falsch gelösten Aufgaben ermittelt und in einem Feedback ausgegeben werden.
Es wäre gut, wenn man diese Auswertung auch über einen längeren Zeitraum speichern könnte. Hier gibt es zwei Möglichkeiten:
- Daten könnten vom Benutzer, dem Client, zum Server übertragen und dort gespeichert werden. Um diese Daten dem Benutzer zuordnen zu können, müsste man Namen mit einem Login speichern und hätte neben den technischen Aspekten die Pflicht, den Datenschutz zu berücksichtigen.
- Eine clientseitige Speicherung mit Web Storage verzichtet auf die Erfassung und Speicherung des Namens. Die Daten bleiben beim Nutzer und können von ihm, wenn gewünscht, wieder gelöscht werden.
// Statistiken aus LocalStorage holen
function getStorage () {
let eintraegeArray = localStorage.getItem('item');
if (eintraegeArray) {
output.innerText = eintraegeArray;
stat = JSON.parse(eintraegeArray);
}
else {
output.textContent = 'Herzlich willkommen in unserem Mathe-Quiz! Gib das Ergebnis der Rechnung ein und drücke die Enter-Taste!';
}
return stat;
}
// Statistiken zurücksetzen
function resetStatistics () {
stat.correct = 0;
stat.incorrect = 0;
getStatistics();
document.querySelector('#ergebnis').textContent = 'Statisiken wurden zurückgesetzt!';
}
// Statisiken ausgeben
function getStatistics () {
document.querySelector('#richtig').textContent = stat.correct;
document.querySelector('#falsch').textContent = stat.incorrect;
let percentage = stat.correct/( stat.correct+stat.incorrect)*100;
document.querySelector('#prozent').textContent = percentage.toFixed(2);
}
Die Funktion getStorage()
überprüft den Web Storage, ob es einen Eintrag eintraegeArray
gibt. Falls ja, wird er mit JSON.parse() in das Objekt stat
umgewandelt.
Die Funktion resetStatistics ()
setzt die Werte für stat
wieder auf 0.
Die Funktion getStatistics ()
ermittelt aus den Werten für stat
die Gesamtsumme und den Anteil zwischen richtigen und flaschen Lösungen und gibt diesen aus.
Um die Speicherung der Statisitik zu starten, wird die Funktion createGame()
erweitert:
function createGame () {
document.querySelector('#rechenarten').addEventListener('change', createTask);
// Beim Verlassen der Seite wird vorher die Statistik gespeichert
window.addEventListener('beforeunload', function(event) {
localStorage.setItem('item', JSON.stringify(stat));
});
// gespeicherte Statistik laden. Wenn keine vorhanden, Info-Template laden
getStorage();
// Button um Statistiken zurückzusetzen
document.querySelector('#reset-stat').addEventListener('click', resetStatistics);
// submit-Button einrichten
document.querySelector('#task').addEventListener('submit', evaluateResponse);
createTask();
}
Beim Start der Funktion createGame()
wird ein EventListener eingerichtet, der beim Feuern des beforeunload-Events vor dem Verlassen der Seite die aktuelle Statistik speichert. Dafür wird das stat
-Objekt mit JSON.stringify() in eine Zeichenkette umgewandelt.
Anschließend wird mit der oben beschriebenen Funktion getStorage()
überprüft, ob es bereits eine Statistik gibt.
Ausgabe der Statistik
Für die Ausgabe der Statistik benötigen wir kein JavaScript, sondern machen uns das Dialog-Element zunutze:
<button id="open-dialog">Ergebnisse und Statistiken</button>
<dialog role="dialog">
<button id="close-dialog">X</button>
<h2>Ergebnis</h2>
<p>Richtig: <span class="exact" id="richtig"></span></p>
<p>Falsch: <span class="inexact" id="falsch"></span></p>
<p>Prozent: <span id="prozent"></span>%</p>
<button id="reset-stat">Statistiken zurücksetzen</button>
<p id="ergebnis"></p>
</dialog>
Das fertige Spiel
Kritik
Das Spiel bietet eine Fülle an Kritikpunkten, bzw. positiver formuliert: Verbesserungsmöglichkeiten:
- Die Zufallszahlen sind zufällig; können sich also auch wiederholen. Besser wäre es einen Array aus Zufallszahlen oder Aufgaben zu bilden und mit einer Wechsellogik die Methode „Ziehen ohne Zurücklegen“ aus dem Urnenmodell anzuwenden.
- Die hartkodierten Faktoren, mit denen bei der Aufgabenerstellung die
maxValue
multipliziert werden, um größere Zahlen bei den einfacheren Additionen und Subtraktionen zu erreichen, sollten in Variablen umgewandelt und je nach Erfolgslevel angepasst werden. So kann man mit leichten Aufgaben starten und mit fortschreitendem Erfolg schwierigere Aufgaben lösen müssen. - Apropos Erfolgslevel – dies sollte entweder der Nutzer selbst einstellen können oder mit fortschreitender Punktezahl automatisch angepasst werden können.
- In einem solchen Level könnten dann auch Dezimalzahlen und evtl. Brüche abgefragt werden. Andererseits benötigt man für Brüche wohl 2; für gemischte Zahlen 3 Eingabefelder. Hier wäre eine Zuordnungsaufgabe übersichtlicher.
Weblinks
- ↑ SELF-Forum: Alternative zu eval() für arithmetische Berechnungen vom 26.06.2017