Cómo dibujar un mapa de losetas con canvas

May 13, 2026 08:11 — desarrollo de videojuegos, javascript

Llevo tiempo tonteando con la idea de hacer un videojuego. No porque tenga una gran idea, sino porque quiero aprender cómo se hacen. Como estoy interesado en aprender desde "cero", no quiero usar un game engine como Unity o Godot. He probado Dragon Ruby y es genial poder programar videojuegos en ruby, mi lenguaje preferido. Al final me he decidido por javascript por una sencilla razón: siempre que tenga acceso a un navegador puedo probar conceptos desde la consola o páginas como Codepen.

En la MDN hay toda una guía de desarrollo de videojuegos en javascript. Esa guía y ChatGPT gratis han sido mis recursos por ahora.

Tengo la intención de documentar lo que voy aprendiendo con ejemplos lo más sencillos posibles, para que me sea fácil reaprender en el futuro. Retomo el aprendizaje cada ciertos meses y aunque los conceptos se quedan, reimplementarlos me cuesta un montón y acabo usando ayuda de tutoriales e IA.

Voy a empezar sin organizar el código. He leído sobre algunos patrones de diseño comunes en el desarrollo de videojuegos, pero no me quiero adelantar. Quiero ir desarrollando en base a mis necesidades.

Puedes explorar el código y ver el resultado en Codepen. A continuación está la explicación paso a paso.

Lo primero, necesitamos un canvas.

<canvas id="canvas"></canvas>

Accedemos al canvas, y obtenemos su contexto. No voy a pararme a explicar cómo funciona un canvas en este post. Puedes revisar la guía de canvas en MDN.

const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
ctx.imageSmoothingEnabled = false

De momento no me quiero liar con imágenes y sprites, así que para los primeros posts usaré cuadrados de colores.

const TILES = {
  1: "brown",
  2: "green",
  3: "blue"
}

Siguiendo el tutorial de MDN, pongo toda la información del mapa en una constante. tileSize es el temaño de cada loseta. Como en este caso son cuadrados de color, no importa mucho y se puede cambiar este valor por cualquiera. Cuando se usen imágenes, tendremos que ajustar este valor.

const MAP = {
  tileSize: 32,
  cols: 6,
  rows: 6,
  tiles: [
    2, 2, 2, 3, 2, 2,
    1, 1, 1, 3, 1, 2,
    1, 2, 1, 3, 1, 2,
    1, 1, 1, 1, 1, 2,
    5, 1, 1, 3, 1, 2,
    3, 3, 3, 3, 3, 3,
  ]
}

Una vez tenemos el mapa, podemos determinar el temaño del canvas:

canvas.width = MAP.cols * MAP.tileSize
canvas.height = MAP.rows * MAP.tileSize

Para dibujar una loseta en el mapa, he creado una función:

function drawTile(col, row, tileNumber) {
  if (tileNumber in TILES) {
    ctx.fillStyle = TILES[tileNumber]

    ctx.fillRect(
      col * MAP.tileSize,
      row * MAP.tileSize,
      MAP.tileSize,
      MAP.tileSize
    )
  }
}

Para poder debuggear más fácilmente, puedes mostrar el número de loseta añadiendo después de ctx.fillRect. Los ajustes en el contexto son globales, por lo que hay que sobreescribirlos cada vez.

ctx.fillStyle = "black"
ctx.font = "16px monospace"
ctx.textAlign = "center"
ctx.textBaseline = "middle"

ctx.fillText(
  tileNumber,
  col * MAP.tileSize + MAP.tileSize / 2,
  row * MAP.tileSize + MAP.tileSize / 2
)

Ya con el mapa definido y la función para dibujar las losetas, lo único que falta es iterar sobre cada loseta del mapa y dibujarla. Esta es una versión bastante explicita en la que creo que es bastante fácil seguir qué es lo que está pasando.

function renderMap(map) {
  let currentCol = 0
  let currentRow = 0

  map.tiles.forEach(tile => {
    drawTile(currentCol, currentRow, tile)

    if (currentCol == map.cols - 1) {
      currentCol = 0
      currentRow++
    } else {
      currentCol++
    }
  })
}

Otra versión con menos código sería:

function renderMap(map) {
  map.tiles.forEach((tile, index) => {
    const col = index % map.cols
    const row = Math.floor(index / map.cols)

    drawTile(col, row, tile)
  })
}

Hay muchas maneras diferentes de iterar sobre las losetas del mapa para dibujarlas. En el tutorial de MDN lo hacen con dos bucles for anidados, uno para las columnas y otro para las filas.

