JavaScript/Tutorials/Spiele/Memo-Quiz

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
Memory Card ist ein einfaches Kartenspiel, bei dem alle Karten mit der Rückseite nach oben auf eine Fläche gelegt werden, und bei jedem Zug zwei Karten umgedreht werden. Das Ziel dieses Spiels ist es, alle Kartenpaare zusammenzubringen.

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.

Vorüberlegungen

Spielidee

  1. Das Script erzeugt
    • für jedes Bild eine Spielkarte mit einer Rückseite,
    • dupliziert diese
    • mischt und verteilt sie auf dem Spielfeld
  2. 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.

Bilder - noch ohne Funktion
  <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:

Buttons mit Bildern - klick- und mit der Tastatur antabbar ansehen …
  <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-Cards 25 Jahre SELFHTML ansehen …
.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:

Karten erzeugen ansehen …
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:

Karten auswählen ansehen …
      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:

Karten mischen ansehen …
  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);


ausgewählte Karten auswerten ansehen …
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.