Gra szubienica

Prawie każdy 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.
Podobną zabawę kojarzymy pod nazwą Koło Fortuny.
W dzisiejszym odcinku postaramy się stworzyć właśnie taką grę w JS - tyle że bez samego koła.

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
  2. Na początku za pomocą skryptów wypełnimy planszę z klikalnymi literkami
  3. Po kliknięciu na przycisk start odpalamy grę. Pobieramy hasło, aktywujemy guziki z literkami, wypisujemy hasło i zerujemy próby
  4. 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
  5. Jeżeli próby się skończyły, kończymy grę i wyłączamy przyciski z literkami.
  6. 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 :)

Tworzymy html i css

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.


<div class="game">
    <div class="game-attempts"></div>

    <div class="game-sentence"></div>

    <div class="game-letters"></div>

    <button type="button" class="button game-start">Losuj nowe hasło</button>
</div>

Przygotujmy też podstawowy CSS. Znowu - nie jest to kurs o CSS, więc nie będziemy się tutaj zbytnio na nim koncentrować. Jeżeli jednak chcesz się o coś zapytać, napisz.


* {
    box-sizing:border-box;
}

body {
    background:#eee;
    display: flex;
    justify-content: center;
    align-items: space-between;
}

/* cała gra */
.game {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-direction: column;
}

/* plansza z klikalnymi literami */
.game-letters {
    list-style-type:none;
    margin:10px auto;
    padding:0;
    width:1200px;
    text-align:center;
}
.game-letter {
    display:inline-block;
    width: 4rem;
    height: 4rem;
    margin:0.3rem;
    cursor:pointer;
    text-transform:uppercase;
    font-size: 2rem;
    font-family: sans-serif;
    color:#444;
    text-align:center;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    border:1px solid #ddd;
    background: #fff;
}
.game-letter:disabled {
    background: #eeeeee;
    color:#ddd;
    text-shadow:none;
    cursor:default;
}
.game-letter:not(:disabled):hover {
    background: #F15C5C;
    color:#fff;
}

/* hasło */
.game-sentence {
    padding:0;
    margin:0;
    width:100%;
    text-align:center;
}
.game-sentence-box {
    text-transform: uppercase;
    width: 5rem;
    height: 5rem;
    background: #fefefe;
    border: 1px solid #ddd;
    margin: 1rem 0.5rem 2rem;
    font-size: 2rem;
    font-family: sans-serif;
    color: #333;
    vertical-align: top;
    display: inline-flex;
    justify-content: center;
    align-items: center;
}
.game-sentence-box-space {
    border: 0;
    background: none;
    box-shadow: none;
}

/* informacja o próbach */
.game-attempts {
    margin:1rem 0;
    font-size: 1.6rem;
    font-weight: bold;
    font-family: sans-serif;
    text-align: center;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
    background: #fff;
    color:#333;
    border:1px solid #ddd;
}

button.button,
input[type=button].button,
input[type=submit].button {
    padding: 0.8em 3em;
    background: #F15C5C;
    color: #FFF;
    border: 0;
    font-weight: bold;
    border-radius: 0.3rem;
    transition: 0.5s background-color, 0.5s box-shadow;
    cursor: pointer;
    display: block;
    font-size: 1rem;
    margin:3rem auto 1rem;
}

Piszemy silnik gry

Naszą grę napiszemy w formie prostego obiektu. Skrypt rozpoczniemy od utworzenia podstawowych zmiennych, takich jak aktualne hasło, hasła z których będziemy losować i oczywiście pobrane elementy gry na których zaraz będziemy pracować:


const game = {
    currentSentence : null, //aktualnie pobrane hasło
    currentSentenceLetters : null,
    attempts : 5, //ile prób zostało dla aktualnej gry
    elemBoardElem : document.querySelector('.game-board'), //element z całą grą
    elemSentence : document.querySelector('.game-sentence'), //element z hasłem do zgadnięcia
    elemAttempts : document.querySelector('.game-attempts'), //element z liczba prob
    elemLetters : document.querySelector('.game-letters'), //lista z literkami do klikania
    sentences : [ //hasła z których losujemy
        "Fantomas",
        "Super Szamson",
        "Hasło",
        "Myszka",
        "Super bohaterowie",
        "Super pies",
        "Przyjaciel",
        "Kurs Javascript",
        "Terminator",
        "Superman",
        "Herkules",
        "Batman",
        "Spiderman",
        "Kapitan Ameryka"
    ],
}