Imagino que el método elegido afectará en el futuro cuando tenga que dibujar varias capas o interactuar con ellas. De momento me parece más intuitivo un único bucle.

Por último solo queda llamar a la función para pintar el mapa.

renderMap(MAP)

Espero que con esto de copiar y pegar no se me haya pasado nada. Si tienes alguna duda o ves que algo no funciona como esperas, déjame un comentario y lo miramos.

En las siguientes iteraciones exploraré cómo añadir un cuadrado que represente al personaje jugador y se pueda mover por el mapa, así como una cámara, para que el personaje esté siempre en el centro de la pantalla y solo se vea la zona de mapa a su alrededor.

Migrar ActiveStorage de Disk a otro servicio

January 19, 2026 18:28 — active storage, ruby on rails

Cuando empiezo una aplicación nueva de Rails intento configurar lo menos posible, especialmente cuando se trata de servicios externos. Cuando ya se hace insostenible es cuando me decido a dar el paso. En este caso tengo una aplicación corriendo en una Raspberry, a la que accede mi familia desde diferentes partes del mundo. Es una pequeña red social privada. Ha llegado el momento en el que he sentido que lo mejor es mover las imágenes y vídeos a un proveedor externo, para mejorar la latencia y el cach

Lo primero de todo es configurar un servicio y usarlo como mirror para ver que todo funciona correctamente.

Tras configurarlo, la mejor manera de comprobar que funciona es subir algún fichero desde la aplicación e ir al servicio configurado y ver que efectivamente se han creado ficheros en él.

El servicio de mirroring solo funciona desde el momento en que se configura, no tiene carácter retroactivo. Tendremos que, por lo tanto, migrar todos los archivos antiguos a mano.

La opción más efectiva que he encontrado consiste en lo siguiente:

# Fetch services
local_service      = ActiveStorage::Blob.services.fetch(:local)
cloudflare_service = ActiveStorage::Blob.services.fetch(:cloudflare)

# Loop through all blobs, skipping already migrated ones
ActiveStorage::Blob.find_each(batch_size: 50) do |blob|
  next if blob.service_name == "cloudflare"

  begin
    # Download from local disk and upload to Cloudflare
    local_service.download(blob.key) do |file|
      cloudflare_service.upload(blob.key, file, checksum: blob.checksum)
    end

    # Update blob to point to Cloudflare
    blob.update!(service_name: "cloudflare")

    puts "Migrated blob #{blob.id}"
  rescue => e
    puts "Failed to migrate blob #{blob.id}: #{e.message}"
  end
end

Aunque intento llegar a soluciones por mi mismo, en este caso me estaba complicando la vida y he acabado tirando de una IA gratuita para que me revise el código. Resulta que ActiveStorage::Blob tiene métodos download y upload que vienen finísimos para esta tarea.

Lo ideal es poner esto en una tarea de rake que encole jobs y bla bla bla, pero aquí hemos venido a jugar. Ha tardado un buen rato y al final todos los ficheros han acabado duplicados.

El siguiente paso es cambiar el mirror de tal forma que el servicio primario sea cloudflare (o el que tú quieras) y el secundario sea local. Y en cuanto hayas comprobado que todo funciona como esperas, puedes cargarte el mirror y funcionar solo con el nuevo servicio.

Lo que me falta por hacer ahora es borrar los ficheros del disco, pero no tengo prisa y no quiero cargarme nada, así que creo que se quedarán ahí hasta que sea inevitable mover ficha.

Si lo que deseas o necesitas es migrar entre servicios que no sean locales, el proceso es más fácil todavía, ya que puedes usar el método .mirror_later en cada blob. Puedes leer más sobre este método en el blog de Oliver Eidel.

Instalar Ruby con una versión de openssl específica

December 10, 2025 23:34 — openssl, ruby

Hace poco descubrí Sendamatic, un servicio para enviar emails transaccionales con un precio decente, y decidí usarlo en varios de mis proyectos.

Configuré Rails para usar Action Mailer. Todo estaba bien configurado, porque en producción funcionaba, pero en local me daba un error de SSL cada vez que intentaba enviar un email.

Encontré la solución en Github. El problema es una incompatibilidad de mis versiones locales de openssl y ruby. La solución es decirle al instalador de ruby qué openssl quieres usar.

Como uso rbenv para gestionar mis versiones de ruby y brew para instalar programas, he de instalar nuevas versiones de ruby con el siguiente comando:

RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix [email protected])" rbenv install 3.4.7

En el trabajo uso mise y el comando necesario es el siguiente:

MISE_RUBY_BUILD_OPTS="--with-openssl-dir=$(brew --prefix [email protected])" mise install

En mi caso tengo openssl 3.6 en local y parece que no es compatible con ruby 3.4 y posteriores, así que uso la 3.5 que no me ha dado problemas.

Para comprobar qué versión de openssl usa tu versión actual de ruby hay unos cuantos comandos que son útiles.

ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION'
ruby -ropenssl -e 'puts OpenSSL::OPENSSL_LIBRARY_VERSION rescue puts "N/A"'
ruby -rrbconfig -e 'puts RbConfig::CONFIG["configure_args"]'

Este último comando escupe un montón de cosas. Busca por algo como --with-openssl-dir=/opt/homebrew/opt/[email protected].

Me ha pasado dos veces, y a la tercera he decidido dejarlo por escrito. Me volverá a pasar.

Un método simple para prevenir el spam

October 30, 2025 17:45 — 2 comentarios — blog

Hace unos días leí una teoría del por qué es más fácil que te entre spam teniendo formularios en la web que publicando tu email.

De momento lo que he experimentado es que desde que activé los comentarios, un montón de bots han estado validando mi código. Efectivamente, se pueden dejar comentarios. Me hubiese gustado que los comentarios viniesen de humanos, pero qué se le va a hacer.

Se me ocurrió una manera muy sencilla de filtrar comentarios de spam. He asumido que los comentarios spam vienen de scripts que detectan formularios y los rellenan automáticamente. La solución ha consistido en añadir un campo oculto con CSS que, de ser rellenado, ignoro el comentario y no lo persisto en la base de datos.

La solución es muy chorra y solo sirve para spam muy genérico, pero ¿quién va a dirigir una campaña personalizada hacia un mindundi como yo?

Añade etiquetas a un modelo con Ruby on Rails

August 29, 2025 12:35 — ruby on rails

Aprovecho que he añadido etiquetas al blog para explicar lo fácil que ha sido hacerlo con Ruby on Rails. Si tienes curiosidad por ver el código en profundidad, puedes ver la PR en Github. Se me fue un poco la mano y acabé añadiendo estilos y tests que faltaban por ahí.

Lo primero son los modelos. La manera fácil de hacerlo es usando has_and_belongs_to_many pero en el futuro quiero poder etiquetar cualquier modelo, así que me he decidido por usar un modelo intermedio en el que en el futuro migraré a una asociación polimórfica (exacto, Yagni).

Los modelos asociados quedan así:

class Entry < ApplicationRecord
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings
end

class Tag < ApplicationRecord
  has_many :taggings, dependent: :destroy
  has_many :entries, through: :taggings
end

class Tagging < ApplicationRecord
  belongs_to :entry
  belongs_to :tag
end

De esta manera es super fácil acceder a las etiquetas desde una entrada (Entry.first.tags) o ver todas las entradas etiquetadas con una determinada etiqueta (Tag.first.entries).

Asociando desde la UI

He creado la sección correspondiente del panel de administración para añadir/editar/eliminar etiquetas.

Al añadir/editar una entrada se muestra una lista de etiquetas con un checkbox al lado para seleccionar las que quiera. Rails hace que esto sea extremadamente simple. Tanto el mostrar la lista de etiquetas como el asociarlas directamente al crear/actualizar una entrada.

Lo primero es actualizar los parámetros permitidos en el controlador. Como se pueden asociar varias etiquetas, esta es la sintaxis a usar:

params.expect(entry: [ :title, ..., tag_ids: [] ])

En el formulario de las entradas he usado este helper que se encarga de mostrar todas las etiquetas con un checkbox para seleccionarlas.

<%= form.collection_checkboxes(:tag_ids, Tag.all, :id, :name) %>

El resultado no es el más bonito del mundo, pero necesito que funcione todo sin Javascript. Tengo algunas ideas para mejorar progresivamente usando turbo-frames y autocompletar, para poder añadir nuevas etiquetas directamente desde el formulario.

El helper acepta diferentes parámetros y hasta bloques, permitiendo personalizar cómo se ven las etiquetas. Por ejemplo, para la barra lateral el código luce tal que así:

<%= form.collection_checkboxes(:tags, Tag.all, :name, :name, include_hidden: false, checked: params[:tags]) do |b| %>
  <%= b.label { b.checkbox + b.text } %>
<% end %>