13. Juegos

Versión para imprimir.

En esta lección se presentan técnicas para elaborar juegos.

A. Sprites

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Sprite</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Sprite</h1>
 <p>
  Este ejemplo te muestra como
  colocar una figura en
  cualquier lugar de la
  ventana.
 </p>
 <output class="sprite"
   style="left: 100px;
    bottom: 150px">
  😄
 </output>
</body>

</html>

B. Desplazamiento horizontal

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>
  Animación horizontal
 </title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Animación horizontal</h1>
 <output id="carita"
   class="sprite">
  😄
 </output>
 <script>
  const REFRESCO = 5
  const VELOCIDAD = 0.5
  let x = 0
  setInterval(avanza, REFRESCO)

  function avanza() {
   const y = innerHeight / 2
   const xMáxima = innerWidth
   carita.style =
    `left: ${x}px; bottom: ${y}px`
   x = (x + VELOCIDAD) % xMáxima
  }
 </script>
</body>

</html>

C. Recta

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Animación recta</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Animación recta</h1>
 <output id="carita"
   class="sprite">
  😄
 </output>
 <script>
  const X_INICIAL = 0
  const REFRESCO = 5
  const VELOCIDAD = 0.5
  let x = X_INICIAL
  setInterval(avanza, REFRESCO)

  function avanza() {
   const xFinal = innerWidth
   const yInicial = innerHeight / 2
   const yFinal = innerHeight
   const y = ((yFinal - yInicial) /
    (xFinal - X_INICIAL)) *
    (x - X_INICIAL) +
    yInicial
   carita.style =
    `left: ${x}px; bottom: ${y}px`
   x = (x + VELOCIDAD) % xFinal
  }
 </script>
</body>

</html>

D. Ondula

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Ondula</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Ondula</h1>
 <output id="carita"
   class="sprite">
  😄
 </output>
 <script>
  const REFRESCO = 5
  const VELOCIDAD = 0.5
  const FRECUENCIA = 0.03
  let x = 0
  setInterval(avanza, REFRESCO)

  function avanza() {
   const xMáxima = innerWidth
   const amplitud = innerHeight / 3
   const yBase = innerHeight / 2
   y = yBase +
    amplitud *
    Math.sin(FRECUENCIA * x)
   carita.style =
    `left: ${x}px; bottom: ${y}px`
   x = (x + VELOCIDAD) % xMáxima
  }
 </script>
</body>

</html>

E. Girando

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Gira</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Gira</h1>
 <output id="mariposa"
   class="sprite">
  🦋
 </output>
 <script>
  const REFRESCO = 120
  const VELOCIDAD = 0.3
  let angulo = 0
  setInterval(gira, REFRESCO)

  function gira() {
   const xBase = innerWidth / 2
   const amplitudX = innerWidth / 3
   const yBase = innerHeight / 2
   const amplitudY =
    innerHeight / 3
   const x = xBase +
    amplitudX * Math.cos(angulo)
   const y = yBase +
    amplitudY * Math.sin(angulo)
   mariposa.style =
    `left: ${x}px; bottom: ${y}px`
   angulo += VELOCIDAD
  }
 </script>
</body>

</html>

F. Desplazamiento con botones

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Mueve con Botones</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Mueve con Botones</h1>
 <p>
  <button onclick="retrocede()">
   ◀
  </button>
  <button onclick="avanza()">
   ▶
  </button>
 </p>
 <output id="carita"
   class="sprite">
  😄
 </output>
 <script>
  let yBase
  let xMenor
  let xMayor
  const VELOCIDAD = 30
  actualiza()
  let x = xMenor
  dibuja()

  function retrocede() {
   actualiza()
   if (x > xMenor) {
    x -= VELOCIDAD
   }
   dibuja()
  }

  function avanza() {
   actualiza()
   if (x < xMayor) {
    x += VELOCIDAD
   }
   dibuja()
  }

  function actualiza() {
   yBase = innerHeight / 2
   xMenor = innerWidth / 5
   xMayor = 4 * innerWidth / 5
  }

  function dibuja() {
   carita.style = `left: ${x}px;
    bottom: ${yBase}px`
  }
 </script>
