Один ранимый, глупый, мечтательный верстальщик решил стать программистом и у него ничего не вышло… Но он не бросил программировать и решил начать с малых программ…
Это лучшее описание, которое я мог придумать. Именно с это целью я начал писать простенькие программы чтобы отточить свои навыки, познакомиться с новыми конструкциями в привычном мне языке и если честно, то это даже стало приносить мне удовольствие.
Если у вас мало опыта разработки, то статья будет полезной, а если у вас уже есть опыта разработки, то потратьте время на что-то более стоящее.
Это не обучение. Больше похоже на блог.
Была цель сделать 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;
}
}
Если подытожить, то было интересно провести разработку в несколько этапов. Пусть не идеальный цикл разработки, но с чего то мне нужно было начинать.
Спасибо, что прочли и до встречи.
Ссылки
Github
Доклад
Оф. сайт PixiJS