Snake: El Juego de la Serpiente

10 abril 2019

 

Cómo programar un juego sencillo

Snake: el juego de la serpiente

 

EPSE

 

JORNADA DE PUERTAS ABIERTAS
11 de Abril de 2019

 

(si no visualizas el contenido del marco inferior, haz click en este link)

 

Partes del juego

 

Todo juego on-line es una página web con la que el usuario (el jugador) va a interactuar. Como, en este caso particular, la página es también un juego, además de incluir contenidos, también hay que programar ciertos tipos de actividades y respuestas ante las acciones del jugador. Nuestro juego on-line va a estar compuesto de los siguientes ficheros:

 

  • index.php: Este fichero contendrá los elementos visibles de la página web, el rótulo con el título del juego, el marco de juego, botones y otros contenidos similares. Un elemento muy importante que debe contener este dichero es el objeto CANVAS (lienzo), es, por decirlo así, la «pantalla» del juego, el tablero o zona donde ocurren todas las animaciones y movimientos que dan lugar al propio juego. Este fichero está programado en un lenguaje denominado HTML (libro on-line introductorio a HTML).
  • estilos.css: Archivo donde se definen los estilos y formas gráficas que van a tener los elementos del fichero anterior. Por decirlo de otro modo, el fichero «index.php» indica qué elementos hay en la página web y este fichero, «estilos.css», indica que formato gráfico van a tener dichos elementos. El lenguaje de programación en el que está escrito este fichero se denomina CSS (libro on-line introductorio a CSS).
  • funciones.js: Es la parte más compleja, contiene la programación de las operaciones del juego, la respuesta ante acciones del jugador y todo lo que tiene que ver con la animación y movimientos de los elementos del juego. El lenguaje de programación en el que está escrito este fichero se llama Javascript (libro on-line introductorio a Javascript).

Puedes descargarte el juego desde este link (incluye algunos ficheros de sonido y de imagen)

 

El CANVAS: la zona de juego

 

La zona donde ocurre el juego es un elemento CANVAS (lienzo) donde se dibujan los distintos elementos (serpientes y comida). El suelo cuadriculado se crea haciendo una capa contenedora con un fondo con el diseño deseado (la cuadrícula) y el CANVAS se sitúa dentro de dicho contenedor:

<div id="zona">
    <canvas id="can" width="600px" height="360px"></canvas>
</div>

Como se observa, el área de juego tiene unas dimensiones de 600 pixels de ancho por 360 pixels de alto.  Las cuadrículas del juego tienen unas dimensiones de 15×15 pixels, por lo que se trata de un área de 40×24 cuadrículas (40 <– 600/15) (24 <– 360/15). Hay que resaltar que la numeración de las coordenadas dentro del tablero de juego empiezan a numerarse desde ‘0’, por tanto la esquina superior izquierda del tablero se corresponderá con la coordenada [0,0] y la esquina inferior derecha con la coordenada [39,23].

canvas1

En el fichero «funciones.js» se utilizarán las variables «anchoZona» y «altoZona» para indicar el número total de casillas que tiene la zona de juego. Estas variables luego serán de utilidad en varias funciones del juego.

var anchoZona = 40;
var altoZona = 24;

En el mismo fichero «funciones.js» también se inicializará la variable de contexto del canvas ‘ctx’ del siguiente modo:

ctx = document.getElementById("can").getContext("2d"); // 'can' --> id del canvas

Con esta variable ‘ctx’ será posible dibujar elementos en el canvas. En este juego todos los elementos gráficos son círculos. La función que define como dibujar un arco en el canvas es la siguiente:

ctx.arc(x, y, r, a1, a2);

donde ‘x’ e ‘y’ son las coordenadas del centro del arco, ‘r’ es el radio y, ‘a1’ y ‘a2’ son los arcos inicial y final, expresados en radianes (360º = 2·PI radianes), de la curva que se quiere trazar. Para trazar un circulo completo hay que hacer a1=0 y a2=2*Math.PI

Para facilitar los cálculos para dibujar los elementos del juego se han definido algunos elementos que simplificarán algunas operaciones. Por ejemplo, para determinar el tamaño de los bloques del tablero y el arco de 360º:

var block = 15; // cada bloque tiene un tamaño de 15x15 pixels
var pi2 = 2*Math.PI; // 2*PI radianes -> 360º

Para calcular la coordenada central en pixels de un bloque del tablero se define la siguiente función:

