JavaScript/Tutorials/Spiele/Lotto

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Im Forum gibt es gerade zu Beginn jedes Semesters immer wieder Poster, die mitten in einem Projekt (fest)stecken und schnell eine Lösung für ein bestimmtes Problem suchen. Oft steckt die Ursache aber bereits in der Herangehensweise.

Deshalb soll hier anhand einer Lotterie ein Projekt entwickelt und neben dem fertigen Produkt vor allem die Herangehensweise und die möglichen Lösungswege beleuchtet werden.

"Wussten Sie schon, dass Giacomo Casanova die Lotterie in Deutschland eingeführt hat?"

Ursprünglich wetteten die Bürger Genuas, welche der 90 Stadträte in die Regierung gelost wurden. Casanova brachte das System erst nach Frankreich und dann zu Friedrich dem Großen, um so die Staatskassen zu sanieren. Dabei konnten die Bürger eine Zahl setzen und wetten, ob diese bei den 5 gezogenen dabei sei. Das heutige "6 aus 49" gibt es erst seit 1953.[1]

Aufgabe

Aus dem Ausgangspost konnte man zwei Aufgaben entnehmen:[2]

  • Für ein Uni-Projekt soll eine Lotterie-Website erstellt werden.
  • Diese 6 Zahlen (+ Superzahl) sollen in einem Array gespeichert werden.

Vorüberlegungen

Wir möchten eine Webseite erstellen, in der man …

  1. seine Lottozahlen inklusive Zusatzzahl tippen und dann absenden kann.
  2. in einem zweiten Schritt soll eine Ziehung simuliert und
  3. mit unserem Tipp verglichen werden.

Datenstruktur

Die sechs getippten, aber auch die gezogenen Zahlen können in einem Array gespeichert werden. Es ist eher weniger sinnvoll, die Superzahl ebenfalls in diesen Array zu integrieren.

Deshalb erstellen wir ein Lotto-Objekt mit mehreren Eigenschaften:

let lotto = {
    chosenNumbers: '',
    winningNumbers: '',
    size: 6,  //Anzahl der zu ziehenden Zahlen
    highestNumber : 49  // Anzahl der möglichen Zahlen
};


Zahlen eingeben

Erster Teil der Aufgabe ist es, nun ein Formular zu erstellen, mit dem wir unsere Glückszahlen eingeben können. Dabei sollte die Separation of concerns, die Trennung von Inhalt, Präsentation und Verhalten, beachtet werden:

  • HTML legt fest, was auf der Seite stehen soll (struktureller Aufbau einer Webseite)
  • CSS legt fest, wie es dargestellt werden soll (Formatierung & Gestaltung)
  • JavaScript legt fest, was passieren soll. (interaktives Verhalten)

HTML

Für die Eingabe unserer Zahlen verwenden wir hier ein HTML-Formular. Interessant ist, dass es aus zwei verschiedenen Feldern besteht:

  • ein Feld mit Zahlen, von denen mehrere ausgewählt werden können
  • ein Feld mit Zahlen, bei denen nur eine (Super)-zahl ausgewählt werden kann

Diese Aufgabenstellung findet sich immer wieder, so z. B. in diesem Geburtstagsformular.

HTML-Markup für Lotto ansehen …
<form class="lotto">
	<fieldset>
		<legend>Tippfeld</legend>
		<input type="checkbox" id="1"><label for="1">1</label>
		<input type="checkbox" id="2"><label for="2">2</label>
		<input type="checkbox" id="3"><label for="3">3</label></fieldset>
	<fieldset>
		<legend>Superzahl</legend>
		<input type="radio" name="superzahl" id="s0"><label for="s0">0</label>
		<input type="radio" name="superzahl" id="s1"><label for="s1">1</label>
		<input type="radio" name="superzahl" id="s2"><label for="s2">2</label></fieldset>
</form>

Für beide Formulare werden input-Elemente verwendet:

  • Checkboxen erlauben eine Mehrfachauswahl, mit JavaScript wird überprüft, ob die maximale Anzahl erreicht wird.
  • Radio-Buttons wählen ein Feld aus, eine vorher getroffene Auswahl wird wieder entfernt.

Jedem input-Element folgt ein label - eine für die Zugänglichkeit nötige Beschriftung. Es ist über das for-Attribut mit der id des Eingabefelds verbunden. Wenn man auf das label klickt, wird das input-Element angekreuzt.


