Практика 2: Крестики-нолики

Задача:

Написать игру крестики-нолики на js (без использования php). Важно, чтобы данные сохранялись при обновлении страницы, были кнопки сбросить результат.  Написать бота (метод игры случайно).

Решение:

Шаг 1:

Первым делом сверстаем сайт. «Вначале было поле». Сделаем кнопки размером 150px.

<div class="poligon">
    <a></a> <a></a> <a></a>
    <a></a> <a></a> <a></a>
    <a></a> <a></a> <a></a>
</div>

А также создадим табло для вывода результатов и кнопку сброса.

<div class="info">
    <p></p>
    <a class="reset"></a>
</div>

Ну как бы и все… Давайте украшать. У нас может быть 2 варианта ячейка пустая и заполненная. Обозначим заполненную ячейку классом .set. Пустая так и останется пустой. Украсим кнопки:

.poligon a{
    cursor: default;
    display: block;
    width: 150px;
    height: 150px;
    margin: 5px;
    color: #fff;
    background: #9e7d80;
    border: 2px solid #6f5f61;
    box-sizing: border-box;
    border-radius: 15%;
    text-align: center;
    font-size: 100px;
    line-height: 130px;
    text-transform: lowercase;
}
.poligon a:not(.set){
    cursor: pointer;
}
.poligon a:not(.set):hover{
    background: #c7b8b8;
    border: 2px solid #696060;
}

cursor — тип курсора, по умолчанию стрелка.
display — поменяем тип на блок, по умолчанию у тега <a> — inline
width и height — ширина и высота у нас квадрат поэтому 150
margin — отступы между квадратами
background — цвет фона
border — рамка
border-radius — закруглили края (не знаю зачем, мне больше нравится острые, но пусть будет)
text-align — выравниваем текст по центру. нули и кресты будем делать буквами о и х.
font-size — увеличили размер букв
line-height — высота строки у букв (похоже на невидимую полоску как в прописях). таким способом выравниваем по высоте.
text-transform — ну и сделаем в нижнем регистре
a:not(.set) — если у тега а класс не set, то курсор сделаем «руку».
.poligon a:not(.set):hover — если ячейка пустая, то при наведении (hover) заливка и рамка будут посветлее.

Получилось красота. Теперь центруем и выровняем по 3 в ряд.

.poligon{
    display: flex;
    flex-flow: wrap;
    width: 480px;
    margin: 0 auto;
}

flex-flow — по умолчанию у flex блоки не переносятся, сообщили что переносить все же нужно.
width — выравниваем шириной, у нас кнопки по 150px с 5px отступы со всех сторон. получается 150+5*2 = 160px — одна кнопка, и таких 3 в ряд = 480px. можно и подругому выровнять, но мы поступим так.
margin — выравниваем все по центру.

Немного украсили табло:

.info{
    text-align: center;
    font-size: 16px;
    margin-bottom: 50px;
}
.info p{
    font-weight: bold;
    height: 20px;
}
.reset{
    cursor: pointer;
    background: #9e7d80;
    border: 2px solid #6f5f61;
    box-sizing: border-box;
    text-align: center;
    padding: 8px 32px;
    color: #fff;   
}

height — укажем чтобы не прыгал при отсутствии текста

Супер. Теперь добавим событие установки знака в ячейку. Первыми будут ходить крестики. Создадим глобальную переменную и установим значение по умолчанию «х»

var simbol = "x";

Нам понадобится функция смены символа с крестика на нолик и наоборот.

function simbolTurn(){
    simbol = (simbol=='x')? 'o' : 'x';
}

В функции мы не создаем переменную simbol, а обращаемся к уже существующей — глобальной. Если приписать var то мы создадим новую переменную внутри функции о которой снаружи «никто» не знает. Также мы воспользовались короткой записью условного оператора if, читается так, если переменная simbol равна x, то присваиваем ей значение о, во всех других случаях х.

Теперь по клику на кнопку будем записывать симов внутрь и устанавливать класс set.

$('.poligon a').on('click', function(event){
    $(this).addClass('set');
    $(this).html(simbol);
});

