Dobrze, mamy więc siatkę płytek, które możemy wyświetlać obrazkiem do góry lub na dół. Ale nie mamy jak grać w tą grę. Pozwólcie, że przypomnę zasady gry:
  • Gdy gra się zaczyna, wszystkie pola są odwrócone tak, aby strona z tłem była widoczna.
  • Gracz następnie obraca dwie karty, wybierając je poprzez kliknięcie na nie.
  • Jeśli płytki mają ten sam obrazek, zostają na ekranie obrazkiem do góry. Jeśli nie, powinny zostać odwrócone ponownie po krótkiej chwili.

Obracanie płytek

W tej chwili mamy program, który rysuje siatkę płytek i nic poza tym. Wychodząc stąd jako z punktu wyjścia, będziemy dodawać po kolei różne inne możliwości - program zacznie od rysowania płytek ułożonych obrazkiem do dołu, ale potem będzie rysować płytki, w które kliknął gracz, odwrócone obrazkiem do góry, i tak dalej, i tak dalej, a jak wszystko pójdzie dobrze (trzymamy kciuki), na końcu gracz zobaczy planszę, informującą go o zwycięstwie.
Przenieśmy więc nasz kod do funkcji draw w ProcessingJS. Komputer będzie wywoływał funkcję draw() w czasie działania programu, a płytki będą za każdym razem rysowane zgodnie z tym, czy są w pozycji obrazkiem do góry, czy do dołu:
draw = function() {
    background(255, 255, 255);
    for (var i = 0; i < tiles.length; i++) {
        tiles[i].draw();
    }
};
Obróćmy teraz kilka płytek obrazkiem do góry! By obrócić płytkę, gracz musi na niej kliknąć. By odpowiedzieć na kliknięcie w ProcessingJS, możemy zdefiniować metodę mouseClicked. Jej kod zostanie wykonany przez komputer za każdym razem, gdy myszka zostanie przyciśnięta.
mouseClicked = function() {
  // kod obsługujący kliknięcie
};
Kiedy nasz program dowie się, że gracz gdzieś kliknął, chcemy sprawdzić czy kliknął na płytkę za pomocą mouseX i mouseY. Dodajmy metodę isUnderMouse do płytki Tile, która będzie zwracać true jeśli pozycja x i y myszki znajduje się wewnątrz powierzchni płytki.
Biorąc pod uwagę sposób w jaki rysowaliśmy płytki, współrzędne x i y płytek odpowiadają ich lewym górnym wierzchołkom, więc powinniśmy zwracać true tylko gdy zadane x jest pomiędzy this.x i this.x + this.width, a zadane y jest pomiędzy this.y a this.y + this.width:
Tile.prototype.isUnderMouse = function(x, y) {
  return x >= this.x && x <= this.x + this.width  &&
    y >= this.y && y <= this.y + this.width;
};
Teraz, gdy mamy tą metodą, możemy użyć pętli for w mouseClicked aby sprawdzić, czy któraś z płytek jest pod mouseX i mouseY. Jeżeli tak, nadajemy związanej z tą płytką własnościisFaceUp wartość true:
mouseClicked = function() {
  for (var i = 0; i < tiles.length; i++) {
    if (tiles[i].isUnderMouse(mouseX, mouseY)) {
      tiles[i].isFaceUp = true;
    }
  }
};
Oto jak prezentuje się program na podstawie tego kodu. Kliknijcie na parę różnych płytek i zobaczcie, co się stanie:

Ograniczenie liczby płytek odwracanych w jednym ruchu

Zauważyliście coś? Zaimplementowaliśmy jeden z aspektów gry - gracz ma możliwość odwracania płytek - ale zapomnieliśmy o ważnym ograniczeniu: gracz nie powinien być w stanie obrócić więcej niż dwóch płytek na raz.
Musimy wymyślić sposób na przechowywanie liczby odwróconych płytek. Jednym z łatwiejszych sposobów jest utworzenie globalnej zmiennej numFlipped, którą zwiększalibyśmy za każdym razem, gdy gracz odwraca płytkę. Odwrócenie kolejnej płytki byłoby możliwe tylko wtedy, gdy wartość zmiennej numFlipped byłaby mniejsza od 2 i gdy płytka nie byłaby już odwrócona obrazkiem do góry:
var numFlipped = 0;
mouseClicked = function() {
    for (var i = 0; i < tiles.length; i++) {
        var tile = tiles[i];
        if (tiles.isUnderMouse(mouseX, mouseY)) {
            if (numFlipped < 2 && !tile.isFaceUp) { 
              tile.isFaceUp = true;
              numFlipped++;
            }
        }
    }
};

Odwracanie płytek z opóźnieniem

Dobrze, obracanie dwóch płytek zostało przez nas napisane. Co teraz? Przypomnijmy sobie jeszcze raz zasady gry:
Jeżeli oba pola mają taki sam obrazek, zostają w tym stanie. W przeciwnym wypadku zostają ponownie odwrócone po krótkim odstępie czasu.
Wpierw zaimplementujemy drugą część, która automatycznie odwraca płytki z powrotem, ponieważ ciężko będzie testować pierwszą część, jeżeli nie możemy łatwo sprawdzać nowych par.
Wiemy, jak obracać płytki z powrotem za pomocą nadawania własności turnFaceDown wartości false, ale jak zrobić to z opóźnieniem, po pewnym okresie czasu? Każdy język i środowisko ma inne podejście do zagadnienia opóźniania wykonania kodu, musimy więc sprawdzić, jak zrobić to w ProcessingJS. Potrzebujemy pewnego sposobu do przechowywania upływu czasu - w celu sprawdzania, czy minął już wyznaczony czas - i wywoływania kodu po tym czasie. Oto proponowane przeze mnie rozwiązanie:
  • Tworzymy zmienną globalną delayStartFC, początkowo posiadającą wartość null.
  • W funkcji mouseClicked, zaraz po odwróceniu drugiej płytki, przechowujemy wartość frameCount w delayStartFC. Ta zmienna informuje nas ile klatek minęło odkąd program został uruchomiony i jest jedną z metod sprawdzania czasu w naszych programach.
  • W funkcji draw sprawdzamy, czy nowa wartość frameCount jest znacząco większa od starej wartości, a jeżeli tak, obracamy wszystkie płytki obrazkiem do dolu i ustawiamy wartość numFlipped na 0. Oprócz tego restartujemy wartość delayStartFC na null.
To całkiem dobre rozwiązanie, które nie wymaga zbyt dużo kodu do zaimplementowania. W celu zwiększenia wydajności, możemy użyć funkcji loop i noLoop aby upewnić się, że kod draw będzie wykonywany tylko wtedy, gdy opóźnienie ma miejsce. Oto całość:
var numFlipped = 0;
var delayStartFC = null;

mouseClicked = function() {
  for (var i = 0; i < tiles.length; i++) {
    var tile = tiles[i];
    if (tile.isUnderMouse(mouseX, mouseY)) {
      if (numFlipped < 2 && !tile.isFaceUp) {
        tile.isFaceUp = true;
        numFlipped++;
        if (numFlipped === 2) {
          delayStartFC = frameCount;
        }
        loop();
      } 
    }
  }
};

draw = function() {
  if (delayStartFC &&
     (frameCount - delayStartFC) > 30) {
    for (var i = 0; i < tiles.length; i++) {
      tiles[i].isFaceUp = false;
    }
    numFlipped = 0;
    delayStartFC = null;
    noLoop();
  }

  background(255, 255, 255);
  for (var i = 0; i < tiles.length; i++) {
    tiles[i].draw();
  }
};
Obróć kilka płytek poniżej - nieźle to wygląda, gdy same odwracają się z powrotem tłem do góry, prawda? W zrozumieniu tego mechanizmu pomogą Ci male eksperymenty, na przykład spróbuj zmienić długość opóźnienia, albo liczbę płytek, które muszą być odwrócone zanim opóźnienie zaczyna działać.

Sprawdzanie, czy obrazki na odwróconych płytkach są takie same

Jeśli udało Ci się obrócić dwie płytki z tym samym obrazkiem, było Ci pewnie smutno gdy obróciły się z powrotem tłem do góry, bo przecież, hej, przecież to były dwie płytki z tym samym obrazkiem i nie powinny być obrócone z powrotem! Najwyższy czas, aby nauczyć nasz program stosować tę regułę:
Dwie odwrócone płytki, na których są takie same obrazki, pozostają odwrócone na zawsze.
Oznacza to, że powinniśmy sprawdzić zgodność po odwróceniu pary płytek, przed ustawieniem opóźnienia. Na poziomie pseudokodu, może wyglądać to tak:
Jeżeli dwie płytki są odwrócone:
  jeżeli pierwsza płytka posiada ten sam obrazek, co druga:
    zostaw obie płytki odwrócone
