Colofón

Sobre el Autor

Camilo Castro es un Ingeniero en Software especializado en el desarrollo de aplicaciones Web y Móviles. Actualmente su lenguaje de programación preferido es Elixir en paradigma funcional y Wren en paradigma orientado a objetos.

En este pequeño documento se detallarán lo básico del lenguaje de programación Wren.

1. Pequeña Historia de Wren

Wren es un lenguaje de programación orientado a objetos creado por Bob Nystrom, un ex desarrollador de videojuegos y creador de otros lenguajes de programación como Vigil, Finch y Magpie. Ha escrito dos libros: Game Programming Patterns y Crafting Interpreters.

El primer commit de Wren fue escrito el 22 de Octubre del 2013 y su versión más reciente es la 0.3. Actualmente Bob Nystrom pasó a ser colaborador y la evolución del lenguaje fue asignada a _discovery (creador del game engine Luxe).

Wren fue creado como una alternativa a Lua y otros lenguajes de scripting para personas que estuvieran más familiarizadas con lenguajes orientados a objetos como Java, C#, o C++. Su principal foco es la simpleza.

La página oficial de Wren es Wren.io.

La principal característica de Wren es ser un lenguaje minimalista. Esto significa que no todas las ideas y utilidades serán implementadas en la API oficial. Muchas veces se tendrá la obligación de implementar funcionalidades adicionales que en otros lenguajes están disponibles de fábrica. Normalmente la recomendación es tener una serie de archivos con utilidades a mano y si algún día son implementadas en la API oficial, poder reemplazarlos adecuadamente.

1.1. ¿Qué es un Wren?

Wren es un tipo de ave pequeña perteneciente a la familia de los troglodítidos (que habita en cavernas o cuevas). En Chile tenemos al Chercán o Chochin Criollo.

Chochin Criollo - Fotografía por Juan Tassara B.

Fotografía por Juan Tassara B en https://www.avesdechile.cl.

Esta familia de aves se caracteriza por sus canciones elaboradas y complejas. Es uno de los más queridos compositores de las aves. Sus canciones tienen una rica variedad de cromatura musical.

También son aves muy hogareñas que prefieren quedarse en un mismo territorio a emigrar. Cuando vuelan sorprenden con la gran altura que pueden alcanzar y su rapidez. ¡Nunca sabes en que dirección van a salir!.

En las tradiciones celtas, el Wren es un símbolo de vivacidad y actividad.

1.2. ¿Por qué aprender Wren?

Existen algunos lenguajes interpretados usados para ser incrustados dentro de aplicaciones. Lua es uno de los más populares. Actualmente también son populares JavaScript o Python.

Lua es genial, simple y rápido. Pero muchas veces su naturaleza es un poco incómoda para personas más acostumbaradas a lenguajes como C++ o Java. La sintaxis es diferente y la semántica del modelo de objetos es algo inusual.

Wren es una alternativa a Lua pensada para personas mas familiarizadas con lenguajes orientados a objetos.

Acá podemos comparar la implementación de una clase llamada Cuenta entre Lua y Wren.

Lua
-- cuenta.lua

Cuenta = {}
Cuenta.__index = Cuenta

function Cuenta.crear(balance)
  local cuenta = {}             -- nuevo objeto
  setmetatable(cuenta,Cuenta)   -- hacer el objeto cuenta tenga las propiedades de Cuenta
  cuenta.balance = balance      -- inicializar el objeto
  return cuenta
end

function Cuenta:retirar(monto)
  self.balance = self.balance - monto
end

-- crear y usar una Cuenta
cuenta = Cuenta.crear(1000)
cuenta:retirar(100)
Wren
// cuenta.wren

class Cuenta {
  construct crear(balance) { _balance = balance }
  retirar(monto) { _balance = _balance - monto }
}

// crear y usar una Cuenta
var cuenta = Cuenta.crear(1000)
cuenta.retirar(100)

Los lenguajes orientados a objetos y clases tradicionalmente tienen una reputación de ser complejos. Esto es principalmente por que los lenguajes que implementan este paradigma son complejos: C++, Java, C#, Ruby y Python.

Uno de los objetivos de Wren es demostrar que son dichos lenguajes los complejos, no la orientación a objetos y clases en sí. Smalltalk fue uno de los precursores y la inspiración para muchos de aquellos lenguajes. Es tan simple que toda su sintaxis puede ser escrita en una tarjeta kardex. Wren desea ser igual de minimalista, pero al mismo contar con el poder expresivo de las clases y orientación a objetos.

Finalmente para responder la pregunta de "¿Por qué aprender Wren?". Principalmente por que aprender un nuevo lenguaje de programación siempre entregará experiencia que puede ser extrapolada a otros ámbitos y contextos. Tal vez sea más simple de aprender un concepto en Wren que en Python o Lua. Quizás un lenguaje de nicho como Wren no tenga muchas ofertas laborales que lo soliciten, pero todo lo aprendido siempre será nutritivo para el desarrollo profesional.

1.3. ¿Cómo usar Wren?

Wren es un lenguaje interpretado, lo que significa que no requiere de compilación. Basta simplemente tener un intérprete para ejecutar los algoritmos. Hay varias alternativas.

  1. Compilar tu propio intérprete de Wren. Esta es la opción para valientes.

  2. Utiliza Wren CLI para ejecutar archivos *.wren.

  3. Utiliza un engine de juegos como Dome, TIC 80 o Luxe que traen Wren listo para llegar y utilizar.

  4. Utiliza Wren Try para probar el lenguaje en tu navegador.

Una vez que tengas tu intérprete instalado puedes ejecutar por ejemplo`./wren main.wren` para ver el resultado de tu script.

1.4. Wren CLI

La terminal de Wren llamada Wren CLI (Interfaz de línea de comandos) permite dos modos de operación. El primero es el modo REPL (Bucle Lectura-Evaluación-Impresión) el cual nos permite evaluar las instrucciones una a una. El segundo modo es el intérprete que permite ejecutar scripts Wren.

Si ejecutamos simplemente el comando ./wren entraremos al modo REPL. Donde se nos mostrará un simpático pajarito y la versión de Wren instalada.

Wren CLI

1.4.1. Operaciones Matemáticas

Podremos ejecutar operaciones matemáticas como 5 * 5.

5 * 5

1.4.2. System.print()

O También mostrar un mensaje utilizando System.print().

System.print()

¿Por qué aparece la palabra Plop! dos veces?. Simplemente por que la primera vez es el resultado de la operación System.print() (mostrar un valor), mientras que la segunda vez está mostrando el valor retornado por System.print(), el cual es el mismo texto.

1.4.3. Errores

Si cometemos algún error Wren CLI nos avisará con un bonito mensaje.

Error

1.4.4. Ejecución de archivos

Cuando necesitamos algo mucho más avanzado podemos utilizar archivos que finalicen con la extensión *.wren. El nombre más común es main.wren, aunque puedes llamarlo con cualquier nombre. Para ejecutar un archivo específico simplemente se debe utilizar ./wren <archivo.wren>.

  1. Creamos un nuevo archivo llamado main.wren.

  2. Escribimos nuestro código (En este caso System.print("Plop!")) y guardamos.

  3. Finalmente Ejecutamos ./wren main.wren.

Deberíamos ver un resultado similar a este:

Plop!

Notar como solamente aparece una vez "Plop!", ya que no estamos en modalidad REPL.

1.4.5. Códigos de salida (Exit code)

Cada vez que un programa termina su ejecución devuelve un código numérico para indicar si terminó exitosamente o tuvo un algún tipo de error mientras se ejecutaba.

La convención tradicional es considerar el código cero (0) como éxito y cualquier otro número como error. No hay un estándar definido para la asignación de números de error. Wren se basa levemente en los códigos utilizados por el sistema operativo BSD.

Actualmente no puedes elegir el código de error. Wren automáticamente asignará el código que mejor se ajuste a la situación. Por ejemplo si utilizar la instrucción para terminar la ejecución del programa Fiber.abort("Mensaje de Error") el código asignado sera de 70 (error interno del software).

La siguiente es una tabla con los códigos de BSD más algunos otros adicionales.

0 - Fin del programa exitoso
1 - Error desconocido
2 - (grep) Uso incorrecto del comando | (bash) Error de entrada/salida
64 - Uso incorrecto del comando
65 - Error de formato de datos
66 - Sin acceso a la entrada
67 - Dirección desconocida
68 - Nombre de dominio (host name) desconocido
69 - Servicio no disponible
70 - Error interno del software
71 - Error de sistema
72 - Archivo crítico del sistema no encontrado
73 - Imposible de crear archivo de salida
74 - Error de entrada/salida
75 - Falla temporal
76 - Error remoto en protocolo
77 - Permiso denegado
78 - Error de configuración
126 - Comando encontrado, pero no es ejecutable
127 - Comando no encontrado
128 - Código de salida no válido
128   Error fatal terminado por kill -9
140 - Comando terminado por Ctrl-C
141 - Comando terminado por Ctrl-D
255 - Código de salida fuera de rango

Esta es la lista de los errores usados en Wren:

// Exit codes used by the wren binaries, following the BSD standard
//
// The interpreter was used with an incorrect number of arguments
#define WREN_EX_USAGE 64

// Compilation error
#define WREN_EX_DATAERR 65

// Runtime error
#define WREN_EX_SOFTWARE 70

// Cannot open input file
#define WREN_EX_NOINPUT 66

// I/O Error
#define WREN_EX_IOERR 74

Puedes verificar el código numérico de salida utilizando el siguiente comando (bash)

./wren main.wren | echo "Exit code ${PIPESTATUS[0]}"

Exit Code 0

1.5. Ejercicios

1.5.1. ¡Hola Wren!

Es una tradición que cuando se esté aprendiendo un lenguaje de programación se escriba una variante del mensaje "!Hola mundo¡". Este es un programa muy simple que permite verificar que tu computadora esta correctamente configurada para correr programas en Wren.

Para este ejercicio simplemente se debe crear un nuevo archivo llamado hola.wren con una única instrucción que muestre el mensaje "¡Hola Wren!".

Luego ejecutar el programa utilizando el intérprete de Wren.

Ejecución

$ ./wren hola.wren

Salida
¡Hola Wren!
Código
// muestra: ¡Hola Wren!

2. Nociones Básicas

Todo lenguaje de programación define estructuras base que determinan la forma de escribir el lenguaje. El siguiente capítulo detalla las normas básicas con las que funciona Wren.

2.1. Tipos de datos

Todos los tipos de datos en Wren son objetos, instancias de una clase específica. Las operaciones son métodos disponibles en dichas clases.

Wren

Estas son las clases disponibles dentro del lenguaje Wren "vainilla".

  1. Bool

  2. Class

  3. Fiber

  4. Fn

  5. List

  6. Map

  7. Null

  8. Num

  9. Object

  10. Range

  11. Sequence

  12. String

  13. System

  14. Meta

  15. Random

Importante

No se puede heredar desde estas clases debido al problema de Reentrancia (Wren no tiene esta capacidad). Para extender solo es posible usando la composición (Crear una nueva clase). Esto significa que al llamar a un método de una clase que herede de un tipo base, la máquina virtual no podrá diferenciar los métodos (Ya que son clases creadas con código de bajo nivel), lo que puede causar errores, por esta razón no está permitido heredar de las clases primitivas.

Wren CLI

Estas clases son exclusivas de la Wren CLI. No estarán disponibles fuera del entorno de ejecución de este intérprete de Wren.

  1. Directory

  2. File

  3. Stat

  4. Stdin

  5. Stdout

  6. Platform

  7. Process

  8. Scheduler

  9. Timer

2.2. Variables

Una variable es un contenedor de un valor específico. Este valor puede ser una cadena de caracteres, númerico, booleano u otro tipo de objeto.

var mensaje = "Hola mundo Wren"
System.print(mensaje)

Se puede modificar el valor posteriormente

var mensaje = "Hola mundo Wren"
System.print(mensaje)

mensaje = "El chercán es una bonita ave"
System.print(mensaje)

Si usamos una variable sin haberla declarado antes recibiremos un error similar a lo siguiente:

var mensaje = "Hola"
System.print(mensaj)
[repl line 1] Error at 'mensaj': Variable is used but not defined.

Debemos siempre procurar que todas nuestras variables estén declaradas dentro del contexto y que no existan errores de escritura en sus nombres.

2.3. Identificadores

Similar al Lenguaje C, para los indentificadores (nombres de variables, clases, metodos, funciones) se pueden utilizar los caracteres de la lista ascii y comenzar con un caracter alfabético o guión bajo. Los identificadores en Wren diferencian entre mayúsculas y minúsculas. Solo se permiten letras (A - Z, a - z), números (0 - 9) y guión bajo (_). No se permiten espacios o comenzar con un número o guión alto (-).

2.3.1. Ejemplo de Identificadores válidos

hola
camelCase
PascalCase
_under_score
abc123
TODAS_MAYUSCULAS

2.3.2. Ejemplo de Identificadores no válidos

13hola
mi-variable
$miVariable
mi variable
ñandú
👨miMetodo
Mi👩clase

2.3.3. Unicode

No están permitidos caracteres UTF-8 como la Ñ o los emojis en los identificadores. Sin embargo las Strings las soportan en su contenido sin problemas. Hay lenguajes como Swift o Emoji Code que si soportan identificadores con emojis, aunque la utilidad de esta práctica es debatible.

EmojiCode
🏁 🍇
  😀 🔤Hello World!🔤❗️
🍉

2.3.4. Identificadores que inician con guión bajo

