Gra Memory :)

W dzisiejszym odcinku zajmiemy się stworzeniem prostej gry "memory".
Gra taka polega na odkrywaniu par obrazków.
Do stworzenia naszej aplikacji po raz kolejny wykorzystamy jQuery (musisz ją dodać do strony). Nie jest to konieczne, gdyż poniższy kod bez problemu można napisać w czystym javascript.

Zobacz DEMO naszej gry

Podstawowe założenia gry

Tradycyjnie rozpiszmy zasadę działania naszej gry:

  1. Domyślnie plansza gry jest nieaktywna. Kliknięcie na start aktywuje planszę.
  2. Plansza składa się z x na y kafelków
  3. Kliknięcie na kafelek odsłania go.
  4. Jeżeli drugi kliknięty kafelek jest taki sam jak pierwszy, wtedy oba kafelki zostają usunięte z planszy.
  5. Jeżeli drugi kliknięty kafelek jest różny od pierwszego, wtedy oba kafelki są zasłaniane i zwiększany jest licznik prób.
  6. Gra kończy się z chwilą usunięcia wszystkich kafelków z planszy

Skoro określiliśmy co mamy do zrobienia, przejdźmy do pracy.

Szkielet aplikacji

Szkielet aplikacji będzie bardzo prosty. Sprowadzać się będzie tylko do planszy, punktacji i przycisku startującego grę. Reszta to czysta dynamika napisana w JS.


    <div class="plansza">
        ...tutaj dynamicznie umieścimy kafelki
    </div>

    <div class="punktacja">
    </div>

    <button class="startGame">Rozpocznij grę</button>

Punktację umieszczamy poza planszą, by nie przysłaniała nam kafelków.

Aby widzieć rezultaty nadajmy naszym elementom przykładowe style:


body {
    padding: 10px;
}

.plansza {
    margin:30px auto;
    position: relative;
    background: #fff;
    overflow: hidden;
    width: 550px;
    height: 444px;
    border: 2px solid #ddd;
}

.kafelek {
    position: absolute;
    cursor: pointer;
    float: left;
    width: 100px;
    height: 100px;
    background: #f6da17 url(images/title.png) center center no-repeat;
    margin: 10px;
}

.kafelek:hover {
    background-color:#F7C11A;
}

W tej chwili nasze elementy wyglądają następująco:

Jedyną rzeczą która warta jest uwagi to centrowane przezroczyste tło kafelka, które jest obrazkiem znaku zapytania:

Piszemy silnik gry

Aby "odsłaniać" kafelki zastosujemy pewien trick. Nasz kafelek jak przed chwilą określiliśmy ma domyślne centrowane tło "znaku zapytania".
Po kliknięciu na dany kafelek podmienimy to tło na odpowiedni inny obrazek dzięki czemu uzyskamy efekt "odsłonięcia". Każdy para kafelków na planszy będzie miała swoje tło.

Naszą pracę zaczniemy od stworzenia wymieszanej tablicy numerów kafelków (które określać będą właśnie tła dla danego kafelka), umieszczenia kafelków na planszy, a następnie przypisania im zdarzenia click, które będzie sprawdzać czy wybrana para jest taka sama czy też nie (jak to mają w zwyczaju mawiać Amerykańscy naukowcy).

Zaczynamy!

Na sam początek stwórzmy kilka zmiennych i stałych:


(function() {
    var LICZBA_KAFELKOW = 20;
    var KAFELKI_NA_RZAD = 5;
    var kafelki = [];
    var pobraneKafelki = [];
    var liczbaRuchow = 0;
    var obrazkiKafelkow = [
        'title_1.png',
        'title_2.png',
        'title_3.png',
        'title_4.png',
        'title_5.png',
        'title_6.png',
        'title_7.png',
        'title_8.png',
        'title_9.png',
        'title_10.png'
    ]
})();