</body>

</html>

G. Detecta colisiones

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Choca</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Choca</h1>
 <output id="abrazo"
   class="sprite"
   style="background-color:
            greenyellow;">
  🤗
 </output>
 <output id="triste"
   class="sprite"
   style="background-color:
            grey;">
  😪
 </output>
 <script>
  let x = 0
  const REFRESCO = 5
  setInterval(avanza, REFRESCO)

  function avanza() {
   const velocidad =
    innerWidth / 2000
   const x2 = innerWidth
   const y = innerHeight / 2
   triste.style.left =
    `${innerWidth / 2}px`
   triste.style.bottom = `${y}px`
   abrazo.style.left = `${x}px`
   abrazo.style.bottom = `${y}px`
   if (!seTocan(abrazo, triste)) {
    x = (x + velocidad) % x2
   }
  }

  /**
   * Devuelve true si 2
   * elementos se tocan.
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true
   *  si los elementos se
   *  tocan.
   */
  function seTocan(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }
 </script>
</body>

</html>

H. Haz algo si chocamos

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Sonrie</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Sonrie</h1>
 <output id="abrazo"
   class="sprite"
   style="background-color:
            greenyellow;">
  🤗
 </output>
 <output id="triste"
   class="sprite"
   style="background-color:
            grey;">
  😪
 </output>
 <script>
  let x = 0
  const REFRESCO = 5
  let intervalo =
   setInterval(avanza, REFRESCO)

  function avanza() {
   const velocidad =
    innerWidth / 2000
   const x2 = innerWidth
   const y = innerHeight / 2
   triste.style.left =
    `${innerWidth / 2}px`
   triste.style.bottom = `${y}px`
   abrazo.style.left = `${x}px`
   abrazo.style.bottom = `${y}px`
   if (seTocan(abrazo, triste)) {
    triste.value = "😊";
    triste.style.
     backgroundColor = "pink";
    clearInterval(intervalo);
   } else {
    x = (x + velocidad) % x2
   }
  }

  /**
   * Devuelve true si 2
   * elementos se tocan.
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true
   *  si los elementos se
   *  tocan.
   */
  function seTocan(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }
 </script>
</body>

</html>

I. Muévete al azar

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Sonrie</title>
 <style>
  .sprite {
   position: fixed;
   font-size: 3rem;
  }
 </style>
</head>

<body>
 <h1>Sonrie</h1>
 <output id="abrazo"
   class="sprite"
   style="background-color:
            greenyellow;">
  🤗
 </output>
 <output id="triste"
   class="sprite"
   style="background-color:
            grey;">
  😪
 </output>
 <script>
  let x = 0
  const REFRESCO = 5
  let intervalo =
   setInterval(avanza, REFRESCO)

  function avanza() {
   const velocidad =
    innerWidth / 2000
   const x2 = innerWidth
   const y = innerHeight / 2
   triste.style.left =
    `${innerWidth / 2}px`
   triste.style.bottom = `${y}px`
   abrazo.style.left = `${x}px`
   abrazo.style.bottom = `${y}px`
   if (seTocan(abrazo, triste)) {
    triste.value = "😊";
    triste.style.
     backgroundColor = "pink";
    clearInterval(intervalo);
   } else {
    x = (x + velocidad) % x2
   }
  }

  /**
   * Devuelve true si 2
   * elementos se tocan.
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true
   *  si los elementos se
   *  tocan.
   */
  function seTocan(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }
 </script>
</body>

</html>

J. Usa custom element

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Custom Elements</title>
 <style>
  figura-web {
   position: fixed;
   font-size: 60px;
  }
 </style>
 <script>
  class FiguraWeb
   extends HTMLElement {
   connectedCallback() {
    this.x = 0
    const attrY =
     this.getAttribute("y")
    const attrVel =
     this.getAttribute("velocidad")
    this.y = parseInt(attrY, 10)
    this.velocidad =
     parseInt(attrVel, 10)
   }
   muevete() {
    this.style.right =
     `${this.x}px`
    this.style.top = `${this.y}px`
    this.x =
     (this.x + this.velocidad) %
     innerWidth
   }
  }
  customElements.define(
   "figura-web", FiguraWeb)
 </script>
</head>

<body>
 <h1>Custom Elements</h1>
 <figura-web id="fantasma"
   y="0" velocidad="20">
  👻
 </figura-web>
 <figura-web id="sonrisa"
   y="100" velocidad="10">
  😁
 </figura-web>
 <script>
  setInterval(anima, 120)

  function anima() {
   fantasma.muevete()
   sonrisa.muevete()
  }
 </script>
</body>

</html>

K. Asociaciones

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Asociaciones</title>
 <script>
  /**
   * Devuelve un número
   * aleatorio entre el valor
   * menor y el valor mayor.
   * @param {number} menor el
   *  menor valor que se puede
   *  generar
   * @param {number} mayor el
   *  mayor valor que se puede
   *  generar
   * @returns {number} un
   *  número aleatorio entre
   *  menor y mayor.
   */
  function
   aleatorio(menor, mayor) {
   /* Math.floor(x): elimina
    *  los decimales.
    * Math.random(): genera
    *  un número aleatorio
    *  >= 0 y < 1. */
   return menor +
    Math.floor(
     Math.random() *
     (mayor - menor + 1))
  }

  class PerroWeb
   extends HTMLElement {
   connectedCallback() {
    this.x = 0
    this.y = 0
    this.innerHTML = "🐕"
    this.style.position = "fixed"
    this.style.fontSize = "2rem"
    this.style.right =
     `${this.x}px`
    this.style.bottom =
     `${this.y}px`
   }

   muevete() {
    this.x = (this.x + 30) %
     innerWidth
    this.style.right =
     `${this.x}px`
   }
  }
  customElements.define(
   "perro-web", PerroWeb)

  class AguilaWeb
   extends HTMLElement {
   connectedCallback() {
    this.x = aleatorio(0,
     Math.floor(innerWidth))
    this.y = aleatorio(0,
     Math.floor(innerHeight))
    this.innerHTML = "🦅"
    this.style.position = "fixed"
    this.style.fontSize = "2.5rem"
    this.style.left = `${this.x}px`
    this.style.top = `${this.y}px`
   }

   muevete() {
    this.y = (this.y + 10) %
     innerHeight
    this.style.top = `${this.y}px`
   }
  }
  customElements.define(
   "aguila-web", AguilaWeb)

  class ControladorWeb
   extends HTMLElement {
   connectedCallback() {
    this.muevete =
     this.muevete.bind(this)
    this.innerHTML = /* html */
     `<button onclick="this.
        parentElement.muevete()">
       Mueve
      </button>`
    this.perro = new PerroWeb()
    this.aguilas = [
     new AguilaWeb(),
     new AguilaWeb()]
    this.append(this.perro)
    for (const a of this.aguilas) {
     this.append(a)
    }
   }

   muevete() {
    this.perro.muevete()
    for (let a of this.aguilas) {
     a.muevete()
    }
   }
  }
  customElements.define(
   "controlador-web",
   ControladorWeb)
 </script>
</head>

<body>
 <h1>Asociaciones</h1>
 <controlador-web>
 </controlador-web>
</body>

</html>

L. Polimorfismo

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html lang="es">

<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Polimorfismo</title>
 <script>
  function
   aleatorio(menor, mayor) {
   return menor +
    Math.floor(
     Math.random() *
     (mayor - menor + 1))
  }

  /** @interface */
  class SeMueve {
   muevete() {
    throw new Error("intf")
   }
  }

  /** @implements {SeMueve} */
  class PerroWeb
   extends HTMLElement {
   connectedCallback() {
    this.x = 0
    this.y = 0
    this.innerHTML = "🐕"
    this.style.position = "fixed"
    this.style.fontSize = "2rem"
    this.style.right =
     `${this.x}px`
    this.style.bottom =
     `${this.y}px`
   }

   muevete() {
    this.x = (this.x + 30) %
     window.innerWidth
    this.style.right =
     `${this.x}px`
   }
  }
  customElements.define(
   "perro-web", PerroWeb)

  /** @implements {SeMueve} */
  class AguilaWeb
   extends HTMLElement {
   connectedCallback() {
    this.x = aleatorio(0,
     Math.floor(innerWidth))
    this.y = aleatorio(0,
     Math.floor(innerHeight))
    this.innerHTML = "🦅"
    this.style.position = "fixed"
    this.style.fontSize = "2.5rem"
    this.style.left = `${this.x}px`
    this.style.top = `${this.y}px`
   }

   muevete() {
    this.y = (this.y + 10) %
     window.innerHeight
    this.style.top = `${this.y}px`
   }
  }
  customElements.define(
   "aguila-web", AguilaWeb)
 </script>
</head>

<body>
 <h1>Polimorfismo</h1>
 <p>
  <button onclick="mueve()">
   Mueve
  </button>
 </p>
 <script>
  const figuras = [
   new AguilaWeb(),
   new PerroWeb(),
   new AguilaWeb()]
  for (let f of figuras) {
   document.body.append(f)
  }
  function mueve() {
   for (var f of figuras) {
    f.muevete()
   }
  }
 </script>
</body>

</html>

M. Jueguito 1

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html>

<head>
 <meta charset="utf-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Jueguito 1</title>
 <style>
  body {
   /* Rompe el flujo normal para
    * poder hacer swipe hacia
    * abajo. */
   position: fixed;
   top: 0px;
   left: 0px;
   /* ocupa todo el espacio. */
   width: 100%;
   height: 100%;
   /* Elimina márgenes. */
   margin: 0;
   /* Evita el scroll */
   overflow: hidden;
  }

  .sprite {
   position: fixed;
  }
 </style>
 <script>
  //@ts-check
  class JugadorPaloma
   extends HTMLElement {
   connectedCallback() {
    this.classList.add("sprite")
    this.innerHTML += "🕊"
    this.style.fontSize = "60px"
    /* Coloca el elemento a la
     * mitad de la pantalla. */
    const raiz =
     document.documentElement
    /* Obtiene las coordenadas del
     * element. */
    const r =
     this.getBoundingClientRect()
    const left =
     (raiz.clientWidth - r.width) /
     2
    const top =
     (raiz.clientHeight -
      r.height) /
     2
    this.style.left = `${left}px`
    this.style.top = `${top}px`
   }

   sube() {
    const top =
     this.getBoundingClientRect().
      top -
     20
    this.style.top = `${top}px`
   }

   baja() {
    const top =
     this.getBoundingClientRect().
      top +
     20
    this.style.top = `${top}px`
   }

   izquierda() {
    const left =
     this.getBoundingClientRect().
      left -
     20
    this.style.left = `${left}px`
   }

   derecha() {
    const left =
     this.getBoundingClientRect().
      left +
     20
    this.style.left = `${left}px`
   }
  }
  customElements.define(
   "jugador-paloma", JugadorPaloma)

  class FiguraAguila
   extends HTMLElement {
   connectedCallback() {
    this.classList.add("sprite")
    this.innerHTML = "🦅"
    this.style.fontSize = "40px"
    const r =
     this.getBoundingClientRect()
    this.style.left = `${r.left}px`
    this.style.top = `${r.top}px`
    this.style.bottom = "auto"
    this.style.right = "auto"
   }

   /**
    * Mueve la figura para que se
    * acerque al jugador, usando la
    * ecuación de la recta.
    * @param {HTMLElement} jugador
    *   el jugador que es
    *   perseguido.
    */
   muevete(jugador) {
    const r =
     this.getBoundingClientRect()
    const rJ = jugador.
     getBoundingClientRect()
    const y2 = rJ.top
    const y1 = r.top
    const x2 = rJ.left
    const x1 = r.left
    const pendiente = x2 === x1 ?
     0 :
     (y2 - y1) / (x2 - x1)
    const dirección =
     x2 > x1 ? 1 : -1
    const x = x1 + dirección * 5
    const y =
     pendiente * (x - x1) + y1
    this.style.left = `${x}px`
    this.style.top = `${y}px`
   }
  }
  customElements.define(
   "figura-aguila", FiguraAguila)
 </script>
