Tic Tac Toe (PixiJS)

imagem

Um tipógrafo vulnerável, estúpido e sonhador decidiu se tornar um programador e nada disso resultou ... Mas ele não desistiu da programação e decidiu começar com pequenos programas ...

Essa é a melhor descrição que eu poderia apresentar. Foi com esse objetivo que comecei a escrever programas simples para aprimorar minhas habilidades, familiarizar-me com novos designs em minha linguagem usual e, para ser sincero, isso até me trouxe prazer.

Se você tiver pouca experiência em desenvolvimento, o artigo será útil e, se você já tiver experiência em desenvolvimento, dedique algum tempo a algo mais valioso.

Isto não é treinamento. Mais como um blog.

O objetivo era fazer 3 versões do jogo tic tac toe.

1 - O mais simples ( sem um visual bonito, usando o DOM )
2 - Dê a oportunidade de jogar juntos (um computador )
3 - Transfira tudo isso para o canvas

Não vou descrever o jogo da velha, espero que todos saibam o princípio do jogo. Todos os links úteis (repositório, documentação) estarão no final do artigo.

O que aconteceu com isso? Hum ...

Primeira versão


imagem

Este é o mais simples. Para ser sincero, as versões subseqüentes não são difíceis ...

Precisamos de um layout do contêiner no qual precisaremos colocar nosso campo de jogo. Eu adicionei item de dados a cada elemento desde Eu pensei que precisaria de um identificador, mas não o usei.

<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>

Quero avisar você imediatamente! Este código não deve ser considerado o único verdadeiro, e escrever de outra forma é considerado um erro. Esta é a minha solução e nada mais.

Assim. Primeiro, precisamos vincular um clique na célula. Durante o clique, vamos (o bot também, mas por sua vez) e verificamos a célula.

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);
			}
		}
	});
}

O bot caminha aleatoriamente.

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));
}

Cell Check

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}
}

Aqui você pode escrever tudo através de loops. Eu escolhi uma maneira mais simples. Meu campo é sempre estático. Portanto, uma simples verificação das células. Vale ressaltar que eu retorno o objeto para verificar no futuro quem venceu. No objeto, val e win properties. Val é responsável por terminar o jogo.

Fim do jogo.

// /
function exit(obj) {
	alert(obj.win + " - game over");
	location.reload();
};

Durante o clique, temos uma verificação para ver se checkMap retornou val: true. Nesse caso, complete o jogo.

Segunda versão


Dois jogadores no mesmo computador.

Participei da lógica do manipulador de cliques em uma função separada e passei o contexto de chamada para a função, porque precisamos determinar qual botão foi clicado.

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);
			}
		}
	});
}


Dividi em duas funções, mas elas têm duplicação de código. Idealmente, divida por 3. Um principal e dois trabalhando com o contexto.

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;
}

Terceira versão


Talvez este seja o ponto mais interessante porque Agora, o jogo realmente parece um jogo, não a interação dos elementos DOM.

Eu escolhi trabalhar no PixiJS. Não posso dizer nada sobre o + e - desta biblioteca, mas observei um exemplo em que havia 60.000 elementos e todos eram animados. A animação é simples, mas o FPS ficou em 50-60. Gostei e comecei a ler a documentação. Devo dizer imediatamente que meu conhecimento do idioma inglês é mínimo, foi difícil, mas há muito poucos artigos em russo (ou pareci mal). Eu tive que cutucar e com a ajuda de um tradutor do google para superar os espinhos.

Eu assisti apenas um relatório sobre este tópico por Yulia Pucnina "Animação gorda com Pixi js" .

O relatório é de 2014 e você precisa entender que a API pode mudar. Um olho na documentação e o segundo no vídeo. Então eu estudei. Demorou 4 horas para escrever um protótipo tão simples. Mais perto do código.

Tornamos pixi de inicialização padrão

const app = new PIXI.Application({
	width: 720,
	height: 390,
	resolution: window.devicePixelRation || 1,
});
document.body.appendChild(app.view);

e também criar um invólucro (o contêiner principal com células) e colocá-lo em nossa tela

let wrapper = new PIXI.Container();
app.stage.addChild(wrapper);

No loop, criamos nossas células, definimos os tamanhos, coordenadas e também adicionamos o valor padrão à célula como uma string vazia, pois isso será útil no futuro e travará manipuladores nas células, depois de ativar o sinalizador de interatividade do contêiner.

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 é responsável pelo progresso de cada jogador. Não encontrei uma maneira melhor do que declarar meus próprios estilos para cada texto. Aí a cor muda, mas eu não entendi como mudar a cor. Cada vez que novos estilos precisam ser atribuídos ao texto. Também há uma verificação de células.

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();
}

Em relação à verificação em si. checkMap. Pelo que entendi, o pixiJS não pode acessar um elemento por nome ou ID. Temos que classificar a coleção inteira no contêiner por causa disso, o código parece complicado. A função não é diferente das anteriores, exceto pelos parâmetros que ela retorna.

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};
}

Bem, as duas últimas funções são responsáveis ​​por terminar o jogo e limpar a tela. Parece-me que a explicação aqui é supérflua.

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;
    }
}

Para resumir, foi interessante realizar o desenvolvimento em várias etapas. Não é um ciclo de desenvolvimento ideal, mas com o que eu precisava para começar.

Obrigado pela leitura e até breve.

Referências



Relatório do Github
de. Site PixiJS

All Articles