Alternative: ein Element mehr, aber weniger HTML-Attribute
	<fieldset>
		<legend>Tippfeld</legend>
		<label><input type="checkbox"><span>1</span></label>
		<label><input type="checkbox"><span>2</span></label>
		<label><input type="checkbox"><span>3</span></label></fieldset>

Einfacher scheint es, das label als Eltern-Element zu verwenden und so auf die for- und id-Attribute verzichten zu können. Um die Zahl mit CSS formatieren zu können, wird sie in einem span-Element notiert. Insgesamt werden so aber nur zwei Zeichen gespart.


<table id="sechsaus49">
  <caption>6 aus 49</caption>
  <tbody>
     <tr>
       <td><input type="checkbox" name="box" value="1" onClick="check(0)">1<br></td>
       <td>…

Im Originalpost wurden die Eingabefelder in einer HTML-Tabelle angeordnet. Dies ist heute nicht mehr nötig und kann mit wenigen Zeilen CSS besser erreicht werden:

Grid Layout für Tippfeld ansehen …
fieldset {
	display: grid;
	gap: .25rem;
}
#tippfeld {
	grid-template: repeat(7, 1.5rem) / repeat(14, 1.5rem);
}
#superzahl {
	grid-template: 1.5rem / repeat(10, 1.5rem);
}

Die beiden fieldsets werden mit display:grid zu einem Raster. Mit grid-template kann die Anzahl der Reihen und Spalten festgelegt werden.

Hauptartikel: CSS/Tutorials/Grid

CSS - Checkboxen optisch verstecken

Jetzt wäre es natürlich schön unser Formular wie einen Lottoschein zu gestalten. Im HTML-Formular-Tutorial wird die Vorgehensweise ausführlich beschrieben.

input-Elemente optisch verstecken ansehen …
input {
	position: absolute !important;
	clip: rect(1px, 1px, 1px, 1px);
	padding:0 !important;
	border:0 !important;
	height: 1px !important; 
	width: 1px !important; 
	overflow: hidden;
}
label {
	position: relative;
	text-align: center;
	color: firebrick;
	border: thin solid;
	line-height: 1.5;
}
:checked + label::after {
  content: "×";
	position: absolute;
	left: 0;
	top: -1.15rem;
	right: 0;
	font-size: 2.5rem;
	color: darkblue;
	opacity: .6;
}

.lotto input:hover + label,
.lotto input:focus + label{
	background: #ffebe6; 
}

Die input-Felder werden ausgeblendet. Dies geschieht, indem sie eine feste Größe von jeweils 1x1px erhalten, der dann mit clip ausgeschnitten wird.

Die labels mit den Nummern erhalten nun eine rote Textfarbe und mit border eine dünne Randlinie, sodass sie wie Kästchen aussehen. Mit line-height und text-align wird die Zahl im Kästchen zentriert.

Mit CSS können Sie alle Bestandteile Ihrer Webseite selektieren. Dies geschieht entweder, wie oben, mit dem Elementnamen, mit einer Klasse oder über weitere Selektoren.

Dabei müssen sowohl die Zustände, wie auch das Verhalten mit CSS gekennzeichnet werden:

  • Die Pseudoklasse :checked selektiert Formularelemente danach, ob sie angewählt (checked, check = Haken) sind. Über den Nachbarkombinator + erhält nun jedes label ein Pseudoelement:
     :checked + label::after {…}
    Dieses Pseudoelement zeigt mit content ein X, das entsprechend gefärbt und absolut positioniert wird. (Referenzpunkt ist die relative Positionierung des label-Elements.)
  • Wenn man einen (unsichtbaren) Radio-Button mit der Tastatur ansteuert, wird das Label entsprechend (input:focus + label) mit einem roten Hintergrund gekennzeichnet. Analog wird auch das Hovern mit der Maus gekennzeichnet.

JavaScript - Formular auswerten

Während das fieldset mit der Superzahl ohne weitere Programmierung nur eine Zahl auswählt, könnte man im Tippfeld beliebig viele Zahlen auswählen. Eine Begrenzung auf 6 Zahlen und die Auswertung der gewählten Zahlen kann nur mit JavaScript (oder einer serverseitigen Programmiersprache wie PHP) erreicht werden.

 <td><input type="checkbox" name="box" value="1" onClick="check(0)">1<br></td>

