Memory w javascript

Dostałem kilka maili z prośbami, by pokazać sposób jak można zapisywać wyniki w naszej Memorce.

Pomyślałem sobie, że przy okazji nauki zapisu i odczytu, moglibyśmy zająć się dopracowaniem naszej gry.
Dotychczas bardziej przypomina ona średniej jakości skrypt niż pełnoprawną aplikację. Kod gry nie jest ładny, a i logikę jej działania przydało by się mocno dopieścić. Dodatkowym założeniem będzie dodanie naszej aplikacji responsywności.

Założenia wstępne

W chwili obecnej po wejściu do naszej aplikacji pojawia się pole z pustą planszą i guzik start. Jako prosty pojedynczy skrypt może być, ale pełnoprawna gra powinna zachowywać się nieco inaczej.
Po wejściu do aplikacji powinien przywitać nas ekran początkowy który będzie zawierał jakieś menu. Po rozpoczęciu gry powinniśmy móc podać nazwę gracza, po czym byśmy przechodzili do gry właściwej. Po zakończeniu gry powinna się nam ukazywać tabela z wynikami, a potem wszystko zaczynało by się od początku. Tak to mniej więcej powinno wyglądać, i takie właśnie będą nasze założenia.

Jak więc możesz wywnioskować z powyższego opisu, nasza gra powinna się składać z kilku widoków, które będziemy odpowiednio serwować graczowi. Czasami to będzie widom z menu, czasami z grą, a czasami z czymś innym (właściwie każda gra działa w ten sposób!).
Wszystko oczywiście powinno się dziać dynamicznie bez jakiś przeładowań i podobnych utrudnień dla gracza.

Zanim jednak przejdziemy do kodowania, musimy sobie sprawić krótką lekcję odnośnie lepszego pisania skryptów.

Zabezpieczenie skryptu

Gdybyś spojrzał na poprzedni skrypt naszej gry, zauważył byś tam kilkanaście pojedynczych funkcji i zmiennych. Wszystko fajnie gdy nasz skrypt jest tylko jeden na stronie. Co się jednak stanie, gdy do strony dołączymy jakąś inną biliotekę, która będzie miała swoje własne funkcje o takiej samej nazwie jak nasze? To nas zaboli. I to bardzo. Dlatego właśnie nasz kod powinniśmy zabezpieczyć przed takimi rzeczami. Zastosujemy do tego pewien znany w javascript wzór:

(function(){
    //tutaj wyląduje nasz zabezpieczony cały skrypt np
    x = 0;
    init = function() {
       ...
    }
    init();
})();

Powyższy kod składa się z samo wywołującej się funkcji, której kod jest niedostępy na zewnętrznego środowiska.
Więcej na temat tego wzoru możesz poczytać tutaj: http://markdalgleish.com/2011/03/self-executing-anonymous-functions/.
Dzięki takiemu rozwiązaniu wszystkie zmienne i nazwy funkcji z naszego skryptu będą dostępne tylko dla siebie samych, a dostęp do nich z zewnątrz będzie niemożliwy.

Możemy też zastosować tutaj wzór modułowy http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript, który wcale tak bardzo się nie różni od poprzedniego, a udostępnia na zewnątrz jakieś wybrane przez nas rzeczy:

var memory = (function(){
    //metody i właściwości prywatne np
    x = 0;
    init = function() {
       ...
    }

    //metody i właściwości publiczne.
    return {
      init : init
    }
})();

memory.init();
console.log(memory.x) //undefined - bo nie mamy dostępu

W powyższym przykładzie z zewnątrz mamy dostęp tylko do pojedynczej metody init naszej gry. Cała reszta jest zamknięta w szczelnych schowku.

Jest to bardzo przyjemny sposób organizowania naszego kodu. W naszym przypadku skorzystamy jednak z poprzedniego wzoru, bo na zewnątrz nie musimy nic udostępniać.

Dobrym pomysłem jest stosować przedrostek _ przy nazwach metody i właściwości prywatnych. To bardzo często stosowana metoda. My dla czytelności kodu sobie ją odpuścimy. U nas wszystko jest prywatne :).

Przygotowanie widoków gry

Jak wspomniałem na początku, nasza gra będzie składała się z kolejnych widoków.

memory-stages

Każdy widok naszej gry to oddzielny div o klasie .slide-…. Domyślnie taki widok powinien być niewidoczny dla gracza, a pokazywać go powinniśmy w zależności od sytuacji.

<!-- plansza menu poczatkowego -->
<div class="slide-start" id="stageStart">                        
    ...kod widoku z menu...
</div>  

<!-- plansza z pobieraniem nazwy gracza -->
<div class="slide-player-name" id="stagePlayerName">
    ...kod widoku z zapytaniem o ksywkę...
</div>

<!-- plansza z gra -->
<div class="slide-game" id="stageGame">
    ...kod widoku z plansza i punktacją...
</div>

<!-- tabela z wynikami -->
<div class="slide-highscore" id="stageHighscore">
    ...kod widoku z planszą wyników...
</div>

Stylowanie dla takich widoków jest bardzo proste:

/* game stage (slides) */
[class^=slide-] {
    z-index:1;
    position: absolute;
    left:0;
    top:-300%; /* ukrycie poprzez wysunięcie diva w górę */
    width:100%;
    min-height: 100%;
    background:#fff;    
    padding:2em;
    -webkit-transition: 0.2s;
    -moz-transition: 0.2s;
    -ms-transition: 0.2s;
    -o-transition: 0.2s;
    transition: 0.2s;
}
[class^=slide-].show {
    z-index:2;
    top:0;
}

Zauważyłeś jaką zastosowaliśmy sztuczkę? Domyślnie wszystkie divy których klasa rozpoczyna się od .slide- są wysunięte o 300% poza ekran (w górę ku niebu!).