Tak więc sprawdzamy czy dwie płytki są odwrócone (numFlipped === 2), a jak sprawdzimy czy na obu płytkach jest ten sam obrazek? Na początku musimy w jakiś sposób dostać się do tych dwóch odkrytych płytek. Ale jak mamy je znaleźć?
Moglibyśmy iterować po naszej tablicy, znaleźć wszystkie płytki które mają wartość isFaceUp równą true i przechowywać je w tablicy.
Jest jednak prostszy sposób: Przechowujmy nasze odwrócone płytki w tablicy, do której będziemy mieć łatwy dostęp. W ten sposób nie będziemy musieli za każdym razem iterować całej naszej tablicy tiles za każdym razem, kiedy gracz przekręci płytkę.
Najpierw zastąpmy numFlipped tablicą, a później użyjmy flippedTiles.length wszędzie, gdzie do tej pory używaliśmy numFlipped. Nasza funkcja mouseClick będzie teraz wyglądać tak:
var flippedTiles = [];
var delayStartFC = null;

mouseClicked = function() {
  for (var i = 0; i < tiles.length; i++) {
    var tile = tiles[i];
    if (tile.isUnderMouse(mouseX, mouseY)) {
      if (flippedTiles.length < 2 && !tile.isFaceUp) {
        tile.isFaceUp = true;
        flippedTiles.push(tile);
        if (flippedTiles.length === 2) {
          delayStartFC = frameCount;
          loop();
        }
      } 
    }
  }
};
Teraz musimy wymyślić, jak sprawdzić, czy dwie płytki w tablicy flippedTiles mają ten sam obrazek. Cóż, czym jest własność twarz? To obiekt - rzeczywiście, obrazek pasujących płytek powinien być dokładnie tym samym obiektem, gdyż zmienna wskazuje na to samo miejsce w pamięci komputera w obu przypadkach. Wynika to z faktu, że stworzyliśmy każdy obiekt obrazka tylko raz (jak z getImage("avatars/old-spice-man") po czym dodaliśmy ten sam obiekt dwa razy do tablicy obrazków:
var face = possibleFaces[randomInd];
selected.push(face);
selected.push(face);
Przynajmniej w przypadku JavaScriptu operator równości zwraca wartość true jeżeli jest użyty na dwóch zmiennych wskazujących na obiekt, a obie te zmienne wskazują na ten sam obiekt w pamięci. To oznacza, że sprawdzenie jest bardzo proste - użyjemy operatora równości na właściwości face każdej płytki:
if (flippedTiles[0].face === flippedTiles[1].face) {
  ...
}
Teraz skoro wiemy, że płytki mają te same obrazki, chcemy żeby pozostały odkryte. W tej chwili zostaną one odwrócone po wyznaczonym czasie. Moglibyśmy po prostu nie wywoływać animacji odwracania, ale pamiętaj, że wywoła się ona sama po kolejnych ruchach - a więc nie jest to sposób, na którym możemy polegać.
Zamiast tego musimy mieć jakiś mechanizm, dzięki któremu zapamiętamy "hej, kiedy będziemy odwracać płytki, omiń te konkretne". Brzmi jak dobre miejsce na użycie wartości logicznej. Dodajmy wartość isMatch do konstruktora Tile, a potem ustawiajmy isMatch na true tylko wewnątrz tego warunku if.
if (flippedTiles[0].face === flippedTiles[1].face) {
  flippedTiles[0].isMatch = true;
  flippedTiles[1].isMatch = true;
}
Teraz możemy wykorzystać tę właściwość, żeby zdecydować, że nie chcemy przekręcać tych zmiennych po zakończeniu opóźnienia.
for (var i = 0; i < tiles.length; i++) {
  var tile = tiles[i];
  if (!tile.isMatch) {
    tile.isFaceUp = false;
  }
}
Pobaw się poniższym programem! Jeżeli teraz znajdziesz dwie pasujące płytki w poniższym przykładzie, powinny one zostać odwrócone po opóźnieniu (oraz po kolejnych rundach):
Ładowanie