Cały kod ukryjemy przed zewnętrznymi skryptami. Stała LICZBA_KAFELKOW określa jak sama nazwa wskazuje liczbę kafelków. Pamiętajmy, że liczba ta jest dzielona na 2, gdyż nasze kafelki występują w parach.
Stala KAFELKI_NA_RZAD określa ile kafelków będziemy pokazywać w rzędzie. Tak więc nasza plansza będzie się składała z LICZBA_KAFELKOW / KAFELKI_NA_RZAD kolumn :).

Nazwy stałych pisane są z dużych liter. Jest to tylko ułatwiająca czytanie kodu tradycja stosowana w różnych językach (której nawiasem mówiąc warto się trzymać)

W tablicy kafelki będziemy przechowywać "typ" poszczególnych kafelków. Typy te będą występowały parami i będą określać po prostu numer obrazka dla danego kafelka.

liczbaRuchow określa nam ile razy użytkownik wykonał ruch. Zwykła zmienna informacyjna :)

Ostatnia zmienna obrazkiKafelkow definiuje obrazki, które będą się pojawiały na kafelkach po kliknięciu na nie. Każda para kafelków będzie posiadała inny obrazek, który określimy na podstawie tablicy kafelki :)

Przykładowe obrazki kafelków:

Metodą jaką napiszemy na początku będzie startGame.


    function startGame() {
        ...
        ...
    }

Pierwszym zadaniem jaki musi zrobić nasza startująca funkcja to czyszczenie wszystkich zmiennych (nasza gra może rozpoczynać się kilka razy) i stworzenie pomieszanej tablicy par numerów dla kafelków:


var plansza = $('.plansza').empty();;

kafelki = [];
pobraneKafelki = [];
moznaBrac = true;
liczbaRuchow = 0;

for (var i=0; i<LICZBA_KAFELKOW; i++) {
    kafelki.push(Math.floor(i/2));
}

for (i=LICZBA_KAFELKOW-1; i>0; i--) {
    var swap = Math.floor(Math.random()*i);
    var tmp = kafelki[i];
    kafelki[i] = kafelki[swap];
    kafelki[swap] = tmp;
}          

Po stworzeniu tablicy z numerami kafelków, możemy na jej podstawie wstawić kafelki na planszę:


    for (i=0; i<LICZBA_KAFELKOW; i++) {
        var tile = $('<div class="kafelek"></div>');
        plansza.append(tile);

        tile.data('cardType',kafelki[i]);
        tile.data('index', i);

        tile.css({
            left : 5+(tile.width()+5)*(i%KAFELKI_NA_RZAD)
        });
        tile.css({
            top : 5+(tile.height()+5)*(Math.floor(i/KAFELKI_NA_RZAD))
        });

        tile.bind('click',function() {
            klikniecieKafelka($(this))
        });
    }

Po kliknięciu na dany kafelek będziemy pobierać numer który został jemu przypisany. Numer taki musimy więc gdzieś przechować. Idealnie do tego celu nadaje się metoda jQuery .data(). Służy ona właśnie do podpinania elementom html dodatkowych informacji. Zasadę jej działania przedstawia poniższy skrypt:


    var $p = $('p');

    $p.data('numer', 'Jestem ważną informacją' );

    console.log( $p.data('numer') ); //wypisze "Jestem ważną informację"

Nasze kafelki pozycjonujemy absolutnie wykorzystując do tego prostą pętlę. Pomyślisz, że wystarczyło by floatować je do lewej strony i były by ładnie ułożone. Jednak takie podejście sprawiło by, że po usunięciu z planszy kafelków, reszta by się przemieszczała na ich miejsce...

Po kliknięciu na dany kafelek odpalamy jego obsługę.
Służy do tego metoda klikniecieKafelka(), którą zaraz napiszemy.

Zasada działania tej metody jest bardzo prosta.
Na samym początku sprawdzamy, czy dany kafelek nie jest już wcześniej kliknięty (czyli czy nie jest wrzucony do tablicy pobraneKafelki). Jeżeli nie jest, a index klikniętego elementu nie występuje już w tablicy (nasza tablica będzie maksymalnie 2 elementowa, więc wystarczy sprawdzić jej 1 element), wtedy możemy go do niej dodać (zapobiegnie to sytuacji, gdy 2x klikniemy ten sam element).
Dodatkowo przy dodawaniu elementu do tablicy ustawiamy mu odpowiednie tło, którego numer pobieramy z "data" danego kafelka.

