Один ранимый, глупый, мечтательный верстальщик решил стать программистом и у него ничего не вышло… Но он не бросил программировать и решил начать с малых программ…
Это лучшее описание, которое я мог придумать. Именно с это целью я начал писать простенькие программы чтобы отточить свои навыки, познакомиться с новыми конструкциями в привычном мне языке и если честно, то это даже стало приносить мне удовольствие.
Если у вас мало опыта разработки, то статья будет полезной, а если у вас уже есть опыта разработки, то потратьте время на что-то более стоящее.
Это не обучение. Больше похоже на блог.
Была цель сделать 3 версии игры крестики нолики.
1 — Самое простое(без красивого визуала, с помощью DOM)
2 — Дать возможность играть вдвоем(один компьютер)
3 — Перенести все это в canvas
Описывать крестики-нолики я не буду, надеюсь, все знают принцип игры. Все полезные ссылки(репозиторий, документация) будут в конце статьи.
Первая версия

Это самое простое. Если честно, то и последующие версии не отличаются сложностью…
Нам нужна верстка из контейнера в котором потребуется разместить наше игровое поле. Я добавил data-item каждому элементу т.к. думал, что потребуется идентификатор, но его я не использовал.
<div class="app"> <div class="app_block" data-item="0"></div> <div class="app_block" data-item="1"></div> <div class="app_block" data-item="2"></div> <div class="app_block" data-item="3"></div> <div class="app_block" data-item="4"></div> <div class="app_block" data-item="5"></div> <div class="app_block" data-item="6"></div> <div class="app_block" data-item="7"></div> <div class="app_block" data-item="8"></div> </div>
Сразу хочу предупредить! Данный код не стоит расценивать как единственно верным и писать иначе считать ошибкой. Это мой способ решения и не более того.
Так. Для начала нам потребуется забиндить клик по ячейке. Во время клика мы ходим(бот тоже, но по очереди) и проверяем ячейку.
var items = document.getElementsByClassName("app_block"); // Коллекция элементов var movePlayer = true; // Ход игрока var game = true;// состояние игры // Перебираем все элементы и назначаем событие клик на ячейку. for (var i = 0; i < items.length; i++) { items[i].addEventListener("click", function() { var collecion = document.querySelectorAll(".app_block:not(.active)"); // Проверка на ничью if(collecion.length == 1) { exit({win: "other"}); } // проверка на значение внутри ячейки if( !this.classList.contains("active") ){ // если ходит игрок if( movePlayer) { // если ячейка свободна if(this.innerHTML == "") { // занять ячейку this.classList.add("active"); this.classList.add("active_x"); this.innerHTML = "x" } // проверка ячеек и выход var result = checkMap(); if( result.val) { game = false; setTimeout(function() { exit(result); }, 10); } movePlayer = !movePlayer; } // если все еще играем, то ходит бот if(game) { setTimeout(function() { botMove(); }, 200); } } }); }
Бот ходит рандомно.
function botMove() { var items = document.querySelectorAll(".app_block:not(.active)"); var step = getRandomInt(items.length); items[ step ].innerHTML = "0"; items[ step ].classList.add("active"); items[ step ].classList.add("active_o"); var result = checkMap(); if( result.val) { setTimeout(function() { exit(result); }, 1); } movePlayer = !movePlayer; } function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); }
Проверка ячеек
function checkMap() { var block = document.querySelectorAll(".app_block"); var items = []; for (var i = 0; i < block.length; i++) { items.push(block[i].innerHTML); } if ( items[0] == "x" && items[1] == 'x' && items[2] == 'x' || items[3] == "x" && items[4] == 'x' && items[5] == 'x' || items[6] == "x" && items[7] == 'x' && items[8] == 'x' || items[0] == "x" && items[3] == 'x' && items[6] == 'x' || items[1] == "x" && items[4] == 'x' && items[7] == 'x' || items[2] == "x" && items[5] == 'x' && items[8] == 'x' || items[0] == "x" && items[4] == 'x' && items[8] == 'x' || items[6] == "x" && items[4] == 'x' && items[2] == 'x' ) return { val: true, win: "player"} if ( items[0] == "0" && items[1] == '0' && items[2] == '0' || items[3] == "0" && items[4] == '0' && items[5] == '0' || items[6] == "0" && items[7] == '0' && items[8] == '0' || items[0] == "0" && items[3] == '0' && items[6] == '0' || items[1] == "0" && items[4] == '0' && items[7] == '0' || items[2] == "0" && items[5] == '0' && items[8] == '0' || items[0] == "0" && items[4] == '0' && items[8] == '0' || items[6] == "0" && items[4] == '0' && items[2] == '0' ) return { val: true, win: "bot"} return {val: false} }
Здесь можно было написать все через циклы. Я выбрал более простой путь. У меня поле всегда статично. Поэтому простая проверка ячеек. Стоит отметить, что я возвращаю объект чтобы в будущем проверить кто одержал победу. В объекте свойства val и win. Val отвечает за окончание игры.
Конец игры.
// выход/перезагрузка function exit(obj) { alert(obj.win + " - game over"); location.reload(); };
Во время клика у нас есть проверка, а вернул ли checkMap val: true. Если да, то завершаем игру.
Вторая версия
Два игрока за одним компьютером.
Вынес часть логики из обработчика клика в отдельную функцию и передаю в функцию контекст вызова, ведь нам нужно определить на какую кнопку жмякнули.
var items = document.getElementsByClassName("app_block"); var movePlayer = true; var game = true; for (var i = 0; i < items.length; i++) { items[i].addEventListener("click", function() { var collecion = document.querySelectorAll(".app_block:not(.active)"); if(collecion.length == 1) { exit({win: "other"}); } if( !this.classList.contains("active") ){ if( movePlayer) { firstPlayer(this); } else { secondPlayer(this); } } }); }
Я разделил на две функции, но в них есть дублирование кода. В идеале разделить на 3. Одна основная, а две работающие с контекстом.
function firstPlayer(that) { if(that.innerHTML == "") { that.classList.add("active"); that.classList.add("active_x"); that.innerHTML = "x" } var result = checkMap(); if( result.val) { game = false; setTimeout(function() { exit(result); }, 10); } movePlayer = !movePlayer; } function secondPlayer(that) { if(that.innerHTML == "") { that.classList.add("active"); that.classList.add("active_o"); that.innerHTML = "0" } var result = checkMap(); if( result.val) { game = false; setTimeout(function() { exit(result); }, 10); } movePlayer = !movePlayer; }
Третья версия
Пожалуй это самый интересный пункт т.к. теперь игра действительно похожа на игру, а не на взаимодействие DOM элементов.
Я выбрал для работы PixiJS. Не могу сказать ничего о + и — этой библиотеки, но я посмотрел один пример в котором было 60 000 элементов и все они анимированные. Анимация простая, но FPS держался на 50-60. Мне это понравилось и я стал читать документацию. Скажу сразу, знания анг языка у меня минимальны, было сложно, а на Русском статей очень мало.(или я плохо искал). Пришлось методом тыка и с помощью гуугл переводчика пробираться через тернии.
Посмотрел лишь один доклад на эту тему Юлия Пучнина «Жирная анимация с Pixi js».
Доклад от 2014 года и нужно понимать, что API могло измениться. Одним глазом в документацию, а вторым на видео. Так и изучал. Хватило 4 часа чтобы написать такой простенький прототип. Ближе к коду.
Производим дефолтную инициализацию pixi
const app = new PIXI.Application({ width: 720, height: 390, resolution: window.devicePixelRation || 1, }); document.body.appendChild(app.view);
а так же создадим wrapper(основной контейнер с ячейками) и поместим его в наш canvas
let wrapper = new PIXI.Container(); app.stage.addChild(wrapper);
В цикле мы создаем наши ячейки, задаем им нужные размеры, координаты, а так же добавляет дефолтное значение ячейке в виде пустой строки т.к. это пригодится в будущем и вешаем на ячейки обработчики, предварительно включив флаг интерактивности у контейнера.
for (let i = 0; i < 9; i++) { let container = new PIXI.Container(); let block = new PIXI.TilingSprite( PIXI.Texture.from("images/bg.png") , 240, 130); container.x = (i % 3) * 240; container.y = Math.floor(i / 3) * 130; container.addChild(block); let text = new PIXI.Text(""); text.anchor.set(0.5); text.x = container.width / 2; text.y = container.height / 2; container.addChild(text); container.interactive = true; container.on("mousedown", function () { addValueInBlock(this); }); wrapper.addChild(container); }
addValueInBlock отвечает за ход каждого игрока. Я не нашел лучше способа чем объявлять для каждого текста свои стили. Там меняется цвет, а как изменить цвет так и не разобрался. Приходится каждый раз новые стили задавать тексту. Также здесь идет проверка ячеек.
function addValueInBlock(that) { if(firstPlayer) { // Ход первого игрока - X if( that.children[1].text == " " ) { that.children[1].style = { fill: "#d64c42", fontFamily: "Arial", fontSize: 32, fontWeight: "bold", }; that.children[1].text = "x" firstPlayer = !firstPlayer; } } else { // Ход второго игрока - 0 if( that.children[1].text == " " ) { that.children[1].style = { fill: "#e2e3e8", fontFamily: "Arial", fontSize: 32, fontWeight: "bold", }; that.children[1].text = "0" firstPlayer = !firstPlayer; } } endGame(); }
Касаемо самой проверки. checkMap. Я так понял, у pixiJS нельзя обратиться к элементу по имени или id. Приходится перебирать всю коллекцию в контейнере из-за этого код выглядит громозким. Функция ничем не отличается от предыдущих, кроме параметров, которые она возвращает.
function checkMap() { let items = wrapper.children; if ( items[0].children[1].text == "x" && items[1].children[1].text == 'x' && items[2].children[1].text == 'x' || items[3].children[1].text == "x" && items[4].children[1].text == 'x' && items[5].children[1].text == 'x' || items[6].children[1].text == "x" && items[7].children[1].text == 'x' && items[8].children[1].text == 'x' || items[0].children[1].text == "x" && items[3].children[1].text == 'x' && items[6].children[1].text == 'x' || items[1].children[1].text == "x" && items[4].children[1].text == 'x' && items[7].children[1].text == 'x' || items[2].children[1].text == "x" && items[5].children[1].text == 'x' && items[8].children[1].text == 'x' || items[0].children[1].text == "x" && items[4].children[1].text == 'x' && items[8].children[1].text == 'x' || items[6].children[1].text == "x" && items[4].children[1].text == 'x' && items[2].children[1].text == 'x' ) { return {active: true, win: "player 1"}; } if ( items[0].children[1].text == "0" && items[1].children[1].text == '0' && items[2].children[1].text == '0' || items[3].children[1].text == "0" && items[4].children[1].text == '0' && items[5].children[1].text == '0' || items[6].children[1].text == "0" && items[7].children[1].text == '0' && items[8].children[1].text == '0' || items[0].children[1].text == "0" && items[3].children[1].text == '0' && items[6].children[1].text == '0' || items[1].children[1].text == "0" && items[4].children[1].text == '0' && items[7].children[1].text == '0' || items[2].children[1].text == "0" && items[5].children[1].text == '0' && items[8].children[1].text == '0' || items[0].children[1].text == "0" && items[4].children[1].text == '0' && items[8].children[1].text == '0' || items[6].children[1].text == "0" && items[4].children[1].text == '0' && items[2].children[1].text == '0' ) { return {active: true, win: "player 2"}; } return {active: false}; }
Ну и две последних функции отвечают за окончание игры и очистку канваса. Мне кажется, объяснение здесь лишнее.
function endGame() { var result = checkMap(); console.log(result); if( result.active ) { setTimeout(function() { alert(result.win + " - win"); clearMap(); }, 100); } } function clearMap() { console.log("sdf"); let items = wrapper.children; for(let i = 0; i < items.length; i++) { console.log( items[i].children[1].text ); items[i].children[1].text = ""; firstPlayer = true; } }
Если подытожить, то было интересно провести разработку в несколько этапов. Пусть не идеальный цикл разработки, но с чего то мне нужно было начинать.
Спасибо, что прочли и до встречи.
Ссылки
ссылка на оригинал статьи https://habr.com/ru/post/489490/
Добавить комментарий