singe/docs/es/lessons/03-decisions.adoc

526 lines
19 KiB
Text

[#_lesson_3_making_decisions]
== Lección 3: Tomar decisiones
image::learn/03-decisions.png[La lección terminada, 480]
Todo lo que has escrito hasta ahora hace lo mismo en cada fotograma. La palabra
de la lección dos se movía quisieras o no, y lo único que la hacía dar la vuelta
eran cuatro bloques de código que tuviste que creerte sin más.
Esta lección trata de esos cuatro bloques, como es debido. Al final de ella tu
programa hará preguntas, actuará según las respuestas y -- como la pregunta más
interesante que puede hacer un juego es «¿qué está haciendo el jugador?» --
estarás manejando una caja por la ventana con las teclas de flecha. Guarda esa
caja. Es el jugador del juego que construyes en la lección siete.
[#_start_a_new_folder_2]
=== Empieza una carpeta nueva
Una carpeta llamada `decisions`, dentro un archivo llamado `decisions.singe`, y
`Singe -R decisions` ejecutándose en una terminal. Igual que la última vez.
[#_what_an_if_really_says]
=== Lo que dice de verdad un `if`
Aquí está el bloque de la lección dos por separado.
[source,lua]
----
if wordX > lastColumn then
wordX = lastColumn
end
----
Cuatro partes. La palabra `if`. Una pregunta. La palabra `then`. Y un cuerpo de
líneas que solo se ejecutan cuando la respuesta a la pregunta es sí, cerrado con
`end`.
La pregunta es `wordX > lastColumn`, y el `>` es el que hace el trabajo de
verdad. Compara los dos números que tiene a cada lado y da una respuesta que es
*verdadero* o *falso*. Esas dos palabras no son texto ni son números; son
valores por derecho propio, llamados *booleanos*, y un booleano es lo único que
le interesa a un `if`.
La sangría es la misma idea que en la lección uno: las líneas de dentro van
sangradas para que veas de un vistazo qué pertenece al `if`, y el `end` se
alinea con el `if` que lo abrió. Lua lo ejecutaría sin los espacios. Tú no
podrías leerlo.
Hay seis comparaciones como esta y las usarás todas.
[cols="1,4"]
|===
| `a < b` | a es menor que b
| `a > b` | a es mayor que b
| `a <= b` | a es menor o igual que b
| `a >= b` | a es mayor o igual que b
| `a == b` | a es lo mismo que b
| `a ~= b` | a no es lo mismo que b
|===
Las dos últimas son con las que hay que tener cuidado, por motivos distintos.
`~=` significa «no es igual a». La mayoría de los lenguajes lo escriben `!=`, y
si has visto algún programa antes, tus dedos escribirán `!=` y Lua lo
rechazará. La virgulilla está en tu teclado junto al `1` o debajo del `#`, según
dónde vivas.
`==` son dos signos igual, y no es una errata por uno. Es uno de los errores más
comunes en programación, y vale la pena pararse en él un momento.
En la lección dos aprendiste que un signo igual significa *mete el lado derecho
en el nombre de la izquierda*. Eso es algo que haces. Cambia algo.
Dos signos igual significan *¿son iguales estas dos cosas?* Eso es una pregunta
que haces. No cambia nada, y devuelve verdadero o falso.
[source,lua]
----
lives = 3
if lives == 3 then
----
La primera línea pone `lives` a tres. La segunda pregunta si vale tres. Trabajos
distintos, símbolos distintos, a un carácter de distancia. Lua te pillará si los
confundes, y verás el error que usa al final de esta lección.
[#_when_the_answer_is_no]
=== Cuando la respuesta es no
Un `if` solo no hace nada en absoluto cuando la respuesta es falsa. Muchas veces
quieres que en ese caso pase otra cosa, y eso es `else`.
[source,lua]
----
if lives > 0 then
message = "Ready"
else
message = "Game Over"
end
----
Se ejecuta exactamente uno de esos dos cuerpos, nunca los dos, nunca ninguno,
así que `message` siempre acaba guardando una de las dos cadenas y nunca nada.
Cuando hay más de dos posibilidades, `elseif` las encadena.
[source,lua]
----
if score >= 10000 then
rank = "ace"
elseif score >= 5000 then
rank = "pilot"
elseif score >= 1000 then
rank = "rookie"
else
rank = "passenger"
end
----
Lua prueba las preguntas desde arriba y se para en la primera que es verdadera.
Una puntuación de 6000 no es mayor que 10000, así que prueba la siguiente, que
es verdadera, así que `rank` pasa a ser `pilot` y las otras dos preguntas no se
llegan a hacer. Eso último importa: el `elseif` de 1000 también es verdadero
para una puntuación de 6000, y nunca se llega a él, y por eso el orden va de
mayor a menor.
Todo el bloque necesita exactamente un `end`, da igual cuántas partes `elseif`
tenga. Es una sola decisión, no cuatro.
[#_true_false_and_nothing_at_all]
=== Verdadero, falso y nada de nada
No hace falta comparar dos cosas para obtener un booleano. Puedes guardar uno.
[source,lua]
----
paused = false
----
Es una variable como cualquier otra, y el valor que tiene es el propio booleano
`false`, escrito sin comillas porque no es la palabra «false», es la idea. Lo
mismo pasa con `true`. Una variable que guarda uno de los dos puede ir
directamente en un `if` sin ninguna comparación:
[source,lua]
----
if paused then
----
Léelo como «si está en pausa». Se entiende mejor que `if paused == true`, que
hace lo mismo dando un rodeo, y los programadores con experiencia te mirarán
raro si lo escribes.
Tres palabras unen y dan la vuelta a estas respuestas.
[source,lua]
----
if alive and not shielded then
if coins > 0 or freePlay then
----
`and` es verdadero cuando lo son los dos lados. `or` es verdadero cuando lo es
cualquiera de los dos, o los dos. `not` convierte verdadero en falso y falso en
verdadero. Se leen como el inglés y, cosa rara en programación, significan lo
mismo que en inglés.
Hay un valor más que conviene conocer, y es el que produce los errores más
confusos de tu primer año.
[source,lua]
----
overlayPrint(0, 0, "lives " .. tostring(lives))
----
Si nunca has metido nada en `lives`, eso imprime `lives nil`. Lua no se queja de
un nombre que no ha visto nunca; te da `nil`, que es su palabra para *aquí no
hay nada*. No es cero y no es una cadena vacía. El cero es un número que querías
guardar. `nil` es la ausencia de un valor.
Ya te has encontrado con `nil` dos veces sin que nadie te lo dijera.
`frames = frames + 1` sin la línea `frames = 0` encima falla porque le pediste a
Lua que sumara uno a nada. Y un callback mal escrito también es `nil`, y por eso
un `onOverlayUpdate` mal escrito te da una ventana negra y ningún error: Singe
busca el nombre, no encuentra nada y sigue tan tranquilo con lo suyo.
`tostring` está en esa línea porque `..` no une un booleano ni un `nil` con
nada. Los números te los convierte; estos no. Esa era la promesa del final de
la lección dos, y ahora tienes para qué usarlo.
Por último, la regla que lo ata todo, porque `if` acepta cualquier cosa que le
des, no solo verdadero y falso:
[quote]
En Lua todo es verdadero salvo `false` y `nil`.
El cero es verdadero. Una cadena vacía es verdadera. Solo esos dos no lo son, y
una vez que lo sabes, `if movingLeft then` se lee perfectamente tanto si
`movingLeft` guarda `true` como `false`, o si nunca le has dado valor.
[#_reading_the_controls]
=== Leer los controles
Es hora de preguntarle algo al jugador. Para eso hace falta un callback nuevo.
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_RIGHT then
boxX = boxX + 10
end
end
----
`onInputPressed` es el segundo callback que escribes, y funciona exactamente
como describía la lección uno: tú lo escribes, Singe lo llama. A
`onOverlayUpdate` se le llama con un temporizador. A este se le llama cuando
pasa algo -- cuando el jugador pulsa cualquier cosa que el motor reconoce.
La diferencia con `onOverlayUpdate` es la palabra que hay dentro de los
paréntesis. Singe tiene que decirte *qué* control se ha pulsado, así que le da
un valor a la función, y `what` es el nombre con el que has elegido recibirlo.
La lección uno llamaba argumentos a las cosas que le pasas a una función; desde
dentro, el nombre que recoge uno es un *parámetro*. El nombre es tuyo: llámalo
`what`, llámalo `button`, llámalo `k`. Singe lo rellena igual.
`SWITCH_RIGHT` es un nombre que el motor ya le ha dado a un valor, como
`OVERLAY_UPDATED` en la lección uno. No tienes que cargar nada para usarlo;
Singe los define todos antes de que se ejecute tu script. Hay uno para cada cosa
que tiene una recreativa:
`SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, de `SWITCH_BUTTON1`
a `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1` y más. La tabla de
Constantes del motor de la referencia cubre toda la familia, que va de
`SWITCH_UP` a `SWITCH_RECORD`. A propósito, no se llaman «la tecla de flecha derecha», porque llega el
mismo `SWITCH_RIGHT` tanto si el jugador ha pulsado una tecla de flecha como si
ha empujado un joystick o el stick de un mando. Escribes el juego una vez y
funciona con los tres.
Añade ese callback, dale a `boxX` un valor inicial fuera de las funciones y
dibuja algo en `boxX`. Pulsa la flecha derecha. La caja salta diez píxeles cada
vez que la pulsas.
[#_it_happened_and_it_is_happening]
=== Ha pasado, y está pasando
Mantén pulsada la tecla de flecha. No pasa nada más.
A `onInputPressed` se le llama una vez, al bajar la tecla, y no otra vez hasta
que la sueltas y la vuelves a pulsar. No hay repetición de teclas. Eso es justo
lo correcto para algunas cosas -- disparar, echar una moneda, elegir una opción
de un menú -- y justo lo incorrecto para caminar, que tiene que seguir pasando
mientras la tecla esté pulsada.
Son dos preguntas realmente distintas. *Ha pasado* es un evento, y te enteras
una vez. *Está pasando* es un estado, y dura. Singe te avisa de los eventos, y la
forma de convertir eventos en estado es recordarlos tú.
Dos callbacks y una variable entre los dos.
[source,lua]
----
movingRight = false
function onInputPressed(what)
if what == SWITCH_RIGHT then
movingRight = true
end
end
function onInputReleased(what)
if what == SWITCH_RIGHT then
movingRight = false
end
end
----
`onInputReleased` es el tercer callback, y es el reflejo del segundo: Singe lo
llama cuando la tecla vuelve a subir, con el mismo valor `SWITCH_`. Entre los
dos consiguen que `movingRight` diga la verdad sobre lo que está haciendo el
dedo del jugador en este momento.
Todavía no se ha movido nada, y de eso se trata. Pulsar y moverse son ahora dos
trabajos separados. El movimiento va donde va todo el trabajo de cada
fotograma.
[source,lua]
----
function onOverlayUpdate()
if movingRight then
boxX = boxX + BOX_SPEED
end
...
----
Ahora la caja se desliza suavemente mientras la tecla esté pulsada, a una
velocidad que controlas tú, y la aritmética vuelve a estar en la única función
que se ejecuta sesenta veces por segundo, que es donde debe estar. Esta forma --
los eventos ponen indicadores, el fotograma actúa según los indicadores -- es
como casi todos los juegos manejan sus controles, y la volverás a usar en cada
lección que tenga un jugador.
[#_drawing_a_box]
=== Dibujar una caja
`overlayBox` recibe dos esquinas: el borde izquierdo y el de arriba del
rectángulo, y luego el derecho y el de abajo.
[source,lua]
----
colorForeground(255, 220, 0)
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
----
Dibuja un *contorno*, cuatro líneas de un píxel de grosor, no un rectángulo
relleno. En las llamadas de dibujo del overlay no hay ningún rectángulo relleno.
Cuando más adelante quieras un bloque sólido hay formas de conseguirlo, y la
lección nueve tiene la habitual.
Sus números son píxeles, no celdas de carácter. Esto pilla a la gente, porque
`overlayPrint` en la lección dos contaba en celdas de seis por trece píxeles y
todo lo demás en Singe cuenta en píxeles. `overlayPrint` es el raro; el resto de
las llamadas de dibujo -- `overlayBox`, `overlayLine`, `overlayCircle`,
`overlayEllipse` y `overlayPlot` -- trabajan todas en los mismos píxeles que da
`overlayGetWidth`.
`colorForeground` pone el color, con tres números de 0 a 255: cuánto rojo,
cuánto verde, cuánto azul. Los tres a 255 es blanco, todos a 0 es negro, y
`(255, 220, 0)` es un amarillo cálido. Fíjate en que no es un argumento de
`overlayBox`. Es un ajuste: lo cambias, y todo lo que dibujes después sale en el
color nuevo hasta que lo vuelvas a cambiar. `overlayPrint` vuelve a ser la
excepción -- la fuente de consola trae sus propios colores de serie y no te hace
caso.
[#_the_whole_thing_2]
=== Todo junto
[source,lua]
----
BOX_SIZE = 20
BOX_SPEED = 2
width = overlayGetWidth()
height = overlayGetHeight()
maxX = width - BOX_SIZE - 1
maxY = height - BOX_SIZE - 1
boxX = width // 2
boxY = height // 2
movingLeft = false
movingRight = false
movingUp = false
movingDown = false
function onInputPressed(what)
if what == SWITCH_LEFT then
movingLeft = true
elseif what == SWITCH_RIGHT then
movingRight = true
elseif what == SWITCH_UP then
movingUp = true
elseif what == SWITCH_DOWN then
movingDown = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
movingLeft = false
elseif what == SWITCH_RIGHT then
movingRight = false
elseif what == SWITCH_UP then
movingUp = false
elseif what == SWITCH_DOWN then
movingDown = false
end
end
function onOverlayUpdate()
if movingLeft and not movingRight then
boxX = boxX - BOX_SPEED
elseif movingRight and not movingLeft then
boxX = boxX + BOX_SPEED
end
if movingUp and not movingDown then
boxY = boxY - BOX_SPEED
elseif movingDown and not movingUp then
boxY = boxY + BOX_SPEED
end
if boxX < 0 then
boxX = 0
elseif boxX > maxX then
boxX = maxX
end
if boxY < 0 then
boxY = 0
elseif boxY > maxY then
boxY = maxY
end
overlayClear()
if movingLeft or movingRight or movingUp or movingDown then
colorForeground(255, 220, 0)
else
colorForeground(80, 160, 255)
end
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY)
return OVERLAY_UPDATED
end
----
Hay cuatro cosas ahí dentro que merecen una segunda mirada.
`movingLeft and not movingRight` está ahí porque un jugador puede mantener
pulsadas las dos flechas a la vez. Sin el `not`, las dos se pelearían y la caja
temblaría o se iría hacia un lado; con él, si mantienes pulsadas las dos no gana
ninguna y la caja se queda quieta, que es lo que espera un jugador.
`maxX` se calcula una vez arriba, con `overlayGetWidth` como en la lección dos,
y el `- 1` es porque el último píxel de la superficie es uno menos que su ancho.
Quita el `- 1` y el borde derecho de la caja se sale de la superficie y
desaparece, porque Singe tira sin quejarse todo lo que dibujas fuera del
overlay.
Los bloques que limitan la posición usan `elseif` en lugar de dos bloques `if`
separados. La caja no puede salirse por el borde izquierdo y por el derecho a la
vez, así que hacer la segunda pregunta cuando la primera era verdadera es
trabajo perdido.
Y la decisión del color es un `if` cuyo único trabajo es elegir un valor.
Amarillo mientras el jugador está haciendo algo, azul mientras no, que es una
forma gratis de ver que tu manejo de la entrada funciona de verdad.
[#_try_it_3]
=== Pruébalo
. *Cambia la velocidad.* `BOX_SPEED = 1` y `BOX_SPEED = 8`. Con ocho, fíjate en
lo que hace el límite cuando empujas la caja contra una esquina.
. *Muestra tus indicadores.* Añade una línea a `onOverlayUpdate`:
`overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Mantén pulsada y suelta
la flecha izquierda y mira cómo cambia. Luego quita el `tostring` y lee el
error que te da Lua.
. *Que vaya más rápido con un botón pulsado.* Añade un indicador `running`
controlado por `SWITCH_BUTTON1` en los dos callbacks de entrada, y usa un `if`
en `onOverlayUpdate` para moverte el doble de `BOX_SPEED` mientras sea
verdadero.
. *Marca el centro.* Dibuja una segunda caja, pequeña, en el centro de la
ventana, que cambie de color cuando la caja del jugador se solape con ella.
Necesitarás `and` para unir cuatro comparaciones, y es más difícil de lo que
parece. Que te salga un poco mal es normal; la lección once lo hace como es
debido.
. *Quita lo de soltar.* Borra toda la función `onInputReleased` y ejecútalo. La
caja da un paso y luego no para nunca. Averigua qué línea no se alcanza nunca
ahora.
[#_break_it_on_purpose_3]
=== Rómpelo a propósito
Busca el bloque que limita la posición y cambia el `<` de su primera comparación
por un solo signo igual, para que quede así:
[source,lua]
----
if boxX = 0 then
----
Guárdalo. La ventana se queda ahí vacía y la terminal dice:
----
Error running script: decisions/decisions.singe:57: 'then' expected near '='
----
Este es el error del que avisaba toda la sección sobre `==`, y el mensaje es un
buen ejemplo de un error que es totalmente exacto y totalmente inútil si te lo
tomas al pie de la letra. Lua no te está pidiendo que añadas la palabra `then`.
Lua llegó hasta `if boxX`, esperaba a continuación una comparación o un `then`,
y se encontró `=`, que no puede ser ninguna de las dos cosas.
Siempre que veas `'then' expected` después de un `if`, busca un `=` simple que
debería ser doble. Casi siempre es eso.
Fíjate en dónde ha aparecido este: el juego ni siquiera llegó a empezar, igual
que con la cadena sin terminar de la lección uno, porque Lua no podía leer el
archivo. El error del contador de fotogramas de la lección dos pasó durante la
ejecución. Los errores en la forma de tu código te paran antes de que empiece el
juego. Los errores en lo que hace tu código esperan a que se ejecute.
[#_what_you_learned_3]
=== Lo que has aprendido
* `if question then ... end` ejecuta un cuerpo solo cuando la respuesta es
verdadera.
* `else` da el otro caso, y `elseif` encadena más; gana la primera pregunta
verdadera y las demás no se llegan a hacer.
* Las comparaciones son `<`, `>`, `<=`, `>=`, `==` y `~=`.
* `=` mete un valor en un nombre. `==` pregunta si dos cosas son iguales.
* `true` y `false` son valores, y puedes guardarlos en variables.
* `and`, `or` y `not` unen y dan la vuelta a esas respuestas.
* `nil` es lo que obtienes de un nombre al que nunca has dado valor, y no es
cero.
* Todo es verdadero salvo `false` y `nil`.
* `onInputPressed` y `onInputReleased` son callbacks que Singe llama con un
valor `SWITCH_` cuando el jugador pulsa o suelta un control.
* Una pulsación es un evento y ocurre una vez; guarda el estado en una variable
y actúa según él en cada fotograma.
* `overlayBox` dibuja un contorno en píxeles, y `colorForeground` es un ajuste,
no un argumento.
[#_next_time_3]
=== La próxima vez
Una caja es fácil. Cien estrellas no lo son, al menos no tal como has escrito
las cosas hasta ahora, porque escribir cien líneas `overlayPlot` no es el plan
ideal para una tarde. La lección cuatro trata del bucle: decirle al ordenador
que haga lo mismo muchas veces, con algo distinto en cada vuelta, que es lo que
hace posible llenar una pantalla de cualquier cosa.