</head>

<body>
 <div>
  <jugador-paloma></jugador-paloma>
  <figura-aguila
    style="right: 0; top: 0;">
  </figura-aguila>
  <figura-aguila
    style="right: 0; bottom: 0;">
  </figura-aguila>
 </div>
 <script>
  //@ts-check
  class Juego {
   constructor() {
    /** @type {JugadorPaloma} */
    this.jugador = document.
     querySelector(
      "jugador-paloma")
    /** @type {FiguraAguila[]} */
    this.figuras = Array.from(
     document.querySelectorAll(
      "figura-aguila"))
    this.iniciaX = null
    this.iniciaY = null
    this.interval = null
    this.activo = true
   }

   inicia() {
    document.addEventListener(
     "keydown",
     evt => this.teclas(evt))
    this.interval = setInterval(
     () => this.mueveFiguras(), 60)
   }

   mueveFiguras() {
    for (const f of this.figuras) {
     f.muevete(this.jugador)
    }
   }

   /** @param {KeyboardEvent} ev*/
   teclas(ev) {
    if (this.activo) {
     switch (ev.key) {
      case "ArrowLeft":
       this.jugador.izquierda()
       break
      case "ArrowRight":
       this.jugador.derecha()
       break
      case "ArrowUp":
       this.jugador.sube()
       break
      case "ArrowDown":
       this.jugador.baja()
       break
     }
    }
   }
  }

  const juego = new Juego()
  juego.inicia()
 </script>
</body>

</html>

N. Jueguito 2

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html>