Nasza plansza z literami w HTML jest obecnie pusta. Pierwszą rzeczą jaką zrobimy to wypełnienie jej literami. Funkcję taką odpalimy we wspólnej metodzie initBoard(), która odpali nam wszystkie początkowe funkcje:


const game = {
    ...

    generateLetterButtons : function() {
        const alphabet = ['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','ź','ż'];

        alphabet.forEach(function(letter) {
            const button = document.createElement('button');
            button.classList.add('game-letter');
            button.type = 'button';
            button.dataset.letter = letter;
            button.innerHTML = letter;
            this.elemLetters.appendChild(button);
        }.bind(this));
    },

    initBoard : function() {
        this.generateLetterButtons();
    }
};

game.initBoard();

Kolejnym krokiem jest podpięcie klików pod takie litery. Podobnie jak w innych przypadkach eventów nie będziemy podpinać bezpośrednio pod litery, a pod rodzica:


const game = {
    ...

    bindEvents : function() {
        this.elemLetters.addEventListener('click', function(e) {
            if (e.target.nodeName.toUpperCase() === "BUTTON" && e.target.classList.contains('game-letter')) {
                const letter = e.target.dataset.letter;
                console.log(letter); //narazie wypiszmy literę w konsoli
                e.target.disabled = true;
            }
        }.bind(this));
    },

    initBoard : function() {
        this.generateLetterButtons();
        this.bindEvents();
    }
};

game.initBoard();

Do przechowania danej litery wykorzystaliśmy dataset. Na razie po kliknięciu na literę wyłączamy kliknięty element, oraz wypisujemy daną literę w konsoli.

Demo 1 - przygotowanie planszy

metoda gameStart()

Mamy przygotowaną planszę. Po kliknięciu na przycisk "Losuj nowe hasło" rozpoczniemy właściwą grę. Posłuży nam do tego metoda gameStart(). Po odpaleniu takiej metody powinniśmy wyzerować wszystkie potrzebne właściwości (u nas tyle jedna - liczba prób). Dodatkowo powinniśmy wylosować nowe hasło do zgadnięcia, pokazać graczowi liczbę prób oraz włączyć litery:


const game = {
    ...

    startGame : function() {
        this.attempts = 5, //ile prób zostało dla aktualnej gry
        this.randomSentence(); //losujemy hasło do zgadnięcia
        this.showAttempts(); //pokazuje liczbę prób
        this.enableLetters(); //włączamy litery
    }

    ...
};

game.initBoard();

metody enableLetters() i disableLetters()

Przed rozpoczęciem gry litery powinny być nieklikalne. Dopiero po rozpoczęciu gry powinniśmy umożliwić graczowi ich klikanie. Potrzebujemy do tego 2 metod: enableLetters() i enableLetters():


const game = {
    ...

    enableLetters : function() {
        //pobieramy litery i robimy po nich pętlę włączając je
        const letters  = this.elemLetters.querySelectorAll('.game-letter');
        [].forEach.call(letters, function(letter) {
            letter.disabled = false;
        });
    },

    disableLetters : function() {
        //pobieramy litery i robimy po nich pętlę wyłączając je
        const letters  = this.elemLetters.querySelectorAll('.game-letter');
        [].forEach.call(letters, function(letter) {
            letter.disabled = true;
        });
    },

    initBoard : function() {
        this.generateLetterButtons();
        this.bindEvents();
        this.disableLetters(); //przy stworzeniu planszy wyłączamy litery
    },

    startGame : function() {
        this.attempts = 5, //ile prób zostało dla aktualnej gry
        this.randomSentence(); //losujemy hasło do zgadnięcia
        this.showAttempts(); //pokazuje liczbę prób
        this.enableLetters(); //włączamy litery
    }
};

game.initBoard();

Żeby przetestować powyższy skrypt podepnijmy pod przycisk "losuj nowe hasło" odpalenie startGame():


const game = {
    ...
};

game.initBoard();

document.querySelector('.game-start').addEventListener('click', function() {
    game.startGame();
});

