En este post te mostraré el juego de Conecta 4 programado en JavaScript con HTML y Vue, con estilos de Bootstrap.
Es el juego de Conecta 4 pero versión web con opción jugador contra jugador, así como jugador contra CPU que usa una pequeña inteligencia artificial.
A lo largo del post te mostraré cómo funciona el juego, qué tecnologías he usado, estilos, etcétera. También te mostraré cómo descargar el código fuente, pues el juego es totalmente gratuito y open source. Finalmente te dejaré una demostración para jugar conecta 4 en línea.
El tablero de juego es una tabla dinámica que se dibuja a partir de un arreglo. Le he quitado los bordes y he reducido el padding, además de agregarle un color de fondo azul para que parezca el tablero del juego real (es decir, el del mundo real, el físico).
<style>
td {
background-color: #638CFF;
border: 0px ;
padding: 5px ;
}
.img-player{
max-width: 100px;
}
</style>
Dentro de cada celda de la tabla (o td
) he colocado una imagen que puede ser:
Estas imágenes se pueden cambiar ya sea en el código o realmente en el sistema de archivos. La imagen viene dada por una función que dependiendo del valor devuelve una imagen distinta.
Como lo dije anteriormente, el tablero es una tabla que se ve así:
<table class="table table-bordered">
<thead>
<tr>
<th v-for="i in COLUMNS">
<button :disabled="!canPlay" @click="makeMove(i)" class="btn btn-warning">Make move
here <i class="fa fa-arrow-down"></i></button>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in board">
<td v-for="cell in row">
<img class="img-fluid img-player" :src="cellImage(cell)" alt="">
</td>
</tr>
</tbody>
</table>
El encabezado de la tabla tiene un botón que hace que la pieza se coloque en esa columna, es decir, deja caer la pieza.
Debido a que es una matriz o arreglo de dos dimensiones, primero recorremos toda la matriz para tener la fila. Y dentro de esa fila repetimos una celda por cada valor que haya en la fila.
En cada celda habrá una imagen que tendrá como fuente lo que devuelva el método cellImage
, el cual veremos a continuación cuando veamos la programación en Vue.
Eso es todo en cuanto al diseño de la interfaz, es momento de pasar a ver el código JavaScript para este juego de Conecta 4 versión web.
El método que evalúa la imagen y devuelve la ruta es el siguiente:
cellImage(cell) {
if (cell === this.PLAYER_1) {
return "img/player1.png";
} else if (cell === this.PLAYER_2) {
return "img/player2.png";
} else {
return "img/empty.png"
}
},
Como lo dije, es un simple if. Lo dejé de esa manera para que sea expresivo, aunque si tú quieres, puedes usar diccionarios, concatenar la celda, etcétera, así como usar distintas imágenes.
Podemos definir el tamaño del tablero o el número de piezas que se deben conectar, de este modo se podría jugar a Conecta 3, conecta 5, etcétera.
const COLUMNS = 7,
ROWS = 6,
EMPTY_SPACE = " ",
PLAYER_1 = "o",
PLAYER_2 = "x",
PLAYER_CPU = PLAYER_2,
CONNECT = 4; // <-- Change this and you can play connect 5, connect 3, connect 100 and so on!
Si te fijas, cuando se juega conecta 4 contra el CPU, se toma al mismo como el jugador 2.
Tenemos la siguiente función que resetea el juego. Lo que hace primero es preguntar el modo de juego: jugador contra jugador, o jugador contra CPU.
El siguiente paso es limpiar el tablero de juego, rellenándolo con espacios vacíos. Después, selecciona un jugador al azar, que es el que toma el primer turno.
async resetGame() {
await this.askUserGameMode();
this.fillBoard();
this.selectPlayer();
this.makeCpuMove();
},
Finalmente le indica al CPU que haga su movimiento en caso de que sea su turno.
Cuando el jugador o usuario presiona el botón para colocar la pieza, se ejecuta el siguiente código en donde se valida si la columna todavía no está llena, y se comprueba si hay un empate o si el jugador ha ganado.
El método recibe el número de columna.
async makeMove(columnNumber) {
const columnIndex = columnNumber - 1;
const firstEmptyRow = this.getFirstEmptyRow(columnIndex, this.board);
if (firstEmptyRow === -1) {
Swal.fire('Cannot put here, it is full');
return;
}
Vue.set(this.board[firstEmptyRow], columnIndex, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
this.makeCpuMove();
} else {
this.askUserForAnotherMatch();
}
},
La pieza es colocada en la matriz, usando la columna indicada (menos 1) y en la fila superior vacía. Es decir, simplemente se cambia el valor que hay en esa celda y Vue se encarga, automáticamente, de refrescarlo en la tabla.
Finalmente se intercambia de jugador y se le indica al CPU que haga su movimiento en caso de que sea su turno y de que el modo de juego sea CPU contra jugador.
Por cierto, en caso de que exista un empate o un ganador, se le pregunta al usuario si quiere jugar de nuevo. En ese caso, se resetea el juego.
En este juego hay un ganador cuando hay 4 piezas conectadas en cualquier dirección en línea recta. Por lo tanto simplemente hay que contar en todas las posibles direcciones para ver si en la matriz existe un Conecta 4 para el jugador en cuestión.
Para ello he creado algunas funciones que realizan el conteo en varias direcciones:
countUp(x, y, player, board) {
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
for (; startY <= y; startY++) {
if (board[startY][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let counter = 0;
for (; x <= endX; x++) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countUpRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
while (x <= endX && startY <= y) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y--;
}
return counter;
},
countDownRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let endY = (y + CONNECT < ROWS) ? y + CONNECT - 1 : ROWS - 1;
let counter = 0;
while (x <= endX && y <= endY) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y++;
}
return counter;
},
Lo que hace el código es recorrer el tablero de un punto a otro y aumentar un contador en caso de encontrar piezas adyacentes. Si encuentra una pieza que no es del mismo color, entonces reinicia el contador a cero.
La función que decide si es un ganador es la siguiente:
isWinner(player, board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
let count = 0;
count = this.countUp(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countUpRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countDownRight(x, y, player, board);
if (count >= CONNECT) return true;
}
}
return false;
},
Lo que hace es contar en todas las direcciones, y si detecta que hay una fila seguida de piezas que conectan y que son mayor al número necesario para ganar, entonces regresa true
. De lo contrario, false
.
Por otro lado, la función para saber si hay un empate en este juego, se utiliza una función que recorre toda la matriz. Si se encuentra un espacio vacío, significa que todavía no es empate, así que se regresa false
.
En caso de terminar de recorrer toda la matriz y no encontrar un espacio vacío, se regresa true
.
isTie(board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
const currentCell = board[y][x];
if (currentCell === EMPTY_SPACE) {
return false;
}
}
}
return true;
},
Como lo dije anteriormente, este juego soporta jugar contra el CPU. He programado una pequeña inteligencia artificial que elige la mejor columna basándose en el algoritmo para intentar ganar conecta 4.
async makeCpuMove() {
if (!this.isCpuPlaying || this.currentPlayer !== PLAYER_CPU) {
return;
}
const bestColumn = this.getBestColumnForCpu();
const firstEmptyRow = this.getFirstEmptyRow(bestColumn, this.board);
console.log({ firstEmptyRow });
Vue.set(this.board[firstEmptyRow], bestColumn, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
} else {
this.askUserForAnotherMatch();
}
},
Primero se verifica si es el turno del CPU y si el modo de juego es jugador contra cpu. Después, se elige la mejor columna y se coloca el valor en el tablero, es decir, se coloca la pieza.
Después de eso, se revisa el estado del juego para saber si hay un empate o un ganador. Si no hay ganador ni empate, se intercambia el jugador y es el turno del humano.
Las funciones que permiten elegir la mejor columna se ven a continuación y se basan en una serie de reglas:
getBestColumnForCpu() {
const winnerColumn = this.getWinnerColumn(this.board, this.currentPlayer);
if (winnerColumn !== -1) {
console.log("Cpu chooses winner column");
return winnerColumn;
}
// Check if adversary wins in the next move, if so, we take it
const adversary = this.getAdversary(this.currentPlayer);
const winnerColumnForAdversary = this.getWinnerColumn(this.board, adversary);
if (winnerColumnForAdversary !== -1) {
console.log("Cpu chooses take adversary's victory");
return winnerColumnForAdversary;
}
const cpuStats = this.getColumnWithHighestScore(this.currentPlayer, this.board);
const adversaryStats = this.getColumnWithHighestScore(adversary, this.board);
console.log({ adversaryStats });
console.log({ cpuStats });
if (adversaryStats.highestCount > cpuStats.highestCount) {
console.log("CPU chooses take adversary highest score");
// We take the adversary's best move if it is higher than CPU's
return adversaryStats.columnIndex;
} else if (cpuStats.highestCount > 1) {
console.log("CPU chooses highest count");
return cpuStats.columnIndex;
}
const centralColumn = this.getCentralColumn(this.board);
if (centralColumn !== -1) {
console.log("CPU Chooses central column");
return centralColumn;
}
// Finally we return a random column
console.log("CPU chooses random column");
return this.getRandomColumn(this.board);
},
getWinnerColumn(board, player) {
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
//Proceed only if row is ok
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
// If this is winner, return the column
if (this.isWinner(player, boardClone)) {
return i;
}
}
}
return -1;
},
getColumnWithHighestScore(player, board) {
const returnObject = {
highestCount: -1,
columnIndex: -1,
};
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
const firstFilledRow = this.getFirstFilledRow(i, boardClone);
if (firstFilledRow !== -1) {
let count = 0;
count = this.countUp(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countUpRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countDownRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
}
}
}
return returnObject;
},
getRandomColumn(board) {
while (true) {
const boardClone = JSON.parse(JSON.stringify(board));
const randomColumnIndex = this.getRandomNumberBetween(0, COLUMNS - 1);
const firstEmptyRow = this.getFirstEmptyRow(randomColumnIndex, boardClone);
if (firstEmptyRow !== -1) {
return randomColumnIndex;
}
}
},
getCentralColumn(board) {
const boardClone = JSON.parse(JSON.stringify(board));
const centralColumn = parseInt((COLUMNS - 1) / 2);
if (this.getFirstEmptyRow(centralColumn, boardClone) !== -1) {
return centralColumn;
}
return -1;
},
Lo que se hace es simular un movimiento en el tablero (clonando primero al original, para no modificarlo) y a partir del mismo saber:
Finalmente el código completo de JavaScript queda como se ve a continuación (el proyecto completo lo dejaré al final del post).
No he detallado algunas funciones, pues me parece que son muy simples, por ejemplo, en donde se muestra al ganador o se pregunta el modo de juego usando Sweet Alert 2.
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Creado por Parzibyte (https://parzibyte.me). Este encabezado debe mantenerse intacto,
excepto si este es un proyecto de un estudiante.
*/const COLUMNS = 7,
ROWS = 6,
EMPTY_SPACE = " ",
PLAYER_1 = "o",
PLAYER_2 = "x",
PLAYER_CPU = PLAYER_2,
CONNECT = 4; // <-- Change this and you can play connect 5, connect 3, connect 100 and so on!
new Vue({
el: "#app",
data: () => ({
board: [],
COLUMNS,
ROWS,
PLAYER_1,
PLAYER_2,
PLAYER_CPU,
EMPTY_SPACE,
currentPlayer: null,
isCpuPlaying: true,
canPlay: false,
}),
async mounted() {
await Swal.fire(
'Connect 4 game',
'Brought to you by parzibyte - https://parzibyte.me',
'info'
);
this.resetGame();
},
methods: {
async resetGame() {
await this.askUserGameMode();
this.fillBoard();
this.selectPlayer();
this.makeCpuMove();
},
async askUserGameMode() {
this.canPlay = false;
const result = await Swal.fire({
title: 'Choose game mode',
text: "Do you want to play against another player or against CPU?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#fdbf9c',
cancelButtonColor: '#4A42F3',
cancelButtonText: 'Me Vs another player',
confirmButtonText: 'Me Vs CPU'
});
this.canPlay = true;
this.isCpuPlaying = !!result.value;
},
countUp(x, y, player, board) {
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
for (; startY <= y; startY++) {
if (board[startY][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let counter = 0;
for (; x <= endX; x++) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countUpRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
while (x <= endX && startY <= y) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y--;
}
return counter;
},
countDownRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let endY = (y + CONNECT < ROWS) ? y + CONNECT - 1 : ROWS - 1;
let counter = 0;
while (x <= endX && y <= endY) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y++;
}
return counter;
},
isWinner(player, board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
let count = 0;
count = this.countUp(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countUpRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countDownRight(x, y, player, board);
if (count >= CONNECT) return true;
}
}
return false;
},
isTie(board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
const currentCell = board[y][x];
if (currentCell === EMPTY_SPACE) {
return false;
}
}
}
return true;
},
getRandomNumberBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
selectPlayer() {
if (this.getRandomNumberBetween(0, 1) === 0) {
this.currentPlayer = PLAYER_1;
} else {
this.currentPlayer = PLAYER_2;
}
},
togglePlayer() {
this.currentPlayer = this.getAdversary(this.currentPlayer);
},
getAdversary(player) {
if (player === PLAYER_1) {
return PLAYER_2;
} else {
return PLAYER_1;
}
},
fillBoard() {
this.board = [];
for (let i = 0; i < ROWS; i++) {
this.board.push([]);
for (let j = 0; j < COLUMNS; j++) {
this.board[i].push(EMPTY_SPACE);
}
}
},
cellImage(cell) {
if (cell === this.PLAYER_1) {
return "img/player1.png";
} else if (cell === this.PLAYER_2) {
return "img/player2.png";
} else {
return "img/empty.png"
}
},
async makeMove(columnNumber) {
const columnIndex = columnNumber - 1;
const firstEmptyRow = this.getFirstEmptyRow(columnIndex, this.board);
if (firstEmptyRow === -1) {
Swal.fire('Cannot put here, it is full');
return;
}
Vue.set(this.board[firstEmptyRow], columnIndex, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
this.makeCpuMove();
} else {
this.askUserForAnotherMatch();
}
},
// Returns true if there's a winner or a tie. False otherwise
async checkGameStatus() {
if (this.isWinner(this.currentPlayer, this.board)) {
await this.showWinner();
return true;
} else if (this.isTie(this.board)) {
await this.showTie();
return true;
}
return false;
},
async askUserForAnotherMatch() {
this.canPlay = false;
const result = await Swal.fire({
title: 'Play again?',
text: "Do you want to play again?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#fdbf9c',
cancelButtonColor: '#4A42F3',
cancelButtonText: 'No',
confirmButtonText: 'Yes'
});
if (result.value) {
this.resetGame();
}
},
async makeCpuMove() {
if (!this.isCpuPlaying || this.currentPlayer !== PLAYER_CPU) {
return;
}
const bestColumn = this.getBestColumnForCpu();
const firstEmptyRow = this.getFirstEmptyRow(bestColumn, this.board);
console.log({ firstEmptyRow });
Vue.set(this.board[firstEmptyRow], bestColumn, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
} else {
this.askUserForAnotherMatch();
}
},
getBestColumnForCpu() {
const winnerColumn = this.getWinnerColumn(this.board, this.currentPlayer);
if (winnerColumn !== -1) {
console.log("Cpu chooses winner column");
return winnerColumn;
}
// Check if adversary wins in the next move, if so, we take it
const adversary = this.getAdversary(this.currentPlayer);
const winnerColumnForAdversary = this.getWinnerColumn(this.board, adversary);
if (winnerColumnForAdversary !== -1) {
console.log("Cpu chooses take adversary's victory");
return winnerColumnForAdversary;
}
const cpuStats = this.getColumnWithHighestScore(this.currentPlayer, this.board);
const adversaryStats = this.getColumnWithHighestScore(adversary, this.board);
console.log({ adversaryStats });
console.log({ cpuStats });
if (adversaryStats.highestCount > cpuStats.highestCount) {
console.log("CPU chooses take adversary highest score");
// We take the adversary's best move if it is higher than CPU's
return adversaryStats.columnIndex;
} else if (cpuStats.highestCount > 1) {
console.log("CPU chooses highest count");
return cpuStats.columnIndex;
}
const centralColumn = this.getCentralColumn(this.board);
if (centralColumn !== -1) {
console.log("CPU Chooses central column");
return centralColumn;
}
// Finally we return a random column
console.log("CPU chooses random column");
return this.getRandomColumn(this.board);
},
getWinnerColumn(board, player) {
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
//Proceed only if row is ok
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
// If this is winner, return the column
if (this.isWinner(player, boardClone)) {
return i;
}
}
}
return -1;
},
getColumnWithHighestScore(player, board) {
const returnObject = {
highestCount: -1,
columnIndex: -1,
};
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
const firstFilledRow = this.getFirstFilledRow(i, boardClone);
if (firstFilledRow !== -1) {
let count = 0;
count = this.countUp(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countUpRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countDownRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
}
}
}
return returnObject;
},
getRandomColumn(board) {
while (true) {
const boardClone = JSON.parse(JSON.stringify(board));
const randomColumnIndex = this.getRandomNumberBetween(0, COLUMNS - 1);
const firstEmptyRow = this.getFirstEmptyRow(randomColumnIndex, boardClone);
if (firstEmptyRow !== -1) {
return randomColumnIndex;
}
}
},
getCentralColumn(board) {
const boardClone = JSON.parse(JSON.stringify(board));
const centralColumn = parseInt((COLUMNS - 1) / 2);
if (this.getFirstEmptyRow(centralColumn, boardClone) !== -1) {
return centralColumn;
}
return -1;
},
async showWinner() {
if (this.currentPlayer === PLAYER_1) {
await Swal.fire('Winner is player 1');
} else {
await Swal.fire('Winner is player 2');
}
},
async showTie() {
await Swal.fire('Tie');
},
getFirstFilledRow(columnIndex, board) {
for (let i = ROWS - 1; i >= 0; i--) {
if (board[i][columnIndex] !== EMPTY_SPACE) {
return i;
}
}
return -1;
},
getFirstEmptyRow(columnIndex, board) {
for (let i = ROWS - 1; i >= 0; i--) {
if (board[i][columnIndex] === EMPTY_SPACE) {
return i;
}
}
return -1;
}
}
});
Como varios de mis proyectos, este software es open source y gratuito. Puedes descargar el código del repositorio de GitHub.
Puedes ver un vídeo en el que se realiza una demostración, además de una explicación con palabras:
Finalmente, puedes probar la versión en línea.
Me emocionó bastante portar este juego a JavaScript con mi framework favorito: Vue. Recuerda que anteriormente ya hice este juego en lenguaje C.
Siempre me ha gustado cómo es que hice que el CPU elija la mejor columna y que, si bien no es invencible, sí que puede ganar algunas veces.
¿Quieres ver más videojuegos programados por mí? click aquí.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.