En este post te mostraré un juego que he programado recientemente. Se trata de El ahorcado o hangman, en su versión web programado con JavaScript; totalmente gratuito y open source.
El juego está escrito con el lenguaje JavaScript, usando Vue.js y Bootstrap. Cuenta con:
A continuación te mostraré cómo está hecho, en dónde puedes descargarlo, etcétera. Pues es un juego open source y gratuito que puedes modificar sin problemas.
El único estilo importante es el que se aplica a la palabra que el jugador está adivinando. La misma tiene un letter-spacing de 4px. Lo demás es para el footer y la barra de navegación, además de la fuente:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ 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.
*/@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
body {
padding-bottom: 70px;
padding-top: 70px;
font-family: 'Montserrat', sans-serif;
}
.displayed-word {
letter-spacing: 4px;
}
Para gestionar las palabras tenemos algunas funciones globales que serán importadas ya sea en el momento de jugar o al momento de gestionar las palabras. Dentro del código también encontramos el que hace funcionar al botón del menú:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ 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.
*/// Tomado de https://parzibyte.me/blog/2019/06/26/menu-responsivo-bootstrap-4-sin-dependencias/
document.addEventListener("DOMContentLoaded", () => {
const menu = document.querySelector("#menu"),
botonMenu = document.querySelector("#botonMenu");
if (menu) {
botonMenu.addEventListener("click", () => menu.classList.toggle("show"));
}
});
const LOCAL_STORAGE_WORDS_KEY = "words";
const getWords = () => (JSON.parse(localStorage.getItem(LOCAL_STORAGE_WORDS_KEY)) || []);
const saveWords = (words) => (localStorage.setItem(LOCAL_STORAGE_WORDS_KEY, JSON.stringify(words)));
Si te fijas, la gestión de palabras es muy fácil. Solo usamos Local storage de JavaScript para guardar o regresar un arreglo codificado como JSON.
Ahora veamos cómo es que se desarrolla el juego.
En el juego se muestra una imagen del ahorcado dependiendo del intento. Al final de todo, son imágenes estáticas (bien se podría hacer con canvas pero sería algo más complejo) que tienen los nombres de Ahorcado-X.png en donde X es la fase del ahorcado.
Lo que hace que se vayan cambiando es el método imagePath que hace la resta de los máximos intentos menos los intentos restantes y a partir de ello devuelve la cadena de la imagen:
imagePath() {
return `img/Ahorcado-${MAX_ATTEMPTS - this.remainingAttempts}.png`;
}
Si te fijas, usamos las plantillas de cadena. Después, en el HTML, simplemente usamos a Vue para que recargue la imagen cada que sea necesario, es decir, cada que el valor cambie:
<img class="img-fluid" :src="imagePath()" alt="">
Por cierto, en el repositorio he dejado la imagen PSD (se edita con Photoshop) modificable en caso de que quieras agregar otros detalles. Sé que se pueden usar otros editores, pero solo ese tenía a la mano.
También quiero decirte que puedes modificar las imágenes o remplazarlas por otras, para darle otro estilo al juego. Solo asegúrate de colocar el nombre de manera correcta.
Las teclas son botones dibujados con Vue.js a través de un diccionario. Al inicio de todo, tenemos las letras que se usarán:
const ALPHABET = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";
A partir de las mismas generamos un diccionario en donde la clave es la letra y el valor es un objeto que tiene la letra, y si la misma debe ser deshabilitada.
setupKeys() {
// We make a dictionary from the letters
for (const letter of ALPHABET) {
Vue.set(this.letters, letter, {
letter,
disabled: false, // We disable it when the user clicks on it
});
}
},
Al inicio, disabled
está en false
pues ninguna tecla está deshabilitada (se va a deshabilitar cuando el usuario haga clic sobre ella). Y finalmente lo dibujamos con Vue en el DOM:
<div class="col-12 text-center">
<button :disabled="letter.disabled" @click="attemptWithLetter(letter.letter)" v-for="letter in letters"
class="btn btn-success m-1 btn-lg">{{letter.letter}}</button>
</div
Al hacer clic en el botón se invoca al método attemptWithLetter
, mismo que recibe la letra. Esto hace que el usuario intente adivinar la palabra con esa letra. Antes de ver cómo funciona eso, veamos cómo se prepara y elige la palabra.
Nota: tú puedes modificar la cadena ALPHABET para agregar o quitar letras disponibles al usuario. El programa se encarga de generar el teclado.
Lo primero que se hace es obtener todas las palabras y elegir una al azar:
async chooseWord() {
// Get words stored in localstorage
const words = getWords();
if (!words.length) {
await Swal.fire("Please add some words so you can play (go to Manage words)");
window.location = "./words.html";
}
// Choose random
let word = words[Math.floor(Math.random() * words.length)];
this.prepareWord(word);
},
Por cierto, en caso de que no haya palabras, se le indica al usuario que agregue algunas. En caso contrario (línea 9) se elige una palabra aleatoria para jugar a hangman en JS y luego se invoca a prepareWord
.
La función convierte la palabra a mayúsculas, y luego declara un arreglo que tendrá la palabra oculta. El arreglo tendrá objetos, mismos que tendrán la letra y la propiedad hidden
en true
.
prepareWord(word) {
word = word.toUpperCase();
const hiddenWord = [];
for (const letter of word) {
hiddenWord.push({
letter,
hidden: true,
});
}
this.hiddenWord = hiddenWord;
},
Esta propiedad hidden
va a ser cambiada cuando el jugador acierte la letra.
Cuando el jugador presiona o hace clic en el botón, se invoca al método attemptWithLetter:
attemptWithLetter(letter) {
Vue.set(this.letters[letter], "disabled", true);
if (!this.letterExistsInWord(letter)) {
this.remainingAttempts -= 1;
} else {
this.discoverLetter(letter);
}
this.checkGameStatus();
}
Lo primero que hacemos es deshabilitar la tecla. Después verificamos si la letra no existe en la palabra usando una búsqueda secuencial:
letterExistsInWord(searchedLetter) {
for (const letter of this.hiddenWord) {
if (letter.letter === searchedLetter) {
return true;
}
}
return false;
},
Si no existe, entonces restamos un intento. Pero si la letra es correcta, invocamos a discoverLetter
que va a recorrer la palabra oculta y va a desocultar cada letra que coincida con la que acaba de intentar el usuario:
discoverLetter(letter) {
for (const index in this.hiddenWord) {
if (this.hiddenWord[index].letter === letter) {
this.hiddenWord[index].hidden = false;
}
}
},
De cualquier modo, al final se verifica el estado del juego para saber si el jugador pierde o gana.
checkGameStatus() {
if (this.playerWins()) {
Swal.fire("You win! The word was " + this.getUnhiddenWord());
this.resetGame();
}
if (this.playerLoses()) {
Swal.fire("You lose. The word was " + this.getUnhiddenWord());
this.resetGame();
}
},
getUnhiddenWord() {
let word = "";
for (const letter of this.hiddenWord) {
word += letter.letter;
}
return word;
},
playerWins() {
// If there's at least a hidden letter, the player hasn't win yet
for (const letter of this.hiddenWord) {
if (letter.hidden) {
return false;
}
}
return true;
},
playerLoses() {
return this.remainingAttempts <= 0;
},
Si el jugador gana o pierde, se reinicia el juego, cosa que veremos más adelante. Por ahora veamos cómo saber si gana.
El jugador gana si todas las letras de la palabra ya no están ocultas. Y el jugador pierde si los intentos son menores o iguales a cero. De cualquier modo se le indica al jugador si ha perdido o ganado, además de mostrarle la palabra original.
Lo que se hace al reiniciar el juego es reiniciar los intentos, configurar el teclado (para habilitar las teclas nuevamente) y elegir una palabra aleatoria:
resetGame() {
this.resetAttempts();
this.setupKeys();
this.chooseWord();
},
Hace falta mencionar algunas otras funciones. Por ejemplo, la que muestra la palabra enmascarada para el jugador:
displayWord() {
let displayedWord = "";
for (const letter of this.hiddenWord) {
if (letter.hidden) {
displayedWord += MASK_CHAR;
} else {
displayedWord += letter.letter;
}
displayedWord += " ";
}
return displayedWord;
},
O la que muestra la palabra original sin importar si el jugador adivina o no:
getUnhiddenWord() {
let word = "";
for (const letter of this.hiddenWord) {
word += letter.letter;
}
return word;
},
Las palabras se guardan en localstorage como lo dije anteriormente. Esto no es tan complejo, solo tenemos la vista que dibuja las palabras en una tabla usando v-for, y que muestra un formulario para agregar alguna palabra:
<!--
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ 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>Hangman game by parzibyte</title>
<script src="js/sweetalert2.all.min.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-success fixed-top">
<a class="navbar-brand" target="_blank" href="//parzibyte.me/blog">Hangman Game</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" id="botonMenu"
aria-label="Mostrar u ocultar menú">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">Play <i class="fa fa-home"></i></a>
</li>
<li class="nav-item">
<a class="nav-link active" href="words.html">Manage words <i class="fa fa-home"></i></a>
</li>
</ul>
<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" id="app">
<div class="col-12 text-center">
<h1>Manage words</h1>
</div>
<div class="col-12 text-center">
<div class="form-inline mb-2">
<label class="mr-2" for="newWord">Add new word:</label>
<input @keyup="deleteWhiteSpaces()" placeholder="Write word here..." class="m-2 form-control"
id="newWord" type="text" v-model="newWord">
<button @click="saveWord()" :disabled="newWord.length <= 0" class="btn btn-success">Save</button>
</div>
</div>
<div class="col-12 text-center">
<div class="jumbotron jumbotron-fluid" v-show="!words.length">
<div class="container">
<h1 class="display-4">Such empty</h1>
<p class="lead">Please add some words by using the form above</p>
</div>
</div>
<div class="table-responsive" v-show="words.length">
<table class="table">
<thead>
<tr>
<th>Word</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(word, index) in words">
<td>{{word}}</td>
<td>
<button @click="deleteWord(index)" class="btn btn-outline-danger">🗑️</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
<footer class="px-2 py-2 fixed-bottom bg-dark">
<span class="text-muted">Hangman game written by
<a class="text-white" href="//parzibyte.me/blog">Parzibyte</a>
|
<a target="_blank" class="text-white" href="//github.com/parzibyte/hangman-javascript">
View source
</a>
</span>
</footer>
</body>
<script src="js/vue.min.js"></script>
<script src="js/common.js"></script>
<script src="js/manage_words.js"></script>
</html>
El funcionamiento igualmente es con Vue, muy simple y usando las funciones globales vistas previamente:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ 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.
*/new Vue({
el: "#app",
data: () => ({
words: [],
newWord: "",
}),
mounted() {
this.refreshWords();
},
methods: {
refreshWords() {
this.words = getWords();
},
saveWord() {
// Clean it again ._.
this.deleteWhiteSpaces();
const word = this.newWord.toUpperCase();
// Only save if it does not exist
if (this.words.indexOf(word) === -1) {
this.words.push(word);
saveWords(this.words);
this.newWord = "";
} else {
Swal.fire("The word already exists");
}
},
async deleteWord(index) {
const result = await Swal.fire({
title: 'Deleting word',
text: "Are you sure?",
icon: 'question',
showCancelButton: true,
cancelButtonText: 'No, take me back',
confirmButtonText: 'Yes, delete it'
});
if (!result.value) return;
this.words.splice(index, 1);
saveWords(this.words);
},
deleteWhiteSpaces() {
this.newWord = this.newWord.replace(/ /g, "")
}
}
});
Lo único a destacar aquí es que se evita que haya palabras duplicadas, y que no se permiten espacios en blanco en las palabras.
Como la mayoría de mis programas, este software es open source. Puedes probar la demostración en este enlace (recuerda agregar palabras).
Esto puede ser montado en un servidor o en localhost. Lo he probado con XAMPP (es decir, Apache).
Si quieres puedes ver la explicación del vídeo así como la demostración de uso en el siguiente vídeo:
El código fuente lo encuentras en mi GitHub.
Te invito a ver más sobre videojuegos y JavaScript. También puedes ver otro software que he creado.
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.