С помощью стандартной функции jquery $(‘ селектор ‘) найдем наши кнопки и прикрутим к ним событие click, по которому будет выполняться функция function. В переменную event записываются данные события, но они нас не интересуют, ее можно не указывать. В нашем случае поиск по селектору вернет все 9 кнопок, и автоматически для всех них установит функцию.

Обратите внимание мы устанавливаем класс и выводим значение. Это можно было записать и так:

$(this).addClass('set').html(simbol);

Так же сделаем проверку, если класс set уже установлен то выйдет из функции. Выйти нужно до выполнения действий, так что добавим ее в начало.

if ( $(this).hasClass('set') ) return; 

И вызываем функцию смены символа. Вот что у нас в итоге получилось.

$('.poligon a').on('click', function(event){
    if ( $(this).hasClass('set') ) return; 
    $(this).addClass('set').html( simbol );
    simbolTurn();
});

Теперь мы можем заполнить все ячейки поочередно, давайте напишем сброс. При клике установим в стартовое положение символ, сбросим содержимое и класс ячеек.

$('.reset').on('click', function(event){
    simbol = "x";
    $('.poligon a').removeClass('set').html('');
});

Нужно определить победителя. Создадим глобальную переменную, в которую будем сохранять символ победителя (х или о). Вначале у нас она будет пустая.

var winner = "";

Создадим функцию поиска победителя win(). Чтобы определить победителя нужно перебрать все ячейки и сравнить комбинации. Будем записывать не пустые значения, определим их по классу set, во временный массив matrix.

function win(){
    var matrix = new Array();
    $('.poligon a').each(function(k,v){
        matrix[k] = ( $(v).hasClass('set') ) ? 1 : 0;
    });
}

Функция .each() библиотеки jquery это цикл, выполняет указанную функцию для каждого элемента. Функция принимает 2 параметра, ключ и значение. В нашем случае поиск по селектору вернет 9 кнопок, а цикл пробежится по каждому из них. Значение будет <a></a>, а ключем порядковый номер в списке. Порядковый номер в списке начинается с 0, будте внимательны.

Теперь у нас все значения попадут в массив, а нам нужно разделить крестики от ноликов. Я создам массив и переберу его точно так же как предыдущий раз DOM объекты.

$(['x','o']).each(function(key,s){
    .... цикл с перебором кнопок
});

У нас получается следующая схема:

В первом цикле в параметре s у нас содержится х. Далее мы бежим по кнопкам, если значение кнопки не х, то пропускаем. В результате в массиве matrix будет хранится только значения х. Так же будет и при следующей итерации цикла для о.

$(['x','o']).each(function(key,s){
    $('.poligon a').each(function(k,v){
        if ($(v).html()!=s) return;
        ...
    });
});

Теперь мы можем сравнить комбинации и записать победителя. Нумерация списков в массивах начинается с 0, поэтому мы имеем такое поле:

0   1 2
3   4 5
6   7 8

Собираем комбинации, если 0 и 1 и 2 то выиграл и т.д., в результате получаем комбинации:

if ( matrix[0] && matrix[1] && matrix[2] ) winner = s;
if ( matrix[3] && matrix[4] && matrix[5] ) winner = s;
if ( matrix[6] && matrix[7] && matrix[8] ) winner = s;
if ( matrix[0] && matrix[3] && matrix[6] ) winner = s;
if ( matrix[1] && matrix[4] && matrix[7] ) winner = s;
if ( matrix[2] && matrix[5] && matrix[8] ) winner = s;
if ( matrix[0] && matrix[4] && matrix[8] ) winner = s;
if ( matrix[2] && matrix[4] && matrix[6] ) winner = s;

Ну и наконец посчитаем всего установленных значений, если все 9 и никто не победил, значит ничья. Ах да, еще и вывод в табло текста. Получилась такая функция:

function win(){
    var count = 0;
    $(['x','o']).each(function(key,s){
        var matrix = new Array();
        $('.poligon a').each(function(k,v){
            if ($(v).html()!=s) return;
            matrix[k] = ( $(v).hasClass('set') ) ? 1 : 0;
            count++;
        });
        
        if ( matrix[0] && matrix[1] && matrix[2] ) winner = s;
        if ( matrix[3] && matrix[4] && matrix[5] ) winner = s;
        if ( matrix[6] && matrix[7] && matrix[8] ) winner = s;
        if ( matrix[0] && matrix[3] && matrix[6] ) winner = s;
        if ( matrix[1] && matrix[4] && matrix[7] ) winner = s;
        if ( matrix[2] && matrix[5] && matrix[8] ) winner = s;
        if ( matrix[0] && matrix[4] && matrix[8] ) winner = s;
        if ( matrix[2] && matrix[4] && matrix[6] ) winner = s;
    });
    
    if (winner) $(".info p").html("Победа за "+winner);
    else if (count==9) $(".info p").html("Ничья "+winner);
}