<head>
 <meta charset="utf-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Jueguito 2</title>
 <style>
  body {
   position: fixed;
   top: 0px;
   left: 0px;
   width: 100%;
   height: 100%;
   margin: 0;
   overflow: hidden;
  }

  .sprite {
   position: fixed;
  }
 </style>
 <script>
  //@ts-check
  /** @abstract */
  class Jugador
   extends HTMLElement {
   izquierda() {
    throw new Error("abstract")
   }
   derecha() {
    throw new Error("abstract")
   }
   sube() {
    throw new Error("abstract")
   }
   baja() {
    throw new Error("abstract")
   }
  }

  /** @abstract */
  class Figura
   extends HTMLElement {
   /**
    * @param {HTMLElement} jugador
    *   el jugador que persigue.
    */
   muevete(jugador) {
    throw new Error("abstract");
   }
  }

  class Juego2 {
   constructor() {
    /** @type {Jugador} */
    this.jugador = document.
     querySelector(".jugador")
    /** @type {Figura[]} */
    this.figuras = Array.from(
     document.querySelectorAll(
      ".figura"))
    this.iniciaX = null
    this.iniciaY = null
    this.interval = null
    this.activo = true
   }

   inicia() {
    document.addEventListener(
     "keydown",
     evt => this.teclas(evt))
    document.addEventListener(
     "touchstart",
     evt => this.iniciaTouch(evt))
    document.addEventListener(
     "touchmove",
     evt =>
      this.desplazaTouch(evt))
    this.interval = setInterval(
     () => this.mueveFiguras(), 60)
   }

   mueveFiguras() {
    for (const f of this.figuras) {
     f.muevete(this.jugador)
    }
    this.detectaColisiones()
   }

   detectaColisiones() {
    for (const f of this.figuras) {
     if (colisiona(
      this.jugador, f)) {
      this.termina()
      break
     }
    }
   }

   termina() {
    this.activo = false
    clearInterval(this.interval)
    alert("Jueguito 2 terminado.")
   }

   /** @param {KeyboardEvent} evt*/
   teclas(evt) {
    if (this.activo) {
     switch (evt.key) {
      case "ArrowLeft":
       this.jugador.izquierda()
       break
      case "ArrowRight":
       this.jugador.derecha()
       break
      case "ArrowUp":
       this.jugador.sube()
       break
      case "ArrowDown":
       this.jugador.baja()
       break
     }
     this.detectaColisiones()
    }
   }

   /** @param {TouchEvent} evt */
   iniciaTouch(evt) {
    if (this.activo) {
     const toqueInicial =
      evt.touches[0]
     this.iniciaX =
      toqueInicial.clientX
     this.iniciaY =
      toqueInicial.clientY
    }
   }

   /** @param {TouchEvent} evt */
   desplazaTouch(evt) {
    if (this.activo
     && this.iniciaX
     && this.iniciaY) {
     const desplazamiento =
      evt.touches[0]
     var desplazamientoX =
      desplazamiento.clientX
     var desplazamientoY =
      desplazamiento.clientY
     var difX = desplazamientoX -
      this.iniciaX
     var difY = desplazamientoY -
      this.iniciaY
     /* Checa que el movimiento no
      * sea muy corto. */
     if (Math.abs(difX)
      + Math.abs(difY)
      > 150) {
      if (Math.abs(difX)
       > Math.abs(difY)) {
       if (difX > 70) {
        this.jugador.derecha()
       } else {
        this.jugador.izquierda()
       }
      } else if (difY > 70) {
       this.jugador.baja()
      } else {
       this.jugador.sube()
      }
      this.detectaColisiones()
      // Reinicia valores.
      this.iniciaX = null;
      this.iniciaY = null;
     }
    }
   }
  }

  /**
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true si los
   *    element colisionan.
   */
  function colisiona(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }

  customElements.define(
   "jugador-paloma",
   class extends Jugador {
    connectedCallback() {
     this.classList.add("sprite")
     this.classList.add("jugador")
     this.innerHTML += "🕊"
     this.style.fontSize = "60px"
     const raiz =
      document.documentElement
     const r =
      this.getBoundingClientRect()
     const left =
      (raiz.clientWidth
       - r.width) /
      2
     const top =
      (raiz.clientHeight -
       r.height) /
      2
     this.style.left = `${left}px`
     this.style.top = `${top}px`
    }

    /** @override */
    sube() {
     const top =
      this.getBoundingClientRect().
       top -
      20
     this.style.top = `${top}px`
    }

    /** @override */
    baja() {
     const top =
      this.getBoundingClientRect().
       top +
      20
     this.style.top = `${top}px`
    }

    izquierda() {
     const left =
      this.getBoundingClientRect().
       left -
      20
     this.style.left = `${left}px`
    }

    derecha() {
     const left =
      this.getBoundingClientRect().
       left +
      20
     this.style.left = `${left}px`
    }
   })

  customElements.define(
   "figura-aguila",
   class extends Figura {
    connectedCallback() {
     this.classList.add("sprite")
     this.classList.add("figura")
     this.innerHTML = "🦅"
     this.style.fontSize = "40px"
     const r =
      this.getBoundingClientRect()
     this.style.left =
      `${r.left}px`
     this.style.top = `${r.top}px`
     this.style.bottom = "auto"
     this.style.right = "auto"
    }

    /**
     * @param {HTMLElement} jugador
     *   el jugador que es
     *   perseguido.
     * @override
     */
    muevete(jugador) {
     const r =
      this.getBoundingClientRect()
     const rJ = jugador.
      getBoundingClientRect()
     const y2 = rJ.top
     const y1 = r.top
     const x2 = rJ.left
     const x1 = r.left
     const pendiente = x2 === x1 ?
      0 :
      (y2 - y1) / (x2 - x1)
     const dirección =
      x2 > x1 ? 1 : -1
     const x = x1 + dirección * 5
     const y =
      pendiente * (x - x1) + y1
     /* Evita que las figuras se
      * peguen, añadiendo un
      * movimiento aleatorio. */
     this.style.left =
      `${desvía(x)}px`
     this.style.top =
      `${desvía(y)}px`
    }
   })

  /**
   * Obtiene una desviación
   * aleatoria de 10 alrededor del
   * valor i.
   * @param {number} i valor base
   */
  function desvía(i) {
   return i + 10 -
    20 * Math.random()
  }
 </script>