Un caso especial es para los identificadores con guión bajo como _color (un guión bajo al principio) y __sabor (dos guiones bajos al principio). Con un guión bajo indica que es una propiedad de instancia, mientras que con dos guiones bajos indican que es una propiedad de clase. Más detalles en la sección de Clases.

2.3.5. Identificadores de clase

Para Wren es importante que las clases comiencen su nombre con mayúsculas. Si bien es posible definir clases con letras minúsculas no es recomendable hacerlo debido a que pueden colisionar con variables dentro del contexto de clase o método.

Ejemplos

// Asociamos la clase `Numero` como un substituto para llamar a la clase `Num`
var Numero = Num
// Dará error. No se puede heredar de las clases primitivas.
class Numero is Num {}
// Es posible nombrar con minúsculas, pero puede dar
// conflictos de contexto al momento de usarlo dentro
// de una clase. (Puede hacer colisión con nombres de variable)
class numero {}

2.3.6. Recomendaciones

  • Los nombres de los identificadores deben ser descriptivos, sin ser muy largos. Ejemplo "ruedasMotocicleta" es mejor que solo "ruedas" y "numero_de_ruedas_en_una_moto".

  • La letra "l" mínuscula y la letra "O" mayúscula puede ser confundida con el número "1" y "0" respectivamente.

2.4. Saltos de línea

Wren utiliza los saltos de línea (\n), por lo que no es necesario utilizar el punto y coma (;) para separar instrucciones. Sin embargo omite los saltos de línea si la instrucción espera más información para ser válida.

2.4.1. Ejemplo: Lista de elementos

var animales = [
  "perro",
  "gato",
  "condor",
  "huemul"
]

2.4.2. Ejemplo: Parámetros de un método

MiClase.metodo(
  parametro1,
  parametro2,
  parametro3
)

2.4.3. Ejemplo: Condicionales

Los saltos de línea son considerados en la declaración de bloques de código. Por lo tanto es importante la posición de las llaves ("{}") dentro de una instrucción.

// Correcto
if (condiccion == true) { x = 0 }
// Correcto
if (condiccion == true)   {
  x = 0
}
// Correcto
if (condicion == true) x = 0

Condicionales de una sola línea. Tienen el formato de condición ? retorno si es verdadero : retorno si es falso.

  // Si la condición es verdadera, x será 0. Si es falsa, x será 1
  var x = condicion ? 0 : 1

Utilizando el operador OR (||). Se puede asignar un valor predeterminado a una variable.

  // Si x es nulo se asignará 0. Caso contrario mantiene su valor anterior.
  x = x || 0

Si la llave no está presente en la misma línea, gatillará un error.

// Error
if (condiction == true)
{
  x = 0
}

2.4.4. Ejemplo: Generar un número al azar del 0 al 9

Al considerar los saltos de línea significativos, provoca un comportamiento inusual al momento de llamar métodos.

Los números pseudo aleatorios son generados utilizando la clase `Random`. Para generar un número del 0 al 9 se necesita utilizar 10, ya que el número máximo utilizado no está incluido dentro de la secuencia. Es decir se incluye 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 (total 10 números) dentro de los posibles resultados.

Instrucción en una sola línea.

Random.new().int(10)

Esto no es válido en Wren (Pero si es válido en la mayoría de los lenguajes de programación).

Random
.new()
.int(10)

Para ser válido necesitamos poner un punto al final de la línea.

Random.
new().
int(10)

Este comportamiento inusual espera ser reparado en la versión 0.4.

2.4.5. Ejemplo: Error

La siguiente línea arroja error debido a que no tiene un salto de línea o una operación.

Random.new().int(10) Random.new().int(10)

2.5. Retorno implícito

Al tener instrucciones de una sola línea, Wren asume la existencia de una instrucción de retorno ("return"). Si no hay un valor, se asume que el valor de retorno es null.

class Auto {
  // ruedas retornará siempre el valor "4"
  ruedas {4}

  // similar a ruedas, pero usando una instrucción "return" explícita
  puertas {
    return 2
  }
}

2.6. Palabras Reservadas

Wren es un lenguaje simple y pequeño. Sus palabras reservadas son las siguientes:

break class construct else false for foreign if import
in is null return static super this true var while

Una palabra reservada tiene un significado especial para Wren, por lo que no se recomienda usarlas para llamar a variables o clases. De esta forma se evita conflictos y confusiones.