Добавим эту функцию в конец обработчика клика по ячейке. А также запретим кликать если определен победитель.

$('.poligon a').on('click', function(event){
    if ( winner ) return; 
    ...
    simbolTurn();
});

И добавим в сброс очистку табло и сброс победителя.

$('.reset').on('click', function(event){
    ...
    winner = "";
    $(".info p").html('');
});

Вот уже можно играть вдвоем.

Шаг 2:

Теперь сделаем робота-игрока — бота, пусть играет за крестики. Он будет случайным образом ставить в свободную ячейку символ. Создадим функцию bot. Вначале запретим (выйдем из функции) ходить боту, если ходит нолик или есть победитель.

if (simbol=='o' || winner) return;

Получим список всех пустых ячеек, т.е. тех у которых не установлен класс set.

var list = $('.poligon a:not(.set)');

Получим их количество.

var length = list.length;

Теперь нужно получить случайное число от 0 (начало массива), до length. В js есть функция которая возвращает случайное дробное число от 0 до 1. Создадим функцию получения случайного целого числа.

function rand(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

Теперь можно ей воспользоваться.

var random = rand(0,length);

Ну и наконец получим из ранее полученного массива списка элемент по ключу.

var ceil = $('.poligon a:not(.set)')[random];

Теперь можно имитировать клик по этой кнопке с помощью jquery функции trigger.

$(ceil).trigger('click');

Теперь нам надо запустить бота и каждый раз проверять походил человек или нет. Можно было бы подставить ход бота в функцию клика, т.о. получалось что бот ходил бы после вас по клику. Но крестики у нас ходят первыми, поэтому запустим интервал. Это механизм js позволяет вызывать укаанную функцию через указанный промежуток времени. Будем дергать функцию bot каждые 100 миллисекунд (1сек = 1000мсек).

setInterval(bot,100);

Вот что у нас получилось в итоге:

function rand(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

function bot(){
    if (simbol=='o' || winner) return;
    var list = $('.poligon a:not(.set)'),
        length = list.length,
        random = rand(0,length-1),
        ceil = $('.poligon a:not(.set)')[random];
        
    $(ceil).trigger('click');
}

setInterval(bot,100);

Теперь сохраним состояние. В js существует локальное хранилище. После каждого хода будем сохранять текущее состояние. Напишем функцию сохранения save(). И не забудем добавить ее в конец каждого клика.

function save(){
    var matrix = new Array();
    $('.poligon a').each(function(k,v){
        matrix[k] = $(v).html();
    });
    localStorage.setItem('poligon', matrix.join());
    localStorage.setItem('simbol', simbol);
}

Мы перебираем все ячейки и сохраняем данные в массив. Потом мы разбиваем массив в строку matrix.join(), через разделитель по умолчанию это запятая. И сохраняем в локальном хранилище  localStorage.setItem( Ключ, значение ). А так же сохранили состояние хода (кто ходит крестик или нолик).Теперь надо загрузить сохранения при загрузки страницы.

function load(){
    var poligon = localStorage.getItem('poligon'),
        matrix = poligon.split(',');

    simbol = localStorage.getItem('simbol');
        
    $('.poligon a').each(function(k,v){
        if (matrix[k]) $(v).html( matrix[k] ).addClass('set');
    });
}
load();

Получили значения из локального хранилища localStorage.getItem(‘poligon’), разбили в массив, перебрали все ячейки и если в сохраненном массиве есть заполненное значение, то записываем его в ячейку и не забываем ставить класс set. Право хода simbol записали в глобальную переменную (без var впереди). Ну и тут же вызвали функцию загрузки. Вот и все, чтобы отключить бота достаточно закомментировать строку «setInterval(bot,100);«.