Następuje wybranie 2 kafelka (if (pobraneKafelki.length==2). Jeżeli "cardType" obydwu elementów tablicy pobraneKafelki jest taki sam, znaczy to, że para kafelków została dopasowana. Odpalamy więc funkcję usunKafelki(). Jeżeli "data" jest różna, znaczy to że kafelki są różne, więc musimy je ukryć na nowo. Służy do tego metoda zresetujKafelki.
Obie metody odpalamy z opóźnieniem 500ms, tak by wybrane kafelki nie ukrywały się od razu :)


function klikniecieKafelka(element) {

        //jeżeli jeszcze nie pobraliśmy 1 elementu
        //lub jeżeli index tego elementu nie istnieje w pobranych...
        if (!pobraneKafelki[0] || (pobraneKafelki[0].data('index') != element.data('index'))) {
            pobraneKafelki.push(element);
            element.css({'background-image' : 'url('+obrazkiKafelkow[element.data('cardType')]+')'})    
        }

        if (pobraneKafelki.length == 2) {
            moznaBrac = false;
            if (pobraneKafelki[0].data('cardType')==pobraneKafelki[1].data('cardType')) {
                window.setTimeout(function() {
                    usunKafelki();
                }, 500);
            } else {
                window.setTimeout(function() {
                    zresetujKafelki();
                }, 500);
            }
            liczbaRuchow++;
        }

    }
}

W postaci takiej jak powyższa nasz skrypt nie będzie działał prawidłowo. Wyobraź sobie sytuację: gracz wybiera 2 kafelki, zostaje odpalona z opóźnieniem funkcja usunKafelki lub zresetujKafelki. Jednak w tym samym czasie gracz może spokojnie klikać resztę kafelków i nasze działania zostają zakłócone.
Aby to naprawić dodamy do naszego skryptu dodatkową zmienną moznaBrac. I nie - nie określa ona podejścia typowego polityka :)


var LICZBA_KAFELKOW = 20;
var KAFELKI_NA_RZAD = 5;
var kafelki = [];
var pobraneKafelki = [];
var liczbaRuchow = 0;
var moznaBrac = true;
    
....
   
function klikniecieKafelka(element) {
    if (moznaBrac) {
        //jeżeli jeszcze nie pobraliśmy 1 elementu
        //lub jeżeli index tego elementu nie istnieje w pobranych...
        if (!pobraneKafelki[0] || (pobraneKafelki[0].data('index') != element.data('index'))) {
            pobraneKafelki.push(element);
            element.css({'background-image' : 'url('+obrazkiKafelkow[element.data('cardType')]+')'})    
        }

        if (pobraneKafelki.length == 2) {
            moznaBrac = false;
            if (pobraneKafelki[0].data('cardType')==pobraneKafelki[1].data('cardType')) {
                window.setTimeout(function() {
                    usunKafelki();
                }, 500);
            } else {
                window.setTimeout(function() {
                    zresetujKafelki();
                }, 500);
            }
            liczbaRuchow++;

            $('.moves').html('Liczba ruchów: ' + liczbaRuchow)
        }
    }
}

Ostatnie funkcje jakie zostały nam do napisania do usunKafelki() i zresetujKafelki().
Jak widać powyżej, jeżeli oba pobrane kafelki mają taki sam "cartType", wtedy odpalamy funkcję usunKafelki.
Kafelki do usunięcia znajdują się teraz w tablicy pobraneKafelki. Wystarczy je więc usunąć, po czym na nowo ustawić tablicę pobraneKafelki. Oczywiście po usunięciu kafelków włączamy na nowo możliwość pobrania nowej pary:


    function usunKafelki() {
        pobraneKafelki[0].fadeOut(function() {
            $(this).remove();
        });
        pobraneKafelki[1].fadeOut(function() {
            $(this).remove();

            moznaBrac = true;
            pobraneKafelki = new Array();
        });
    }