</head>

<body>
 <jugador-paloma></jugador-paloma>
 <figura-aguila
   style="right: 0; top: 0;">
 </figura-aguila>
 <figura-aguila
   style="right: 0; bottom: 0;">
 </figura-aguila>
 <script>
  //@ts-check
  const juego2 = new Juego2()
  juego2.inicia()
 </script>
</body>

</html>

O. Jueguito 3

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html>

<head>
 <meta charset="utf-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Jueguito 3</title>
 <style>
  body {
   position: fixed;
   top: 0px;
   left: 0px;
   width: 100%;
   height: 100%;
   margin: 0;
   overflow: hidden;
  }

  .sprite {
   position: fixed;
  }
 </style>
 <script>
  //@ts-check
  /** @abstract */
  class Jugador
   extends HTMLElement {
   /** @param {number} velocidad */
   izquierda(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   derecha(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   sube(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   baja(velocidad) {
    throw new Error("abstract")
   }
  }

  /** @abstract */
  class Figura
   extends HTMLElement {
   /**
    * @param {HTMLElement} jugador
    *   el jugador que es
    *   perseguido.
    */
   muevete(jugador) {
    throw new Error("abstract")
   }
  }

  /** @interface */
  class Fabrica {
   /**
    * Devuelve el único jugador,
    * por lo que se considera
    * Singleton.
    *  @returns {Jugador}
    */
   jugador() {
    throw new Error("interface")
   }
   /**
    * Devuelve un arreglo con las
    * figuras del juego.
    * @returns {Figura[]}
    */
   figuras() {
    throw new Error("interface")
   }
  }

  class Juego3 {
   /** @param {Fabrica} fabrica */
   constructor(fabrica) {
    this.jugador =
     fabrica.jugador()
    this.figuras =
     fabrica.figuras()
    this.iniciaX = null
    this.iniciaY = null
    this.interval = null
    this.activo = true
   }

   inicia() {
    document.addEventListener(
     "keydown",
     evt => this.teclas(evt))
    document.addEventListener(
     "touchstart",
     evt => this.iniciaTouch(evt))
    document.addEventListener(
     "touchmove",
     evt =>
      this.desplazaTouch(evt))
    this.interval = setInterval(
     () => this.mueveFiguras(), 60)
   }

   mueveFiguras() {
    for (const f of this.figuras) {
     f.muevete(this.jugador)
    }
    this.detectaColisiones()
   }

   detectaColisiones() {
    for (const f of this.figuras) {
     if (colisiona(
      this.jugador, f)) {
      this.termina()
      break
     }
    }
   }

   termina() {
    this.activo = false
    clearInterval(this.interval)
    this.jugador.innerHTML = "💥"
   }

   /** @param {KeyboardEvent} evt*/
   teclas(evt) {
    if (this.activo) {
     switch (evt.key) {
      case "ArrowLeft":
       this.jugador.izquierda(20)
       break
      case "ArrowRight":
       this.jugador.derecha(20)
       break
      case "ArrowUp":
       this.jugador.sube(20)
       break
      case "ArrowDown":
       this.jugador.baja(20)
       break
     }
     this.detectaColisiones()
    }
   }

   /** @param {TouchEvent} evt */
   iniciaTouch(evt) {
    if (this.activo) {
     const toqueInicial =
      evt.touches[0]
     this.iniciaX =
      toqueInicial.clientX
     this.iniciaY =
      toqueInicial.clientY
    }
   }

   /** @param {TouchEvent} evt */
   desplazaTouch(evt) {
    if (this.activo
     && this.iniciaX
     && this.iniciaY) {
     const desplazamiento =
      evt.touches[0]
     var desplazamientoX =
      desplazamiento.clientX
     var desplazamientoY =
      desplazamiento.clientY
     var difX = desplazamientoX -
      this.iniciaX
     var difY = desplazamientoY -
      this.iniciaY
     if (Math.abs(difX)
      + Math.abs(difY)
      > 150) {
      if (Math.abs(difX)
       > Math.abs(difY)) {
       if (difX > 70) {
        this.jugador.derecha(40)
       } else {
        this.jugador.izquierda(40)
       }
      } else if (difY > 70) {
       this.jugador.baja(40)
      } else {
       this.jugador.sube(40)
      }
      this.detectaColisiones()
      this.iniciaX = null
      this.iniciaY = null
     }
    }
   }
  }

  /**
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true si los
   *    element colisionan.
   */
  function colisiona(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }

  customElements.define(
   "jugador-paloma",
   class extends Jugador {
    connectedCallback() {
     this.classList.add("sprite")
     this.innerHTML += "🕊"
     this.style.fontSize = "60px"
     const raiz =
      document.documentElement
     const r =
      this.getBoundingClientRect()
     const left =
      (raiz.clientWidth
       - r.width) /
      2
     const top =
      (raiz.clientHeight -
       r.height) /
      2
     this.style.left = `${left}px`
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    sube(velocidad) {
     const top =
      this.getBoundingClientRect().
       top -
      velocidad
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    baja(velocidad) {
     const top =
      this.getBoundingClientRect().
       top +
      velocidad
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    izquierda(velocidad) {
     const left =
      this.getBoundingClientRect().
       left -
      velocidad
     this.style.left = `${left}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    derecha(velocidad) {
     const left =
      this.getBoundingClientRect().
       left +
      velocidad
     this.style.left = `${left}px`
    }
   })

  customElements.define(
   "figura-aguila",
   class extends Figura {
    connectedCallback() {
     this.classList.add("sprite")
     this.innerHTML = "🦅"
     this.style.fontSize = "40px"
     const r =
      this.getBoundingClientRect()
     this.style.left =
      `${r.left}px`
     this.style.top = `${r.top}px`
     this.style.bottom = "auto"
     this.style.right = "auto"
    }

    /**
     * @param {HTMLElement} jugador
     *   el jugador que persigue.
     * @override
     */
    muevete(jugador) {
     const r =
      this.getBoundingClientRect()
     const rJ = jugador.
      getBoundingClientRect()
     const y2 = rJ.top
     const y1 = r.top
     const x2 = rJ.left
     const x1 = r.left
     const pendiente = x2 === x1 ?
      0 :
      (y2 - y1) / (x2 - x1)
     const dirección =
      x2 > x1 ? 1 : -1
     const x = x1 + dirección * 5
     const y =
      pendiente * (x - x1) + y1
     this.style.left =
      `${desvía(x)}px`
     this.style.top =
      `${desvía(y)}px`
    }
   })

  function desvía(i) {
   return i + 10 -
    20 * Math.random()
  }
 </script>
</head>

<body>
 <jugador-paloma></jugador-paloma>
 <figura-aguila
   style="right: 0; top: 0;">
 </figura-aguila>
 <figura-aguila
   style="right: 0; bottom: 0;">
 </figura-aguila>
 <script>
  //@ts-check
  /** @implements {Fabrica} */
  class MiFabrica {
   constructor() {
    /** @type {Jugador} */
    this.jugadorSingleton =
     document.querySelector(
      "jugador-paloma");
   }
   /**
    * Devuelve el único jugador,
    * por lo que se considera
    * Singleton.
    * @returns {Jugador}
    */
   jugador() {
    return this.jugadorSingleton
   }
   /**
    * Devuelve un arreglo con las
    * figuras del juego.
    * @returns {Figura[]}
    */
   figuras() {
    return Array.from(
     document.querySelectorAll(
      "figura-aguila"))
   }
  }
  const juego =
   new Juego3(new MiFabrica())
  juego.inicia()
 </script>
</body>

</html>

P. Resumen