Gdy do takiego diva dodamy klasę .show, wtedy dzięki transition widok wjedzie nam na ekran (więcej o tej technice przeczytasz http://domanart.pl/transition-css3-i-slider/).
Ukrywanie i pokazywanie widoków wcale nie musi wyglądać tak jak my to zrobiliśmy. Moglibyśmy np. ukrywać widoki poprzez visibility:hidden i opacity:0, a następnie dla klasy .show przywracać widoczność poprzez visiblity:visible i opacity:1. Moglibyśmy też wysuwać widok w lewo czy prawo, lub zastosować dowolną z setek transformacji CSS3. Możliwości są tysiące, ale pozostańmy na razie przy „próbach zdobycia nieba”.

Żeby pokazywanie widoków było dla nas wygodne, napiszmy do tego odpowiednią funkcję (pamiętaj, że wrzucamy ją do wnętrza naszego powyższego wzoru):

showStage = function(stage) {
    $('[class^=slide-]').removeClass('show'); //ukrywamy wszystkie widoki
    $('#'+stage).addClass('show'); //pokazujemy widok o danym ID
}

//i przykladowe późniejsze wywolanie
showStage('stageHighscore')

Za pomocą jQuery pobieramy wszystkie widoki, których klasa rozpoczyna się od .slide, usówamy im klasę .show, a następnie dodajemy ją tylko do wybranego widoku. Tutaj też można by zastosować inne podejście. Np wrzucić widoki do tablicy z widokami i pokazywać poprzez użycie indeksu. Kto co lubi.

Zauważyłeś możę, że każdy nasz widok ma zarówno klasę jak i ID? To dobra praktyka, która pomaga w czytaniu kodu. Wszędzie tam gdzie nadajemy elementowi jakiś wygląd stosujemy klasy. Wszędzie tam, gdzie elementu będziemy używać w skyptach stosujemy ID. Osobiście nie styluję elementów za pomocą ID i właśnie takie podejscie ci polecam. Zyski na szybkości są niezauważalne, a czytelność kodu CSS poprawia się znacznie (szczególnie gdy lubisz pisać jednolinijkowce). Dodatkowo stylowanie za pomocą samych klas jest o wiele bardziej użyteczne. Za tydzień zachce ci się nadać podobny wygląd na innego elementu? Użyjesz tej samej klasy.

Po wejściu do naszej aplikacji pierwszy widok powinien być od razu widoczny. Nie jest to problemem, ponieważ każdy z widoków ma swoją klasę. Dzięki temu możemy im nadawać indywidualny widok oraz domyślnie nadawać im widoczność:

[class^=slide-] {
    z-index:1;
    position: absolute;
    left:0;
    top:-300%;
    width:100%;
    min-height: 100%;
    background:#fff;    
    padding:2em;
    -webkit-transition: 0.2s;
    -moz-transition: 0.2s;
    -ms-transition: 0.2s;
    -o-transition: 0.2s;
    transition: 0.2s;
}
[class^=slide-].show {
    z-index:2;
    top:0;
}
.slide-start {
    top:0; /* widoczny od początku */
    background: #fff;
}
.slide-player-name {
    background: #fff; /* u mnie każdy widok ma takie samo tło, ale czemu by tego nie upiększyć? */
}
.slide-game {
    background: #fff;
}    
.slide-highscore {
    background: #fff;
}

Po przygotowaniu ogólnej logiki działania widoków zajmijmy się kolejno poszczególnymi widokami.

W chwili obecnej widoki domyślnie są ukryte (poza pierwszym). Praca na ukrytych rzeczach nie jest najprzyjemniejsza, dlatego w ramach testów będziemy z palca kolejno do każdego dodawać klasę .show. Dodatkowo zakomentujmy na chwile kod pokazujący domyślnie pierwszy widok.

.slide-start {
    /* top:0;  widoczny od początku */
    background: #fff;
}

Widok startowy – początkowe menu

Do pierwszego widoku dodajemy klasę .show oraz wstawiamy tam menu:

<!-- plansza menu poczatkowego -->
<div class="slide-start show" id="stageStart">                        
    <div class="start-menu">
        <a href="#slideGame" class="start-game button" id="startGame">Start</a>
        <a href="#slideHighscore" class="show-highscore button" id="showHighscore">Pokaż wyniki</a>
    </div>
</div>

Nasze menu to dwa linki. Tak naprawdę nie ma znaczenia czy będą to linki czy odpowiednio ostylowane spany, ponieważ kliknięcie i tak będzie obsługiwane tylko za pomocą skryptów. Ja używałem linków, które niby prowadzą do odpowiednich divów (#), ale bez działającego skryptu takie przeniesienie i tak nie ma sensu. Jak chcesz je zmienić na spany – droga otwarta. Tutaj znaczenie ma klasa, bo to za jej pomocą nadajemy wygląd.
Tradycyjnie dla większości elementów zastosowaliśmy ID i klasy. Linki dostały wspólną klasę .button, która nada im jakiś atrakcyjny wygląd.

.start-menu {
    max-width:600px;
    margin:10px auto;
    background:#eee;
    padding:2em;
    border-radius:10px;
}
/* guziorki */
.button {
    cursor: pointer;
    max-width:500px;
    width:100%;
    border-radius:6px;
    font:bold 30px/80px 'Alegreya SC', sans-serif;
    text-decoration: none;
    text-align: center;
    color:#fff;
    text-shadow:0 0 1px rgba(255,255,255,0.9);
    background: #ff3019;
    background: -moz-linear-gradient(top,  #ff3019 0%, #cf0404 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff3019), color-stop(100%,#cf0404));
    background: -webkit-linear-gradient(top,  #ff3019 0%,#cf0404 100%);
    background: -o-linear-gradient(top,  #ff3019 0%,#cf0404 100%);
    background: -ms-linear-gradient(top,  #ff3019 0%,#cf0404 100%);
    background: linear-gradient(to bottom,  #ff3019 0%,#cf0404 100%);
    display: block;
    margin:15px auto;
    box-shadow:inset 0 30px 0 rgba(255,255,255,0.1);
    border:2px solid #fff;
}
.button:hover, .button:focus {
    text-shadow:0 0 2px rgba(255,255,255,0.9);
    background: #f1e767;
    background: -moz-linear-gradient(top,  #f1e767 0%, #feb645 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f1e767), color-stop(100%,#feb645));
    background: -webkit-linear-gradient(top,  #f1e767 0%,#feb645 100%);
    background: -o-linear-gradient(top,  #f1e767 0%,#feb645 100%);
    background: -ms-linear-gradient(top,  #f1e767 0%,#feb645 100%);
    background: linear-gradient(to bottom,  #f1e767 0%,#feb645 100%);
    box-shadow:inset 0 2px 4px rgba(0,0,0,0.2);
}

Panie! Tak to my się nie bawimy. Za dużo tego stylowania.
Najwięcej powyżej zajmują style gradientu. Czytelnicy tego bloga wiedzą, że do takich rzeczy używamy edytora ze strony http://www.colorzilla.com/gradient-editor/. Ja osobiście z takich gradientów wyrzucam linijkę z filter która jest hackiem dla IE8. Zauważyłem, że są z nią problemy gdy taki filtrowy gradient chcemy potem nadając elementowi dodatkową klasę z innym tłem. Użytkownicy IE8 nie obrażą się gdy zobaczą jednokolorowe przyciski.

Do naszych guzików podepnijmy zdarzenia, które będą wywoływały odpowiednie widoki:

(function(){
    showStage = function(stage) {
        $('[class^=slide-]').removeClass('show');
        $('#'+stage).addClass('show');
    }    

    showPlayerName = function() {
        //...przed rozpoczeciem gry gracz musi podać ksywkę
    }
    showHighScore = function() {
        //...pokazujemy widok z tabela wynikow
    }

    bindEvents = function() {        
        $('#startGame').on('click', function(e) {
            e.preventDefault();
            showPlayerName();
        });
        $('#showHighscore').on('click', function(e) {
            e.preventDefault();
            showHighScore();
        })
    }    

    init = function() {
        $(function() { //dopiero po wczytaniu DOM
            bindEvents();
        });
    }

    init();
})();

Przed rozpoczęciem gry właściwej, gracz musi podać swoje imię lub ksywkę. Musimy mu więc pokazać kolejny widok.

Widok z pobraniem nazwy gracza

Kod tego widoku ma postać:

<!-- plansza z pobieraniem nazwy gracza -->
<div class="slide-player-name" id="stagePlayerName">
    <div class="player-name-box">
        <label for="playerName">Nazwa gracza:</label>
        <input type="text" id="playerName" class="player-name" maxlength="10" />
        <div class="player-name-error">Musisz wpisać nazwę gracza</div>
        <button class="check-name button" id="checkName">Rozpocznij grę</button>
    </div>
</div>

Mamy tutaj pole do wprowadzania ksywki o nazwe #playerName. Po kliknięciu w guzik #checkName sprawdzimy czy pole zostało wypełnione. Jeżeli nie, to pokazujemy błąd (klasa .player-name-error). Jeżeli pole zostało wypełnione, pokażemy widok z grą właściwą. Zanim to zrobimy napiszmy funkcje dla sprawdzania:

showPlayerName = function() {
    showStage('stagePlayerName');

    $('#checkName').on('click', function() {
        if ($('#playerName').val()!='') {
            $('.player-name-box').removeClass('error');
            startGame();
            showStage('stageGame');                
        } else {
            $('.player-name-box').addClass('error');
            return false;
        }
    })                        
}

Zauważ, że klasę .error (która odpowiada za pokazanie błędu) nie dodałem bezpośrednio na komunikat błędu, a na zewnętrzny div .slide-player-name. Dzięki temu o wiele łatwiej będzie mi zmienić wygląd komunikatu błędu, pola i innych elementów wewnątrz tego boxa gdy zajdzie taka potrzeba.

Stylowanie powyższego widoku ma postać:

/* player name box */
.player-name-box {
    max-width:500px;
    width:80%;
    margin:1em auto;
}
.player-name-box label {
    font-size:2em;
    text-align:center;
    margin:0 auto;
    color:#333;
    display: block;        
    line-height: 1.5em;
}
.player-name-box .player-name {
    width:100%;
    height:90px;    
    padding:0 15px;
    font:bold 30px 'Alegreya', sans-serif;        
    text-align: center;
    background: #eee;
    color:#333;
    border:0;
    display: block;
    border-radius:5px;
    border:1px solid #fff;
    box-shadow:inset 0 2px 3px rgba(0,0,0,0.2);
}
/* 
Gdy ustawiamy line-height w inpucie, wtedy w chromie zajmuje on całą wysokość inputa, co wygląda bardzo kiepsko. W IE natomiast bez line-height kursor jest ustwiony w złym miejcu. Rozwiązaniem jest nadanie
line-height dla IE < 9, natomiast dla reszty pominięcie tej właściwości.
 */
.lt-ie9 .player-name-box .player-name {
    line-height:90px;
}
.player-name-box .player-name:focus {    
    box-shadow:inset 0 0 10px #2996f2;
    border:1px solid #2996f2;
    background: #fff;
    outline:none;
}
.player-name-box .player-name-error {
    font:bold 25px/32px 'Alegreya', sans-serif;
    background: url(error.png) no-repeat;
    padding-left:40px;
    line-height: 32px;
    color:#D80D0D;
    margin:5px auto;
    display: none; /* domyślnie error jest ukryty */
}
.player-name-box.error .player-name-error {
    display: block; /* jak imię nie jest wpisane, dodajemy do boxa klasę .error która pokaże nam bład */
}

Pokazywaniem planszy z wynikami zajmiemy się później. Zanotować na kartce, a tym czasem przechodzimy do rozpoczęcia gry.

Widok gry właściwej

Tak samo jak poprzednio działania rozpoczynamy od stworzenia widoku z planszą gry:

<!-- plansza z gra -->
<div class="slide-game" id="stageGame">
    <div class="game-board" id="gameBoard"></div>
    <div class="game-moves-box">
        <strong>Liczba ruchów:</strong>         
        <div class="game-moves" id="gameMoves"></div>
    </div>
</div>

Mamy tu właściwie dwa elementy. Plansza na klocki oraz element z liczbą ruchów.
Stylowanie liczby ruchów jest całkiem proste. W końcu to tylko zwykły tekst:

/* game moves box */
.game-moves-box {
    width:100%;
    margin:10px auto;    
}
.game-moves-box strong {
    font:bold 1.5em/1em 'Alegreya', sans-serif;
    color:#333;
    text-align: center;
    display: block;
}
.game-moves-box .game-moves {
    border-radius:10px;
    height:30px;
    max-width:300px;
    font:bold 2em/30px 'Alegreya', sans-serif;
    text-align: center;
    background: #fff;
    margin:10px auto;    
}

Stylowaniem samej planszy też jest całkiem łatwe:

/* game board */
.game-board {
    position: relative;    
    overflow: hidden;
    max-width: 600px;
    height:400px;
    width:80%;
    margin:0 auto;   
    border-radius:10px;
}

Tak samo jak w poprzedniej wersji będziemy potrzebowali kilku zmiennych.
Ps. Tym razem użyjemy angielskiego w nazwach zmiennych :)

$(function() { //super początek naszego skryptu

    var TILES_COUNT = 20; //5x4 stała określająca ilość kafelków na planszy.
    var tiles = []; //tablica z wygenerowanymi numerami kafelków
    var clickedTiles = []; //kliknięte kafelki (max 2 a potem czyścimy)
    var canGet = true; //czy aktualnie można kliknąć
    var movesCount = 0; //liczba ruchów gracza
    var tilesPair = 0; //sparowane kafelki. Maksymalnie 2x mniej niż TILES_COUNT

    startGame = function() {
        tiles = [];
        clickedTiles = [];
        canGet = true;
        movesCount = 0;
        tilesPair = 0;

        var $gameBoard = $('#gameBoard').empty(); //czyścimy planszę z grą

        //ustawiamy tablicę z numerami kafelków
        for (var i=0; i<TILES_COUNT; i++) { 
            tiles.push(Math.floor(i/2));
        }

        //mieszamy tablicę z numerami kafelków
        for (i=TILES_COUNT-1; i>0; i--) { 
            var swap = Math.floor(Math.random()*i);
            var tmp = tiles[i];
            tiles[i] = tiles[swap];
            tiles[swap] = tmp;
        }

        //generujemy kafelki i wrzucamy je na planszę
        for (i=0; i<TILES_COUNT; i++) {            
            var $cell = $('<div class="cell"></div>');
            var $tile = $('<div class="tile"><span class="avers"></span><span class="revers"></span></div>');
            $tile.addClass('card-type-'+tiles[i]); 
            $tile.data('cardType', tiles[i])
            $tile.data('index', i);

            $cell.append($tile);
            $gameBoard.append($cell);                               
        }
        $gameBoard.find('.cell .tile').on('click', function() {
            tileClicked($(this)); //kafelek został kliknięty
        });     
    }

    //...reszta kodu który pisaliśmy powyżej
})();

Rozpoczęcie gry może odbyć się kilka razy (po zakończeniu gramy jeszcze raz), dlatego za każdym razem musimy resetować wszystkie zmienne.
Na początku funkcji czyścimy planszę z grą, a następnie generujemy tablicę z numerami kafelków. Te numery będą określały rodzaj kafelka. Aby memorka miała sens musimy je wymieszać.

Następnie na podstawie tej tablicy generujemy kod kafelków oraz wrzucamy je na planszę (append). Do każdego kafelka podpinamy zdarzenie, które będzie odsłaniać kafelek.
W stosunku do poprzedniej wersji nastąpiła spora zmiana w sposobie generowania planszy. Poprzednio położenie kafelków wyliczaliśmy na sztywno za pomocą zmiennej „i”, na podstawie której wyliczaliśmy położenie każdego z kafelków.

Tym razem plansza składa się z pustych komórek .cell. Ich położenie określamy za pomocą stylowania (inline-block, width na 1/5 szerokości itp). Do tych pustych komórek wrzucamy kafelki, które będziemy odsłaniać lub zakrywać. Dzięki takiemu podejściu nie ma nic na sztywno, a za ułożenie planszy odpowiada stylowanie a nie jakieś skryptowe wyliczenia (to dobre dla dzieci).

Najciekawszym momentem powyższej funkcji są dwie liniki:

var $cell = $('<div class="cell"></div>');
var $tile = $('<div class="tile"><span class="avers"></span><span class="revers"></span></div>');

Pierwsza tworzy pustą komórkę. To już wiemy. Druga tworzy sam kafelek. Jak widzicie taki kafelek zawiera w sobie aż dwa elementy. Czemu aż tyle? Dzięki temu mamy większe możliwości zmiany wyglądu, lub animacji kafelka. W naszym przypadku domyślnie widoczny będzie .revers czyli tył kafelka, a po kliknięciu nastąpi animacja, która zakryje .revers a pokaże .avers (przód kafelka). Całą zamianę opiszemy za pomocą stylowania wykorzystując do tego animacje CSS3. Nic a nic skryptów! Dzięki temu znowu odłączamy logikę od wyglądu.

Możemy teraz nadać wygląd zarówno komórkom planszy, jak i samym kafelkom:

/* komórka na planszy */
.game-board .cell {        
    width:19%; 
    margin:0.5%;
    height:23%;
    vertical-align: top;
    display: inline-block;
    position: relative;
}

/* kafelek */
.game-board .tile {
    width:100%;
    height:100%;
    cursor: pointer;
    position: relative;
}
/* w kafelku są dwa spany - avers i revers  - poniżej wspólne właściwości */
.game-board .tile span {
    width:100%;
    height:100%;
    display: block;
    position:absolute;
    top:0; 
    left:0;
}
.game-board .tile .avers {        
    border:1px solid #C1C5D7;
    box-shadow:0 0 3px rgba(0,0,0,0.2), inset 0 40px 10px rgba(0,0,0,0.1), inset 0 0 2px 1px rgba(0,0,0,0.4);    
    z-index:3;    
    -webkit-transition: 0.2s;
    -moz-transition: 0.2s;
    -ms-transition: 0.2s;
    -o-transition: 0.2s;
    transition: 0.2s;    
    -webkit-transform:scaleX(0);
    -moz-transform:scaleX(0);
    transform:scaleX(0);
}
.game-board .tile .revers {
    border:1px solid #C1C5D7;
    background: #ffffff;
    background: -moz-linear-gradient(top,  #ffffff 0%, #f6f6f6 47%, #ededed 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(47%,#f6f6f6), color-stop(100%,#ededed));
    background: -webkit-linear-gradient(top,  #ffffff 0%,#f6f6f6 47%,#ededed 100%);
    background: -o-linear-gradient(top,  #ffffff 0%,#f6f6f6 47%,#ededed 100%);
    background: -ms-linear-gradient(top,  #ffffff 0%,#f6f6f6 47%,#ededed 100%);
    background: linear-gradient(to bottom,  #ffffff 0%,#f6f6f6 47%,#ededed 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ededed',GradientType=0 );
    z-index:2;
    display: block;
    box-shadow:0 0 3px rgba(0,0,0,0.2);
    -webkit-transition: 0.2s 0.2s;
    -moz-transition: 0.2s 0.2s;
    -ms-transition: 0.2s 0.2s;
    -o-transition: 0.2s 0.2s;
    transition: 0.2s 0.2s; 
    -webkit-transform:scaleX(1);
    -moz-transform:scaleX(1);
    transform:scaleX(1);
}
/* zasłonięty kafelek będzie miał dodatkowy znak ? na sobie */
.game-board .tile .revers::before {
    content:'?';
    color:#C1C5D7;
    width:100%;
    text-shadow:0 0 2px #C1C5D7;
    height: 100%;
    position: absolute;
    top:0;
    left:0;
    text-align: center;
    font-size:3em;
    line-height:2em;
    -webkit-transition: 0.2s;
    -moz-transition: 0.2s;
    -ms-transition: 0.2s;
    -o-transition: 0.2s;
    transition: 0.2s;
}
.game-board .tile:hover .revers::before {
    opacity:0.3;
}
/* show */
.game-board .tile.show .avers { 
    -webkit-transition: 0.2s 0.2s;
    -moz-transition: 0.2s 0.2s;
    -ms-transition: 0.2s 0.2s;
    -o-transition: 0.2s 0.2s;
    transition: 0.2s 0.2s;    
    -webkit-transform:scaleX(1);
    -moz-transform:scaleX(1);    
    transform:scaleX(1);
} 
.game-board .tile.show .revers {
    -webkit-transition: 0.2s;
    -moz-transition: 0.2s;
    -ms-transition: 0.2s;
    -o-transition: 0.2s;
    transition: 0.2s;
    -webkit-transform:scaleX(0);
    -moz-transform:scaleX(0);    
    transform:scaleX(0);
}  
/* ie < 9 fix */
.lt-ie9 .game-board .tile .avers {display: none;}
.lt-ie9 .game-board .tile .revers {display: block;}
.lt-ie9 .game-board .tile.show .avers {display: block;}
.lt-ie9 .game-board .tile.show .revers {display: none;}

/* indywidualny wygląd każdej grupy kafelków */
.game-board .card-type-0 .avers {background:url(title_1.png) center center no-repeat;}
.game-board .card-type-1 .avers {background:url(title_2.png) center center no-repeat;}
.game-board .card-type-2 .avers {background:url(title_3.png) center center no-repeat;}
.game-board .card-type-3 .avers {background:url(title_4.png) center center no-repeat;}
.game-board .card-type-4 .avers {background:url(title_5.png) center center no-repeat;}
.game-board .card-type-5 .avers {background:url(title_6.png) center center no-repeat;}
.game-board .card-type-6 .avers {background:url(title_7.png) center center no-repeat;}
.game-board .card-type-7 .avers {background:url(title_8.png) center center no-repeat;}
.game-board .card-type-8 .avers {background:url(title_9.png) center center no-repeat;}
.game-board .card-type-9 .avers {background:url(title_10.png) center center no-repeat;}

Ciut przydługie, ale już tłumaczę o co chodzi. Plansza składa się z nastu komurek (.cell), które dzięki inline-block układają się automatycznie w kostkę. Do tych komórek wrzuciliśmy kafelki .tile. Każdy taki kafelek ma w sobie .avers i .revers. W stanie spoczynkowym .avers jest ukryty, natomiast .revers pokazany.
Po kliknięciu na .tile nadajemy mu klasę .show co powoduje zmianę stanu – .avers się pokazuje,.revers ukrywa. Uff.

Ogólna zasada działania ma więc postać:

.tile .avers {...} /* domyślnie ukryty */
.tile .revers {...} /* domyślnie pokazany */
.tile.show .avers {...} /* po kliknięciu pokazany */
.tile.show .avers {...} /* po kliknięciu ukryty */

U nas ukrywanie i pokazywanie uzyskałem przez zastosowanie transform:scaleX(). Domyślnie .avers ma scaleX(0) (czyli ściskamy go maksymalnie w poziomie), a .revers scaleX(1) (czyli nic a nic go nie ściskamy). Po kliknięciu na .tile zmieniamy scaleX dla .avers i .revers (na odwrót). Dzięki temu uzyskujemy efekt „przewracania” kafelka.
Przeglądarki IE w wersji < 9 nie obsługują transformacji, dlatego trzeba dodać dla nich fixa. Pamiętaj, że do takich rzeczy musimy mieć odpowiednie nagłówki strony!

<!doctype html>
<!--[if lt IE 7]>  <html class="no-js ie ie6 lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>     <html class="no-js ie ie7 lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>     <html class="no-js ie ie8 lt-ie9"> <![endif]-->
<!--[if IE 9]>     <html class="no-js ie ie9"> <![endif]-->
<!--[if !IE]><!--> <html class="no-js"> <!--<![endif]-->
<html lang="pl">
Tutaj mała uwaga. Jeżeli chcemy bardziej się zatroszczyć o starsze przeglądarki, warto zastosować filter:alpha(opacity=…) wszędzie tam gdzie działamy z opacity dla elementu. Osobiście pominąłem ten krok by style były czytelniejsze.

Stylowanie kończy się indywidualnymi stylami dla każdego typu kafelka. W końcu to memorka :)

Samo odsłanianie kafelków może wyglądać zupełnie inaczej. Tak samo jak w przypadku widoków możemy tu np zastosować np visiblity:hidden, opacity:0 a potem je odpowiednio zmieniać:

Podobnie jak w poprzedniej wersji musimy dorobić kilka funkcji. Wiem, że nie każdy czytał tamten artykuł, dlatego jeszcze raz musimy przelecieć przez każdą z nich:

tileClicked = function(element) {
    if (canGet) {
        //jeżeli jeszcze nie pobraliśmy 1 elementu
        //lub jeżeli index tego elementu nie istnieje w pobranych...
        if (!clickedTiles[0] || (clickedTiles[0].data('index') != element.data('index'))) {
            clickedTiles.push(element);
            element.addClass('show');
        }

        if (clickedTiles.length == 2) {
            canGet = false;
            if (clickedTiles[0].data('cardType') == clickedTiles[1].data('cardType')) {
                setTimeout(function() {deleteTiles()}, 600);
            } else {
                setTimeout(function() {resetTiles()}, 600);
            }

            movesCount++;
            showMoves(movesCount);
        }
    }
}

Gdy klikamy na kafelek, jego numer (data(‚index’)) który ustawiliśmy w startGame) dodajemy do talicy clickedTiles, oraz kafelkowi dajemy klasę show.

Tablica clickedTiles zawiera numery typów aktualnie wybranych kafelków. Jeżeli długość tablicy clickedTiles jest równa 2 (czyli 2 kafelki zostały kliknięte) wyłączamy możliwość klikania (na chwilę!) oraz sprawdzamy typ obu kafelków w tej tablicy. Jeżeli są takie same, wtedy usuwamy je z planszy (deleteTiles()), jeżeli są różne, ponownie je ukrywamy (resetTiles()).
Dodatkowo każdorazowo zwiekszamy liczbę wykonanych ruchów oraz pokazujemy ją w stosownym miejscu (showMoves()).

deleteTiles = function() {
    clickedTiles[0].fadeOut(function() {
        $(this).remove();
    });
    clickedTiles[1].fadeOut(function() {
        $(this).remove();

        tilesPair++; //zwiększamy liczbę odgadniętych par
        if (tilesPair >= TILES_COUNT / 2) {
            gameOver(); //jeżeli odgadliśmy wszystkie pary to koniec gry
        }

        clickedTiles = new Array();
        canGet = true;
    });
}

Funkcja deleteTiles() ukrywa oba kafelki (wykorzystując do tego tablicę clickedTiles). Po ich ukryciu (fadeOut) zwiększa liczbę odgadniętych par oraz sprawdza czy odgadliśmy już wszystkie pary. Jeżeli tak, to kończymy grę. Dodatkowo musimy włączyć możliwość klikania oraz wyczyścić tablicę z aktualnie wybranymi kafelkami.

Funkcja resetTiles() jest o wiele prostsza. Po prostu ukrywa kafelki (usuwając im klasę show), czyści tablicę z wybranymi aktualnie kafelkami, oraz włącza możliwość klikania.

resetTiles = function() {
    clickedTiles[0].removeClass('show');
    clickedTiles[1].removeClass('show');
    clickedTiles = new Array();
    canGet = true;
}

W funkcji deleteTiles() po odgadnięciu ostatniej pary kafelków wywołaliśmy funkcję gameOver(). Jest to bardzo długa i skomplikowana funkcja:

gameOver = function() {
    saveHighScore();        
}

Wat! Czemu więc od razu nie wywołaliśmy saveHighScore()? Ponieważ saveHighScore() jak sama nazwa wskazuje zapisuje wyniki, a nie kończy grę. U nas koniec gry to tylko zapisanie wyników, ale któż wie co może tam się jeszcze znaleźć.

Ostatnia mini funkcja którą wywołaliśmy przy kliknięciu na kafelek pokazanie punktacji:

showMoves = function(moves) {
    $('#gameMoves').html(moves);
}

 

Zapisywanie wyników

Hula hop. Wreszcie dochodzimy do tematu, który tak długo był przez niektórych wyczekiwany.

Pozostały nam właściwie dwie rzeczy. Zapisanie i odczyt tablicy wyników.
Nazwę gracza już mamy, bo pobraliśmy go przed rozpoczęciem gry. Musimy go tylko zapisać do pliku. Założenie było takie, że nie ma mowy o żadnym przeładowywaniu, więc musimy skorzystać z ajaxa:

saveHighScore = function() {        
    $('.loading').show();
    var playerName = $('#playerName').val(); //pole jest już wypełnione - pobieramy je     
    $.ajax({
        url : 'miniengine.php',
        type : 'POST',            
        data : {
            action : 'save',
            player : playerName,
            moves  : movesCount //nasza globalna zmienna z liczbą odgadnietych par
        },
        success : function() {
        },         
        error : function() {
            console.log('Wystąpił jakis błąd :(')
        },
        complete : function() {
            $('.loading').hide();
            showHighScore();            
        }
    })
}

Jeżeli nie wiesz z czym się je jquery ajax, zapraszam cię do tego artykułu. Niestety tutaj nie mamy czasu na ponowne rozpisywanie się o tej technologii.

Zapisywanie wyników może chwilkę potrwać. Dlatego musimy użytkownikowi pokazać coś co powie mu, że trwa zapisywanie. Pokazujemy więc .loading, który ma stylowanie:

.loading {
    position: fixed;
    top:10px;
    left:10px;
    background:#fff url(loading.gif) center center no-repeat;
    display: none;
    width:40px;
    height: 40px;
    border-radius:10px;
    z-index:10;
    opacity:0.8;
}

To zwykły div ustawiony na sztywno w lewym górnym rogu ekranu. Obrazków wczytywania w necie jest miliony. Ja wykorzystałem pierwszy z brzegu:

loading

W naszej funkcji zapisywania wyników wywołaliśmy połączenie do pliku miniengine.php. Plik naszego „super mini engine” wykorzystamy zarówno do zapisu jak i do odczytu wyników, musimy więc jakoś określić akcję którą chcemy aktualnie wykonać. Przekazujemy ją w zmiennej action, którą przesyłamy z resztą zmiennych.

Po zakończeniu połączenia z serwerem pokazujemy użytkownikowi tablicę wyników. Metoda complete jquery ajax wywoływana jest po zakończeniu połączenia, niezależnie czy zakończyło się ono powodzeniem, czy nie. My tak czy siak powinniśmy pokazać tabelę wyników, dlatego właśnie robimy to w complete. Dodatkowo ukrywamy loading! :)

Odczyt tablicy wyników

Samym skryptem php zajmiemy się za chwilę. Aby móc odczytać i wyświetlić wyniki potrzebujemy jeszcze dwóch rzeczy. Funkcji, która odczyta wyniki oraz wypełni naszą listę z wynikami. Drugą rzeczą będzie sam widok wyników, który ma postać:

<!-- tabela z wynikami -->
<div class="slide-highscore" id="stageHighscore">
    <h3>Wyniki gry</h3>
    <div class="highscore-board" id="highscoreBoard"></div>
    <a href="" class="close-highscore button">Zamknij</a>
</div>

Jak widzisz, w widoku tym jest właściwie tylko pusty div #highscoreBoard, do którego wstawimy pobrane wyniki, oraz guzik zamknij, który zamknie tablicę wyników i przeniesie gracza do początkowego widoku. Jego podpięcie wstawmy do funkcji bindEvents() którą już wcześniej napisaliśmy:

bindEvents = function() {
    $('#startGame').on('click', function(e) {
        e.preventDefault();
        showPlayerName();                
    });
    $('#showHighscore').on('click', function(e) {
        e.preventDefault();
        showHighScore();
    })
    $('.close-highscore').on('click', function(e) {
        e.preventDefault();
        showStage('stageStart');
    });
}

Stylowanie dla tego widoku ma postać:

h3 {
    font:bold 2.5em/1em 'Alegreya SC', sans-serif;
    margin:0 10px;
    text-transform: uppercase;
    text-align:center;
}
/* high score */
.highscore-board {
    background: #f2296b;
    width:90%;
    max-width:500px;
    border:10px;
    margin:10px auto;
    padding:20px;
    font-size:1.5em;
    border-radius:6px;
    box-shadow:0 3px 6px rgba(0,0,0,0.2);
}
.highscore-board .line {
    border-bottom:1px solid rgba(0,0,0,0.3);
}
.highscore-board .line:nth-of-type(2n) {
    background:rgba(0,0,0,0.1);
}
.highscore-board .line .player {
    vertical-align: top;
    display: inline-block;
    width:50%;
    padding:15px 10px;
    white-space: nowrap;
    text-overflow:ellipsis;
    overflow: hidden;
}
.highscore-board .line .moves {
    vertical-align: top;
    display: inline-block;
    width:50%;
    padding:15px 10px;
    text-align: right
}

Funkcja która pokaże nam tablicę wyników ma postać:

showHighScore = function() {
    showLoading();
    $.ajax({
        url : 'miniengine.php', //uwaga - nazwę do pliku użyliśmy już 2x, czyli dobrze jest ją wynieść do zmiennej
        type : 'POST',            
        data : {
            action : 'read'                
        },
        dataType : 'json',
        success : function(r) {                
            $('#highscoreBoard').empty();
            for (x=0; x<r.length; x++) {
                var record = r[x];                    
                var $div = $('<div class="line"><strong class="player">'+record.player+' :</strong><span class="moves">'+record.moves+'</span></div>');
                $('#highscoreBoard').append($div);
            }                   
        },         
        error : function() {
            console.log('Wystąpił jakis błąd :(')                
        },
        complete : function() {
            hideLoading();
            showStage('stageHighscore');
        }
    })    
}

Po raz kolejny do naszego serwera wysłaliśmy akcję – tym razem „read”.
Zwrot z serwera dostaliśmy w postaci json, który jest tablicą obiektów par, które zawierają właściwości player – nazwa gracza i moves – liczba jego ruchów.
Po tej tablicy w bardzo prosty sposób możemy zrobić pętlę i utworzyć kolejne elementy tablicy wyników. Po zakończeniu wypełniania tablicy, pokazujemy jej widok i ukrywamy loading.

Żeby być bardziej elastycznym, pokazywanie i ukrywanie loadingu przenieśliśmy do oddzielnych funkcji (dzięki temu w przyszłości ładowanie danych łatwo zmienimy na zupełnie inne?):

showLoading = function() {
    $('.loading').show();
}

hideLoading = function() {
    $('.loading').hide();
}

Zanim przejdziemy do ostatniego wyzwania, czyli napisania skryptu engine w php, musimy podpiąć pokazanie wyników pod pierwsze menu! Ale to przecież już zrobiliśmy. Na samym początku :) Nie było sprawy.

Engine – czyli zabawa po stronie serwera

Skoro dotarłeś aż do tego miejsca mój Padawanie, znak to, że naprawdę sporo mocy mieć musisz.
Mogłeś też po prostu przewinąć stronę. W każdym bądź razie do poniższej części pracy potrzebować będziemy serwera z PHP. Najlepiej oczywiście zainstalować sobie serwer lokalny, który bardzo łatwo się instaluje. Pisałem o tym w tym artykule: http://domanart.pl/serwer-lokalny/

Zapisywanie wyników możemy przeprowadzić na bazie danych lub zwykłym pliku tekstowym. W przypadku tak prostej gry jak nasza wybrałem drugą metodę. Nic jednak nie stoi na przeszkodzie, by zmienić ją na zapis do bazy danych…

Nasze serwerowe środkowisko (jeżeli tak to można nazwać) będzie składać się z 2 plików. Pierwszy to nasz skrypt php – miniengine.php. Drugi to tekstowy plik z zapisanymi wynikami – highscore.dat.

Highscore.dat ma postać:

Nazwa gracza
10
Nazwa kolejnego gracza
20
...
...

Jak widzisz, każdy rekord zajmuje 2 linie. Moglibyśmy zapisywać wyniki w jednej linii oddzielając jakimś znakiem nazwę gracza od jego wyniku, ale wtedy musielibyśmy dodatkowo stworzyć zabezpieczenie dla niedozwolonych znaków (czyli gracz nie mógł by używać w nazwie znaków, które użyliśmy do oddzielenia).
Tyle odnośnie pliku z danymi.

Odczytywanie wyników z pliku

<?php 
$fileSrc = 'highscore.dat'; //sciezka do pliku z wynikami
$resultsMaxCount = 10; //ile maksymalnie wynikow ma mieć tablica
$action = (string)$_POST['action']; //przekazana akcja

function readHighScore() {
    global $fileSrc;

    //pobieramy plik do tablicy
    $fileLines = file($fileSrc);

    $highScore = Array();

    //robimy pętlę po liniach pliku i wrzucamy je do talbicy $highScore
    //każdy wynik zapisany jest w 2 liniach. Dlatego x za każdym razem zwiększamy o 2
    if (count($fileLines) > 1) {
        $i = 0;
        for ($x=0; $x<count($fileLines); $x+=2) {
            $highScore[$i] = Array(
                'player' => trim($fileLines[$x]),
                'moves' => trim($fileLines[$x+1])
            );
            $i++;
        }
    }
    return $highScore;
}
?>

Na samym początku skryptu ustawiamy kilka zmiennych. Dodatkowo podstawiamy pod zmienne dane, które odbierzemy w połączeniu (ajax), dodatkowo rzutując je na dany typ danych (np. string). Jest to zalecana praktyka – szczególnie gdy zapisujemy coś do bazy danych.

Jak widzimy, pierwszą funkcją w naszym engine jest funkcja readHighScore(), która wczytuje wyniki z pliku. Robi to za pomocą funckji file, która zwraca kolejne linie pliku w postaci tablicy. Każdy wynik zapisany jest w 2 liniach, dlatego robiąc pętlę for zmienną $x każdorazowo zwiększamy o 2. Funkcja zwraca dwuwymiarową tablicę, w której każdy rekord to tablica składająca się w pól player i moves.

[
    ['nazwa-gracza', 'liczba-ruchow-gracza'],
    ['nazwa-gracza', 'liczba-ruchow-gracza'],
    ['nazwa-gracza', 'liczba-ruchow-gracza']
]

Zapisywanie wyników do pliku

<?php
$fileSrc = 'highscore.dat'; //sciezka do pliku z wynikami
$resultsMaxCount = 10; //ile maksymalnie wynikow ma mieć tablica
$action = (string)$_POST['action']; //przekazana akcja

function readHighScore() {
    //...
}

function sortByMovesMaxToMin($a, $b) {
    if ($a['moves'] == $b['moves']) {
        return 0;
    } 
    return ($a['moves'] < $b['moves'])? -1 : 1;                
}

function saveHighScore($playerName, $playerMoves) {
    global $resultsMaxCount;
    global $fileSrc;

    $arrayTemp = Array(
        'player' => $playerName,
        'moves' => $playerMoves
    );

    $scoreBoard = readHighScore();

    array_push($scoreBoard, $arrayTemp);

    usort($scoreBoard, 'sortByMovesMaxToMin');

    $length = min(count($scoreBoard), $resultsMaxCount);
    $newScoreBoard = array_slice($scoreBoard, 0, $length);

    $fp = fopen($fileSrc, 'w');
    flock($fp, LOCK_EX);        
    foreach ($newScoreBoard as $record) {
        fwrite($fp, $record['player']."\r\n");
        fwrite($fp, $record['moves']."\r\n");
    }
    fclose($fp);
}
?>

Zapisywanie wyników jest nieco trudniejsze, ponieważ pojawia się tutaj pewien istotny problem.

Przypuśćmy, że właśnie wczytaliśmy tablicę wyników i chcemy do niej dodać naszego gracza. W które miejsce ma on trafić? Pierwsze, trzecie czy może ósme? Czy po dodaniu go do tablicy nie przekroczy ona maksymalnej liczby wyników (w naszym przypadku 10)?

Rozwiążemy to pewną sztuczką, a właściwie zrobimy to na logikę.
Korzystając z funkcji którą przed chwilą napisaliśmy wczytujemy wyniki do tablicy (pamiętaj – zwraca ona tablicę złożoną z tablic). Następnie wrzucamy do niej tablicę $arrayTemp z przesłanymi danymi naszego gracza. Po tej czynności tablicę wyników sortujemy tak, by rozpoczynała się od graczy z najmniejszą liczbą ruchów. Jako, że jest to dwuwymiarowa tablica, nie możemy zastosować zwykłego sortowania sort, a użyć naszego własnego (sortByScoreMaxToMin), które sprawdza wartość score.
Posortowaną tablicę musimy jeszcze przyciąć do maksymalnej liczby wyników $ileWynikow. Robimy to za pomocą funkcji array_slice. Ostatni parametr tej funkcji określa indeks do którego będziemy przycinać. Musimy go wyliczyć, bo przecież pobrana tablica po dodaniu naszego gracza wcale nie musi być większa od $ileWynikow (może do tej pory grało tylko 5 graczy)? Robimy to za pomocą funkcji max().

Gdy mamy już przygotowaną tablicę, pozostaje nam zapisać ją do pliku. Otwieramy go za pomocą fopen (‚w’ jak otwarcie), blokujemy jego odczyt (który w czasie zapisu mógł by nieźle namieszać), po czym za pomocą prostej pętli foreach zapisujemy tablicę do pliku.

Włala. Mamy już przygotowane główne funkcje do naszego super silnika gry. Musimy jeszcze jakoś je wywołać:

<?php
$fileSrc = 'highscore.dat'; //sciezka do pliku z wynikami
$resultsMaxCount = 10; //ile maksymalnie wynikow ma mieć tablica
$action = (string)$_POST['action']; //przekazana akcja

function readHighScore() {
    //...
}

function sortByMovesMaxToMin($a, $b) {
    //...
}

function saveHighScore($playerName, $playerMoves) {
    //...
}

if ($action == 'read') {
    $scoreBoard = readHighScore(); //wczytujemy wyniki
    echo json_encode($scoreBoard); //zwracamy tablicę do gry
}
if ($action == 'save') {
    $player = (string)$_POST['player'];
    $playerMoves = (int)$_POST['moves'];
    saveHighScore($player, $playerMoves); //zapisujemy wyniki do pliku
}
?>

Jeżeli akcja jest równa ‚read’, wczytujemy tablicę wyników i zwracamy ją do gry w postaci zakodowanego ciągu json. Tyle. Zapisywania danych nie musimy zwracać.

Ostatnie wyzwanie

Pozostał nam już ostatni krok. Pójdźmy do barku, otwórzmy wino i wypijmy nasze zdrowie, bo właśnie zakończyliśmy całkiem niezły kawałek pracy. Oczywiście do naszej gry można wprowadzić sporo usprawnień. Może jakieś dodatkowe animacje, może rysowane tła dla kolejnych widoków? Może dodatkowe zabezpieczenia przed oszukiwaniem? (w tym przypadku życzę powodzenia :) A może usprawnienie dla ekranów, które są bardzo małe? A tak. O tym zapomnieliśmy:

/* media */
@media only screen and ( max-width: 320px) {    
    .game-board .cell {
        width:49%;
        height:9%;
    }
    .game-board .tile .revers::before {
        text-align: center;
        font-size:2em;
        line-height:1em;       
    }
}

Resztę pozostawiam jako pracę domową. Oczywiście dopiero po skończeniu wina…

Pokaż demo
W powyższej wersji demonstracyjnej wyłączyłem zapisywanie wyników. Znając ludzką fantazję miał bym tam ciekawe powitania. W zamian dodałem do funkcji zapisu i odczytu sleep() ustawiony na 2 sekundy. Dzięki temu wyraźniej zobaczycie jak działa proces „wczytywania”.

Podsumowanie

Zdaję sobie sprawę, że powyższy tekst wcale nie musi być łatwy w odbiorze. Podawane urywkowo kody mogą powodować pewien chaos w organizacji kodu.Dlatego zalecam zajrzeć w pełne źródła wersji demonstracyjnej lub ściągnięcie całej paczki z grą, którą dla was przygotowałem. I właściwie tyle na dziś. Jeżeli macie pytania, śmiało piszcie w komentarzach.

Komentarze

  • mamingame

    Świetny tutorial i wogóle wszystkie tutki na tej stronie świetne. Dokładnie czegoś takiego szukałem.

  • M.

    A jak odblokować to zapisywanie wyników? Możecie pomóc?

    • kartofelek007

      A co dokładnie pytasz?

      • M.

        Po ściągnięciu tej paczki nie działa mi zapisywanie wyników do pliku. Działa tak jak w wersji demo, wyrzuca gotową tabelkę ale moich wyników nie chce zapisywać :( jak to odblokować żeby do .dat wpisywały się moje wyniki?

      • kartofelek007

        Dwie sprawy. Czy wrzucasz to na swój serwer (może być lokalny). Możliwe też, że po przegraniu plików musisz nadać im uprawnienia. W windowsie wystarczy, jeżeli klikniesz prawym na pliku .dat i wyłączysz mu tylko do odczytu. A jeżeli dalej będą występować problemy, pisz.

      • M.

        Mam to na localhost… Tylko jak mam to załączyć bez bazy .sql?

      • kartofelek007

        Ale to nie używa bazy danych. To zapisuje do pliku .dat i żeby to działało skrypt musi mieć prawo zapisu. Jak wrzucisz na serwer, wejdź sobie jakimś free commander (albo klienta ftp) i ustaw plikowi highscore.dat atrybuty 0777

      • M.

        Nic u mnie to niestety nie pomogło, cały czas mam pustą tabelę…

      • kartofelek007

        Metoda więc hardkorowa: rozpakuj to na serwer i wszystkim plikom nadaj 0777 (za pomocą klienta ftp albo wspomnianego commandera). Jeżeli dalej nic, to wtedy sprawdź posty w debugerze. Chociaż powiem ci że własnie ściągnąłem to na swój lokalny serwer (xampp) i nic nie zmieniając wszystko działało. Mi się wydaje że to albo uprawnienia, albo wersja php. Sprawdź jeszcze ten plik highscore czy aby na pewno masz w nim dane.

      • M.

        Zadziałało! Wszystkim zmieniłam na 0777! Dziękuję bardzo za pomoc! :)

      • kartofelek007

        Ale tak nie może być. Jak wrzucisz to na zewnętrzny serwer to w ciągu całkiem krótkiego czasu boty zrobią ci sajgon z tych plików – zaaplikują wirusy itp rzeczy. Ustaw 0777 na highscore.dat a na reszcie 0755. Poczytaj sobie o uprawnieniach dla plików. Ja najczęściej korzystam z tego wątku http://codex.wordpress.org/Changing_File_Permissions ale to jest nastawione na wordpressa

  • Czytałem ostatnio na blogu Stevena Lamberta o technice Dirty Rectangles. W skrócie chodzi o to, żeby zamiast jednego canvas było ich kilka i dzięki temu żeby nie odświeżać całego obszaru gry tylko te kawałki które potrzeba. Używałeś może takiego rozwiązania? W moim odczuciu zwolniło to jeszcze moją grę ale może robię coś nie tak;)

  • Pingback: WTF, forms? - domanart.pl()

  • betka

    Co zrobić, żeby stworzyć kilka rund takiej gry? Rozwiąże to kilka podstron? Czy lepiej zastosować jakieś pętle?

    • Oba rozwiązania są dość proste do wprowadzenia. To ze stronami: wynik gry i zakonczenie partii zapisujesz do ukrytych pól w formularzu. Po zakończeniu gry wysyłasz ten formularz, a on kieruje na nową stronę (robisz to za pomocą form.submit() lub podobną metodą). Na nowej stronie sprawdzasz czy przekazano oba parametry. Jeżeli tak to masz w jednym wynik i możesz kontynuować.
      To na jednej stronie: po zakończeniu gry zapisujesz sobie wynik do ukrytego pola lub do jakiejś globalnej zmiennej. Dajesz jakiś komunikat (może np confirm który daje możliwość przerwania gry) i rozpoczynasz grę jak to robiliśmy wcześniej. Wiem, że te opisy są bardzo ogólne. W razie problemów pisz.

      • betka

        wymyśliłam, że dodam dodatkowe „slajdy” – i to jakoś wyszło :) ale mam problem też z tym, żeby zmienić liczbę kwadratów – wciąż mi wychodzi domyślna 4×5, a chcę zrobić 4×4, 5×5 (i dodać jednej karcie atrybut odsłaniania pary po jej odkryciu) no i 6×6… Pewnie za mało jeszcze wiem o js, ale się wkręciłam i próbuję.

    • betka

      dodatkowo mam wątpliwość co do wymiarowania obrazków – ale to pewnie mój brak wiedzy dotyczącej js mi przeszkadza w skalowaniu :) Dziękuję bardzo za szybką odpowiedź