Czemu resetowanie zmiennych i usuwanie obiektów robimy wewnątrz fadeOut()? Ponieważ chcemy te działania przeprowadzić dopiero PO zakończeniu animacji fadeOut. A jak wiemy z jQApi, wystarczy wewnątrz deklaracji fadeOut podać funkcję, by ta została odpalona po zakończeniu animacji zanikania :)

Funckja resetująca kafelki do stanu zakrytego jest jeszcze prostsza. Oba kafelki wracają po prostu do stanu początkowego, po czym tak samo jak powyżej ustawiamy na nowo talicę pobraneKafelki i zmienną moznaBrac.


    function zresetujKafelki() {
        pobraneKafelki[0].css({'background-image':'url(title.png)'})
        pobraneKafelki[1].css({'background-image':'url(title.png)'})
        pobraneKafelki = new Array();
        moznaBrac = true;
    }

Ostatnimi rzeczami którymi się zajmiemy to pokazywanie graczowi liczby ruchów które wykonał oraz zakończenie gry.

Liczenie ruchów to nic trudnego. Przy każdym kliknięciu na kafelek po prostu zwiększamy zmienną, którą dodamy do naszego skryptu:

var LICZBA_KAFELKOW = 20; var KAFELKI_NA_RZAD = 5; var kafelki = []; var pobraneKafelki = []; var moznaBrac = true; var liczbaRuchow = 0; ... function klikniecieKafelka(element) { if (moznaBrac) { ... if (pobraneKafelki.length == 2) { ... liczbaRuchow++; $('.moves').html('Liczba ruchów: ' + liczbaRuchow) } } }

Pamiętajmy też, żeby przy starcie gry wyzerować nasz licznik:


    function startGame() {
        kafelki = [];
        pobraneKafelki = [];
        moznaBrac = true;
        liczbaRuchow = 0;

        ...

        $('.moves').html(liczbaRuchow)
    }

Pozostało nam do wykonania sprawdzenie czy użytkownik nie odkrył wszystkich kafelków i tym samym nie zakończył gry. Wystarczy wprowadzić dodatkową zmienną, w której będziemy przechowywać liczbę odkrytych par. Jeżeli liczba ta będzie >= połowie wszystkich kafelków znaczy to, że gra została zakończona :)
Oczywiście sprawdzenie takie wykonujemy tylko przy usunięciu kafelków z planszy:


    var LICZBA_KAFELKOW = 20;
    var KAFELKI_NA_RZAD = 5;
    var kafelki = [];
    var pobraneKafelki = [];
    var moznaBrac = true;
    var liczbaRuchow = 0;
    var paryKafelkow = 0;

    function usunKafelki() {
        pobraneKafelki[0].fadeOut(function() {
            $(this).remove();
        });
        pobraneKafelki[1].fadeOut(function() {
            $(this).remove();

            paryKafelkow++;
            if (paryKafelkow >= LICZBA_KAFELKOW / 2) {
                alert('gameOver!');
            }
            moznaBrac = true;
            pobraneKafelki = new Array();
        });
    }

Tak jak poprzednio, nie zapomnijmy wyzerować naszej zmiennej przy starcie gry


    function startGame() {
        kafelki = [];
        pobraneKafelki = [];
        moznaBrac = true;
        liczbaRuchow = 0;
        paryKafelkow = 0;

        ...

        $('.moves').html('Liczba ruchów: ' + liczbaRuchow)
    }

Naszą pracę kończymy podpięciem pod przycisk rozpoczęcia gry:


$(document).ready(function() {

    $('.start_game').click(function() {
        startGame();
    });

})

Cały kod

Cały kod naszej aplikacji wygląda teraz tak:

HTML:


    <div class="plansza">

    </div>
    <div class="moves"></div>

    <button class="start_game">Rozpocznij Grę</button>        

CSS:


body {
    padding: 10px;
}

.plansza {
    margin:30px auto;
    position: relative;
    background: #fff;
    overflow: hidden;
    width: 550px;
    height: 444px;
    border: 2px solid #ddd;
}