2.6.1. Caracteres significativos

  • Caracteres comunes (+ - * / % < > = ! ( ) [ ] | . " { } , & ^ ? : ~ _)

  • Retorno de carro (\n)

2.7. Funciones (Fn)

Las funciones en Wren son instancias de la clase Fn y deben ser llamadas utilizando su método call(). Al ser un objeto más, pueden ser pasadas como parámetros a métodos u otras funciones.

var mostrar = Fn.new {|parametro|
  System.print(parametro)
}

// muestra: hola
mostrar.call("hola")

2.8. Fibras (Fiber)

Son similares a las funciones, con la diferencia en que se ejecutarán en un hilo (thread) distinto. Además del método call() tienen un método try() usado normalmente para "atajar" errores.

var mostrar = Fn.new {|parametro|
  System.print(parametro)
}

// Si existe un error se almacenará en la variable error
var error = Fiber.new { mostrar.call("hola") }.try()

2.9. Clases (class)

Las clases se declaran utilizando la palabra clave class antes del nombre.

class MiClase {}

La herencia se define utilizando la palabra clave is. Solo se heredan los métodos de instancia. Propiedades de clase (_var) y de instancia (_var), constructores y métodos de clase (_static) no son heredados (todos son privados).

class Animal {
  // la propiedad _nombre solo puede ser accesible
  // a clases hijos si se definen como métodos accesadores
  nombre {_nombre}

  // creamos un mutador de la clase
  nombre = (value) {
    _nombre = value
  }

  imprimir() {
    System.print(nombre)
  }
}

class Perro is Animal {

  imprimir() {
    System.print("imprimir en hijo")
  }

  // constructores no son heredables
  construct nuevo(nom) {
    // llamamos al método mutador de la clase padre
    this.nombre = nom

    // Usamos `super` para llamar a un método de la clase padre
    // Muestra "Joe" usando el método accesador del padre
    super.imprimir()

    // Muestra null
    System.print(_nombre)
  }
}

var perrito = Perro.nuevo("Joe")

Para definir un constructor se utiliza la palabra clave construct. Para llamar a la instancia se usa this (opcional).

class Auto {
  construct nuevo() {
    System.print(this)
  }
}

Para definir un método o propiedad de clase se utiliza la palabra clave static.

class Fruta {
  // accesador (getter)
  static cantidad {__cantidad}

  // mutador (setter)
  static cantidad = (value) {
    // doble guión bajo indica propiedad de clase
    __cantidad = value
  }

  // método de clase
  static comer() {
    cantidad = cantidad - 1
  }
}

Para saber el padre de la clase se utiliza la propiedad supertype. Esta propiedad debe ser llamada recursivamente para obtener el árbol de herencia.

  • La clase primitiva base de todas es Object.

  • No se puede heredar de los primitivos como String, Num, Map, entre otros.

class Animal {}

class Perro is Animal {}

System.print(Perro.supertype) // Animal

if (Perro.supertype == Animal) {
  // Perro es animal
}

Si queremos saber el tipo de una instancia se puede usar is.

var perrito = Perro.new()
if (perrito is Perro) {
  // perrito es Perro
}

if (Perro is Animal) {
  // Falso, ya que son clases distintas
}
  • Las propiedades type, supertype y name vienen dentro de cada clase.

  • Object es la única clase que no tiene supertype.

class Animal {}

class Perro is Animal {}

System.print(Perro.name)
System.print(Perro.type)

System.print(Perro.supertype.name)
System.print(Perro.supertype.type)
System.print(Perro.supertype.supertype)

System.print(Object.supertype)

2.10. Control de Flujo

Existen las estructuras de control de flujo tradicionales: if, for, while y break.

if (condicion) {
  // codigo
}
while (condicion) {
  // codigo
}
for (elemento in secuencia) {
  // codigo
  break
}

2.11. Importar

Para separar el código en diversos archivos esta la instrucción import. El leer los archivos e importar su contenido dependerá del intérprete.

La instrucción import va de la mano con la palabra for.

// El módulo se llama "random" e importamos la clase "Random" que está dentro de este módulo
import "random" for Random

2.12. Object

Es la clase padre de todas las demás clases en Wren.

Algunos métodos destacables:

  • toString: Convierte el objeto a un String.

  • type: Retorna la clase a la cual el pertenece el objeto.

// Convertimos los datos a un String serializable
3.toString
["1",2,"tres", true].toString

// similar a "1" == "2", pero sin posibilidad de sobre escribir el operador
Object.same("1", "2")

// verificamos que la instancia pertenezca a la clase
if ("1" is String) {
  System.print("hola")
}

2.13. Comentarios

Cuando se comienza a escribir código más elaborado, existe mayor necesidad de pensar como codificar las soluciones a los problemas. Una vez que se soluciona el problema, se dedicará una gran cantidad de tiempo en revisar y perfeccionar el algoritmo.

Los comentarios te permiten escribir en lenguaje humano como Inglés o Español, dentro de los programas.

Los comentarios en Wren utilizan la misma sintaxis que el Lenguaje de Programación C.

Los símbolos son los siguientes: /* */ (multi línea) y // (línea única).

// Comentario de una sola línea

/*
Este comentario
tiene múltiples
líneas
*/

Se pueden anidar los comentarios. útil para comentar código que ya tenga comentarios.

/*
Este comentario
tiene múltiples
líneas.
  /* También puedes incluir comentarios,
  dentro de comentarios multi línea.
  */
*/

2.13.1. Tip: Comentarios Tijera

Puedes combinar los comentarios de una sola línea con los de múltiples líneas para comentar/descomentar rápidamente secciones de código. Se llaman comentarios tijera por que pueden "cortar" un código para no ser ejecutado.

// /*
  codigo()
// */

Al eliminar el comentario de la primera línea, el código será comentado. De esta forma rápidamente puedes activar o desactivar secciones de código.

/*
  codigo()
// */

Puede aún ser más simplificado de esta forma

//*
  codigo()
// */

Si se elimina el primer / el código será comentado. Por lo que se ahorra un par de movimientos al realizar el comentario.

/*
  codigo()
// */

2.13.2. Tip: Comentarios de parámetros

En Wren no es posible llamar a los parámetros por su nombre. Por lo que si utilizas una función con algunos parámetros, puede ser útil comentarlos.

circulo(/* x */ 10, /* y */ 20, /* radio */ 10)

O mejor aún utilizar variables con nombres significativos

var x = 10
var y = 20
var radio = 10
circulo(x, y, radio)

2.13.3. ¿Cómo es un buen comentario?

  • Es completo, corto y directo. La mayoría de los comentarios deberían ser escritos en párrafos.

  • Explica tu forma de pensar, para que cuando regreses a leer el código en el futuro puedas comprender cómo se ha resuelto el problema.

  • También explica pensando en otros, para que otras personas puedan trabajar en tu código y entender cómo lo haz estructurado.

  • Explica una sección difícil con mayor detalle.

2.13.4. ¿Cuándo comentar?

  • Cuando tienes que pensar cómo funciona el código antes de escribirlo.

  • Cuando probablemente olvides como estabas resolviendo un problema.

  • Cuando exista más de una forma de resolver un problema.

  • Cuando es poco probable que otros comprendan cómo haz resuelto un problema.

Escribir buenos comentarios es un indicador de un buen programador. Úsalos siempre. Verás comentarios a lo largo de los ejemplos en este documento.

2.14. Límites

  • Máximo de 16 parámetros en una función o método.

  • Máximo de 255 propiedades en una clase (incluyendo la herencia).

3. Strings: Cadena de caracteres

Las cadenas de caracteres (String) son delimitadas por las comillas dobles ("). Pueden ser de una sola línea o multi línea. En el caso de ser multi línea, estas conservarán todos los caracteres de espacio y salto de línea contenidos en el string. El caracter de comilla simple (') no es significativo para Wren.

Las strings pueden contener caracteres unicode en UTF-8, como también caracteres no válidos para UTF-8.

Además son inmutables. El string "porotos" no podrá ser modificado (cambiando sus caracteres) luego de su creación.

Un string puede contener los siguientes valores:

  • Una cadena de texto compuesto por una secuencia de puntos de código textuales (textual code point).

  • Una cadena iterable compuesta por una secuencia de puntos de código numérico (numeric code point).

  • Un arreglo simple de bytes indexeables.

3.1. String de una línea

"Solo es necesario comillas dobles"

3.2. String multi línea

Wren permite string multi líneas utilizando el mismo caracter de comillas dobles (") de las strings de una sola línea.

"
 Todo esto es una string multi línea
 Wren esperará hasta que aparezca
 la siguiente comilla doble.
 Los strings multi línea en Wren guardan tanto espacios como saltos de línea en su interior.
 (no son omitidos).
"

3.3. Contando el largo del string

Si utilizamos la propiedad count podremos obtener el largo de un string.

Por ejemplo "hola".count devolverá 4. Hay que tener cuidado con los caracteres unicode (emojis), ya que el largo dependerá de lo que se esté contando. Wren cuenta los puntos de código (code point) unicode, por que todos los strings están bajo UTF-8 (similar al comportamiento de Ruby y Python 3).

System.print("a".count) // Retorna 1.
System.print("a".bytes.count) // Retorna 1 (Parte de la tabla ASCII original).

System.print("ñ".count) // Retorna 1.
System.print("ñ".bytes.count) // Retorna 2.

System.print("👹".count) // Retorna 1.
System.print("👹".bytes.count) // Retorna 4.

System.print("👨‍👩‍👧‍👦".count) // Retorna 7 al contar la unidades de código de Unicode
System.print("👨‍👩‍👧‍👦".bytes.count) // Retorna 25 al contar los bytes UTF-8

System.print("👨‍👩‍👧‍👦".count) retorna 7 por que se está contando las unidades de código de unicode, es decir, el emoji 👨‍👩‍👧‍👦 está formado por los siguientes caracteres : 👨 + caracter de unión de ancho cero + 👩 + caracter de unión de ancho cero + 👧 + caracter de unión de ancho cero + 👦.

System.print("👨‍👩‍👧‍👦".bytes.count) retorna 25 por que está contando los bytes necesarios para almacenar estos caracteres. bytes es una secuencia de caracteres en C, lo que permite utilizar los Strings para almacenar información en binario.

Hay dos formas de contar adicionales que Wren no soporta. La primera es contar por unidades de código UTF-16 y la otra es considerar los emojis compuestos como una unidad.

El string "👨‍👩‍👧‍👦" debería ser de largo 11 para la codificación UTF-16. Mientras que debería ser de largo 1 si lo consideramos como un caracter singular (lo que percibe el usuario). Según los amigos de UTF-8 Everywhere contar de estas formas es poco productivo, ya que lo que importa es realmente los code units (unidades de código) UTF-8 que se tiene al escribir un string. Por lo que Wren tiene un comportamiento adecuado. De todas formas ese tipo de conversiones y conteo podría programarse con códigos externos a Wren. Más detalles en el Glosario Unicode.

3.4. Unión e interpolación de strings

Si deseamos unir varios strings o incluir datos dentro de ellos podemos utilizar las siguientes operaciones: + y %().

  • "Hola" + "Wren": Crea un nuevo string con la unión de Hola y Wren. Entregará HolaWren. Es necesario que ambos objetos sean strings. Por ejemplo si se utiliza "Hola" + 1 entregará un error similar a Runtime error: Right operand must be a string.. Para poder unirlos tendremos que utiliza el método getter toString del número. "Hola" + 1.toString, retornando Hola1.

El método toString es parte de la clase Object la cual es la padre de todos los tipos de datos en Wren. Tanto números, booleanos, strings y listas son objetos que heredan de esta clase.

  • "Hola %(mundo)": Crea un string con la frase Hola y el contenido de la variable mundo.

La operación %() permite incluir cualquier instrucción Wren válida, la cual finalmente ejecutará el método toString para ser incluido en la cadena de caracteres. Esto incluso permite tener interpolaciones anidadas, pero eso se vuelve poco legible rápidamente.

// muestra: La respuesta es 42.
System.print("La respuesta es %(20 * 2 + 2).")

// muestra: wow 149
System.print("wow %((1..3).map {|n| n * n}.join())")

3.5. Multiplicación de strings

También se pueden multiplicar para repetir la cadena de caracteres una cantidad de veces determinada.

// muestra hola hola hola
System.print("hola " * 3)
Si bien se pueden sumar (_+_) y multiplicar (_*_), no existe operaciones para resta (_-_) y division (_/_) dentro de una cadena de caracteres.

3.6. Limitar el máximo de caracteres

El String al ser una Secuencia puede utilizar sus métodos como take() que permite obtener los elementos dentro de una secuencia. El método join() es necesario para convertir nuevamente la secuencia en un String.

// muestra: hola
System.print("hola wren".take(4).join())

3.7. Caracteres no imprimibles

Los caracteres no imprimibles o Whitespace son aquellos caracteres que el computador puede ver, pero son invisibles para una persona. Los caracteres no imprimibles más comunes son el espacio (" "), la tabulación ("\t") y el salto de línea ("\n"). La combinación de dos caracteres "\t" crea un nuevo espacio tabulado, mientras que la combinación de dos caracteres "\n" crea una nueva línea dentro del String. ¡Puedes usarlos cuantás veces y dónde desees!.

Prueba el siguiente código:

System.print("Hola a todos")
System.print("\tHola a todos")
System.print("Hola\na todos")
System.print("\n\n\nHola\ta\ttodos")

3.7.1. Eliminando caracteres no imprimibles

Muchas veces al solicitar datos a un usuario, este puede incluir espacios adicionales al principio o final. Usualmente es buena idea eliminar estos caracteres no imprimibles antes de comenzar a procesar el dato.

var nombre = " arturo "

// Elimina solamente espacio final
System.print(nombre.trimEnd())

// Elimina solamente espacio inicial
System.print(nombre.trimStart())

// Elimina espacio inicial y final
System.print(nombre.trim())

Si se necesita ver con mayor claridad lo que sucede se puede usar el siguiente código:

var nombre = " arturo "

// Elimina solamente espacio final
System.print("-" + nombre.trimEnd() + "-")

// Elimina solamente espacio inicial
System.print("-" + nombre.trimStart() + "-")

// Elimina espacio inicial y final
System.print("-" + nombre.trim() + "-")

3.8. Caracteres de escape

Muchas veces se necesitan escribir caracteres especiales en un string. Por ejemplo si quisieramos escribir "Hola Wren" incluyendo las comillas, tendríamos que escribirlo de esta forma "\"Hola Wren\"". Esto incluirá los caracteres de comillas dobles en la frase.

"\0" // byte NUL (Nulo) : 0.
"\"" // Comillas dobles.
"\\" // Barra invertida.
"\%" // Signo porcentaje.
"\a" // Sonido de alarma.
"\b" // Retroceso.
"\f" // Alimentación de formularios.
"\n" // Salto de línea.
"\r" // Retorno de carro.
"\t" // Tabulación.
"\v" // Tabulación vertical.
"\x48"        // Byte sin codificar     (hexadecimal de 2 dígitos).
"\u0041"      // Code point Unicode (hexadecimal de 4 dígitos).
"\U0001F64A"  // Code point Unicode (hexadecimal de 8 dígitos).

// Wren 0.4 ha introducido
"\e"          // Secuencia de escape de terminal ESC.

Para indicar caracteres especiales podemos utilizar \u (unicode para letras disponibles en los idiomas humanos), \U (unicode para letras especiales como emoji) y \x (bytes sin codificar).

  • System.print("\u0041\u0b83\u00DE") = AஃÞ

  • System.print("\U0001F64A\U0001F680") = 🙊🚀

  • System.print("\x48\x69\x2e") = Hi.

3.9. Raw Strings: Cadenas de caracteres sin procesar

En Wren 0.4 se ha introducido un nuevo tipo de String que permite utilizar las comillas sin necesidad de escaparlas. Útil por ejemplo cuando se necesita incrustar código de otros lenguajes como HTML o JSON dentro de un script.

Los Raw Strings tienen todas las mismas características que un String normal. La única excepción es que no ejecutan interpolación ni tampoco las secuencias de escape, ya que todos los caracteres son asimilados sin procesar.

Son delimitados por tres caracteres de doble comilla ("""). Si los delimitadores están en su propia línea los caracteres de espaciado (\t, espacio, \n) no serán considerados. Este delimitador no se puede escapar por lo que a penas se detecte se considerará como terminado el Raw String.

En el siguiente ejemplo vemos un simple JSON. Donde se puede apreciar que no es necesario escapar las comillas.

"""
  {
    "hola": "wren",
    "de" : "json"
  }
"""

Podemos ver la característica de ignorar los espaciados iniciales y finales con la siguiente comparación:

// La siguiente frase tiene 8 caracteres de sangría despúes del delimitador y antes del limitador
var frase = """
        Hola Wren Raw
        """

System.print("'%(frase)'") // Mostrará Hola Wren con 8 espacios de sangría

// La siguiente frase incluye 8 caracteres de espacio
frase = "
        Hola Wren
        "

System.print("'%(frase)'") // Muestra Hola Wren con 24 espacios de sangría

Dando el siguiente resultado (se han agregado comillas para mostrar cuando comienza y termina un resultado):

'        Hola Wren Raw'
'
        Hola Wren
        '

3.9.1. Incluyendo el delimitador dentro de un Raw String

Debido a que el delimitador no se puede escapar. Para incluirlo se debe utilizar la concatenación de Strings o utilizando el método replace.

Concatenación de String.

System.print(""" mi raw string """ + "\"\"\"")

Utilizando replace

System.print(""" mi raw string '''""".replace("'", "\""))

Ambos darán el siguiente resultado:

mi raw string """

3.10. Rangos de caracteres

Un String es una Secuencia de caracteres. Por lo que es posible obtener caracteres en posiciones específicas utilizando rangos (Range). El índice comieza contando desde cero para contar desde el principio de la cadena y puede tener valores negativos para contar desde el final de la cadena.

3.10.1. Caracter en posición específica

// muestra: h
System.print("hola wren"[0])

// muestra: n
System.print("hola wren"[-1])

3.10.2. Rango de caracteres dentro de una cadena

// muestra: la
System.print("hola wren"[2..3])

3.10.3. Invertir una cadena de caracteres

// muestra: nerw aloh
System.print("hola wren"[-1..0])

3.10.4. Eliminando Sangría Sobrante

En muchas ocasiones nos encontramos con la necesidad de eliminar la sangría que se genera al escribir código. Para esto podemos utilizar la siguientes funciones:

var contar_sangria = Fn.new {|string|
    var indice = 0
    var bytes = string.bytes
    while(indice < bytes.count) {
        var caracter = bytes[indice]
        if(caracter == 0x20 || caracter == 0x09) {
          indice = indice + 1
          continue
        }
        return indice
    }
    return 0
}

var borrar_sangrado = Fn.new {|string|
    var inicio = contar_sangria.call(string)
    if(inicio == 0) return string
    var lineas = string.split("\n")

    lineas = lineas.map {|linea|
        var izquierda = contar_sangria.call(linea)
        if(izquierda >= inicio) return linea[inicio..-1]
        return linea
    }
    return lineas.join("\n")
}

Ejemplo de uso:

// Ejemplo
var frase = """        Mucha Sangría"""

// Elimina la sangría de la frase
System.print(borrar_sangrado.call(frase))

3.11. Ejercicios

3.11.1. Donas

Dada una cantidad de un número de donas, retornar un string con el contenido "Número de donas: <cantidad>". Si la cantidad es mayor o igual a 10, usar la palabras 'muchas' en vez del número de cantidad.

Ejercicio basado en [googlepython].

Ejecución

$ ./wren donas.wren

Ejemplo
  • donas.call(5) retorna "Número de donas: 5"

  • donas.call(23) retorna "Número de donas: muchas"

Código
var donas = Fn.new {|cantidad|
  // Tu código aquí
}


// Pruebas para evaluar que el ejercicio
// se ha completado exitosamente.
// no modificar.

var probar = Fn.new {|tengo, quiero|
  var prefijo = " X "
  if (tengo == quiero) {
    prefijo = " OK "
  }
  System.print("%(prefijo) tengo: %(tengo) quiero: %(quiero)")
}

System.print("Probando donas")
probar.call(donas.call(4), "Número de donas: 4")
probar.call(donas.call(9), "Número de donas: 9")
probar.call(donas.call(10), "Número de donas: muchas")
probar.call(donas.call(99), "Número de donas: muchas")
System.print()
Solución

Solución: Donas

4. Num: Números

Los datos numéricos son bastante conocidos, sin embargo hay algunas cosas que se deben tener en cuenta. Como la mayoría de los lenguajes interpretados, Wren tiene un único tipo numérico: punto flotante de doble precisión. Los números en Wren son similares a otros lenguajes y son instancias de la clase Num.

Los siguientes son algunos ejemplos:

// Enteros
0
1234
-5678

// Decimales
3.14159
1.0
-12.34

// Notación científica
0.0314159e02
314.159e-02

4.1. Enteros

Se pueden realizar todas las operaciones tradicionales con números enteros. Suma (+), resta (-), multiplicación (*), división (/) y módulo (%). Para utilizar exponentes se debe usar el método pow().

System.print(3+2)

System.print(3-2)

System.print(3*2)

System.print(3/2)

System.print(3%2)

System.print(3.pow(2))

También se puede utilizar paréntesis para alterar el orden de las operaciones.

var ordenNormal = 2+3*4

System.print(ordenNormal)

var ordenMio = (2+3)*4

System.print(ordenMio)

4.2. Punto Flotante

Los números de punto flotante se refieren a cualquier número con una parte decimal. La mayoría del tiempo se comportan de una forma matemática tradicional. Pero en muchas ocaciones se obtienen resultados con una parte decimal más larga de lo esperado. Esto ocurre por como las computadoras representan los números internamente; no tiene que ver con Wren mismo. Básicamente, los humanos están acostumbrados a trabajar con números base 10, donde 0.1 + 0.2 = 0.3. Pero las computadoras trabajan en base 2 y expresan los resultados como una potencia de dos. No existe una representación exacta para 0.3 en una potencia de dos y por esa razón el resultado presenta tantos decimales.

Como la memoria de las computadoras es limitada, no pueden almacenar números con precisión infinita, en algún momento se debe limitar. Pero ¿Cuánta precisión se necesita? ¿Y dónde se necesita?, ¿Cuántos dígitos enteros y cuántos fraccionarios?

  • Para un ingeniero construyendo una autopista, no importa si tiene 10 metros o 10.0001 metros de ancho ─ posiblemente ni siquiera sus mediciones eran así de precisas.

  • Para alguien diseñando un microchip, 0.0001 metros (la décima parte de un milímetro) es una diferencia enorme ─ pero nunca tendrá que manejar distancias mayores de 0.1 metros.

  • Un físico necesita usar la velocidad de la luz (más o menos 300000000) y la constante de gravitación universal (más o menos 0.0000000000667) juntas en el mismo cálculo.

Para satisfacer al ingeniero y al diseñador de circuitos integrados, el formato tiene que ser preciso para números de órdenes de magnitud muy diferentes. Sin embargo, solo se necesita precisión relativa. Para satisfacer al físico, debe ser posible hacer cálculos que involucren números de órdenes muy dispares.

Básicamente, tener un número fijo de dígitos enteros y fraccionarios no es útil ─ y la solución es un formato con un punto flotante.

4.2.1. Comparación de Punto Flotante

Si realizamos una pequeña comparación de números con punto flotante, nos encontraremos con que algunas comparaciones simples no pueden ser realizadas correctamente de la forma tradicional.

Debido a los errores de redondeo, la mayoría de los números de punto flotante terminan siendo ligeramente imprecisos. Mientras esta imprecisión se mantenga pequeña, normalmente se puede ignorar. Sin embargo, esto significa también que números que se espera que sean iguales (por ejemplo al calcular el mismo resultado utilizando distintos métodos correctos) a veces difieren levemente, y una simple prueba de igualdad falla:

var a = 0.15 + 0.15
var b = 0.1 + 0.2

// puede ser falso
if (a == b) {}

// también puede ser falso
if (a >= b) {}

Como los números de punto flotante tienen una cantidad de dígitos limitados, no pueden representar todos los números reales de forma precisa: cuando hay más dígitos de los que permite el formato, los que sobran se omiten.

La solución es comprobar no si los números son exactamente iguales, sino si su diferencia es muy pequeña. El margen de error frente al que se compara esta diferencia normalmente se llama epsilon.

El valor de epsilon dependerá del contexto. Nunca usar un epsilon fijo. Un epsilon fijo elegido porque «parece pequeño» podría perfectamente ser demasiado grande cuando los números que se comparan son también muy pequeños. La comparación devolvería «verdadero» para números muy diferentes. Y cuando los números son muy grandes, el epsilon puede acabar siendo más pequeño que el mínimo error de redondeo, por lo que la comparación siempre devolvería «falso». Por tanto, es necesario ver si el error relativo es menor que epsilon.

El código a continuación pasa las pruebas para muchos casos especiales importantes, pero como puedes ver, utiliza cierta lógica no trivial. En particular, tiene que utilizar una definición totalmente distinta del margen de error cuando a o b son cero, porque la definición clásica del error relativo es inútil en esos casos.

Hay algunos casos en los que todavía produce resultados inesperados (concretamente, es mucho más estricto cuando un valor es casi cero que cuando es exactamente cero), y algunas de esas pruebas para las que fue desarrollado probablemente especifica un comportamiento que no es apropiado para algunas aplicaciones. Antes de usarlo, ¡asegúrate de que es adecuado para tu aplicación!.

var equivalentes = Fn.new { |a, b, epsilon|

  var absA = a.abs
  var absB = b.abs
  var diff = (a - b).abs

  // Atajo, maneja los infinitos
  if (a == b) {
      return true
  }

  // a o b o ambos son cero
  if (a * b == 0) {
      // El error relativo no es importante aquí.
      // Se tiene que utilizar una definición totalmente distinta
      // del margen de error cuando a o b son cero, porque la definición
      // clásica del error relativo es inútil en esos casos.
      return diff < (epsilon * epsilon)
  }

  // Usar el error relativo
  return diff / (absA + absB) < epsilon
}

// Prueba de la función
var a = 0.15 + 0.15
var b = 0.1 + 0.2

// Epsilon siempre debe ser entregado según el contexto de comparación
// no utilizar epsilon fijos.
var epsilon = 0.001

var eq = equivalentes.call(a, b, epsilon)

// Debería ser verdadero
System.print(eq)

4.2.2. Formateo de Punto Flotante

En muchas ocaciones se necesita limitar la cantidad de decimales que un número muestra. Wren no cuenta con instrucciones para formatear números como C u otros lenguajes (Ejemplo: "%1.2f".)

Sin embargo existen algunas alternativas.

Tomemos por ejemplo la constante PI (Num.pi) =~ 3.141592.

Si solamente quisieramos mostrar la parte entera (3) podemos utilizar el método truncate.

// Solamente muestra 3
System.print(Num.pi.truncate)

Si desearamos mostrar 3.14 podemos usar la siguiente fórmula.

(numero * 1e<posiciones decimales>).truncate / 1e<posiciones decimales>

var formateado = (Num.pi * 1e2).truncate / 1e2

// Muestra 3.14
System.print(formateado)

Podemos crear una función que generalice esta fórmula a cualquier cantidad de decimales.

var formatear = Fn.new {|numero, decimales| (numero * 10.pow(decimales)).truncate / 10.pow(decimales)}

// Muestra 3.14
System.print(formatear.call(Num.pi, 2))

El problema de esta solución es que existe la posibilidad de perder precisión en los decimales, lo que puede causar problemas de cálculo. Para evitar este problema la solución recomendable es transformar el número a String y obtener los decimales por medio de manipulación de strings.

// Convertimos el número a String
var string = Num.pi.toString

// Queremos mostrar 3.14
var posiciones = 2

// Buscamos donde comienzan los decimales
var index = string.indexOf(".")

// Separamos el número entre entero y decimal
var entero = string[0...index]
var decimal = string[index + 1..-1]

// Obtenemos los decimales a mostrar
var decimales = decimal.take(posiciones).join()

// Reunimos la parte entera y decimal
var formateado = entero + "." + decimales

System.print(formateado)

4.3. 0 y -0

Un asunto interesante es la representación del número cero 0 y cero negativo -0. Para Wren ambos valores son tratados como iguales, aún cuando su representación es diferente a nivel de bits. Pero solo aplica si se realiza la comparación utilizando el operador ==. Si se utiliza Object.same() retornará falso debido a que son objetos distintos.

System.print(0 == -0)            // true (verdadero)
System.print(Object.same(0, -0)) // false (falso)

Es importante considerar cuando se esté utilizando índices para acceder a los datos de un arreglo o un diccionario (mapa).

var cero = 0
var cerone = -0

var mapa = {
  cero: 1
}

System.print(cero == cerone) // true (verdadero)
System.print(mapa[cerone]) // null (nulo)

En el anterior código mapa[cerone] devuelve nulo debido a que el índice -0 no existe.

5. Secuencias, Listas y Rangos

5.1. Sequence: Secuencias

La secuencia es la clase padre de List, String, Map, Range y cualquier otra que implemente el protocolo de iteración. La característica principal de una secuencia es que esta podría ser virtualmente infinita (sin elementos que indiquen un fin). Entre los métodos que podemos destacar de Sequence están:

5.1.1. each

Similar a un bucle for. Por cada elemento dentro de la secuencia llama a una función cuyo parámetro es el siguiente elemento en la secuencia.

var numeros = [1, 2, 3]
numeros.each {|num|
  System.print(num)
}

5.1.2. map

Llama a una función cuyo parámetro es el siguiente elemento en la secuencia. Retorna una nueva secuencia con el resultado de las operación en cada elemento. El resultado de map es almacenado por referencia. Lo que significa que cambios posteriores en la secuencia original pueden afectar el valor del resultado de map. Para pasarlo a una variable independiente, se debe ejecutar el método toList.

var numeros = [1, 2, 3]

// .toList es necesario para que System.print() muestre la lista apropiadamente.
// Esto es debido a que una secuencia puede no tener fin. Necesitamos pasarlo a una estructura de datos finita.
// También estamos utilizando el "retorno implícito".
numeros = numeros.map{|num| num + 1}.toList

// Muestra [2, 3, 4]
System.print(numeros)

5.1.3. where

Filtra una secuencia. Entregará una nueva lista la cual cumpla la condicional. Al igual que map se debe ejecutar el método toList para evitar que modificaciones en la secuencia original afecten la nueva secuencia.

var numeros = [1, 2, 3, 4]

var pares = numeros.where{|num| num % 2 == 0}.toList

// Muestra [2, 4]
System.print(pares)

5.1.4. reduce

Llama a una función cuyo parámetro es el siguiente elemento en al secuencia. Retorna un único elemento, producto de las operaciones en cada elemento.

var numeros = [1, 2, 3]

// Si no se pasa un acumulador, se considera el primer elemento como valor inicial
var resultado = numeros.reduce {|acumulador, num| acumulador + num}

// Muestra 6
System.print(resultado)

// Iniciamos el acumulador con valor -1
resultado = numeros.reduce(-1) {|acumulador, num| acumulador + num}

// Muestra 5
System.print(resultado)

5.1.5. take

Obtiene un número de elementos dentro de una secuencia.

var numeros = [1, 2, 3, 4]
var primeros = numeros.take(2).toList

// Muestra [1, 2]
System.print(primeros)

5.1.6. skip

La operación inversa de take. Salta un número determinado de elementos.

var numeros = [1, 2, 3, 4]
var ultimos = numeros.skip(2).toList

// Muestra [3, 4]
System.print(ultimos)

5.1.7. contains

Retorna verdadero si la secuencia contiene al elemento.

var numeros = [1, 2, 3]

// Muestra "false" debido a que no contiene el número 4
System.print(numeros.contains(4))

5.1.8. count

Retorna el número de elementos de la secuencia. También es posible solamente contar elementos que cumplan una condición.

var numeros = [1, 2, 3, 4]

// 4
System.print(numeros.count)

// Contamos solamente los números múltiplos de 2
var pares = numeros.count {|num| num % 2 == 0}

// 2
System.print(pares)

5.1.9. join

Une a los elementos de una secuencia transformándolos en un String. Si no se pasa un parámetro se asume un caracter vació como unión.

var numeros = [1, 2, 3]

// Muestra "123"
System.print(numeros.join())

// Muestra "1$2$3"
System.print(numeros.join("$"))

// Muestra "1 num 2 num 3"
System.print(numeros.join(" num "))

5.1.10. all

Itera sobre los elementos de una secuencia con una condicional. Devuelve verdadero si todos los elementos pasan la condición. Si alguno de los elementos es falso, termina de iterar y devuelve falso.

var numeros = [1, 2, 3]
var resultado = numeros.all {|num| num > 2}

// Falso. No todos los números en la secuencia son mayores a 2
System.print(resultado)

resultado = numeros.all{|num| num < 4}

// Verdadero. Todos los números en la secuencia son menores a 4
System.print(resultado)

5.1.11. any

Similar a all. Devuelve verdadero al primer elemento que pase la condición. Si ninguno de los elementos pasa la condición, devuelve falso.

var numeros = [1, 2, 3]
var resultado = numeros.any {|num| num % 2 == 0}

// Verdadero. Existe un número múltiplo de 2.
System.print(resultado)

resultado = numeros.any {|num| num % 5 == 0}

// Falso. Ningún número es múltiplo de 5.
System.print(resultado)

5.1.12. ejemplo

El siguiente ejemplo utiliza algunas funciones de Sequence para obtener el primer elemento de una secuencia dependiendo de una condición.

  // Creamos una nueva función para nuestra lógica
  var primerElemento = Fn.new {|elementos, predicado|

    // Si existe un elemento que cumpla la condición, obtenemos el primero de la secuencia.
    var resultado = elementos.any(predicado) ? elementos.where(predicado).take(1) : false
    if (resultado && resultado.count > 0) {
      return resultado.toList[0]
    }

    // Caso contrario devolvemos nulo
    return null
  }

  var numeros = [1, 2, 3]
  var resultado = primerElemento.call(numeros) {|item| item > 1}

  // Muestra 2
  System.print(resultado)

  resultado = primerElemento.call(numeros) {|item| item > 3}

  // Muestra null
  System.print(resultado)

5.2. Subscripts: Subíndices

Los Subíndices nos ayudan a obtener valores dentro de una lista, mapa u objeto que implemente el método de subíndice utilizando los paréntesis cuadrados (corchetes) [].

lista[0]    // Obtiene el primer elmento de la lista
mapa["id"] // Obtiene el valor asociado con "id"

El siguiente código muestra como implementar un objeto que acepta dos parámetros en su método de subíndice.

class Matriz {
  [x, y] {"x %(x), y %(y)"}
  construct nueva() {}
}

var matriz = Matriz.nueva()

// Muestra x 1 y 1
System.print(matriz[1,1])

5.3. List: Listas

Una lista es una colección de elementos que son almacenados en una variable. Los elementos pueden estar relacionados de cierta forma o no tener ningún tipo de relación.

El siguiente ejemplo muestra algunas operaciones disponibles para listas:

var estudiantes = ["Pedro", "Javier", "Nicolás"]

// Iteramos sobre los elementos de la lista
for (estudiante in estudiantes) {
    System.print("¡Hola, " + estudiante + "!")
}

// La misma operación anterior, pero utilizando la función each
estudiantes.each{|estudiante|
  System.print("¡Hola, " + estudiante + "!")
}

// Añadimos un nuevo estudiante en la última posición
estudiantes.add("Juan")

// Muestra ["Pedro", "Javier", "Nicolás", "Juan"]
System.print(estudiantes)

// Muestra "false" por que no existe el estudiante María
System.print(estudiantes.contains("María"))

// Muestra ["Pedro", "Javier"]
System.print(estudiantes.take(2).toList)

// Insertamos un nuevo elemento en la última posición disponible
estudiantes.insert(estudiantes.count - 1, "Javier Manzana")

// Filtramos todos los estudiantes que se llamen Javier
// Muestra: ["Javier", "Javier Manzana"]
System.print(estudiantes.where {|nombre| nombre.startsWith("Javier")}.toList)

// Elimina todos los estudiantes
estudiantes.clear()
// Generamos una lista de 4 elementos con valor 1
var elementos = [1] * 4

// Muestra [1, 1, 1, 1]
System.print(elementos)

También podemos combinar listas fácilmente

var numeros = [1, 2, 3]
var letras = ["a", "b", "c"]
var elementos = numeros + letras

// Muestra [1, 2, 3, a, b, c]
System.print(elementos)

5.4. Range: Rangos

Un rango es un objeto que representa una lista finita, incremental e iterable de números. Es una operación disponible en cada instancia de un número. Es dada por la operación .. y la operación …​.

  • inicio..fin: Incluye al número fin. Ejemplo: 1..3 retorna [1, 2, 3].

  • inicio…​fin: No incluye al número fin. Ejemplo 1…​3 retorna [1, 2].

// Mostramos los números del 1 al 5
(1..5).each {|num|
  System.print("Número %(num)")
}
// Mostramos del 3 al 1
((1 + 2)..(3 - 2)).each {|num|
  System.print("Número %(num)")
}
// Mostramos del 1 al 3 utilizando bucle for.
for (num in 1..3) {
  System.print("Número %(num)")
}

Los rangos son muy útiles para obtener subconjuntos dentro de objetos iterables como Strings y Lists.

var nombre = "Juanito"

// Muestra "nito"
System.print(nombre[3..-1])

// Muestra "otinauJ"
System.print(nombre[-1..0])
var letras = ["a", "b", "c", "d", "e"]
var subconjunto = letras[1..3]

// Muestra [b, c, d]
System.print(subconjunto)

6. Funciones y Fibras

Uno de los principios de los lenguajes de programación es evitar código repetitivo (Don’t Repeat Yourself). Si una acción se debe ejecutar más de una vez se puede definir esa acción una única vez y luego reutilizarla cuantas veces se necesite. Hay lenguajes de programación como Elixir que son solamente funciones. Por lo que son una herramienta muy poderosa.

Las funciones y fibras permiten reducir el trabajo necesario y su uso efectivo permite un código con menor cantidad de errores. En escencia son un conjunto de acciones que están agrupadas.

6.1. Functions: Funciones

En Wren las funciones son objetos, esto significa que son iguales que cualquier otra variable y pueden ser usadas como parámetros al igual que un String o un número. Otros lenguajes como Java no tienen esta característica.

En el sentido matemático podemos tener la siguiente expresión:

f(x) = (x + 10) / 2

Podemos interpretarla como

y = (x + 10) / 2

Lo cual puede ser expresado en Wren de la siguiente forma:

var y = Fn.new {|x| (x + 10) / 2}

Como se puede apreciar una función está definida por la clase Fn. Se llama al método new para crear una nueva función. El cuerpo de la función está definido por las llaves {}. Los parámetros están definidos entre las barras ||.

Para poder ejecutar la función y obtener su resultado, basta simplemente con utilizar el método call.

var y = Fn.new {|x| (x + 10) / 2}
var resultado = y.call(10) // x valdría 10
System.print(resultado) // muestra: 10

6.1.1. Recursividad

Podemos llamar a una función de forma recursiva, sin embargo se debe declarar la variable antes de poder referenciarla.

El siguiente ejemplo muestra la implementación del algoritmo de búsqueda binaria (Binary Search).

// Declaramos la variable
var buscar

// Le asignamos el contenido
buscar = Fn.new {|elementos, objetivo, minimo, maximo|
    if (maximo < minimo) {
      return -1
    }

    var posicion = ((maximo + minimo) / 2).round
    var candidato = elementos[posicion]

    if (candidato == objetivo) {
      return posicion
    }

    if (candidato > objetivo) {
      // Ahora podemos hacer una llamada recursiva
      return buscar.call(elementos, objetivo, minimo, posicion - 1)
    }

    return buscar.call(elementos, objetivo, posicion + 1, maximo)
}

var elementos = (0..30).toList
var minimo = 0
var maximo = elementos.count - 1

System.print(buscar.call(elementos, 30, minimo, maximo)) // 30
System.print(buscar.call(elementos, 31, minimo, maximo)) // -1
System.print(buscar.call(elementos, 20, minimo, maximo)) // 20

Si no se declara la variable podría aparecer un error similar a:

[./scratch line 57] Error at '}': Variable 'buscar' referenced before this definition (first use at line 53).

6.2. Fibers: Fibras

Las fibras son similares a las funciones, pero tienen la característica de que pueden ser pausadas y re-ejecutadas a voluntad. Esto es especialmente útil para obtener concurrencia, es decir, ejecutar varias operaciones de forma paralela.

Todas las fibras en Wren son manejadas por la máquina virtual de Wren. Lo cual es excelente ya que no dependerá del sistema operativo para gatillar nuevos procesos, permitiendo ahorrar recursos.

En el siguiente ejemplo podemos ver como pausar y re-ejecutar una fibra.

var fibra = Fiber.new {
  (1..4).each {|num|
    // Pausa la fibra y devuelve un valor
    Fiber.yield(num)
  }
}

System.print(fibra.call()) // muestra: 1

// Podemos ejecutar otras instrucciones y continuar la fibra
System.print(fibra.call()) // muestra: 2

// También podemos pasar un valor como parámetro adicional
System.print(fibra.call(10)) // muestra: 3

System.print(fibra.call()) // muestra: 4
System.print(fibra.call()) // muestra: null

En una función esto no es posible ya que solo tendríamos acceso al último valor calculado.

var funcion = Fn.new {
  var numero = null
  (1..4).each{|num|
    numero = num
  }
  return numero
}

System.print(funcion.call()) // muestra: 4

6.2.1. Manejo de Errores

En muchos lenguajes se utiliza una secuencia llamada try-catch. En Wren las fibras son las encargadas de manejar los errores en tiempo de ejecución.

Utilizando el método try podemos ejecutar una fibra y capturar el error.

var fibra = Fiber.new {
  123.noExiste
}

var error = fibra.try()
System.print("Hubo un Error: " + error)

Si deseamos generar un error propio podemos utilizar Fiber.abort().

Este error puede ser un mensaje normal.

Fiber.abort("Hubo un error")

O también un objeto personalizado.

class MiError {
  construct nuevo() {}
  mensaje {"Este es un objeto error personalizado"}
}

var fibra = Fiber.new {
  Fiber.abort(MiError.nuevo())
}

fibra.try()

if (fibra.error is MiError) {
  System.print(fibra.error.mensaje)
}

7. Control de Flujo

El control de flujo permite modificar la normal ejecución de un programa, cambiando su curso dependiendo de condiciones específicas.

  • if: (sí) permite ejecutar un bloque de código solamente si la condición es cumplida.

  • for: (repetir) permite ejecutar un bloque de código tantas veces como el contador y la condicional permitan.

  • while (mientras) permite ejecutar un bloque de código siempre que la condición se cumpla.

var numero = 8
if (numero % 2 == 0) {
  System.print("El número es par")
}
for(i in 1..3) {
  /*
  Paso 1
  Paso 2
  Paso 3
  */
  System.print("Paso " + i.toString)
}
/*
comienza
paso 1
paso 2
paso 3
finalizado
*/
var verdadero = true
var contador = 1
System.print("comienza")
while(verdadero) {
  System.print("paso " + contador.toString)
  if (contador == 3) {
    verdadero = false
    System.print("finalizado")
  }

  contador = contador + 1
}

8. Mapas

Los mapas, también conocidos como "diccionarios" permiten almacenar distintas variables que pueden ser accedidas mediante un identificador.

var mivar = true

var mapa = {
  "1": 1,
  "dos": 2,
  "mivar"; mivar
}

A diferencia de otros lenguajes, no se puede acceder directamente a las distintas variables con el identificador.

mapa.mivar // error
mapa["mivar"] // ok

9. Clases

Las clases son una estructura de datos bastante flexible. Permiten definir la información y el comportamiento de cualquier unidad que desees modelar en los programas.

9.1. Terminología de Orientación a Objetos

Las clases son parte de un paradígma llamado "Programación Orientada a Objetos" (Object-oriented programming) (P.O.O u O.O.P). La cual se focaliza en construir bloques reusables de código llamados Clases. Cuando se necesita utilizar una clase se debe crear un objeto (o instancia), por eso el término de orientación a objetos. Para entender mejor, se debe conocer la terminología común.

  • Una clase es un bloque de código que define atributos (propiedades) y comportamientos necesarios (métodos) para modelar adecuadamente un elemento del programa. Se puede modelar algo del mundo real como una pelota o una guitarra o algo de un mundo virtual como un personaje de un videojuego y sus leyes físicas.

  • Un atributo (o propiedad) es una pieza de información. Es técnicamente una variable que es parte de una clase.

  • Un comportamiento es una acción definida dentro de una clase. Son comunmente conocidos como métodos, los cuales son las funciones definidoas para la clase.

  • Un objeto es una instancia específica de una clase. Tiene valores definidos para los atributos (variables) de la clase. Se pueden crear tantos objetos de una misma clase, como sea necesario.

9.2. El operador is

El operador is tiene dos funciones. La primera es permitir la herencia simple entre clases y la segunda permite comparar si un objeto pertenece a una metaclase específica.

9.2.1. Ejemplo: Juego de Piedra - Papel - Tijera

El siguiente juego de Piedra, Papel o Tijeras es implementado mediante una herencia simple. Se utiliza el patrón de diseño de double dispatch, para simplificar su lógica.

// Piedra, Papel o Tijeras
// Usando el Patron de Double Dispatch
class Elemento {
  pierdeCon(elemento) {}
  leGanaAPiedra(){}
  leGanaAPapel(){}
  leGanaATijera(){}
}

class Piedra is Elemento {
  construct crear() {}

  pierdeCon(elemento) {
    return elemento.leGanaAPiedra()
  }

  leGanaAPiedra() {
    return false
  }

  leGanaAPapel() {
    return false
  }

  leGanaATijera() {
    return true
  }
}

class Papel is Elemento {
  construct crear() {}

  pierdeCon(elemento) {
    return elemento.leGanaAPapel()
  }

  leGanaAPiedra() {
    return true
  }

  leGanaAPapel() {
    return false
  }

  leGanaATijera() {
    return false
  }
}

class Tijera is Elemento {
  construct crear() {}

  pierdeCon(elemento) {
    return elemento.leGanaATijera()
  }

  leGanaAPiedra() {
    return false
  }

  leGanaAPapel() {
    return true
  }

  leGanaATijera() {
    return false
  }
}

var piedra = Piedra.crear()
var papel = Papel.crear()
var tijera = Tijera.crear()

System.print(piedra.pierdeCon(papel))  // true (verdadero)
System.print(tijera.pierdeCon(piedra)) // true (verdadero)
System.print(piedra.pierdeCon(tijera)) // false (falso)
System.print(papel.pierdeCon(tijera))  // true (verdadero)
System.print(papel.pierdeCon(piedra))  // false (falso)

9.2.2. Ejemplo de comparación

Ahora utilizando las clases de Piedra, Papel o Tijeras vamos a comparar los elementos utilizando el operador is.

var piedra = Piedra.crear()

System.print(piedra is Piedra) // true (verdadero)
System.print(piedra == Piedra) // false (falso)

System.print(piedra is Tijera) // false (falso)
System.print(piedra == Tijera) // false (falso)

System.print(piedra is Elemento) // true (verdadero)
System.print(piedra == Elemento) // false (falso)

System.print(Piedra is Elemento) // false (falso)
System.print(Piedra == Elemento) // false (falso)

System.print(Piedra.supertype is Elemento) // false (falso)
System.print(Piedra.supertype == Elemento) // true (verdadero)

Analizando los resultados verdaderos:

  • (piedra is Piedra): Verdadero puesto que el objeto piedra es una instancia de la clase Piedra.

  • (piedra is Elemento): Verdadero puesto que el objeto piedra es una instancia de la clase Piedra y ésta a su vez hereda de Elemento.

  • (Piedra.supertype == Elemento): Verdadero puesto que el super tipo de Piedra es la misma referencia a la clase Elemento.

Analizando los resultados falsos:

  • (piedra == Piedra): Falso puesto que el objeto piedra no tiene la misma referencia que la clase Piedra.

  • (piedra is Tijera): Falso puesto que el objeto piedra no es una instancia de la clase Tijera.

  • (piedra == Tijera): Falso puesto que el objeto piedra no tiene la misma referencia que la clase Tijera.

  • (piedra == Elemento): Falso puesto que el objeto piedra no tiene la misma referencia que la clase Elemento.

  • (Piedra is Elemento): Falso puesto que la clase Piedra no es una instancia de la clase Elemento.

  • (Piedra == Elemento): Falso puesto que la clase Piedra no es una referencia a la clase Elemento.

  • (Piedra.supertype is Elemento): Falso pues que el super tipo de Piedra no hereda de Elemento (ya que el super tipo de Piedra es Elemento).

9.2.3. Ejemplo: Sobrecarga del operador

Podemos sobrecargar el operador is en nuestras clases y "ocultar" su real naturaleza. Normalmente podría ser usado para reemplazar las clases básicas (String, Num, Bool, Fn, Fiber, Map, List, Null, entre otras) y permitir su extensión. Recordemos que en Wren no se puede heredar desde estas clases debido al problema de Reentrancia (Wren no tiene esta capacidad).

Usarlo de esta forma no es común y solo se ha puesto a modo de ejemplo.

class Original {}

class Extendida {
  construct nueva() {}

  is(otra) {otra == Original}

  type {Original}
  static supertype {Original}
}

var instancia = Extendida.nueva()

System.print(instancia is Original) // true (verdadero)
System.print(instancia is Extendida) // false (falso)
System.print(instancia.type) // Original
System.print(Extendida.supertype) // Original

9.3. Atributos

Las clases en Wren pueden ser adornadas con atributos especiales. Estos permiten añadir información para dar contexto o almacenar datos que pueden ser de utilidad para herramientas de programación y otras utilidades.

Solamente pueden estar dentro de una declaración de clase o de método.

#atributo = "valor"
class MiClase {
  #atributo = "valor"
  metodo {true}
}

Los valores pueden ser String, Num, Bool o el tipo especial group. que permite agrupar distintos atributos en un marco en común.

#!cantidad = 10
#grupo(
  nombre = "Camilo"
)
class MiClase {
}

10. Wren Console

Wren Console es una pequeña y simple herramienta para ejecutar programas en Wren. Está respaldado por libuv (para IO). Está basada en el projecto oficial de Wren CLI, aunque todavía es un trabajo en progreso.

#!/usr/bin/env wrenc

import "io" for Stdin

System.print("Ingresa tu nombre:")

var nombre = Stdin.readLine().trim()

System.print("Hola" + (nombre.isEmpty ? "Mundo" : nombre).toString + "!")
````



## Proyectos de ejemplo en Wren

Una colección de código fuente _Wren_ con juegos y aplicaciones simples.
Se recomienda utilizar un editor de código como https://vscodium.com/[VS Codium]
para escribir los ejemplos.

Cada ejemplo puede ser categorizado en los siguientes niveles:

.Niveles
|===
|Nivel|Descripción
|Novato| Utiliza operaciones simples como suma, resta, multiplicación, división, _System.print()_ y estructuras de control.
|Intermedio| Utiliza funciones, diccionarios y clases simples.
|Avanzado| Utiliza clases, herencia y algoritmos avanzados.
|===

### Terminal

Aplicaciones que no necesitan interfaz gráfica para funcionar, solamente necesitando _Wren CLI_ y la entrada/salida estándar.

Basados principalmente en los códigos diponibles en https://github.com/asweigart/PythonStdioGames[Python Stdio Games] y https://github.com/asweigart/inventwithpython3rded[Invent With Python].

#### ¿Cómo ejecutarlos?

Primero se necesita el https://github.com/wren-lang/wren-cli/releases/latest[intérprete de Wren].

Para mayor documentación sobre la consola y el lenguaje _Wren_ se puede visitar http://wren.io/cli

#### Adivina el Número

Basado en http://inventwithpython.com/es/4.html[Adivina del Número].

En el juego _"Adivina el Número"_. La computadora pensará un número aleatorio entre 1 y 20, y te pedirá que intentes adivinarlo. La computadora te dirá si cada intento es muy alto o muy bajo. Tú ganas si adivinas el número en seis intentos o menos.

Este es un buen juego para codificar ya que usa números aleatorios y bucles, y recibe entradas del usuario en un programa corto. Aprenderás cómo convertir valores a diferentes tipos de datos, y por qué es necesario hacer esto. Dado que este programa es un juego, nos referiremos al usuario como el jugador. Pero llamarlo “usuario” también sería correcto.

.Nivel
Novato

##### Ejemplo de Ejecución

`$ ./wren adivina/adivina.wren`

```sh

¡Hola! ¿Cómo te llamas?

Alberto

Bueno, Alberto, estoy pensando en un número entre 1 y 20.

Intenta adivinar 1/6.

10

Tu estimación es muy alta.

Intenta adivinar 2/6.

2

Tu estimación es muy baja.

Intenta adivinar 3/6.

4

¡Buen trabajo, Alberto! ¡Has adivinado mi número en 3 intentos!
Código fuente
// nombre: adivina.wren
// autor: Camilo Castro <camilo@ninjas.cl>
// original: https://github.com/asweigart/inventwithpython3rded/blob/master/translations/es/src/adivinaElN%C3%BAmero.py
// ejecutar: wren adivina/adivina.wren

import "random" for Random
import "io" for Stdin

var random = Random.new()

var intentos = 0
var maxIntentos = 6

System.print("¡Hola!, ¿Cómo te llamas?")

var nombre = Stdin.readLine()
System.print("Bueno, %(nombre), estoy pensando en un número entre 1 y 20.")

// Un número aleatorio entre 1 y 20.
// se necesita usar 21 debido a que el generador
// no incluye el último número.
var numero = random.int(1, 21)
var estimacion = -1

while (intentos < maxIntentos) {
    intentos = intentos + 1
    System.print("Intenta adivinar %(intentos)/%(maxIntentos)")
    
    estimacion = Num.fromString(Stdin.readLine())

    if (estimacion is Null) {
        System.print("Solo admito números")
    } else if (estimacion < numero) {
        System.print("Tu estimación es muy baja")
    } else if (estimacion > numero) {
        System.print("Tu estimación es muy alta")
    } else {
        break
    }
}

if (estimacion == numero) {
    System.print("¡Buen trabajo, %(nombre)! ¡Haz adivinado mi número en %(intentos) intentos!.")
}

if (estimacion != numero) {
    System.print("Pues no. El número que estaba pensando era %(numero)")
}

10.2. Reino de Dragones

Basado en Reino de Dragones.

El juego se llama "Reino de Dragones", y permite al jugador elegir entre dos cuevas, en una de las cuales encontrará un tesoro y en la otra su perdición.

Nivel

Novato

¿Cómo Jugar a Reino de Dragones?

En este juego, el jugador está en una tierra llena de dragones. Todos los dragones viven en cuevas junto a sus grandes montones de tesoros encontrados. Algunos dragones son amigables, y compartirán sus tesoros contigo. Otros son codiciosos y hambrientos, y se comerán a cualquiera que entre a su cueva. El jugador se encuentra frente a dos cuevas, una con un dragón amigable y la otra con un dragón hambriento. El jugador tiene que elegir entre las dos.

Ejemplo de Ejecución

$ ./wren dragon/dragon.wren

Estás en una tierra llena de dragones. Frente a tí
hay dos cuevas. En una de ellas, el dragón es generoso y amigable y compartirá su tesoro contigo. El otro dragón
es codicioso y está hambriento, y te devorará inmediatamente.

¿A qué cueva quieres entrar? (1 ó 2)
1

Te aproximas a la cueva...
Es oscura y espeluznante...
¡Un gran dragon aparece súbitamente frente a tí! Abre sus fauces y...

¡Te engulle de un bocado!

¿Quieres jugar de nuevo? (sí or no)

no
Código fuente
// nombre: dragon.wren
// autor: Camilo Castro <camilo@ninjas.cl>
// original: https://github.com/asweigart/inventwithpython3rded/blob/master/translations/es/src/drag%C3%B3n.py
// ejecutar: wren dragon/dragon.wren

import "random" for Random
import "timer" for Timer
import "io" for Stdin

var random = Random.new()

var mostrarIntroduccion = Fn.new {
  System.print("Estás en una tierra llena de dragones. Frente a tí")
  System.print("hay dos cuevas. En una de ellas, el dragón es generoso y")
  System.print("amigable y compartirá su tesoro contigo. El otro dragón")
  System.print("es codicioso y está hambriento, y te devorará inmediatamente.")
  System.print()
  }

  var elegirCueva = Fn.new {
  var cueva = null
  var primerDragon = "1"
  var segundoDragon = "2"

  while (cueva != primerDragon && cueva != segundoDragon) {
    System.print("¿A qué cueva quieres entrar? (1 ó 2)")
    cueva = Stdin.readLine()
  }
  return cueva
}

var explorarCueva = Fn.new { |cueva|
    // existen 1000 millisegundos en un segundo
    // por lo que para obtener 2 segundos
    // debemos multiplicar 2 * 1000
    var dosSegundos = 2 * 1000

    System.print("Te aproximas a la cueva...")
    Timer.sleep(dosSegundos)
    
    System.print("Es oscura y espeluznante...")
    Timer.sleep(dosSegundos)
    
    System.print("¡Un gran dragon aparece súbitamente frente a tí! Abre sus fauces y...")
    System.print()
    Timer.sleep(dosSegundos)

    // Un número aleatorio entre 1 y 2.
    // se necesita usar 3 debido a que el generador
    // no incluye el último número
    var cuevaAmigable = random.int(1, 3).toString

    if (cueva == cuevaAmigable) {
      System.print("¡Te regala su tesoro!")
    } else {
      System.print("¡Te engulle de un bocado!")
    }

    System.print()
}

var jugarDeNuevo = "sí"
while (jugarDeNuevo == "sí" || jugarDeNuevo == "si" || jugarDeNuevo == "s") {
  mostrarIntroduccion.call()
  var cueva = elegirCueva.call()
  explorarCueva.call(cueva)

  System.print("¿Quieres jugar de nuevo? (sí o no)")
  jugarDeNuevo = Stdin.readLine()
}

System.print("¡Eso fue divertido, nos vemos luego!")

10.3. Diamantes

Esta es una simple aplicación creativa que muestra diamantes de distintos tamaños. Basado en Diamonds.

Nivel

Novato

Ejemplo de Ejecución

./wren diamantes/diamantes.wren

/\
\/

/\
\/

 /\
/  \
\  /
 \/
...
Código fuente
/*
Diamantes, por Al Sweigart <al@inventwithpython.com>
https://github.com/asweigart/PythonStdioGames/blob/main/src/gamesbyexample/diamonds.py
Dibuja diamantes de varios tamaños.
Adaptado a Wren por Camilo Castro <camilo@ninjas.cl>
https://github.com/NinjasCL/wren-libro/blob/main/juegos/diamantes/diamantes.wren
- Tags: tiny, beginner, artistic, pequeño, novato, artistico
- Version: 1.0.0
*/

var mostrarDiamanteLineal = Fn.new { |dimension|
  // Mostrar la parte superior del diamante
  for (i in 0...dimension) {
    // Espacio izquierdo
    System.write(" " * (dimension - i - 1))
    // Parte izquierda del diamante
    System.write("/")
    // Espacio interior del diamante
    System.write(" " * (i * 2))
    // Parte derecha del diamante
    System.print("\\")
  }

  // Mostrar la parte inferior del diamante
  for (i in 0...dimension) {
    // Espacio izquierdo
    System.write(" " * i)
    // Lado izquierdo
    System.write("\\")
    // Espacio interior
    System.write(" " * ((dimension - i - 1) * 2))
    // Lado derecho
    System.print("/")
  }
}

var mostrarDiamanteRelleno = Fn.new { |dimension|
  // Mostrar la cara superior del diamante
  for (i in 0...dimension) {
    // Espaciado izquierdo
    System.write(" " * (dimension - i - 1))
    // Lado izquierdo
    System.write("/" * (i + 1))
    // Lado derecho
    System.print("\\" * (i + 1))
  }

  // Mostrar la cara inferior del diamante
  for (i in 0...dimension) {
    // Espaciado izquierdo
    System.write(" " * i)
    // Lado izquierdo
    System.write("\\" * (dimension - i))
    // Lado derecho
    System.print("/" * (dimension - i))
  }
}

// Muestra 5 diamantes de tamaño 0 al 5:
for(dimension in 0..5) {
  mostrarDiamanteLineal.call(dimension)
  System.print()
  mostrarDiamanteRelleno.call(dimension)
  System.print()
}

10.4. Carrera de Caracoles

Una alucinante carrera entre 2 a 8 caracoles. Basado en SnailRace.

Nivel

Intermedio

Ejemplo de Ejecución

$ ./wren caracoles/caracoles.wren

La gran carrera de caracoles

    @v <-- caracol


¿Cuántos caracoles correrán? Min:2 Max:8
2
Ingresa el nombre del caracol #1:
Ana
Ingresa el nombre del caracol #2:
Berto

INICIO                                  META
|                                       |
                                     Ana
.....................................@v
                                       Berto
.......................................@v
¡Ha ganado Berto!
Código fuente
/*
Snail Race, por Al Sweigart <al@inventwithpython.com>
https://github.com/asweigart/PythonStdioGames/blob/main/src/gamesbyexample/snailrace.py
Una carrera de caracoles, ¡alucinante!.
Adaptado a Wren por Camilo Castro <camilo@ninjas.cl>
https://github.com/NinjasCL/wren-libro/blob/main/juegos/caracoles/caracoles.wren
- Tags: short, beginner, artistic, corto, novato, artistico
- Version: 1.0.0
*/
import "random" for Random
import "timer" for Timer
import "io" for Stdin

var random = Random.new()

class Constantes {
  static minCaracoles { 2 }
  static maxCaracoles { 8 }
  static maxLargoNombre { 20 }
  static meta { 40 }
}


System.print("
La gran carrera de caracoles

    @v <-- caracol

")

var cantidad = null

while (true) {
  System.print("¿Cuántos caracoles correrán? Min:%(Constantes.minCaracoles) Max:%(Constantes.maxCaracoles)")
  cantidad = Num.fromString(Stdin.readLine())
  if (cantidad && cantidad >= Constantes.minCaracoles && cantidad <= Constantes.maxCaracoles) {
    break
  }
  System.print("Ingresar un número entre %(Constantes.minCaracoles) y %(Constantes.maxCaracoles)")
}

var nombres = []

for (i in 1..cantidad) {
  var nombre = ""
  while (true) {
    
    System.print("Ingresa el nombre del caracol #%(i):")
    nombre = Stdin.readLine()

    // Reducimos el nombre a su largo máximo
    nombre = nombre.take(Constantes.maxLargoNombre).join()
    
    if (nombre.count == 0) {
      System.print("Por favor ingresa un nombre.")
    } else if (nombres.contains(nombre)) {
      System.print("Nombre ya existe")
    } else {
      break
    }
  }
  nombres.add(nombre)
}

System.print("\n" * 40)
System.print("INICIO" + " " * (Constantes.meta - "INICIO".count) + "META")
System.print("|" + " " * (Constantes.meta - "|".count) + "|")

var caracoles = {}

for (nombre in nombres) {
  System.print(nombre)
  System.print("@v")
  caracoles[nombre] = 0
}

// 1.5 segundos
Timer.sleep(15 * 100)

while (true) {

  for (i in 0..(random.int(1, cantidad + 1))) {
    var nombre = random.sample(nombres)
    caracoles[nombre] = caracoles[nombre] + 1

    if (caracoles[nombre] >= Constantes.meta) {
      System.print("¡Ha ganado %(nombre)!")
      // Terminar el proceso del juego
      Fiber.yield()
    }
  }

  // 0.5 segundos
  Timer.sleep(500)

  System.print("\n" * 40)
  System.print("INICIO" + " " * (Constantes.meta - "INICIO".count) + "META")
  System.print("|" + " " * (Constantes.meta - "|".count) + "|")

  for (nombre in nombres) {
    var espacios = caracoles[nombre]
    System.print(" " * espacios + nombre)
    System.print("." * espacios + "@v")
  }
}

10.5. Cañon

Este juego simula el disparo de un cañón hacia un objetivo. Código levemente basado en [alfonseca].

Nivel

Intermedio

¿Cómo Jugar?

El jugador debe ingresar el ángulo para cada tiro del cañón.

Ejemplo de Ejecución
Quedan 1 objetivos
El cañón puede disparar hasta 34595 metros
El objetivo está a 6788 metros
La fórmula de la distancia es: alcance * sin(2 * ángulo * π / 180)

Ingrese el ángulo para disparar
5
Distancia Alcanzada: 10690 Distancia Objetivo: 6788

El tiro se pasó en 3902 metros.
¡Prueba otra vez!, quedan 4 tiros

Ingrese el ángulo para disparar
3.1
Distancia Alcanzada: 6695 Distancia Objetivo: 6788

!!BooOOOM!!
¡Felicidades!, haz destruido al objetivo en 2 intentos.
Código fuente
/**
Juego que simula el disparo de un cañón hacia un objetivo.
El jugador debe ingresar el ángulo para cada tiro del cañón.
Código levemente basado en la obra "Cómo construir juegos de aventura"
de Manuel Afonseca. ISBN 84-7688-019-7
- Author: Camilo Castro <camilo@ninjas.cl>
- Version: 1.0.0
*/

import "random" for Random
import "io" for Stdin

var random = Random.new()

var alcance = 0

var objetivos = 3
var oportunidades = 5

// Ángulos deben estar en grados
// mínimo 0.5, máximo 89.5
var anguloElevacion = 0.5
var anguloMinimo = 0.5
var anguloMaximo = 89.5

// Sentinela para saber si el objetivo fue destruido
var destruido = false

// Para saber cuantos tiros fueron
// necesarios para destruir al objetivo
var tiros = 0

// Distancia mínima requerida para considerar
// al objetivo como destruido
// Si el proyectil llega a los 100 metros del objetivo
// se considera destruido
// (en metros).
var distanciaMinima = 100

// Distancia alcanzada por el tiro (en metros)
var distanciaAlcanzada = 0

// Distancia final
// distanciaAlcanzada - distanciaObjetivo
var distanciaFinal = 0

// Distancia al azar del objetivo
// Debe ser igual o menor al alcance del cañón
// entre 100 hasta el alcance del cañón.
var distanciaObjetivo = 0

var jugar = true
while (jugar) {

  System.print("Quedan %(objetivos) objetivos")

  // Valores iniciales por cada objetivo
  oportunidades = 5
  destruido = false
  anguloElevacion = 0
  distanciaFinal = 0
  tiros = 1
  
  // Un número aleatorio entre 10K y 50K
  // Usamos notación científica para simplificar el número
  alcance = random.int(10e3, 50e3 + 1)

  distanciaObjetivo = random.int(distanciaMinima, alcance + 1)

  System.print("El cañón puede disparar hasta %(alcance) metros")
  System.print("El objetivo está a %(distanciaObjetivo) metros")
  System.print("La fórmula de la distancia es: alcance * sin(ángulo * 2 * π / 180)")

  while (oportunidades > 0) {

    while (anguloElevacion < anguloMinimo || anguloElevacion > anguloMaximo) {
      System.print()
      System.print("Ingrese el ángulo para disparar")
      anguloElevacion = Num.fromString(Stdin.readLine())

      if (!anguloElevacion) {
        System.print("Debe ingresar un ángulo.")
        anguloElevacion = 0
      } else if (anguloElevacion > anguloMaximo) {
        System.print("El ángulo debe ser menor a %(anguloMaximo)")
      } else if (anguloElevacion < anguloMinimo) {
        System.print("El ángulo debe ser mayor a %(anguloMinimo)")
      }
    }

    // Calculamos la distancia que el tiro llega con el ángulo ingresado.
    // La fórmula matemática (ángulo * 2π/180) convierte los grados del ángulo de
    // a radianes. Considerando 360º.
    // Luego se multiplica el alcance por el seno de los radianes obtenidos
    // para obtener la distancia alcanzada.
    // Finalmente se redondea el resultado para obtener un número entero.
    // Ver: http://math2.org/math/trig/es-tables.htm
    // Ver: https://es.khanacademy.org/science/physics/two-dimensional-motion/two-dimensional-projectile-mot/v/projectile-at-an-angle

    distanciaAlcanzada = (alcance * (anguloElevacion * ( 2 * Num.pi / 180)).sin).round
    distanciaFinal = (distanciaAlcanzada - distanciaObjetivo).abs
    System.print("Distancia Alcanzada: %(distanciaAlcanzada.abs) Distancia Objetivo: %(distanciaObjetivo) Diferencia: %(distanciaFinal)")

    destruido = (distanciaMinima >= distanciaFinal)

    if (destruido) {
      objetivos = objetivos - 1
      jugar = (objetivos > 0)
      System.print()
      System.print("!!BooOOOM!!")
      System.print("¡Felicidades!, haz destruido al objetivo en %(tiros) intentos.")
      System.print()
      break
    }

    // Aumentamos el contador de tiros
    tiros = tiros + 1

    // Y reiniciamos el angulo de elevación
    // para volver a solicitarlo
    anguloElevacion = 0
    oportunidades = oportunidades - 1
    System.print()

    if (distanciaAlcanzada > distanciaObjetivo) {
      System.print("El tiro se pasó en %(distanciaFinal) metros.")
    } else {
      System.print("El tiro quedó corto por %(distanciaFinal) metros.")
    }
    
    jugar = (oportunidades > 0)
    if (jugar) {
      System.print("¡Prueba otra vez!, quedan %(oportunidades) tiros")
    } else {
      System.print("Juego terminado. ¡Haz perdido todas tus balas!")
    }
  }
}

10.6. Gráficos

Aplicaciones que necesitan TIC 80 u Dome Engine para ser ejecutados.

10.6.1. Breakout

Un clon simple del famoso juego de Breakout. Debes eliminar todos los bloques dentro de la pantalla utilizando una raqueta y una pelota. El juego termina si la raqueta falla en golpear la pelota.

Breakout

Nivel

Avanzado

Código fuente
// title:  Clone de breakout para TIC-80
// author: Camilo Castro
// desc:   Un simple clon de breakout en Wren
// script: wren
// based on: https://github.com/digitsensitive/tic-80-tutorials/tree/master/tutorials/breakout

import "random" for Random

var Seed = Random.new()
var T = TIC

class ScreenWidth {
    static min {0}
    static max {240}
}

class ScreenHeight {
    static min {0}
    static max {136}
}
    
class Screen {
    static height {ScreenHeight}
    static width {ScreenWidth}
}

class Input {
    static left {T.btn(2)}
    static right {T.btn(3)}
    static x {T.btn(5)}
}

class Color {
    static black {0}
    static white {12}
    static orange {3}
    static greyl {13}
    static greyd {15}
}

class Collisions {
   // Implements
  // https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
  static collide(hitbox, hitbox2) {
    return (hitbox.x < hitbox2.x + hitbox2.width &&
            hitbox.x + hitbox.width > hitbox2.x &&
            hitbox.y < hitbox2.y + hitbox2.height &&
            hitbox.y + hitbox.height > hitbox2.y)
  }
}

class PlayerSpeed {
    x {_x}
    x = (value) {
        _x = value
    }
    
    max {_max}
    max = (value) {
        _max = value
    }
    
    construct new() {
        _x = 0
        _max = 4
    }
}

class Player {
    x {_x}
    y {_y}
    width {_width}
    height {_height}
    color {_color}
    speed {_speed}

    state {_state}

    construct new(state) {
        _state = state
        _width = 24
        _height = 4
        _y = 120
        _color = Color.orange
        reset()
    }

    reset() {
        _x = (Screen.width.max/2) - _width/2
        _speed = PlayerSpeed.new()
    }

    draw() {
        T.rect(x, y, width, height, color)
    }

    wall() {
        if (x < Screen.width.min) {
            _x = Screen.width.min
        } else if(x + width > Screen.width.max) {
            _x = Screen.width.max - width
        }
    }

    collisions() {
        wall()
    }

    update() {
        _x = x + speed.x
        if (speed.x != 0) {
            if (speed.x > 0) {
              speed.x = speed.x - 1
            } else {
              speed.x = speed.x + 1
            }
        }
    }

    input() {
        if (Input.left) {
                if (speed.x > -speed.max) {
                    speed.x = speed.x - 2
                } else {
                    speed.x = -speed.max
                }
            }
            
            if (Input.right) {
                if (speed.x < speed.max) {
                        speed.x = speed.x + 2
                    } else {
                        speed.x = speed.max
                    }
            }
    }
}

class BallSpeed {
    x {_x}
    x = (value) {
        _x = value
    }
    
    y {_y}
    y = (value) {
        _y = value
    }
    
    max {_max}
    
    construct new() {
        _x = 0
        _y = 0
        _max = 1.5
    }
}

class Ball {
    x {_x}
    y {_y}
    width {_width}
    height {_height}
    color {_color}
    deactive {_deactive}
    speed {_speed}
    
    player {_player}
    player = (value) {
        _player = value
    }
    
    state {_state}
    
    construct new(player, state) {
        _width = 3
        _height = 3
        _color = Color.greyl
        _player = player
        _state = state
        reset()
    }
    
    position() {
        _x = player.x + (player.width / 2) - 1.5
        _y = player.y - 5
    }
    
    reset() {
        position()
        _deactive = true
        _speed = BallSpeed.new()
    }
    
    input() {
        if (deactive) {
                position()
                if (Input.x) {
                    speed.x = (Seed.float(0, 10).floor * 2) - 1
                        speed.y = speed.y - 1.5
                        _deactive = false
                }
            }
    }
    
    wall() {
        // top
        if (y < 0) {
            speed.y = -speed.y
            
        // left
        } else if (x < 0) {
            speed.x = -speed.x
        
        // right
        } else if (x > 240 - width) {
            speed.x = -speed.x
        }
    }
    
    ground() {
        if (y > 136 - width) {
                reset()
                state.lifeDown()
        }
    }
    
    paddle() {
        if (Collisions.collide(this, player)) {
            speed.y = -speed.y
            speed.x = speed.x + 0.3 * player.speed.x
        } 
    }
    
    brick(brick) {

        // collide left or right side
        if (brick.y < y &&
            y < brick.y + brick.height &&
            (x < brick.x || brick.x + brick.width < x)) {
                speed.x = -speed.x
            }
        // collide top or bottom
        if (y < brick.y ||
        (y > brick.y && brick.x < x &&
        x < brick.x + brick.width)) {
            speed.y = -speed.y
        }
    }

    collisions() {
        wall()
        ground()
        paddle() 
    }
    
    update() {
        _x = x + speed.x
        _y = y + speed.y
        
        if (speed.x > speed.max) {
            speed.x = speed.max
        }
    }
    
    draw() {
        T.rect(x, y, width, height, color)
    }
}

class Brick {
    x {_x}
    y {_y}
    width {_width}
    height {_height}
    color {_color}

    construct new(x, y, color) {
        _x = x
        _y = y
        _width = 10
        _height = 4
        _color = color
    }
    
    draw() {
        T.rect(x, y, width, height, color)
    }
}

class Board {
    width {19}
    height {12}

    ball {_ball}
    state {_state}

    bricks {
        if (!_bricks) {
            _blicks = []
        }

        return _bricks
    }
        
    construct new(ball, state) {
        _ball = ball
        _state = state
        reset()
    }

    reset() {
        _bricks = []
        for (i in 0..height) {
            for (j in 0..width) {
                var x = 10 + j * 11
                var y = 10 + i * 5
                var color = i + 1
                var brick = Brick.new(x, y, color)
                bricks.add(brick)
            }
        }
								ball.reset()
    }
        
    draw() {
        bricks.each {|brick|
            brick.draw()
        }
    }
    
    collisions() {
        var index = 0
        if (bricks.count <= 0) {
            reset()
        }

        var note = "C-%(Seed.int(4,7))"

        bricks.each {|brick|
            if (Collisions.collide(ball, brick)) {
                T.sfx(0, note, 10)
                bricks.removeAt(index)
                ball.brick(brick)
                state.scoreUp()
            }
            index = index + 1
        }
    }
    
    update() {}
    input() {}
}

class Stage {
    objects {_objects}
    state {_state}
    
    construct new(state) {
        _objects = []
        _state = state
    }
    
    add(object) {
        objects.add(object)
    }
    
    input() {
        if (!state.isPlaying) return
        
        objects.each {|object|
            object.input()
        }
    }
    
    draw() {
        if (!state.isPlaying) return
        
        objects.each {|object|
            object.draw()
        }
    }
    
    update() {
    
        if (!state.isPlaying) return
        
        objects.each {|object|
            object.update()
        }
    }
    
    collisions() {
        if (!state.isPlaying) return 
        
        objects.each {|object|
            object.collisions()
        }
    }
}

class GUI {
    player {_player}
    state {_state}
    
    construct new(player, state) {
        _player = player
        _state = state
    }
    
    scores() {
       // shadow
        T.print("SCORE ", 5, 1, Color.greyd)
        T.print(state.score, 40, 1, Color.greyd)
        
        // forecolor
        T.print("SCORE ", 5, 0, Color.white)
        T.print(state.score, 40, 0, Color.white)
        
        // shadow
        T.print("LIVES ", 190, 1, Color.greyd)
        T.print(state.lives, 225, 1, Color.greyd)
        
        // forecolor
        T.print("LIVES ", 190, 0, Color.white)
        T.print(state.lives, 225, 0, Color.white)
    }
    
    gameover() {
      T.print("Game Over", (Screen.width.max/2) - 6 * 4.5, 136/2, Color.white) 
    }
    
    input() {
        if (!state.isPlaying && Input.x) {
            state.start()
        }
    }
    
    draw() {
        if (state.isPlaying) {
            return scores()
        }
        gameover()
    }
}

class GameState {
    static game {__game}
    static game = (value) {
        __game = value
    }

    static isPlaying {__playing}
    
    static score {
        if (!__score) {
            __score = 0
        }
        
        return __score
    }
    
    static score = (value) {
        __score = value
    }
    
    static lives {
        if (!__lives || __lives < 0) {
            __lives = 3
        }
        
        return __lives
    }
    
    static lives = (value) {
        if (value < 0) {
            over()
        }
        __lives = value
    }
    
    static over() {
        reset()
        __playing = false
    }
    
    static start() {
        __playing = true
    }
    
    static lifeDown() {
        lives = lives - 1
    }
    
    static scoreUp() {
        score = score + 100
    }
    
    static reset() {
        score = 0
        lives = 3
        game.reset()
    }
}



class Game is TIC {
    player {_player }
    stage {_stage }
    gui {_gui }
    
    construct new() {
        reset()
    }

    reset() {
        GameState.start()
        GameState.game = this

        _player = Player.new(GameState)
        _ball = Ball.new(_player, GameState)
        
        _gui = GUI.new(_player, GameState)
        
        _board = Board.new(_ball, GameState)
            
        _stage = Stage.new(GameState)
        _stage.add(_player)
        _stage.add(_ball)
        _stage.add(_board)
    }
    
    TIC() {
        T.cls(0)
        input()
        update()
        collisions()
        draw()
    }
    
    input() {
        gui.input()
        stage.input()
    }
    
    update() {
        stage.update()
    }
    
    collisions() {
        stage.collisions()
    }
    
    draw() {
        stage.draw()
        gui.draw()
    }
}

10.6.2. Snake

Un clon simple del famoso juego de la serpiente. Si come se agranda su cola y el juego termina al momento de que la cabeza toque una parte de su propio cuerpo.

Snake

Nivel

Avanzado

Código fuente
// title:  Clone de Snake para TIC-80
// author: Camilo castro
// desc:   Un pequeño clon de Snake
// script: wren
// based on: https://github.com/nesbox/TIC-80/wiki/Snake-Clone-tutorial

import "random" for Random

var Rand = Random.new()
var T = TIC

class Input {
		static up {T.btn(0)}
		static down {T.btn(1)}
		static left {T.btn(2)}
		static right {T.btn(3)}
		static x {T.btn(5)}
}

class Color {
  static black {0}
		static white {12}
		static green {5}
		static blue {10}
		static orange {3}
		static greyd {15} 
}

class Screen {
  static width {240}
		static height {136}
}

class Position {
  x {
		if (!_x) {
			_x = 0
			}
			return _x
		}
		
		x = (value) {
		_x = value
		}
		
		y {
		if (!_y) {
			_y = 0
			}
			return _y
		}
		
		y = (value) {
		_y = value
		}
		
		construct new(x, y) {
			_x = x
			_y = y
		}
}

class GameState {
  static game {__game}
		static game = (value) {
			__game = value
		}
		
		static snake {__snake}
		static snake = (value) {
			__snake = value
		}
		
		static score {
			if (!__score) {
				__score = 0
			}
			return __score
		} 
		
		static scoreUp() {
			__score = score + 100
		}
		
  static frame {
		if (!__frame || __frame > Num.largest - 1) {
					__frame = 0
				}
				return __frame
		}
		
		static frameUp() {
			__frame = frame + 1
		}
		
		static isTenthFrame {frame % 10 == 0}
		
		static isPlaying {
			if (__isPlaying is Null) {
				__isPlaying = true
			}
			return __isPlaying
		}
		
		static isPlaying = (value) {
			__isPlaying = value
		}
		
		static start() {
			isPlaying = true
		}
		
		static gameover() {
			reset()
				isPlaying = false
		}
		
		static reset() {
			__score = 0
			game.reset()
		} 
}

class Food is Position {
	snake {_snake}
	
	construct new (snake) {
			_snake = snake
			place()
	}
	
	random() {
		x = Rand.int(0, 29)
		y = Rand.int(0, 16)
	}
	
	place() {
		random()
		snake.body.each{|part|
			if (part.x == x && part.y == y) {
					place()
			}
		}
	}
	
	draw() {
	  T.rect(x * 8, y * 8, 8, 8, Color.orange)
	}
	
	update() {}
	input() {}
	
	collisions() {
		snake.eat(this)
	}
}

class Direction {
	static directions {
		if (!__directions) {
				__directions = [
						Position.new(0, -1), // up
						Position.new(0, 1), // down
						Position.new(-1, 0), // left
						Position.new(1, 0) // right
				]
			}
			return __directions
		}
		
		static up {directions[0]}
		static down {directions[1]}
		static left {directions[2]}
		static right {directions[3]}
}

class Snake {
  tail {body[0]}
		neck {body[body.count - 2]}
		head {body[body.count - 1]}
		body {
			if (!_body) {
					var tail = Position.new(15, 8)
					var neck = Position.new(14, 8)
					var head = Position.new(13, 8)
					_body = [tail, neck, head]				 
				}
				return _body
		}
		
		direction {
			if (!_direction) {
				_direction = Direction.up
			}
			return _direction
		}
		
		state {_state}
		
		construct new(state) {
			_state = state
		}
		
		reset() {
			_body = null
			_direction = Direction.up
		}
		
		input() {
			var last = direction
			
			if (Input.up) {
				_direction = Direction.up
			}
			
			if (Input.down) {
				_direction = Direction.down
			}
			
			if (Input.left) {
				_direction = Direction.left
			}
			
			if (Input.right) {
				_direction = Direction.right
			}
			
			if (head.x + direction.x == neck.x &&
				head.y + direction.y == neck.y) {
					_direction = last
			}
		}
		
		draw() {
			body.each{|part|
			 T.rect(part.x * 8, part.y * 8, 8, 8, Color.blue)
			}
		}
		
		update() {
		
			var x = (head.x + direction.x) % 30
			if (x < 0) {
				x = 29
			}
			
			var y = (head.y + direction.y) % 17
			if (y < 0) {
				y = 17
			}
			
			var part = Position.new(x, y)
			
			body.add(part)
		}
		
		removeTail() {
			body.removeAt(0)
		}
		
		eat(food) {
			if (head.x == food.x && head.y == food.y) {
				T.sfx(0, "C-5", 10)
				state.scoreUp()
				food.place()
			} else {
				removeTail()
			}
		}
		
		collisions() {
				
				// Search if head collisions with body parts
				// omit last part since its the head
			body[0...(body.count - 1)].each{|part|
					if (head.x == part.x && head.y == part.y) {
							state.gameover()
					}
			}
		}
}

class Stage {
  items {_items}
		
	construct new() {
		_items = []
	}
		
  add(object) {
			items.add(object)
	}
		
  input() {
			items.each{|object|
					object.input()
			}
	}
		
	draw() {
		items.each{|object|
			object.draw()
		}
	}
	
	update() {
		items.each{|object|
			object.update()
		}
	}
	
	collisions() {
		items.each{|object|
			object.collisions()
		}
	}
}

class GUI {
		state {_state}
		
		construct new(state) {
			_state = state
		}
		
		score() {
			T.print("SCORE %(state.score)", 5, 5, Color.blue)
			T.print("SCORE %(state.score)", 5, 4, Color.white)
		}
		
		gameover() {
			T.cls(Color.black)
			T.print("Game Over", (Screen.width/2) - 6 * 4.5, (Screen.height / 2), Color.white)
		}
		
		input() {
			if (!state.isPlaying && Input.x) {
					state.start()
			}
		}
		
		update() {}
		collisions() {}
		
		draw() {
			if (state.isPlaying) {
					return score()
			}
			gameover()
		}
}

class Game is TIC {
		stage {
			if (!_stage) {
					GameState.game = this
					_stage = Stage.new()
					_stage.add(snake)
					_stage.add(food)
					_stage.add(gui)
			}
			return _stage
		}
		
		gui {
			if (!_gui) {
				_gui = GUI.new(GameState)
			}
			return _gui
		}
		
		snake {
			if (!_snake) {
				_snake = Snake.new(GameState)
				GameState.snake = _snake
			}
			return _snake
		}
		
		food {
			if (!_food) {
				_food = Food.new(snake)
			}
			return _food
		}
		
		construct new() {
				reset()
		}
		
		reset() {
			snake.reset()
		}
		
		TIC() {
			T.cls(Color.green)
			GameState.frameUp()
			input()
			
			if (GameState.isTenthFrame) { 
				update()
				collisions()
			}

			draw()
		}
		
		input() {
			stage.input()
		}
		
		update() {
			stage.update()
		}
		
		collisions() {
			stage.collisions()
		}
		
		draw() {
			stage.draw()
		}
}

11. Soluciones a Ejercicios

Las siguientes son las soluciones a los ejercicios planteados en el libro. No necesariamente son la única forma de resolverlos. Se recomienda intentar resolver los ejercicios antes de ver las soluciones.

11.1. Capítulo 1

11.1.1. Solución: ¡Hola Wren!

Ejecución

$ ./wren hola.wren

Salida
¡Hola Wren!
Código
// muestra: ¡Hola Wren!
System.print("¡Hola Wren!")

11.2. Capítulo 3

11.2.1. Solución: Donas

Ejecución

$ ./wren donas.wren

Código
var donas = Fn.new {|cantidad|
  // Tu código aquí
  var mensaje = "Número de donas:"
  if (cantidad >= 10) {
    return "%(mensaje) muchas"
  }
  return "%(mensaje) %(cantidad)"
}


// Pruebas para evaluar que el ejercicio
// se ha completado exitosamente.
// no modificar.

var probar = Fn.new {|tengo, quiero|
  var prefijo = " X "
  if (tengo == quiero) {
    prefijo = " OK "
  }
  System.print("%(prefijo) tengo: %(tengo) quiero: %(quiero)")
}

System.print("Probando donas")
probar.call(donas.call(4), "Número de donas: 4")
probar.call(donas.call(9), "Número de donas: 9")
probar.call(donas.call(10), "Número de donas: muchas")
probar.call(donas.call(99), "Número de donas: muchas")
System.print()

Glosario

Lista de conceptos utilizados a lo largo de este documento.

class

Significa "clase".

call

Significa "llamar".

construct

Significa "constructor".

object

Significa "objeto".

start

Inicio, principio, comienzo.

end

Fin, término, final.

each

Palabra que significa "cada uno". Usado en ciclos iterativos para evaluar cada uno de los elementos.

for

Instrucción que significa "para". Usada dentro de los bucles y en conjunto a la instruacción "import".

foreign

Instrucción que significa "extranjero". Usada para indicar que la clase, propiedad o variable será implementada fuera de Wren. Normalmente cuando se necesita utilizar código en otros lenguajes como C.

false

Instrucción que significa "falso". Usado para indicar una condición no se está cumpliendo. Su contraparte es la instrucción "true".

fiber

Significa "fibra".

fn

Significa "función".

función

Bloque de código que puede ser ejecutado en distintos puntos del ciclo de vida del código. También puede ser asignado a variables.

new

Significa "nuevo".

print

Instrucción que significa "mostrar" o "imprimir". Usado para mostrar un valor.

parámetro

Son variables que son enviados a las funciones. Pueden ser por valor o por referencia.

static

Instrucción que significa "estático". Usado para indicar que una propiedad o método de una clase no necesita una instancia para ser invocado.

string

Significa "cadena de caracteres".

int

Significa "número entero".

float

Significa "número decimal con punto flotante".

random

Signfica "aleatorio".

num

Significa "número".

true

Instrucción que significa "verdadero". Usado para indica una condición que si se cumple. Su contraparte es la instrucción "false".

trim

Instrucción que significa "cortar". Usado para eliminar caracteres dentro de un string.

import

Instrucción que significa "incluir". Obtener datos desde un archivo e incluir sus contenidos en el contexto. Ejemplo import "random" for Random.

if

Instrucción que significa "si". Permite crear una condicional y controlar el flujo de datos dentro de un programa.

is

Instrucción que significa "es". Permite evaluar si un objeto es miembro de un tipo de clase.

else

Instrucción que significa "en otro caso". Normalmente usado para indicar una condición alternativa si la condición "if" principal no se cumple.

var

Instrucción que significa "variable". Usado para declarar una nueva variable.

while

Instrucción que significa "mientras". Usada dentro de los bucles.

write

Instrucción que signiica "escribir".

Bibliografía

Una lista de recursos complementarios y referenciales.

Libros
Sitios Web

Índice