Nach der oben erwähnten Trennung von Inhalt, Präsentation und Verhalten soll aber kein JavaScript innerhalb des HTML stehen. An Stelle eines onclick auf jeder Checkbox registrieren wir einen Klickhandler auf dem Formular:

Laden des Scripts; Klickhandler ansehen …
document.addEventListener('DOMContentLoaded', function () {
	
	const lotto = document.querySelector('#lotto');
	lotto.addEventListener('input', lottoNumberChanged);

	function lottoNumberChanged(event) {
		console.log('Läuft!');
	}

});

Das nennt man unaufdringliches JavaScript - keine Event-Registrierung im HTML. Das kann das Script alleine. Dieser Rahmen registriert die Funktion lottoNumberChanged als Behandler für ein input-Ereignis auf dem form-Element. Das Ereignis input wird gefeuert, wenn sich der Wert eines input-Elements ändert, und das ist für unser Problem angemessener als ein click. Checkboxen sind zwar so gnädig und simulieren einen click, wenn man sie mit der Tastatur umschaltet, aber richtig ist das nicht.

Diese Behandlungsfunktion bekommt vom Browser einen Parameter mit, das event-Objekt. Darauf gibt's ein Property namens target, und das ist das Element, das sich geändert hat. Irgendwie müssen wir ja wissen, wer sich geändert hat, wenn wir ganz allgemein auf das Fieldset lauschen. Aber solche Ereignisse "blubbern" hoch und man kann sich oben hinstellen und sie alle in Empfang nehmen, ohne jede einzelne Checkbox belauschen zu müssen.

