Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Crea tu DSL con Elixir

dsl.livemd

Crea tu DSL con Elixir

Mix.install([
  :kino,
  :vega_lite,
  {:kino_vega_lite, "~> 0.1.8"}
])

Introducción: ¿Qué es un DSL?

Un DSL (Domain Specific Language) es un lenguaje pequeño, diseñado para expresar conceptos de un dominio específico de forma clara y concisa.

En este Livebook construiremos paso a paso un DSL con Elixir para describir escenarios narrativos interactivos, como los de un motor de novela visual o aventura conversacional.

Objetivo del DSL

Queremos lograr una sintaxis como esta:

escenario "Bosque" do
  texto "Te adentras en un bosque oscuro."
  opcion "Explorar el sendero", va: "Sendero"
  opcion "Volver", va: "Pueblo"
end

Esto debe traducirse en estructuras que puedan interpretarse, visualizarse o ejecutarse como parte de una historia.

Definición de estructuras base

defmodule DSL do
  defstruct nombre: nil, texto: nil, opciones: []
end

defmodule Opcion do
  defstruct texto: nil, va: nil
end

Macros para el DSL

defmodule DSL.Macro do
  defmacro escenario(nombre, do: bloque) do
    quote do
      DSL.Builder.iniciar_escenario(unquote(nombre))
      unquote(bloque)
      DSL.Builder.obtener_escenario()
    end
  end

  defmacro texto(linea) do
    quote do
      DSL.Builder.set_texto(unquote(linea))
    end
  end

  defmacro opcion(titulo, va: destino) do
    quote do
      DSL.Builder.agregar_opcion(%Opcion{texto: unquote(titulo), va: unquote(destino)})
    end
  end
end

Builder acumulador

defmodule DSL.Builder do
  use Agent

  def iniciar_escenario(nombre) do
    Agent.start_link(fn -> %DSL{nombre: nombre} end, name: __MODULE__)
  end

  def set_texto(txt), do: Agent.update(__MODULE__, &%{&1 | texto: txt})

  def agregar_opcion(op), do: Agent.update(__MODULE__, fn esc ->
    %{esc | opciones: esc.opciones ++ [op]}
  end)

  def obtener_escenario, do: Agent.get(__MODULE__, & &1)
end

Probando el DSL

import DSL.Macro

escenario "BosquePrueba" do
  texto "Te adentras en un bosque luminoso y tienes un campo de futbol delante."
  opcion "Explorar el campo de futbol", va: "Campo de fútbol"
  opcion "Volver a casa", va: "Casa"
end

Visualización en Kino

esc = DSL.Builder.obtener_escenario()

Kino.Markdown.new("""
## 🌳 Escenario: #{esc.nombre}

📜 *#{esc.texto}*

**Opciones disponibles:**
#{Enum.map_join(esc.opciones, "\n", fn op -> "- [#{op.texto}#{op.va}]" end)}
""")

Ampliamos

Vamos a extender la Livebook con una demostración más completa, que muestre la utilidad real del DSL como motor narrativo, incluyendo:

🔁 Múltiples escenarios conectados (incluyendo el que ya hemos creado)

🎮 Navegación interactiva con Kino.Input

🧠 Estado del jugador (vida, inventario, etc.)

🗺️ Mapa dinámico de la historia

Escenarios múltiples conectados

escenarios = %{
  "Bosque" =>
    escenario "Bosque" do
      texto "Te adentras en un bosque oscuro."
      opcion "Explorar el sendero", va: "Sendero"
      opcion "Volver", va: "Pueblo"
    end,

  "Sendero" =>
    escenario "Sendero" do
      texto "Encuentras huellas extrañas en el barro."
      opcion "Seguir las huellas", va: "Cueva"
      opcion "Regresar", va: "Bosque"
    end,

  "Pueblo" =>
    escenario "Pueblo" do
      texto "Estás en el pueblo abandonado. Todo está en silencio."
      opcion "Entrar a la taberna", va: "Taberna"
      opcion "Ir al bosque", va: "Bosque"
    end,

  "Cueva" =>
    escenario "Cueva" do
      texto "La cueva está oscura y escuchas un rugido."
      opcion "Huir", va: "Bosque"
    end,

  "Taberna" =>
    escenario "Taberna" do
      texto "Dentro de la taberna hay una nota misteriosa en la barra."
      opcion "Leer la nota", va: "Final"
    end,

  "Final" =>
    escenario "Final" do
      texto "¡Has descubierto el secreto del pueblo!"
    end
}

Estado inicial del jugador

estado_inicial = %{ubicacion: "Bosque", vida: 10, inventario: []}

# Guardamos en un Agent
defmodule EstadoJugador do
  use Agent
  def start_link(init \\ %{}), do: Agent.start_link(fn -> init end, name: __MODULE__)
  def get, do: Agent.get(__MODULE__, & &1)
  def update(fun), do: Agent.update(__MODULE__, fun)
  def set_estado(est), do: Agent.update(__MODULE__, fn _ -> est end)
end

EstadoJugador.start_link(estado_inicial)

Interfaz de navegación interactiva

defmodule Navegador do
  import Kino.Input

  def mostrar_escenario(escenario) do
    Kino.Markdown.new("""
    ## 📍 Ubicación: #{escenario.nombre}
    **🧾 #{escenario.texto}**

    Opciones:
    #{Enum.map_join(escenario.opciones, "\n", fn op -> "- #{op.texto}" end)}
    """)
  end

  def interactuar(escenarios) do
    estado = EstadoJugador.get()
    #IO.puts(estado)
    
    actual = escenarios[estado.ubicacion]

    #IO.puts(actual)

    mostrar_escenario(actual)

    opciones = actual.opciones
                |> Enum.map(fn op -> {op.va, op.va} end)

    selector = Kino.Input.select("Elige tu acción", opciones)

    Kino.render(selector)

    Kino.listen(selector, fn selected ->
      #IO.puts(selected)
      EstadoJugador.update(&Map.put(&1, :ubicacion, selected.value))
      interactuar(escenarios)  # Recursivo
    end)
  end
end

# Ejecutar:
Navegador.interactuar(escenarios)

Vida, inventario y más

Agrega al texto del escenario dinámicamente:

defmodule Navegador2 do
  import Kino.Input

  def mostrar_escenario(escenario) do
    estado = EstadoJugador.get()
  
    Kino.Markdown.new("""
    ## 📍 Ubicación: #{escenario.nombre}
    **🧾 #{escenario.texto}**
  
    🧠 Estado:
    - Vida: #{estado.vida}
    - Inventario: #{Enum.join(estado.inventario, ", ") || "vacío"}
  
    Opciones:
    #{Enum.map_join(escenario.opciones, "\n", fn op -> "- #{op.texto}" end)}
    """)
  end

  def interactuar(escenarios) do
    estado = EstadoJugador.get()
    actual = escenarios[estado.ubicacion]

    mostrar_escenario(actual)

    opciones = actual.opciones
                |> Enum.map(fn op -> {op.va, op.va} end)

    selector = Kino.Input.select("Elige tu acción", opciones)
    Kino.render(selector)

    Kino.listen(selector, fn selected ->
      EstadoJugador.update(&Map.put(&1, :ubicacion, selected.value))
      interactuar(escenarios)  # Recursivo
    end)
  end
end

# Ejecutar:
Navegador2.interactuar(escenarios)