Крестики-нолики (PixiJS)

от автора

image

Один ранимый, глупый, мечтательный верстальщик решил стать программистом и у него ничего не вышло… Но он не бросил программировать и решил начать с малых программ…

Это лучшее описание, которое я мог придумать. Именно с это целью я начал писать простенькие программы чтобы отточить свои навыки, познакомиться с новыми конструкциями в привычном мне языке и если честно, то это даже стало приносить мне удовольствие.

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

Это не обучение. Больше похоже на блог.

Была цель сделать 3 версии игры крестики нолики.

1 — Самое простое(без красивого визуала, с помощью DOM)
2 — Дать возможность играть вдвоем(один компьютер)
3 — Перенести все это в canvas

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

Что из этого вышло? Хм…

Первая версия

image

Это самое простое. Если честно, то и последующие версии не отличаются сложностью…

Нам нужна верстка из контейнера в котором потребуется разместить наше игровое поле. Я добавил 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

ссылка на оригинал статьи https://habr.com/ru/post/489490/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *