Gra szubienica

Pewnie każdy z was grał w wisielca? Gracz zgaduje ukryte hasło wybierając odpowiednie litery.
Jeżeli się pomyli, wtedy dorysowujemy kolejne części szubienicy, aż w końcu nieszczęśnika wieszamy :)
W dzisiejszym odcinku postaramy się stworzyć właśnie taką grę w JS. Nie będzie to żadne wybitne dzieło, a jedynie prosta ćwiczebna gra w odgadywanie haseł. W praktyce wykorzystamy kilka technik które poznaliśmy.

Zobacz DEMO naszej gry

Podstawowe założenia gry

Na sam początek rozpiszmy działania jakie musimy zakodować:

  1. na samym początku tworzymy html dla elementów gry:
    1. Guzik ze startem gry
    2. Guziki z literkami
    3. Plansza gry zawierająca aktualne hasło i liczbę dostępnych prób + dodatkowe dekoracyjne elementy
  2. Po kliknięciu na przycisk start odpalamy grę. Pobieramy hasło, aktywujemy guziki z literkami, wypisujemy hasło i zerujemy próby
  3. Po każdym wyborze litery sprawdzamy czy jest ona w haśle, wyłączamy dany przycisk z literą (tak by nie można było jej ponownie wybrać) i sprawdzamy próby
  4. Jeżeli próby się skończyły, kończymy grę i wyłączamy przyciski z literkami.
  5. Jeżeli hasło zostało odgadnięte kończymy grę i wyłączamy przyciski z literkami.

Cały wygląd oczywiście zrobimy za pomocą CSS - wykorzystamy do tego CSS3 dzięki czemu poznamy kilka nowych technik :)

Rozpoczynamy pracę - tworzymy html

Na początku realizujemy 1 punkt z naszej listy - czyli tworzymy html dla naszej aplikacji. Jak założyliśmy musimy stworzyć planszę gry zawierającą próby oraz hasło do zgadnięcia. Nasze hasło będzie listą. Kolejne litery hasła będą pustymi odpowiednio ostylowanymi LI. Jej wypełnieniem zajmiemy się przy pobieraniu hasła - dynacznie - a co :)


<div class="plansza">
    <div class="proby">
    </div>

    <ul class="haslo">
    </ul>
</div>

Kolejnym elementem będzie lista guzików z literkami które będziemy mogli kliknąć. Jako że jesteśmy leniami, wykorzystamy do tego celu PHP:


<ul class="literki">
    <?php
        $alphabet = array('a','ą','b','c','ć','d','e','ę','f','g','h','i','j','k','l','ł','m','n','ń','o','ó','p','q','r','s','ś','t','u','v','w','x','y','z','ź','ż');
        foreach ($alphabet as $letter) {
            echo '<li>'.$letter."</li>\n";
        }
    ?>
</ul>

Zauważ, że nie interesuje nas wielkość liter. Dzięki właściwości CSS text-transform:uppercase i tab będziemy mieli tylko duże litery :)

Ostatni element to przycisk startujący naszą aplikację


    <input type="button" value="Rozpocznij nową grę :)" id="startGry" />

Całość wygląda następująco:


<div class="plansza">
    <div class="proby">
    </div>

    <ul class="haslo">
    </ul>
</div>

<ul class="literki">
    <?php
        $alphabet = array('a','ą','b','c','ć','d','e','ę','f','g','h','i','j','k','l','ł','m','n','ń','o','ó','p','q','r','s','ś','t','u','v','w','x','y','z','ź','ż');
        foreach ($alphabet as $letter) {
            echo '<li>'.$letter."</li>\n";
        }
    ?>
</ul>

<input type="button" value="Rozpocznij nową grę :)" id="startGry" />

Od tej chwili nie tykamy html :)

Piszemy silnik gry

Ah silnik. Jak to poważnie brzmi.

Skrypt rozpoczniemy od napisania metody startującej grę:


var aktualneHaslo = ''; //aktualnie pobrane hasło
var dostepneProby = 5; //ile prób zostało dla aktualnej gry

//startujemy gre po kliknięciu na przycisk
function startGame() {
    $.ajax({
        type     : "POST",
        url      : "hangman_server.php",
        data     : {
            action : 'startGame'
        },
        success : function(return) {
            dostepneProby = 5;
            ustawHaslo(return);
            pokazProby();
            ustawLiterki();
        }
    });
}

Jak widać powyżej po starcie gry łączymy się ze skryptem na serwerze i wywołujemy akcję "startGame". Ma ona za zadanie zwrócić hasło które będziemy zgadywać.


<?php
$hasla = array(
    "Fantomas",
    "Super Szamson",
    "Hasło"
);

shuffle($hasla); //mieszamy tablicę


if ($_POST['action']=='startGame') {
    echo $hasla[array_rand($hasla, 1)];  //wypisujemy randomowe hasło
}
?>

W praktyce powyższy kod powinien być nieco bardziej skomplikowany - np powinien pobierać hasła z bazy danych itp. W naszym przykładzie jednak ma być jak najprościej ;]

Wróćmy do JS.
Po pobraniu hasła (success) nasza funkcja startująca grę resetuje próby na 5.
Wywołuje także funkcje odpowiedzialne za ustawienie hasła w liście, pokazanie prób i ustawienie przycisków z literkami.


...
dostepneProby = 5;
ustawHaslo(return);
pokazProby();
ustawLiterki();
...

Napiszmy kolejno ich kod.

Pierwsza będzie funkcja ustawiająca hasło. Pobraliśmy je z serwera i przekazaliśmy do poniższej funkcji. Podstawia ona je pod globalną zmienną - dla naszej wygody. Dodatkowo zmienia jej litery na duże - także dla naszej wygody.
Kolejnym zadaniem jakie ma musimy wykonać to utworzenie kratek (LI) na zgadywane litery hasła.
Pobrane (aktualne) hasło rozbijamy na części za pomocą metody split i wykonując prostą pętlę tworzymy kolejne LI. Nasze hasła mogą się składać z kilku wyrazów, dlatego dodatkowo musimy sprawdzać czy dana litera nie jest odstępem. Jeżeli tak jest wtedy dla aktualnie tworzonego LI dodajemy klasę "space" (później odpowiednio ją ostylujemy tak by nie takie LI nie były wyświetlane).


//ustawiamy haslo
function ustawHaslo(haslo) {
    aktualneHaslo = haslo.toUpperCase(); //zamieniamy litery na duże

    var $hasloList = $('.haslo'); //pobieram listę na hasło
    $hasloList.empty(); //czyścimy listę

    var letters = aktualneHaslo.split('');
    for (var i=0; i<letters.length; i++) {
        if (letters[i]==' ') {
            $hasloList.append('<li class="space"></li>');
        } else {
            $hasloList.append('<li></li>');
        }
    }
}

Kolejna prosta funkcja którą napiszemy to pokazProby().


function pokazProby() {
    $('.proby').text(dostepneProby);
}

Niby jedena linijka kodu, ale warto takie działania rozbijać na oddzielne metody, gdyż wtedy jesteśmy bardziej odporni na zmiany (wystarczy że zmienimy np nazwę klasy .proby w 1 miejscu).

Kolejna metoda którą napiszemy to ustawLiterki(). Ma ona za zadanie włączenie działania naszych guzików do wyboru litery oraz podpięcie im zdarzenia click. Wcześniej czyści ona ich stan. Start gry może być przecież odpalony drugi czy trzeci raz, wtedy stan wybranych przycisków-liter będzie zmieniony.


function ustawLiterki() {
    var $literki = $('.literki li');

    $literki.unbind().removeClass('disabled'); //czyścimy stan przycisków

    $literki.bind('click',function() {
        var litera = $(this).text();
        check_letter(litera);
        disableButtons($(this));
    })
}

Po kliknięciu w dany przycisk z literką wybieramy ją i odpalamy funkcję "sprawdź literę". Wybranie litery może się odbyć tylko raz, dlatego wybrany guzik jest wyłączany za pomocą dodatkową metodę disableButtons().


function disableButtons($guziki) {
    $guziki.addClass('disabled').unbind(); //wylaczamy przekazane guziki i dodajemy im klasę disabled
}

Jak przed chwilą napisałem po kliknięciu na przycisk z literą sprawdzamy czy jest ona w danym haśle:


