JavaScript/Tutorials/Spiele/Memo-Quiz
Waren die Karten früher aus Karton, wollen wir sie nun im Computer nachbauen. Es gibt Ähnliches im Netz, wir wollen deshalb versuchen, die Prinzipien und die Techniken anfängergerecht zu erklären.
Während im ersten Kapitel der Zusammenhang zwischen Zufallszahlen und HTML-Formularen erklärt wurde, wollen wir hier zeigen, welche Rollen HTML, CSS und JavaScript spielen.
Inhaltsverzeichnis
Vorüberlegungen
Spielidee
- Das Script erzeugt …
- für jedes Bild eine Spielkarte mit einer Rückseite,
- dupliziert diese
- mischt und verteilt sie auf dem Spielfeld
- Anschließend wartet das Script auf eine Benutzereingabe, bzw. dass zwei Karten ausgewählt werden und
- wertet aus, ob diese identisch sind.
- deckt sie auf.
HTML-Markup
Im ersten Beispiel gibt es nur 4 Karten, bei denen jeweils zwei Karten die gleichen Bilder haben.
Ursprünglich besteht unser Spiel nur aus einer Spielfläche mit n
Bildern.
<div id="gameboard">
<img src="snowman.svg" alt="snowman">
<img src="advent-candles-4.svg" alt="candles">
...
</div>
Für ein Spiel benötigen wir aber interaktive Elemente, die man mit Maus, Tastatur oder Touch anwählen kann. Bevor wir unsere Bilder mit JavaScript interaktiv machen, ist es einfacher ein Element zu verwenden, das dieses Standardverhalten bereits mitbringt:
<div id="gameboard">
<button class="flip-card">
<div class="flip-card-inner">
<img class="flip-card-front" src="snowman.svg" alt="snowman">
<div class="flip-card-back"></div>
</div>
</button>
<button>
<div class="flip-card-inner">
<img class="flip-card-front" src="advent-candles-4.svg" alt="candles">
<div class="flip-card-back"></div>
</div>
</button>
...
</div>
Während ein Link zu einer anderen Resource leitet, ist ein Button genau dafür vorgesehen, Benutzeraktionen auszulösen. Der Button kann mit der Maus, mit einem Tap auf Touchscreens und mit der Tastatur per Tab-Taste angesteuert werden, ohne dass wir etwas programmieren müssen!
Deshalb soll JavaScript beim Laden der Webseite jedes Bild in einen Button kopieren und für die Rückseite ein weiteres div einfügen. Um die 3D-Transformation wirken zu lassen, benötigen wir noch einen weiteren div-Container.
CSS
Die Rückseite hat keinen Inhalt, sondern soll bei jedem Bild die gleiche Rückseite zeigen, damit klar erkennbar ist, welche Karten aufgedeckt sind. Alles was mit Aussehen zu tun hat, überlassen wir CSS.
Im Transform-Tutorial wurden bereits Flip-Cards vorgestellt, deren Deklaration wir hier verwenden:
.flip-card {
width: 250px;
height: 300px;
perspective: 500px;
}
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s;
transform-style: preserve-3d;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
}
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
}
Das Script
Laden
In einem ersten Schritt werden nun alle Bilder in Karten mit Vorder- und Rückseite umgewandelt:
document.addEventListener("DOMContentLoaded", () => {
const gameboard = document.querySelector("#gameboard");
const images = Array.from(gameboard.querySelectorAll("img"));
images.forEach((img) => {
const button = document.createElement("button");
button.classList.add("flip-card");
const flipCardInner = document.createElement("div");
flipCardInner.classList.add("flip-card-inner");
const flipCardFront = document.createElement("img");
flipCardFront.classList.add("flip-card-front");
flipCardFront.src = img.src;
flipCardFront.alt = img.alt;
const flipCardBack = document.createElement("div");
flipCardBack.classList.add("flip-card-back");
flipCardInner.appendChild(flipCardFront);
flipCardInner.appendChild(flipCardBack);
button.appendChild(flipCardInner);
gameboard.replaceChild(button, img);
});
});
Jede Karte besteht nun aus einer (aufgedeckten) Vorderseite und einer Rückseite. Wenn man über die Bilder hovert oder sie mit der Tab-Taste auswählt, drehen sich die Bilder, sodass die Rückseite sichtbar wird.
Auswahl durch den Nutzer
Jede Karte kurz anhovern und sie so anzuschauen, ist ja schummeln. Es sollten Karten ausgewählt werden dürfen, die dann sichtbar bleiben.
Nun ändern wir unser Script ab:
const button = document.createElement("button");
button.classList.add("flip-card");
button.setAttribute("aria-selected", "false");
button.addEventListener("click", () => {
const isSelected = button.getAttribute("aria-selected") === "true";
button.setAttribute("aria-selected", isSelected ? "false" : "true");
});
Die angeklickten Buttons erhalten ein aria-selected-Attribut, das anzeigt, ob sie ausgewählt werden. Früher hatte man so etwas mit Klassen realisiert - ARIA-Attribute haben den Vorteil, dass sie Screenreadern genau anzeigen, was passiert ist. Sie lassen sich genauso wie Klassen selektieren und durch CSS gestalten:
.flip-card[aria-selected="true"] .flip-card-inner {
transform: rotateY(180deg);
}
Die jetzt umgedrehten Karten können mit der Tabtaste erreicht werden, müssen dann aber durch Klick, Enter oder Leertaste ausgewählt werden. Bei einem Klick wird die Karte umgedreht und die Vorderseite wird sichtbar.
Auswertung
Ist dir aufgefallen, dass die Kartensets immer nebeneinander waren? Damit das Spiel ein spannendes Spiel und keine Fleißaufgabe wird, müssen die Karten nun gemischt werden:
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function initializeGame() {
const images = Array.from(gameboard.querySelectorAll("img"));
const cards = [];
images.forEach((img) => {
cards.push({ src: img.src, alt: img.alt });
cards.push({ src: img.src, alt: img.alt });
});
shuffle(cards);
function handleCardClick(card, src) {
if (selectedCards.length >= 2 || card.getAttribute("aria-selected") === "true") {
return;
}
card.setAttribute("aria-selected", "true");
selectedCards.push({ card, src });
if (selectedCards.length === 2) {
const [firstCard, secondCard] = selectedCards;
if (firstCard.src === secondCard.src) {
matchedPairs++;
selectedCards = [];
if (matchedPairs === totalPairs) {
alert("Herzlichen Glückwunsch!", "success");
}
} else {
setTimeout(() => {
firstCard.card.setAttribute("aria-selected", "false");
secondCard.card.setAttribute("aria-selected", "false");
selectedCards = [];
}, 1000);
}
}
}
In der Funktion handleCardClick(card, src)
werden die Karten verglichen, ob die Bilder das gleiche src-Attribut haben.