Videojuegos

El ahorcado (juego) en JavaScript

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 ahorcado en JavaScript – Programación de juego

El juego está escrito con el lenguaje JavaScript, usando Vue.js y Bootstrap. Cuenta con:

  • Gestión de palabras. Puedes agregar y eliminar palabras para jugar al ahorcado
  • Elección de palabra aleatoria: cada vez que juegas se selecciona una palabra aleatoria del banco de palabras que el usuario ha registrado
  • Botones con letras para adivinar la palabra, mismos que se deshabilitan una vez que se ha intentado esa letra
  • Imagen del ahorcado, misma que cambia con el número de intentos
  • Juego totalmente responsivo (adaptable a teléfonos, tabletas, etcétera)

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.

Estilos

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

Funciones comunes

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.

Imagen dependiendo del intento

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.

Generación de teclado

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.

Elegir palabra para el ahorcado, y ocultarla

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.

Intentar adivinar palabra

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.

Saber si jugador pierde o gana ahorcado

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.

Reiniciar juego

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

Otras funciones importantes del juego

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

Gestión de palabras

Gestionar banco de palabras para ahorcado con JavaScript y Local storage

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&nbsp;<i class="fa fa-home"></i></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link active" href="words.html">Manage words&nbsp;<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&nbsp;<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>
            &nbsp;|&nbsp;
            <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.

Demostración

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

Vídeo en YouTube

Si quieres puedes ver la explicación del vídeo así como la demostración de uso en el siguiente vídeo:

Código completo

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.