function check_letter(litera) {
    var literka = litera.toUpperCase();
    if (aktualneHaslo.indexOf(literka)!=-1) {   //jeżeli litera istnieje w haśle
        for (var i=0; i<aktualneHaslo.length; i++) {
            if (aktualneHaslo[i] == literka) {
                $('.haslo li').eq(i).html(litera) //wstawiamy w odpowiednie LI wybraną literę
            }
        }
    } else {    //nie ma takiej litery
        dostepneProby--;
        pokazProby();
    }
}

Sprawdzanie takie jest dziecinne proste. Robimy prostą pętlę po literach hasła i przyrównujemy do nich daną literę. Jeżeli jest równa, wtedy do LI naszej listy z hasłem wstawiamy daną literę.
Jeżeli dana litera nie wystęuje w haśle, wtedy zmniejszamy dostępne próby i aktualizujemy ich wyświetlanie.

Po odpowiednim wypełnieniu listy literami powinniśmy przeprowadzić dwa dodatkowe działania: sprawdzić czy użytkownik odgadł hasło oraz czy gra się nie zakończyła:


function check_letter(litera) {
    var literka = litera.toUpperCase();
    if (aktualneHaslo.indexOf(literka)!=-1) {
        for (var i=0; i<aktualneHaslo.length; i++) {
            if (aktualneHaslo[i] == literka) {
                $('.haslo li').eq(i).html(litera)
            }
        }

        if (!isMissingLetterExists()) { //jeżeli nie ma już nieodgadniętych liter...
            completeGame();
        }

    } else {
        //nie ma takiej litery
        dostepneProby--;
        pokazProby();

        if (dostepneProby <= 0) { //jeżeli nie ma już prób...
            gameOver();
        }
    }
}

Do sprawdzenia czy po wstawieniu litery do hasła istnieją jeszcze nieodgadnięte miejsca posłużymy się prostym trikiem. Wystarczy "przeskanować" wszystkie LI naszej listy ".hasło" i sprawdzić czy nie są puste. Właśnie to zadanie wykona metoda isMissingLetterExists():


function isMissingLetterExists() {
    if ($('.haslo li').filter( function() {return ( !$(this).hasClass('space') && $(this).is(':empty') )? true: false} ).length) {
        return true;
    } else {
        return false;
    }
}

Masakra dla niektórych :). W praktyce powyższy kod jest bardzo prosty. Wystarczy przeczytać go tak jak się czyta "jQuery":

  1. Pobieramy wszystkie LI z listy .haslo;
  2. Za pomocą metody filter filtrujemy z powyższego zbioru tylko te elementy które NIE mają klasy "space" i są puste
  3. Sprawdzamy długość otrzymanego wyniku. Jeżeli jest większa od 0 znaczy to że w naszej liście z hasłem wciąż są puste miejsca na literę

No cóż. Urok jQuery. Metoda filter robi pętlę po wszystkich elementach zbioru. Jeżeli dla danego elementu zwróci true, znaczy to że dany element jest prawidłowy, jeżeli false, wtedy dany element jest "usuwany" ze zbioru. Dla zrozumienia przeanalizuj poniższy kod:


var $p = $('p.red'); //pobieramy wszystkie P które mają klasę "red"

//z powyższego zbioru odfiltrowujemy tylko te akapity, których wysokość jest większa od 100

var $szukane_p = $p.filter(function() {
    return ($(this).css('height')>100) ? true : false
});

W zasadzie pozostały nam 2 funkcje do napisania: pozytywne i negatywne zakończenie gry :). W obydwu przypadkach po zakończeniu gry wyłączmy wszystkie guziki z literkami by przypadkiem gracza nie swędziały paluszki. Do wyłączenia wykorzystamy wcześniej napisaną funkcję disableButtons, tym razem przekazując do niej wszystkie literki na raz :)


//negatywne zakończnie gry
function gameOver() {
    alert("Niestety nie udało ci się odgadnąć chasła. Ps: brzmi ono: \n\n" + aktualneHaslo);
    disableButtons($('.literki li'));
}

//pozytywne zakończenie gry
function completeGame() {
    alert('Udało ci się zgadnąć hasło :)');
    disableButtons($('.literki li'));
}

Silnik naszej gry został napisany. Ostatnie zadanie w JS to podłączenie pod przycisk startujący grę naszej początkowej funkcji:


$(document).ready(function() {
    $('#startGry').click(function() {
        startGame();
    })
})

I koniec!
A właściwie jestśmy w połowie. Sam engine nie spełni swojego zadania jeżeli gra nie będzie wyglądała zachęcająco. Jak założyliśmy do zmiany jej wyglądu wykorzystamy CSS3.

Wygląd czyli CSS

Nasze stylowanie zaczynamy od planszy:


.plansza {
    width:80%;
    height:300px;
    background:#fff;
    border:1px solid #fff;
    margin:20px auto;
    position:relative;
}

Najważniejsza właściwość z powyższych to relatywne pozycjonowanie, dzięki któremu będziemy mogli bez problemu ustawić elementy na planszy.
...Co właśnie zrobimy :) Zaczynamy od hasła:


.haslo {
    position:absolute;
    padding:0;
    margin:0;
    bottom:10px;
    left:0;
    width:100%;
    text-align:center;
}

.haslo li {
    text-transform: uppercase;
    display: inline-block;
    width: 70px;
    height: 70px;
    background: #fefefe;
    border: 1px solid #ddd;
    overflow: hidden;
    margin: 5px;
    font-size: 20px;
    font-family: sans-serif;
    color: #333;
    text-align: center;
    line-height: 70px;
}

.haslo li.space {
    border: 0;
    background: none;
    box-shadow: none;
}

Nasza lista z hasłem do odgadnięcia to zwykła ostylowana lista, w której poszczególne pozycje są ustawione obok siebie oraz centrowane do środka. Uzyskujemy to poprzez nadanie text-align:center dla listy oraz display:inline-block dla LI. Oczywiście LI w których ma być odstęp (spacja) powinny być "pustym" miejscem, czyli dla LI.space usuwamy background itp.

Ostatnim elementem planszy są wyświetlane próby. Tutaj niestety nic nas nie zaskoczy:


.proby {
    position: absolute;
    top: 10px;
    left: 10px;
    width: 40px;
    height: 40px;
    font: 30px kreska-webfont;
    color: #333;
    text-align: center;
}

Po ostylowaniu planszy pozostało nam dostosowanie wyglądu listy z guzikami liter:


.literki {
    list-style-type:none;
    margin:10px auto;
    padding:0;
    width:1200px;
    text-align:center;
}

.literki li,
.literki li.disabled,
.literki li.disabled:hover,
.literki li.clicked,
.literki li.clicked:hover {
    display:inline-block;
    width: 70px;
    height: 70px;
    margin:5px;
    cursor:pointer;
    text-transform:uppercase;
    font-size: 20px;
    font-family: sans-serif;
    color:#444;
    text-align:center;
    line-height:70px;
    border:1px solid #ddd;
    background: #fff;
}

.literki li.disabled,
.literki li.disabled:hover {
    background: #eeeeee;
    color:#ddd;
    text-shadow:none;
    cursor:default;
}

.literki li:hover {
    background: #EC185D;
    color:#fff;
}

W naszym stylowaniu musimy uwzględnić zarówno stan normalny, stan po najechaniu jak i stan kiedy guzik jest wyłączony (disabled).
Wszystkich właściwości CSS3 nie ma sensu wkuwać na pamięć. Było by to zbytecznym marnowaniem pamięci naszego muzgu. O wiele lepiej wejść po prostu na stronę http://css3please.com/ i skopiować to co potrzebujemy :)

Zabezpieczanie przed oszukiwaniem

Nasza gra jest już skończona. Pozostaje utrudnić sprawę potencjalnym oszustom :). Najłatwiej zrobić to pakując nasze skrypty. Wchodzimy więc na stronę http://dean.edwards.name/packer/. Wybieramy opcje Base62 encode i Shrink variables i do górnej textarea wklejamy nasz kod javascript. Uzyskujemy pięknie "zakodowany" kawałek kodu. Zastępujemy nim nasz skrypt (uprzednio robiąc kopię czytelnego kodu!). I tyle zabawy :)