.kafelek {
    position: absolute;
    cursor: pointer;
    float: left;
    width: 100px;
    height: 100px;
    background: #f6da17 url(images/title.png) center center no-repeat;
    margin: 10px;
}

.kafelek:hover {
    background-color:#F7C11A;
}

Javascript:


(function() {    
    var LICZBA_KAFELKOW = 20;
    var KAFELKI_NA_RZAD = 5;
    var kafelki = [];
    var pobraneKafelki = [];
    var moznaBrac = true;
    var liczbaRuchow = 0;
    var paryKafelkow = 0;
    var obrazkiKafelkow = [
        'title_1.png',
        'title_2.png',
        'title_3.png',
        'title_4.png',
        'title_5.png',
        'title_6.png',
        'title_7.png',
        'title_8.png',
        'title_9.png',
        'title_10.png'
    ];

    function startGame() {
        kafelki = [];
        pobraneKafelki = [];
        moznaBrac = true;
        liczbaRuchow = 0;
        paryKafelkow = 0;

        var plansza = $('.plansza').empty();

        for (var i=0; i<LICZBA_KAFELKOW; i++) {
            kafelki.push(Math.floor(i/2));
        }

        for (i=LICZBA_KAFELKOW-1; i>0; i--) {
            var swap = Math.floor(Math.random()*i);
            var tmp = kafelki[i];
            kafelki[i] = kafelki[swap];
            kafelki[swap] = tmp;
        }

        for (i=0; i<LICZBA_KAFELKOW; i++) {
            var tile = $('<div class="kafelek"></div>');
            plansza.append(tile);
            tile.data('cardType',kafelki[i]);
            tile.data('index', i);
            tile.css({
                left : 5+(tile.width()+5)*(i%KAFELKI_NA_RZAD)
            });
            tile.css({
                top : 5+(tile.height()+5)*(Math.floor(i/KAFELKI_NA_RZAD))
            });
            tile.bind('click',function() {klikniecieKafelka($(this))});
        }
        $('.moves').html(liczbaRuchow);
    }

    function klikniecieKafelka(element) {
        if (moznaBrac) {
            //jeżeli jeszcze nie pobraliśmy 1 elementu
            //lub jeżeli index tego elementu nie istnieje w pobranych...
            if (!pobraneKafelki[0] || (pobraneKafelki[0].data('index') != element.data('index'))) {
                pobraneKafelki.push(element);
                element.css({'background-image' : 'url('+obrazkiKafelkow[element.data('cardType')]+')'})    
            }

            if (pobraneKafelki.length == 2) {
                moznaBrac = false;
                if (pobraneKafelki[0].data('cardType')==pobraneKafelki[1].data('cardType')) {
                    window.setTimeout(function() {
                        usunKafelki();
                    }, 500);
                } else {
                    window.setTimeout(function() {
                        zresetujKafelki();
                    }, 500);
                }
                liczbaRuchow++;
                $('.moves').html(liczbaRuchow)
            }
        }
    }

    function usunKafelki() {
        pobraneKafelki[0].fadeOut(function() {
            $(this).remove();
        });
        pobraneKafelki[1].fadeOut(function() {
            $(this).remove();

            paryKafelkow++;
            if (paryKafelkow >= LICZBA_KAFELKOW / 2) {
                alert('gameOver!');
            }
            moznaBrac = true;
            pobraneKafelki = new Array();
        });
    }

    function zresetujKafelki() {
        pobraneKafelki[0].css({'background-image':'url(title.png)'})
        pobraneKafelki[1].css({'background-image':'url(title.png)'})
        pobraneKafelki = new Array();
        moznaBrac = true;
    }

    $(document).ready(function() {

        $('.start_game').click(function() {
            startGame();
        });

    })
})();    

I na tym kończy się kolejna porcja kodowania. Gotową grę możesz zobaczyć tutaj :)

Jeżeli interesuje Cię nowsza wersja powyższej gry, to pisałem na jej temat w tym artykule.