Auswertung der Zahleneingabe ansehen …
	const lotto = document.querySelector('#lotto');
	lotto.addEventListener('input', lottoNumberChanged);

	function lottoNumberChanged(event) {
		let chosenNumbers = lotto.querySelectorAll('input[type=checkbox]:checked');
		if (chosenNumbers.length > 6) {
		event.target.checked = false;
		alert('Mit sieben Zahlen wird Ihr Tippschein ungültig!');
	}

Um zu bestimmen, wieviele Kreuzchen da sind, zählt man nicht von Hand. Der Browser kann das selbst, mit Hilfe eines CSS Selektors und querySelectorAll. Der Selektor, der da angegeben ist, findet input Elemente mit dem Attribut type="checkbox" und der Pseudoklasse :checked - die immer dann gesetzt ist, wenn die Checkbox einen Haken hat.

Sind es mehr als 6, dann wird einfach der checked-Zustand von event.target - also der gerade veränderten Checkbox - wieder auf false gesetzt.

Formular absenden

Damit die getippten Zahlen auch abgesendet werden können, erhält unser Formular einen Absendebutton und das JavaScript eine neue Funktion:

Auslesen der gewählten Zahlen ansehen …
	const button = document.querySelector('#send');
	const size = 6; // Anzahl der gezogenen Zahlen 
	let chosenNumbers;
	lotto.addEventListener('input', lottoNumberChanged);
	button.addEventListener('click', sendLottoNumbers);

	function lottoNumberChanged(event) {
		chosenNumbers = lotto.querySelectorAll('input[type=checkbox]:checked');
		if (chosenNumbers.length > size) {
		  event.target.checked = false;
		  alert('Mit mehr als ' + size + ' Zahlen wird Ihr Tippschein ungültig!');
    }
	}
    
function sendLottoNumbers(event) {
	chosenNumbers = [...document.querySelectorAll('input[type=checkbox]:checked + label')].map(e => e.textContent);
	if (chosenNumbers.length == size) {
		console.log('Lottozahlen = ' + chosenNumbers);
		const superzahl = document.querySelector('[name="superzahl"]:checked + label').textContent;
		console.log('Superzahl = ' + superzahl);			
	}
	else {
		alert('Sie müssen ' + size + ' Zahlen tippen!');
  }		
}

Mit einem Klick auf den Button wird die Funktion sendLottoNumbers aufgerufen. Dabei wird eine erneute Plausibilitätsprüfung durchgeführt:

Sind weniger als 6 Kästchen angekreuzt, wird die Funktion abgebrochen und der Benutzer aufgefordert, die noch fehlenden Nummern zu tippen.

Sie liest dann die angeklickten Zahlen aus - genauer gesagt, den Textinhalt des zur Checkbox gehörenden labels. Dieser wird mit querySelectorAll('input[type=checkbox]:checked + label').textContent ermittelt.

Mit map() wird aus den Zahlen ein Array gebildet.

Zahlen ziehen

Nun können unsere Zahlen zur Lotto-Zentrale gesendet werden. Dort werden die empfangenen Formular-Daten (neben unseren Zahlen noch persönliche Daten wie Namen oder Kundennummer) erneut geprüft und dann bis zur Ziehung zusammen mit den anderen Lotto-Scheinen gespeichert.

Hauptartikel: PHP/Tutorials/Formulare

Im nächsten Kapitel wollen wir eine solche Ziehung einmal simulieren und unsere Zahlen vergleichen. Dies soll ohne Server nur innerhalb des Browsers funktionieren und schließt dann ein Tippen anderer Nutzer natürlich aus.

Ziehen nach dem Urnenmodell

Nun sollen unsere sechs Lottozahlen nach dem Zufallsprinzip gezogen werden. Dabei will man aber keine zufällige Wahl mit rand(), damit es nicht wie beim Würfeln zu mehreren "Sechsern" hintereinander kommen kann, sondern verwendet das Ziehen ohne Zurücklegen (Urnenmodell [3]), damit bereits gezogene Nummern aus dem Gesamtpool entfernt werden.

ToDo (weitere ToDos)

Text muss an Beispiel 5 angepasst werden.

  1. Sortierung der Zahlen als Zahlen nicht als Strings
  2. Vergleich
--Matthias Scharwies (Diskussion) 04:36, 31. Mai 2021 (CEST)

In PHP verwürfelt man ein passend erstelltes Array mit shuffle und reduziert seine Größe auf n Werte:

function pick_n_out_of_m ($n, $m) {
  $pool = shuffle(range(1, $m));
  return array_pad($pool, $n);
}

In JavaScript ist das Verwürfeln nicht so einfach möglich, weil keine native Methode existiert. Man muss sich also behelfen. Eine kurze Recherche verrät, dass eine selbstgebaute Lösung schnell ungleiche Wahrscheinlichkeiten produzieren kann. Hier nun das JS-Pendant zum PHP-Beispiel:

Zufallszahlen ermitteln ansehen …
// "native" shuffle-Methode polyfillen
if (!Array.prototype.shuffle) {
	// source: https://medium.com/@nitinpatel_20236/-15ea3f84bfb
	Array.prototype.shuffle = function () {
		const a = this;
		var i = a.length - 1;
		while (i > 0) {
			const j = Math.floor(Math.random() * i);
			const temp = a[i];
			a[i] = a[j];
			a[j] = temp;
			i--;
		}
		return a;
	};
}

function pick_n_outOf_m(n, m) {
	const pool = Array(m)
		.fill() // damit map() so gelingt
		.map((_, i) => i + n) // Werte von n-m eintragen
		.shuffle();
	return pool.slice(0, n);
}


[4]

DOM-Manipulation

Für die Ziehung der Lotto-Zahlen benötigen wir eine zweite Seite. Hier gibt es zwei Möglichkeiten:

  • Einfacher scheint es, einfach auf eine zweite Seite zu verlinken
  • Wir werden einfach das Zahlenfeld aus dem HTML entfernen und ein neues Ausgabefeld dynamisch erstellen.

Animation der Ausgabe

Es wäre natürlich attraktiv, wenn die Zahlen nicht einfach in einem output-Feld angezeigt werden, sondern das Mischen und Herausrollen angezeigt würden. Einfacher ist das Rollen eines Nummernbands einer Slot machine, die stoppt und eine zufällig gewählte Nummer anzeigt.


Gewonnen oder verloren?

Was noch fehlt ist der Vergleich zwischen unseren und den gezogenen Zahlen. Haben wir gewonnen?




Quellen

  1. Der Westen: Wie Casanova die Lotterie nach Deutschland brachte vom 28.12.2012
  2. SELF-Forum: wie kann ich optisch eine checkbox verstecken? (oder code auf radiobuttons anpassen)
  3. Wikipedia: Urnenmodell
  4. Javascript Challenge: Lotto Number Generator von Matthias Reuter 09/23/2009
    verschiedene Lösungsansätze einen Lottozahlengenerator mit möglichst wenigen Bytes zu programmieren.