function blkCir(n) { return (n+0.5) * block; }

De esta forma, si queremos definir un círculo en la casilla del tablero [4,7] (x=4 , y=7) haremos lo siguiente:

ctx.arc( blkCir(4) , blkCir(y) , block/2 , 0 , pi2);

Por último, hay que resaltar que ninguna de las funciones anteriores produciría un círculo real y visible en el canvas si no se indican los colores que queremos que tenga. El código completo para dibujar un círculo relleno de color rojo sería el siguiente:

ctx.beginPath(); // comienza el trazo
ctx.fillStyle = "#ff0000"; // indica color rojo para relleno del círculo
ctx.strokeStyle = "#ff0000"; // indica color rojo para la línea exterior
ctx.arc(blkCir(c[0][0]), blkCir(c[0][1]) , block/2, 0, pi2); // dibuja círculo
ctx.fill(); // rellena el fondo
ctx.stroke(); // traza la línea exterior

El color viene especificado en un código conocido como RGB (Red-Green-Blue), el código ‘ff0000’ representa una tonalidad roja. Para explorar los códigos de colores puedes usar la siguiente utilidad:

(si no visualizas el contenido del marco inferior, haz click en este link)

 

El objeto ‘Serpiente’

 

El objeto serpiente contiene la información que define la posición, tamaño y color de una serpiente, así como la dirección en la que se mueve. El juego contiene dos serpientes ‘s1’ y ‘s2’. La serpiente ‘s1’ (la naranja) se define inicialmente de este modo:

s1 = {
    "cuerpo": [[7,5], [6,5], [5,5], [4,5]], // vector de coordenadas del cuerpo
    "color": "#ff8800", // código RGB para color NARANJA
    "dirx":1,
    "diry":0
}

Se entiende que el cuerpo de la serpiente, inicialmente ocupa 4 posiciones dentro del área de juego ([[7,5], [6,5], [5,5], [4,5]]), donde la primera de ellas ([7,5]) es la posición de la cabeza.

Los elementos ‘dirx’ y diry’ indican como se mueve inicialmente la serpiente en el eje X (horizontal) y en el eje Y (vertical) respectivamente. Los valores que pueden tomar estos elementos son ‘-1’, ‘0’ y ‘1’:

  • ‘-1’ significa movimiento a la izquierda en el eje X y hacia arriba en el eje Y
  • ‘1’ significa movimiento a la derecha en el eje X y hacia abajo en el eje Y
  • 0 significa que no hay movimiento en ese eje

Como se verá más adelante, no hay opción a que haya movimiento en diagonal, es decir, el movimiento de una serpiente siempre será bien en horizontal o bien en vertical. En otras palabras, siempre habrá uno de los parámetros ‘dirx’ o ‘diry’ con valor ‘0’.

 

El objeto «comida»

 

El objeto comida indica cuantos elementos de comida van a aparecer sobre el tablero de juego, las coordenadas en las que se van a dibujar dichos objetos y de qué color son. El objeto comida se inicializa al principio del juego de la siguiente manera:

comida = {
    "elementos": [],   // vector (array) de coordenadas, inicialmente vacío
    "total": 2,        // número total de elementos de comida
    "color": "#ff0000" // código RGB para el color rojo
};

La comida se representan como círculos de color rojo, cada vez que una serpiente come uno de estos elementos crece de tamaño. El juego está programado para generar dos elementos de comida de forma aleatoria en la zona de juego, cada vez que un elemento es comido se genera automáticamente otro de forma que siempre hay 2 sobre el tablero de juego.

La función «CrearComida()» (ver más adelante) será la encargada de generar varios elementos de comida en posiciones aleatorias del tablero de juego y de guardar sus coordenadas en el vector «elementos» que, como se ve en el cuadro anterior, está inicialmente vacío.

 

Las funciones «CrearComida()» y «DibujarComida()»

 

Estas funciones se encargar respectivamente de generar de forma aleatoria tantos elementos de comida como se indica en el objeto «comida», y de dibujar dichos elementos de comida en el tablero de juego en las coordenadas que corresponda.

En «CrearComida()», lo primero es generar de forma aleatoria las coordenadas donde va a ir una pieza de comida, para ello se emplea el siguiente código:

x = Math.floor((Math.random() * anchoZona));
y = Math.floor((Math.random() * altoZona));

La función «Math.random()» genera un número aleatorio entre ‘0’ y ‘1’, mayor o igual que ‘0’ y menor que ‘1’ (nunca igual a ‘1’). Al multiplicar dicho valor por «anchoZona» (40) se obtiene un valor aleatorio entre ‘0’ y ’39’, pero con decimales. La función «Math.floor()» quita los decimales, así, la variable ‘x’ tomará un valor aleatoria entero (sin decimales) entre ‘0’ y ’39’. Del mismo modo, la variable ‘y’ terminará tomando un valor aleatorio entre ‘0’ y ’23’.

Como las coordenadas [x,y] anteriores se han generado de forma aleatoria, es posible que coincida con una posición del tablero que esté ocupada por alguna de las serpientes o por algún otro elemento de comida generado con anterioridad. Por tanto, hay que verificar que la coordenada [x,y] no choca con el cuerpo de las serpientes ni con otros elementos de comida. Existe una función «Buscar(vector, item)» que devuelve la posición de ‘item’ dentro de ‘vector’, y en caso de que ‘item’ no esté en ‘vector’ devuelve ‘-1’, es decir, para que la coordenada [x,y] sea válida es necesario que las siguientes funciones devuelvan un valor negativo (-1).

Buscar(s1.cuerpo, [x,y])
Buscar(s2.cuerpo, [x,y])
Buscar(comida.elementos, [x,y])

Por último, cuando ya estamos seguros de que las coordenadas [x,y] son válidas, las añadimos al vector de elementos de comida del siguiente modo:

comida.elementos.push([x,y]);

La función «DibujarComida()» es más sencilla, simplemente dibuja en el canvas tantos círculos como elementos de comida hay en el vector «comida.elementos»

ctx.beginPath();
ctx.fillStyle = comida.color;
ctx.strokeStyle = comida.color;
ctx.arc(blkCir(comida.elementos[i][0]), blkCir(comida.elementos[i][1]) , block/2+3, 0, pi2);
ctx.fill();
ctx.stroke();

Nótese que la variable ‘i’ (resaltada en rojo) es la que hace que se dibuje el primer elemento de comida, el segundo, etc… y así sucesivamente hasta completar todos los que haya. El juego está configurado para dos elementos de comida, pero esa cantidad se puede modificar fácilmente si se desea. Por otra parte, el radio de los círculos que se dibujan es ‘block/2+3’ (resaltado en azul), de esta manera se busca el efecto de que las piezas de comida sean más grandes que el cuadro del tablero y al ser dibujadas sobresaldrán un poco de dicho cuadro.

 

La función «MoverSerpiente(sx)»

 

Esta función determina el movimiento de una serpiente según su parámetro ‘cuerpo’ y sus parámetros ‘dirx’ y ‘diry’. Como se indicó más arriba, el ‘cuerpo’ de la serpiente esta formado por un vector de posiciones consecutivas.

cuerpo --> [ [7,5] , [6,5] , [5,5] , [4,5] ] // en rojo la posición de la cabeza

El avance de la serpiente implica que aparezca una nueva cabeza (primera posición del vector) en una nueva ubicación que dependerá de la posición de la cabeza actual y de los parámetros ‘dirx’ y ‘diry’. El cálculo de la nueva posición de la cabeza es muy sencillo, se realiza del siguiente modo:

x = sx.cuerpo[0][0] + sx.dirx; // sx.cuerpo[0][0] es 7, la coordenada X de la cabeza actual
y = sx.cuerpo[0][1] + sx.diry; // sx.cuerpo[0][1] es 5, la coordenada Y de la cabeza actual

Tras el cálculo, la nueva posición [x,y] (la nueva cabeza) se añade al cuerpo de la serpiente al principio, delante de la cabeza anterior del siguiente modo:

sx.cuerpo.unshift([x,y]);

El cuerpo de la serpiente quedará como sigue:

cuerpo --> [ [x,y] , [7,5] , [6,5] , [5,5] , [4,5] ] // en azul la nueva cabeza

La función de movimiento debe verificar si la nueva cabeza de la serpiente ha alcanzado una pieza de comida, esto se puede averiguar con la función «Buscar(vector, item)».

pos = Buscar(comida.elementos, [x,y]); // si devuelve un valor mayor o igual a '0' --> "comer"

En caso de haber alcanzado una pieza (pos>=0), esta se elimina y se vuelve a generar otra en alguna posición aleatoria del tablero de juego (también se hace sonar el sonido de «mordisco»):

comida.elementos.splice(pos, 1); // Eliminar pieza de comida
CrearComida(); // Crear nuevas piezas de comida

Si la serpiente no ha alcanzado una pieza de comida se deberá eliminar la última de las posiciones de su cuerpo, es decir, según el ejemplo anterior, la posición [4,5] deberá eliminarse:

sx.cuerpo.pop(); // Elimina el último elemento del cuerpo

El cuerpo entonces queda del siguiente modo:

cuerpo --> [ [x,y] , [7,5] , [6,5] , [5,5] ]

Nótese que cuando la serpiente come debe aumentar su tamaño. Por esa razón, cuando se alcanza una pieza de comida no se elimina la cola de la serpiente; al añadirle la nueva cabeza sin eliminar la cola queda una serpiente con un cuerpo más grande.

 

La función «DibujarSerpiente(sx)»

 

Esta función simplemente dibuja la serpiente en el canvas a base de círculos rellenos de color. Para distinguir la cabeza, se dibuja en su posición un círculo de radio mayor al resto. El resto de las posiciones del cuerpo se dibuja con dos círculos más pequeños, uno en la posición [x,y] correspondiente y otro consecutivo al lado desplazando 0,5 la coordenada ‘x’ o la ‘y’ según se esté desplazando la serpiente horizontal o verticalmente.

Como se vio anteriormente, para dibujar círculos rellenos de color lo primero es especificar de que color queremos que sean el perímetro y el relleno:

ctx.strokeStyle = sx.color; // color de la línea
ctx.fillStyle = sx.color;   // color de relleno

Finalmente, para dibujar el círculo hay que indicarlo del siguiente modo:

ctx.beginPath(); // comienzo de trazo
ctx.arc(x, y, r, 0, pi2);
ctx.fill(); // rellena
ctx.stroke(); // dibuja perímetro

donde ‘x’ e ‘y’ son las coordenadas del centro del círculo, y ‘r’ es la longitud del radio. Las coordenadas ‘x’ e ‘y’ se calcularán del siguiente modo:

x = blkCir( sx.cuerpo[i][0] ); // hay que ir actualizando la variable 'i' para dibujar
y = blkCir( sx.cuerpo[i][1] ); // todos los círculos del cuerpo de la serpiente

El radio de los círculos de las serpiente se calculará de este modo:

r = block/2; // Radio para la cabeza
---
r = block/2 - 2; // Radio para el resto del cuerpo (es un poco más pequeño)

 

La función «Colision()»

 

La función «Colisión()» es para detectar si la cabeza de alguna de las dos serpientes ha impactado contra algún obstáculo. Los obstáculos son los límites exteriores del área de juego y el cuerpo de ambas serpientes, considerando ‘x’ e ‘y’ las coordenadas de la cabeza de la serpiente ‘s1’. las siguientes condiciones serán estados de colisión:

  • x<0 O x>=anchoZona O y<0 O y>=altoZona –> la cabeza ha impactado contra los límites del tablero
  • Buscar(s2.cuerpo, [x,y])>=0 –> la cabeza ha impactado contra el cuerpo de la otra serpiente
  • Buscar(s1.cuerpo, [x,y])>=1 –> la cabeza ha impactado contra su propio cuerpo

Estas mismas comprobaciones también hay que hacerlas para la serpiente ‘s2’. Finalmente, la función colisión devuelve un valor numérico indicando el resultado de la comprobación:

0: No hay colisión
1: Ha colisionado ‘s1’ (gana ‘s2’)
2: Ha colisionado ‘s2’ (gana ‘s1’)
3: Han colisionado ambas serpientes a la vez (empate)

Si se da alguna de las condiciones de colisión (1, 2 o 3) se reproduce el sonido de colisión/fin de juego y se ejecuta la función «OndaExpansiva(x, y, 1);» que crea un efecto gráfico de detonación en las coordenadas ‘x’ e ‘y’.

 

La función «Empezar()»

 

Esta función es llamada nada más cargar la página:

<body onload="Empezar()">

Lo que hace es establecer las dos serpientes en su posición inicial, inicializa el vector de elementos de comida y luego coloca los elementos gráficos dentro del tablero de juego (serpientes y comida) llamando a las siguientes funciones:

DibujarSerpiente(s1);
DibujarSerpiente(s2); 
CrearComida(); // crea dos elementos aleatorios de comida
DibujarComida(); // Dibuja la comida creada

Adicionalmente, la función «Empezar()» también crea una serie de mensajes que se mostrarán al finalizar el juego para indicar cual de las dos serpientes ha ganado o si hay empate.

 

La función «Jugar()»

 

Esta función se llama cada vez que se pulsa el botón [jugar ] que aparece en pantalla:

<button id="botonJugar" onclick="Jugar();"> Jugar  </button>

«Jugar()» hace las siguientes operaciones:

  1. Oculta el botón [jugar ] ya que si se pulsara accidentalmente durante el juego se reiniciaría la partida sin dejar que se termine la que hay en curso.
  2. Arranca una melodía que indica que el juego va a empezar.
  3. Llama a la función «Empezar()» (vista anteriormente).
  4. Pone en marcha una cuenta atrás indicando los segundos que faltan para que las serpientes empiecen a moverse (ver más abajo la función «CuentaAtras(seg)»).

 

La función «CuentaAtras(seg)»

 

Esta función realiza una cuanta atrás que dura tantos segundos como indica el parámetro ‘seg’. Mientras ‘seg’ es mayor o igual que ‘0’ la función se llama recursivamente a sí misma cada segundo (cada 1000 milisegundos):

setTimeout(function(){CuentaAtras(seg-1)}, 1000);

En cada llamada se le resta ‘1’ al parámetro ‘seg’ de tal forma que cuando dicho parámetro es menor que ‘0’ la cuenta finaliza y empieza la partida. El juego consiste en una función que dibuja las escenas del juego como si fueran fotogramas de una película, dicha función, que llamaremos «ESCENA()», será ejecutada a intervalos regulares. La «frecuencia» de esos intervalos determinará la velocidad a la que transcurre el juego:

idIntervalo = setInterval(function(){ESCENA()}, frecuencia);

Nótese que la llamada «SetInterval(…)» devuelve un valor que se guarda en la variable «idIntervalo». Esta variable es muy importante porque servirá más adelante para terminar el juego cuando ocurra una colisión. Cuando eso ocurra, entre otras cosas, se ejecutará la siguiente instrucción:

clearInterval(idIntervalo);

Con esta orden se cancela el intervalo de las llamadas repetidas a la función «ESCENA()» y así la partida finaliza.

 

La función «ESCENA()»

 

Esta función realiza las siguientes operaciones:

  1. Actualiza el movimiento de las serpientes («MoverSerpiente(s1)» y «MoverSerpiente(s1)»)
  2. Verifica si ha habido alguna colisión (función «Colision()»)
  3. Si ha habido colisión:
    1. Se cancela el intervalo («clearInterval(…)»)
    2. Se indica con un mensaje cuál ha sido el resultado (ganador o empate)
    3. Se vuelve a mostrar el botón de [jugar ].
    4. FIN DE LA FUNCIÓN >> LA PARTIDA HA TERMINADO
  4. Si no ha habido colisión:
    1. Se borran todos los elementos del canvas
    2. Se activa el sonido de «paso»
    3. Se dibujan las serpientes en el canvas en su nueva posición («DibujarSerpiente(s1)» y «DibujarSerpiente(s2)»)
    4. Se dibuja la comida (función «DibujarComida()»)

Después de esto, dado que el la función anterior («CuentaAtras()») quedó definido un intervalo de llamada a «ESCENA()», esta función volverá a ejecutarse periódicamente mientras no haya colisión.

 

Control de eventos del teclado (modo «Teclado»)

 

El control de eventos es lo que permite que un jugador pueda interactuar con el juego en modo [Teclado], es decir, que pueda jugar. Sin esta opción simplemente veríamos unos elementos moviéndose por la pantalla sin posibilidad de dirigirlos ni hacer que se muevan por donde nosotros queremos.

En el modo [Teclado], cada vez que se pulsan determinadas teclas podemos hacer que cambie la dirección de movimiento de alguna de las serpientes. Para que el programa responda a la pulsación de las teclas es necesario capturar alguno de los eventos que permiten detectar la pulsación de teclas. Existen tres tipos de eventos que responden al teclado: «onkeydown» (al pulsar la tecla hacia abajo), «onkeyup» (al soltar la tecla) y «onkeypress» (al presionar la tecla). En este juego usaremos el primero de los eventos «onkeydown«. Para definir una función que permita responder ante este evento incluiremos en nuestro fichero «funciones.js» el siguiente código:

document.onkeydown=function(event)
{
    // operaciones de respuesta ante el evento
    ....
}

El elemento «event» (marcado en rojo) permite detectar cual es la tecla que se ha pulsado. Cada tecla tiene un código asociado y dependiendo de dicho código podemos programar unas acciones u otras. Por ejemplo, la tecla «Q» (mover arriba la serpiente ‘s1’) tiene el código «81». Cuando se detecte ese código haremos que los parámetros ‘dirx’ y ‘diry’ de ‘s1’ se modifiquen para que en el siguiente movimiento la serpiente ‘s1’ se mueva hacia arriba.

switch(event.keyCode)
{
    case 81: // código de la tecla Q (UP: subir 's1')
        if(s1.diry == 0) // solo se sube si antes no había movimiento en el eje 'y'
        {
            s1.dirx = 0; // desactivar movimiento en el eje 'x'
            s1.diry =-1; // subir
        }
        break;
    ...
}

El bloque de código «case … break;» habrá que repetirlo tantas veces como teclas queremos que intervengan en el juego, en nuestro caso son 8 teclas (4 para ‘s1’ y 4 para ‘s2′). Obviamente, habrá que indicar en cada caso el código de la tecla (81->’Q’) que queramos usar y programar debidamente lo que queramos que ocurra según la tecla que se haya pulsado. Para explorar los códigos de teclas puedes usar la siguiente utilidad:

(si no visualizas el contenido del marco inferior, haz click en este link)

 

 

Control Táctil de movimientos (modo «Táctil»)

En el modo [Táctil], el juego se puede controlar mediante pulsaciones sobre la pantalla, solo sería apto para jugar en una tablet o con un monitor táctil. Observad, que al pasar el juego a modo [Táctil], aparecen 4 botones con flechas para cada jugador, dichos botones hacen una llamada a la función «BotonesDirección» del fichero «funciones.js»:

function BotonesDireccion(x)
{
    // operaciones al hacer 'clic' en las flechas de dirección
    ....
}

El elemento ‘x’ (marcado en rojo) es un parámetro que indica cual de los botones ha sido pulsado, por ejemplo, cuando se pulsa el botón ‘flecha arriba’ de la serpiente ‘s1’, el parámetro ‘x’ toma el valor «S1_UP«. Dicho valor, es equivalente o análogo al código ’81’ (letra ‘Q’) de la función de control de eventos de teclado anterior, por eso, el código de esta función es muy parecido al de la función de control de eventos del apartado anterior, solo que ahora, en vez de usar el elemento ‘event’ (que devolvía el código de la tecla pulsada), se usa el elemento ‘x’ que toma un valor determinado según la flecha pulsada sobre la pantalla.

switch(x)
{
    case "S1_UP": // S1_UP: subir 's1'
        if(s1.diry == 0) // solo se sube si antes no había movimiento en el eje 'y'
        {
            s1.dirx = 0; // desactivar movimiento en el eje 'x'
            s1.diry =-1; // subir
        }
        break;
    ...
}

 

Recursos

 

¿Qué necesitas para hacer un juego web?

 

A continuación se enumeran una serie de recursos que pueden resultar de utilidad para realizar juegos on-line.

 

  • XAMPP: Tienes que convertir tu ordenador en un servidor web. La aplicación XAMPP instala en tu ordenador el servidor web Apache y la base de datos MySQL o MariaDB:
    # https://www.apachefriends.org/
  • Bfxr: Si quieres hacer algunos efectos de sonido la aplicación Bfxr te ayuda a generar pequeñas pistas de audio para efectos de golpes, disparos, saltos, etc.:
    https://www.bfxr.net/
  • Emoticonos y caracteres Unicode: Los emoticonos (o emojis) y los caracteres unicode son caracteres que no podemos encontrar en el teclado del ordenador, pero que se pueden incluir en cualquier página web para darle algún tipo de efecto o como adornos estéticos:
    # Unicode Characters ☯ ⚡ ∑ ♥ (caracteres y emojis clasificados por temas)
    https://emojipedia.org/ (busca emojis por si nombre en inglés)
    https://emojipedia.org/snake/ (‘snake’: el emoji de la serpiente)
  • Gimp: Es un programa muy completo para editar fotografías e imágenes de todo tipo
    # https://www.gimp.org/