Hoy te mostraré otro juego programado en JavaScript. Se trata de “Simón dice” (Simon says) también conocido simplemente como Simón; donde se repite una secuencia y el jugador debe imitarla. Según la wikipedia:
Simon es un juego electrónico creado por Ralph Baer y Howard J. Morrison en 1978. Tuvo un gran éxito durante los 80. Tiene forma de disco, en una de sus caras se puede ver cuatro cuadrantes, cada uno con un color: verde, rojo, azul y amarillo en su versión original. Su nombre se debe por el conocido juego tradicional del mismo nombre: Simón dice, de donde se inspira.
Es un juego físico pero lo he creado de manera virtual. En caso de que sigas sin entender, el juego es como el siguiente:
A lo largo del post te mostraré cómo está conformado el juego, explicando su programación. También te dejaré el código fuente y la demostración para que puedas jugar simón dice en la web.
El algoritmo es simple. Tenemos un arreglo, en cada turno del CPU (es decir, después de que el usuario acierta) agregamos un nuevo valor para que se vaya haciendo más grande.
Después, reproducimos esta secuencia para que el usuario la vea y la intente reproducir. Ahora el usuario hará clic en los botones, cada que hace clic se aumenta un contador que indica en qué paso de la secuencia va el jugador.
Este contador sirve como índice del arreglo que tiene la secuencia que el CPU ha elegido; en cada clic se comprueba si el botón que ha elegido el jugador es el mismo que está en el arreglo en esa posición. En caso de que sí, el juego continúa, y si no, se le indica al jugador que ha perdido.
Cuando el contador alcanza la longitud de la secuencia menos 1 significa que el jugador ha reproducido la secuencia completamente, así que se aumenta su puntaje y es el turno del CPU para que agregue otro valor e inicie todo de nuevo.
En este caso solo utilicé la librería d3.js para dibujar el círculo a través de SVG. Lo demás es JavaScript puro. Para el diseño he usado Bootstrap 4.
Para las alertas he usado SweetAlert 2. Básicamente las alertas dan la bienvenida al juego e indican al jugador cuando ha perdido.
El juego reproduce cuatro sonidos distintos dependiendo del color que se presione. Por lo tanto usamos la función que expuse anteriormente para reproducir sonidos con JavaScript. La carga de los mismos se ve así:
const cargarSonido = function (fuente) {
const sonido = document.createElement("audio");
sonido.src = fuente;
sonido.setAttribute("preload", "auto");
sonido.setAttribute("controls", "none");
sonido.style.display = "none";
document.body.appendChild(sonido);
return sonido;
}
const sonidoSuperiorIzquierda = cargarSonido("1.mp3"),
sonidoSuperiorDerecha = cargarSonido("2.mp3"),
sonidoInferiorIzquierda = cargarSonido("3.mp3"),
sonidoInferiorDerecha = cargarSonido("4.mp3");
Obviamente tú puedes cambiar los sonidos por otros, renombrarlos, etcétera. Por cierto, quiero dejar claro que los sonidos fueron recortados a partir de un sonido que encontré en freesound.org
La página en sí es muy simple. Solo se define una barra de navegación, el contenedor del SVG y el pie. La parte importante es el contenedor pues ahí se va a dibujar el juego, y también es importante la carga de game.js.
<!--
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ 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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Simon says - By parzibyte</title>
<script src="js/d3.min.js"></script>
<script src="js/sweetalert2.min.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<style>
.boton {
cursor: pointer;
}
body {
padding-bottom: 70px;
padding-top: 70px;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-success fixed-top">
<a class="navbar-brand" target="_blank" href="//parzibyte.me/blog">Simon says - By Parzibyte</a>
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="//parzibyte.me/blog">Support & help <i
class="fa fa-hands-helping"></i></a>
</li>
</ul>
</div>
</nav>
<main class="container-fluid">
<div class="row text-center">
<div class="col-12">
<h1>Simon says - By Parzibyte</h1>
</div>
<div class="col-12 table-responsive">
<button id="comenzar" class="mb-2 btn btn-success">Comenzar juego</button>
<div id="contenedorJuego"></div>
</div>
</div>
</main>
<footer class="px-2 py-2 fixed-bottom bg-dark">
<span class="text-muted">Simon says game. Written by
<a class="text-white" href="//parzibyte.me/blog">Parzibyte</a>
|
<a target="_blank" class="text-white" href="//github.com/parzibyte/simon-game-javascript">
View source
</a>
</span>
</footer>
<script src="js/game.js"></script>
</body>
</html>
El contenedor está definido en la línea 65.
A partir de aquí comenzaré a explicar el código.
Comenzamos definiendo algunas variables que se van a usar durante el juego. Por ejemplo, partidoEn16
me ayuda a dibujar los arcos o círculos usando 1 dieciseisavo como medida, aunque al final no necesité tanta precisión.
Lo demás es definición del radio de los círculos, además de algunas banderas como puedeJugar
que indica si el juego debería reaccionar a los clics del usuario.
La variable gamma
es el multiplicador para cuando el color se hace brillante. El puntaje es el puntaje del usuario, que se aumenta cuando sigue la secuencia correcta.
El contador sirve para saber en cuál paso de la secuencia va el usuario (cuando la está imitando) y la secuencia es un arreglo de colores aleatorios.
const partidoEn16 = (Math.PI * 2) / 16;
const centroX = 200, centroY = 200;
const radioCirculo = 200;
const radioCuarto = 170;
const radioCirculoCentral = 80;
const distancia = 10;
const gamma = 2;
const milisegundosCpu = 200,
milisegundosUsuario = 100;
const sonidoSuperiorIzquierda = cargarSonido("1.mp3"),
sonidoSuperiorDerecha = cargarSonido("2.mp3"),
sonidoInferiorIzquierda = cargarSonido("3.mp3"),
sonidoInferiorDerecha = cargarSonido("4.mp3");
let puedeJugar = false;
let contador = 0;
let puntaje = 0;
let secuencia = [];
const verde = d3.color("#1B5E20"),
rojo = d3.color("#B71C1C"),
amarillo = d3.color("#F9A825"),
azul = d3.color("#0D47A1"),
negro = d3.color("#212121");
Los colores son colores simples, es decir, no tienen nada de especial; pero es importante que los definamos como lo que regresa la función d3.color
para que más tarde podamos agregarles brillo y dar el efecto de que el botón “enciende”.
Nota: una alternativa sería poner 4 simples botones en una tabla; de este modo evitarías dibujar el círculo y sus partes, ya que gran parte del código es para dibujar el juego.
Una cosa importante sobre los arcos y sus ángulos es que si queremos un círculo completo, el ángulo de cierre debe ser de 2 PI. En caso de que queramos uno a la mitad, sería de PI. Y finalmente para dibujar un cuarto de círculo se usa PI entre 2.
De todo eso se encarga d3. Básicamente es un círculo negro de fondo, después se dibujan los 4 botones usando un cuarto de círculo, se dibuja otro círculo negro en el centro pero con un radio menor y encima de este se coloca el puntaje.
const circuloFondo = d3.arc()
.innerRadius(0)
.outerRadius(radioCirculo)
.startAngle(0)
.endAngle(Math.PI * 2);
const circuloCentral = d3.arc()
.innerRadius(0)
.outerRadius(radioCirculoCentral)
.startAngle(0)
.endAngle(Math.PI * 2);
const $svg = d3.select("#contenedorJuego")
.append("svg")
.attr('width', 400)
.attr('height', 400);
$svg.append("g")
.attr("transform", `translate(${centroX},${centroY})`)
.append("path")
.attr("d", circuloFondo)
.attr("fill", negro);
const superiorIzquierda = $svg.append("g")
.attr("transform", `translate(${centroX - distancia},${centroY - distancia})`)
.attr("class", "boton")
.append("path")
.attr("d",
d3.arc()
.innerRadius(0)
.outerRadius(radioCuarto)
.startAngle(partidoEn16 * 12)
.endAngle(partidoEn16 * 16)
)
.attr("fill", verde);
const superiorDerecha = $svg.append("g")
.attr("transform", `translate(${centroX + distancia},${centroY - distancia})`)
.attr("class", "boton")
.append("path")
.attr("d",
d3.arc()
.innerRadius(0)
.outerRadius(radioCuarto)
.startAngle(0)
.endAngle(partidoEn16 * 4)
)
.attr("fill", rojo);
const inferiorIzquierda = $svg.append("g")
.attr("transform", `translate(${centroX - distancia},${centroY + distancia})`)
.attr("class", "boton")
.append("path")
.attr("d",
d3.arc()
.innerRadius(0)
.outerRadius(radioCuarto)
.startAngle(partidoEn16 * 8)
.endAngle(partidoEn16 * 12)
)
.attr("fill", amarillo);
const inferiorDerecha = $svg.append("g")
.attr("transform", `translate(${centroX + distancia},${centroY + distancia})`)
.attr("class", "boton")
.append("path")
.attr("d",
d3.arc()
.innerRadius(0)
.outerRadius(radioCuarto)
.startAngle(partidoEn16 * 4)
.endAngle(partidoEn16 * 8)
)
.attr("fill", azul);
// Encima de los otros círculos, el círculo central
$svg.append("g")
.attr("transform", `translate(${centroX},${centroY})`)
.append("path")
.attr("d", circuloCentral)
.attr("fill", negro);
const textoPuntaje = $svg.append("text")
.attr("transform", `translate(${centroX},${centroY})`)
.attr("fill", "#ffffff")
.attr("font-size", 30)
.attr("font-weight", "bold")
.attr("font-family", "Courier")
.style("text-anchor", "middle")
.style("dominant-baseline", "central")
.text("0")
Otra cosa importante a resaltar es el atributo transform
que hace que los elementos sean posicionados. En este caso (por ejemplo línea 79) estoy usando las plantillas de cadena para evitar concatenar.
El juego hace que el botón encienda (su color es más brillante), reproduce un sonido, espera ciertos milisegundos, revierte el color del botón y pausa el sonido.
const encenderYApagarBoton = async (boton, duracion) => {
puedeJugar = false;
const colorActual = boton.attr("fill");
let sonidoQueSeReproduce;
if (compararBotones(boton, superiorIzquierda)) {
sonidoQueSeReproduce = sonidoSuperiorIzquierda;
} else if (compararBotones(boton, superiorDerecha)) {
sonidoQueSeReproduce = sonidoSuperiorDerecha;
} else if (compararBotones(boton, inferiorIzquierda)) {
sonidoQueSeReproduce = sonidoInferiorIzquierda
} else {
sonidoQueSeReproduce = sonidoInferiorDerecha;
}
sonidoQueSeReproduce.currentTime = 0;
await sonidoQueSeReproduce.play();
boton.attr("fill", d3.color(colorActual).brighter(gamma))
await sleep(duracion);
boton.attr("fill", d3.color(colorActual));
await sleep(duracion);
await sonidoQueSeReproduce.pause();
puedeJugar = true;
};
Voy a explicar esa función. Lo que hace es buscar cuál sonido debe reproducir según el botón. El sonido es rebobinado para que se empiece a reproducir desde cero. Después reproduce el sonido y le coloca un color más brillante al botón (línea 16) usando el método brighter
propio del color de d3.
Después espera cierta duración con la función sleep
(setTimeout
con promesas); devuelve el color original, vuelve a esperar, pausa el sonido y finalmente establece la bandera de puedeJugar
en true
.
El funcionamiento del juego es simple. Se divide en dos partes; la primera es el turno del CPU en donde se le agrega un botón aleatorio a la secuencia, y después se reproduce en el juego para que el jugador la observe. Durante todo este tiempo el jugador no puede hacer clic en ningún botón.
const turnoDelCpu = async () => {
puedeJugar = false;
agregarBotonAleatorioASecuencia(secuencia);
await reproducirSecuencia(secuencia);
contador = 0;
puedeJugar = true;
}
const reproducirSecuencia = async secuencia => {
for (const boton of secuencia) {
await encenderYApagarBoton(boton, milisegundosCpu);
}
};
La segunda parte es cuando el jugador hace clic en un botón (previamente se comprueba que pueda jugar). En este caso, si el jugador acierta en la secuencia, se reproduce el sonido del botón y, si no, se le indica al jugador que ha perdido.
Es también en esta segunda parte cuando se aumenta el puntaje y el contador de la secuencia, pues solo se le da el turno al CPU cuando el jugador ha repetido toda la secuencia sin equivocarse.
const botones = [superiorIzquierda, superiorDerecha, inferiorIzquierda, inferiorDerecha];
botones.forEach(boton => {
boton.on("click", async () => {
if (!puedeJugar) {
console.log("No puedes jugar ._.");
return;
}
puedeJugar = false;
const ok = compararSecuenciaDeUsuarioConOriginal(secuencia, boton, contador);
if (ok) {
await encenderYApagarBoton(boton, milisegundosUsuario);
if (contador >= secuencia.length - 1) {
puntaje++;
refrescarPuntaje(puntaje);
await sleep(500);
await turnoDelCpu();
} else {
contador++;
}
puedeJugar = true;
} else {
$btnComenzar.disabled = false;
Swal.fire("Perdiste", `Has perdido. Tu puntuación fue de ${puntaje}. Puedes jugar de nuevo cuando quieras`);
}
});
});
También necesitamos otras funciones auxiliares para este juego de Simón dice en JavaScript. Por ejemplo, elegir un botón aleatorio, comparar si dos botones son iguales, etcétera.
const aleatorioDeArreglo = arreglo => arreglo[Math.floor(Math.random() * arreglo.length)];
const agregarBotonAleatorioASecuencia = secuencia => secuencia.push(aleatorioDeArreglo(botones));
const compararBotones = (boton, otroBoton) => {
return boton.attr("fill") === otroBoton.attr("fill");
};
const compararSecuenciaDeUsuarioConOriginal = (secuenciaOriginal, botonDeUsuario, indice) => {
return compararBotones(secuenciaOriginal[indice], botonDeUsuario);
};
const refrescarPuntaje = puntaje => textoPuntaje.text(puntaje.toString());
const reiniciar = () => {
secuencia = [];
puedeJugar = false;
contador = puntaje = 0;
refrescarPuntaje(puntaje);
}
Otro método importante es el de reiniciar el juego, mismo que limpia la secuencia, reinicia puntajes, contadores y refresca el puntaje.
Finalmente, todo el juego es reiniciado al presionar un botón. Cuando esto pasa, el botón se deshabilita. El mismo se vuelve a habilitar cuando el jugador pierde y reinicia el juego.
const $btnComenzar = document.querySelector("#comenzar");
$btnComenzar.addEventListener("click", () => {
$btnComenzar.disabled = true;
reiniciar();
turnoDelCpu();
});
No colocaré aquí el código fuente; pues lo encuentras en mi GitHub. Lo encontrarás ahí para que, en caso de que le dé mantenimiento en el futuro, te descargues la última versión del juego.
Puedes probar el juego de Simón dice en JavaScript en este enlace: https://parzibyte.github.io/simon-game-javascript/
También puedes mirar mi vídeo de YouTube y suscribirte:
Por cierto, solo hay un pequeño problema y es que el juego no es totalmente responsivo, pues el círculo mide 400 pixeles, así que en pantallas pequeñas se añade un scroll para que el círculo quepa. Recuerda que si no te gusta, eres bienvenido a hacer una contribución al código 😉
También te invito a ver otros juegos que he programado, o más tutoriales de JavaScript.
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.