Nie mamy jeszcze metod randomSentence() i showAttepts() więc zanim je napiszemy zakomentujmy ich wywołanie w metodzie startGame.

Demo 2 - włączanie liter

metoda showAttempts()

Zaczynamy od odkomentowania łatwiejszej metody - showAttempts(), która będzie służyć do pokazywania liczby prób graczowi:


const game = {
    ...

    showAttempts : function() {
        this.elemAttempts.innerHTML = this.attempts;
    },

    startGame : function() {
        this.attempts = 5, //ile prób zostało dla aktualnej gry
        //this.randomSentence(); //losujemy hasło do zgadnięcia
        this.showAttempts(); //pokazuje liczbę prób
        this.enableLetters(); //włączamy litery
    }
}

metoda randomSentence()

Kolejna metoda do odkomentowania to randomSentence(), która wylosuje dla nas nowe hasło, które będziemy zgadywać.

Aby wylosować hasło z tablicy sentences musimy wylosować liczbę z przedziału między dwoma liczbami. Użyjemy do tego wzoru ze strony https://stackoverflow.com/questions/4959975/generate-random-number-between-two-numbers-in-javascript#answer-7228322.


const max = this.sentences.length-1;
const min = 0;
const rand = Math.floor(Math.random()*(max-min+1)+min);

Po wylosowaniu hasła podstawiamy go pod zmienną currentSentence. Pod zmienną currentSentenceLetters podstawiamy litery wylosowanego hasła pozbawione spacji. Gdy użytkownik zgadnie daną literę będziemy ją usuwać właśnie z tej zmiennej. Dzięki temu na samym końcu wystarczy sprawdzić, czy z tej zmiennej zostały usunięte wszystkie litery. Jeżeli tak - hasło zostało odgadnięte.

Zanim jednak do tego dojdziemy, musimy wyczyścić planszę z hasłem (elemSentence) a następnie wygenerować w niej bloki na których będziemy pokazywać dane litery. Ważne jest to, że między słowami znajduje się spacja. Klocek z taką spacją nie powinien mieć ani obramowania ani tła. Uzyskujemy to przez dodanie takiemu blokowi dodatkowej klasy game-sentence-box-space.


const game = {
    ...

    randomSentence : function() {
        const max = this.sentences.length-1;
        const min = 0;
        const rand = Math.floor(Math.random()*(max-min+1)+min);

        this.currentSentence = this.sentences[rand].toUpperCase();
        this.currentSentenceLetters = this.currentSentence.replace(/ /g, '');

        this.elemSentence.innerHTML = ''; //czyścimy listę

        const letters = this.currentSentence.split('');
        for (let i=0; i<letters.length; i++) {
            const div = document.createElement('div');
            div.classList.add('game-sentence-box');
            if (letters[i] === ' ') {
                div.classList.add('game-sentence-box-space');
            }
            this.elemSentence.appendChild(div);
        }
    },

    ...

    startGame : function() {
        this.attempts = 5, //ile prób zostało dla aktualnej gry
        this.randomSentence(); //losujemy hasło do zgadnięcia
        this.showAttempts(); //pokazuje liczbę prób
        this.enableLetters(); //włączamy litery
    }
}

Demo 3 - pokaż hasło

metoda checkLettersInSentention()

Po kliknięciu na literkę powyżej wypisywaliśmy ją w konsoli. Napiszmy teraz metodę checkLettersInSentention() w której sprawdzimy czy kliknięta litera istnieje w haśle. Jeżeli istnieje, zaznaczymy ją na planszy, oraz usuniemy jej wystąpienie ze zmiennej currentSentenceLetters:


const game = {
    ...

    checkLettersInSentention : function(letter) {
        if (this.currentSentence.indexOf(letter) !== -1) { //jeżeli litera istnieje w haśle
            for (let i=0; i<this.currentSentence.length; i++) {
                if (this.currentSentence[i] === letter) {
                    this.elemSentence.querySelectorAll('.game-sentence-box')[i].innerHTML = letter; //wstawiamy w odpowiedni box wybraną literę
                }
            }

            //usuwamy trafioną literę z currentSentenceLetters
            this.currentSentenceLetters = this.currentSentenceLetters.replace(new RegExp(letter, 'g'), '');

            //jeżeli już nie ma liter w powyższej zmiennej gracz wygrał
            if (!this.isLetterExists()) {
                this.gameComplete();
            }
        } else {  //nie ma takiej litery w haśle
            this.attempts--;
            this.showAttempts();

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

    ...

    bindEvents : function() {
        this.elemLetters.addEventListener('click', function(e) {
            if (e.target.nodeName.toUpperCase() === "BUTTON" && e.target.classList.contains('game-letter')) {
                const letter = e.target.dataset.letter;
                this.checkLettersInSentention(letter.toUpperCase());
                e.target.disabled = true;
            }
        }.bind(this));
    },
}

Po kliknięciu na literkę wywołujemy powyższą metodę checkLettersInSentention() przekazując jej daną literę.

Metoda ta za pomocą indexOf sprawdza czy litera ta istnieje w aktualnie wylosowanym haśle czyli currentSentence. Jeżeli istnieje, robimy pętlę po tym haśle i dla liter na planszy które mają odpowiedni indeks wstawiamy daną literę. Dzięki temu pojawiają się one na planszy.

Kolejnym krokiem jest usunięcie wszystkich wystąpień danej litery ze zmiennej currentSentenceLetters. Dzięki temu wystarczy później sprawdzić, czy zmienna ta ma jakieś litery. Jeżeli jest pusta, wszystkie litery zostały odkryte i gracz wygrał.

Do takiego sprawdzenia napiszemy oddzielną metodę isLetterExists().

Jeżeli dana litera nie istnieje w zmiennej currentSentence, odejmujemy liczbę prób i jeżeli wynosi ona 0, pokazujemy zakończenie gry, za co odpowiedzialna będzie metoda gameOver().

metoda isLetterExists()

Powyższa metoda do sprawdzenia czy istnieją jeszcze jakieś litery w haśle używa metody isLetterExists(). Tak jak było powyżej opisane, wystarczy tutaj sprawdzić, czy w zmiennej currentSentenceLetters istnieją jakieś litery:


const game = {
    ...

    isLetterExists : function() {
        return this.currentSentenceLetters.length;
    },

    checkLettersInSentention : function(letter) {
        if (this.currentSentence.indexOf(letter) !== -1) {   //jeżeli litera istnieje w haśle
            for (let i=0; i<this.currentSentence.length; i++) {
                if (this.currentSentence[i] === letter) {
                    this.elemSentence.querySelectorAll('.game-sentence-box')[i].innerHTML = letter; //wstawiamy w odpowiedni box wybraną literę
                }
            }

            this.currentSentenceLetters = this.currentSentenceLetters.replace(new RegExp(letter, 'g'), '');

            if (!this.isLetterExists()) { //jeżeli nie ma już nieodgadniętych liter...
                this.gameComplete();
            }
        } else {  //nie ma takiej litery w haśle
            this.attempts--;
            this.showAttempts();

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

    ...
}

metoda gameComplete() i gameOver()

Ostatnie metody jakie napiszemy to zakończenie gry - czy to pozytywne, czy negatywne.


const game = {
    ...

    gameOver : function() {
        alert("Niestety nie udało ci się odgadnąć hasła. Ps: brzmi ono: \n\n" + this.currentSentence);
        this.disableLetters();
    },

    gameComplete : function() {
        alert('Udało ci się zgadnąć hasło :)');
        this.disableLetters();
    },

    ...
}

Dla łatwiejszych testów w poniższym diemie przy losowaniu w konsoli wypisuję aktualne hasło.

Demo 4 - kompletna gra

Podsumowanie

Nasza gra nie jest żadnym zaawansowanym tworem. Nie nadaje się do konkursów, bo wystarczy spojrzeć w źródło by znać odpowiedź. Ale jako ciekawostka dla młodszego rodzeństwa - w sam raz. Oczywiście przydała by się ładniejsza, może nieco słodsza oprawa graficzna.

Jak też zauważysz, alert nie za bardzo pasuje do pokazywania informacji o zakończeniu gry. O wiele lepszym rozwiązaniem było by pokazać jakąś dynamicznie tworzoną warstwę z informacją. Może nawet element dialog, który pojawił się w html

Demo końcowe

Paczkę z grą możesz ściągnąć tutaj.