Pular para o conteúdo

3. Leia comandos de movimento

Fonte atual: curriculum/arrowfall/checkpoints/03-movement · Flecha 0.3.0 · 3/7.

O programa neste checkpoint

Até aqui: O checkpoint 02 renderiza a partir de um estado Position próprio.

O checkpoint 02 tem uma posição confiável, mas toda execução termina com o mesmo valor. Para tornar o programa interativo, o texto do terminal precisa atravessar uma fronteira até o modelo sem transformar strings arbitrárias em regras.

Esta versão usa um piso aberto 5×3 para explicar entrada e mutação sem misturar colisão. W, A, S e D movem; Q encerra o loop; texto inválido relata erro e preserva o estado.

Limitação atual

O arqueiro continua sendo um diagrama: nenhuma ação altera a posição.

Próximo comportamento necessário: Ler W/A/S/D até Q e mostrar um novo frame depois de cada comando válido.

Modelo e invariantes

Normalizar o texto uma vez, convertê-lo para um enum Command fechado e deixar Game aplicar o comando tipado por uma referência mutável verificada.

O parser converte o texto de entrada para Command uma vez na fronteira. Assim, Game trata um enum fechado em vez de repetir comparações de strings nas regras.

O lado da entrada é responsável por normalização e parsing. Ele remove espaços ASCII, exige exatamente um caractere, converte para minúsculo e produz um enum Command fechado. Depois dessa fronteira, Game nunca compara strings.

O loop de controle segue uma ordem fixa: desenha o estado atual, lê um comando, rejeita erro ou saída e então pede que Game aplique um movimento válido. A iteração seguinte desenha o estado alterado, sem que o parser conheça a renderização.

Game::apply recebe uma referência mutável verificada para self. Cada comando seleciona uma mudança de coordenada e testa o limite do piso antes da mutação. Este checkpoint escreve as mudanças diretamente; o checkpoint 04 extrai a aritmética repetida para Direction e um delta.

O invariante é x permanecer em 0…4 e y em 0…2. Entrada inválida e movimento bloqueado no limite são no-ops. Paredes, custo de turno e vitória ficam adiados até a fronteira texto-comando estar estável.

Implementação

A implementação está dividida nas mudanças abaixo. Cada trecho vem literalmente do checkpoint qualificado e é explicado na parte do fluxo de dados em que participa. Os trechos não são programas independentes; o código completo compilado está disponível depois da execução.

  • Adicionar Command e CommandError.
  • Adicionar o loop bloqueante de leitura e parsing.
  • Alterar Game somente após parsing bem-sucedido.

Comandos formam um vocabulário fechado

Command é o vocabulário compartilhado por parser e modelo. As variantes descrevem intenção, não a tecla, portanto Game não precisa saber que MoveUp surgiu de W ou w.

Trecho: src/main.fl:3–15. O arquivo completo aparece na seção de código abaixo.

src/main.fl:3-15 (excerpt)
@imports :: lib::std::text::{ascii, strings};
enum Command {
default Up,
Left,
Down,
Right,
Quit
}
error CommandError {
InvalidCommand
}

Quit pertence ao mesmo vocabulário analisado, mas o loop o trata antes de Game::apply. CommandError mantém entrada malformada separada de um comando legítimo.

Um enum fechado também torna casos ausentes visíveis quando o código usa match. Aceitar strings cruas permitiria que normalização e regras divergissem.

A mutação fica dentro de Game

Somente Game altera a Position privada. Os quatro ramos combinam comando e limite relevante, mudando uma coordenada apenas depois de a verificação passar.

Trecho: src/main.fl:39–50. O arquivo completo aparece na seção de código abaixo.

src/main.fl:39-50 (excerpt)
void Game::apply(&self, Command command) {
if command == Command.Up and self->player.y > 0 {
self->player.y--;
} else if command == Command.Left and self->player.x > 0 {
self->player.x--;
} else if command == Command.Down and self->player.y < 2 {
self->player.y++;
} else if command == Command.Right and self->player.x < 4 {
self->player.x++;
}
}

A referência mutável verificada significa que o chamador empresta Game durante apply; nenhum segundo dono é criado. Um limite rejeitado preserva as duas coordenadas.

Os ramos repetidos são aceitáveis para um retângulo aberto, mas misturam direção e geometria. O próximo checkpoint os substitui por conversão de Direction em delta antes de testar a célula proposta.

Texto vira comando tipado

read_command é a fronteira do terminal. Ela trata fim de entrada como Quit, limpa a linha, valida o tamanho, normaliza caixa uma vez e retorna Command ou erro tipado.

Trecho: src/main.fl:89–116. O arquivo completo aparece na seção de código abaixo.

src/main.fl:89-116 (excerpt)
(Command, error) read_command() {
string line;
inputError := stdin("command>", &line);
if inputError != none {
return (Command.Quit, none);
}
value := strings::trim_ascii(line.view());
if value.len() != 1 {
return (none, CommandError.InvalidCommand);
}
const char KEY = ascii::to_ascii_lowercase(value[0].cast<char>);
if KEY == 'w' {
return (Command.Up, none);
}
if KEY == 'a' {
return (Command.Left, none);
}
if KEY == 's' {
return (Command.Down, none);
}
if KEY == 'd' {
return (Command.Right, none);
}
if KEY == 'q' {
return (Command.Quit, none);
}
return (none, CommandError.InvalidCommand);
}

O acesso value[0] só ocorre depois de len() confirmar que há um byte. A conversão para minúsculo torna W e w equivalentes sem duplicar ramos.

A tupla leva comando e erro porque o loop precisa dos dois resultados. O parser não altera Game; o loop chama apply apenas quando commandError é none.

Execute e examine o resultado

O comando usa a entrada registrada do checkpoint, portanto o comportamento descrito acima pode ser reproduzido sem depender de tempo manual.

Comando de execução
longbow run tutorial/checkpoints/03-movement < tutorial/checkpoints/03-movement/tests/input.txt
Entrada padrão registrada
Entrada padrão
d
s
q

A sequência registrada D, S, Q desenha o piso inicial, move à direita, move para baixo e encerra. O frame representativo coloca @ na coluna 2, linha 2.

Frame de terminal produzido pela versão 3: Leia comandos de movimento
Frame representativo da execução registrada do checkpoint 3.

Uma tecla desconhecida imprime o diagnóstico e segue para a próxima iteração sem chamar apply. O frame seguinte inalterado demonstra que falhas de parsing não chegam à mutação.

Transcript acessível do frame
Transcript do frame
ARROWFALL — open training floor
.....
.....
..@..
Stdout registrada completa
Stdout esperada
ARROWFALL — open training floor
.....
.@...
.....
command>ARROWFALL — open training floor
.....
..@..
.....
command>ARROWFALL — open training floor
.....
.....
..@..
command>Run ended.

O que este checkpoint estabelece

O texto do terminal agora se torna comando tipado antes de chegar a Game, e Game possui toda mutação das coordenadas. O renderer apenas observa o resultado na iteração seguinte.

Toda direção válida ainda funciona, salvo no limite retangular. O próximo checkpoint transforma os símbolos # e > originais em dados de mapa, colisão, contagem de turnos e vitória.

Exercício

Execute a entrada D, S, Q. O último frame jogável deve colocar @ na coluna 2, linha 2.

Código completo e verificação

As seções abaixo preservam o código qualificado completo, o diff exato desde o predecessor e a sequência de comandos usada pela CI. São evidência de apoio para a implementação explicada acima.

Código completo testado (3 arquivos)

longbow.toml

longbow.toml
[package]
name = "arrowfall-step-03"
version = "0.1.0"
kind = "program"
flecha = ">=0.3.0, <0.4.0"
[dependencies]

longbow.lock

longbow.lock
{
"schema": 1,
"root": "root",
"packages": [
{
"id": "root",
"identity": "local/arrowfall_step_03",
"source": {
"kind": "workspace",
"path": "."
},
"version": "0.1.0",
"flecha": ">=0.3.0, <0.4.0",
"dependencies": {
"std": "toolchain-std"
}
},
{
"id": "toolchain-std",
"identity": "flecha/std",
"source": {
"kind": "toolchain"
},
"dependencies": {}
}
]
}

src/main.fl

src/main.fl
module main;
@imports :: lib::std::text::{ascii, strings};
enum Command {
default Up,
Left,
Down,
Right,
Quit
}
error CommandError {
InvalidCommand
}
structure Position {
i32 x;
i32 y;
};
structure Game {
private Position player;
};
Position Position::at(i32 x, i32 y) {
return build Position {
self.x = x;
self.y = y;
};
}
Game Game::start() {
return build Game {
self.player = Position::at(1, 1);
};
}
void Game::apply(&self, Command command) {
if command == Command.Up and self->player.y > 0 {
self->player.y--;
} else if command == Command.Left and self->player.x > 0 {
self->player.x--;
} else if command == Command.Down and self->player.y < 2 {
self->player.y++;
} else if command == Command.Right and self->player.x < 4 {
self->player.x++;
}
}
Position Game::playerPosition(&const self) {
return self->player;
}
void main() {
state := Game::start();
bool running = true;
while running {
draw(&state);
command, commandError := read_command();
if commandError != none {
stdoutln("Unknown command; use W/A/S/D or Q.");
proceed;
}
if command == Command.Quit {
running = false;
} else {
state.apply(command);
}
}
stdoutln("Run ended.");
}
void draw(&const Game state) {
player := state->playerPosition();
stdoutln("ARROWFALL — open training floor");
loop i32 y = 0; y < 3; y++ {
loop i32 x = 0; x < 5; x++ {
if player.x == x and player.y == y {
stdout("@");
} else {
stdout(".");
}
}
stdoutln();
}
}
(Command, error) read_command() {
string line;
inputError := stdin("command>", &line);
if inputError != none {
return (Command.Quit, none);
}
value := strings::trim_ascii(line.view());
if value.len() != 1 {
return (none, CommandError.InvalidCommand);
}
const char KEY = ascii::to_ascii_lowercase(value[0].cast<char>);
if KEY == 'w' {
return (Command.Up, none);
}
if KEY == 'a' {
return (Command.Left, none);
}
if KEY == 's' {
return (Command.Down, none);
}
if KEY == 'd' {
return (Command.Right, none);
}
if KEY == 'q' {
return (Command.Quit, none);
}
return (none, CommandError.InvalidCommand);
}
Diff exato desde a versão anterior
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/README.md tutorial/checkpoints/03-movement/README.md
--- tutorial/checkpoints/02-state/README.md
+++ tutorial/checkpoints/03-movement/README.md
@@ -1 +1 @@
-# 02 — The picture becomes state
+# 03 — The player moves
@@ -5 +5,2 @@
-Checkpoint 01 prints a recognisable but hard-coded room.
+Checkpoint 02 derives the picture from a player position, but the position
+never changes.
@@ -9,2 +10,2 @@
-Changing a character in a string cannot express movement. The player needs a
-position owned by game state, and drawing must read that position.
+A game cannot be played without input. We first learn blocking stdin on an
+open floor, before walls add a second concern.
@@ -14,2 +15,2 @@
-`Game` owns a `Position`; the scene derives `@` from it and reports `(1, 1)`.
-The visible room is unchanged, but its source of truth is now data.
+W/A/S/D moves within a 5x3 training floor. Q exits. Input is normalised once,
+parsed into a `Command` enum, and applied through a mutable game reference.
@@ -28,2 +29 @@
-See `../../diffs/02-state.patch` for the exact predecessor diff. All current
-source is present in this directory.
+See `../../diffs/03-movement.patch` for the exact predecessor diff.
@@ -34,4 +34,4 @@
-longbow check tutorial/checkpoints/02-state
-longbow test tutorial/checkpoints/02-state
-longbow build tutorial/checkpoints/02-state
-longbow run tutorial/checkpoints/02-state < tutorial/checkpoints/02-state/tests/input.txt
+longbow check tutorial/checkpoints/03-movement
+longbow test tutorial/checkpoints/03-movement
+longbow build tutorial/checkpoints/03-movement
+longbow run tutorial/checkpoints/03-movement < tutorial/checkpoints/03-movement/tests/input.txt
@@ -43 +43,2 @@
-2. **The picture becomes state** — structs and methods own the player position.
+2. The picture becomes state — `Game` owns `Position`.
+3. **The player moves** — blocking stdin drives mutation.
@@ -47,2 +48,2 @@
-Set the start position to `(2, 1)`: both `@` and the reported coordinate must
-move. Restore `(1, 1)` afterward.
+Run the recorded input. The last frame before `Run ended.` must place `@` at
+column 2, row 2; an unknown key must not move it.
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/longbow.lock tutorial/checkpoints/03-movement/longbow.lock
--- tutorial/checkpoints/02-state/longbow.lock
+++ tutorial/checkpoints/03-movement/longbow.lock
@@ -7 +7 @@
- "identity": "local/arrowfall_step_02",
+ "identity": "local/arrowfall_step_03",
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/longbow.toml tutorial/checkpoints/03-movement/longbow.toml
--- tutorial/checkpoints/02-state/longbow.toml
+++ tutorial/checkpoints/03-movement/longbow.toml
@@ -2 +2 @@
-name = "arrowfall-step-02"
+name = "arrowfall-step-03"
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/src/main.fl tutorial/checkpoints/03-movement/src/main.fl
--- tutorial/checkpoints/02-state/src/main.fl
+++ tutorial/checkpoints/03-movement/src/main.fl
@@ -2,0 +3,14 @@
+@imports :: lib::std::text::{ascii, strings};
+
+enum Command {
+ default Up,
+ Left,
+ Down,
+ Right,
+ Quit
+}
+
+error CommandError {
+ InvalidCommand
+}
+
@@ -24,0 +39,12 @@
+void Game::apply(&self, Command command) {
+ if command == Command.Up and self->player.y > 0 {
+ self->player.y--;
+ } else if command == Command.Left and self->player.x > 0 {
+ self->player.x--;
+ } else if command == Command.Down and self->player.y < 2 {
+ self->player.y++;
+ } else if command == Command.Right and self->player.x < 4 {
+ self->player.x++;
+ }
+}
+
@@ -31,6 +57,15 @@
- player := state.playerPosition();
- stdoutln("ARROWFALL");
- stdoutln("#####");
- draw_row(player);
- stdoutln("#####");
- stdoutln("Player: (", player.x, ", ", player.y, ")");
+ bool running = true;
+ while running {
+ draw(&state);
+ command, commandError := read_command();
+ if commandError != none {
+ stdoutln("Unknown command; use W/A/S/D or Q.");
+ proceed;
+ }
+ if command == Command.Quit {
+ running = false;
+ } else {
+ state.apply(command);
+ }
+ }
+ stdoutln("Run ended.");
@@ -39,9 +74,10 @@
-void draw_row(Position player) {
- stdout("#");
- loop i32 x = 1; x < 4; x++ {
- if player.x == x and player.y == 1 {
- stdout("@");
- } else if x == 3 {
- stdout(">");
- } else {
- stdout(".");
+void draw(&const Game state) {
+ player := state->playerPosition();
+ stdoutln("ARROWFALL — open training floor");
+ loop i32 y = 0; y < 3; y++ {
+ loop i32 x = 0; x < 5; x++ {
+ if player.x == x and player.y == y {
+ stdout("@");
+ } else {
+ stdout(".");
+ }
@@ -48,0 +85,29 @@
+ stdoutln();
+ }
+}
+
+(Command, error) read_command() {
+ string line;
+ inputError := stdin("command>", &line);
+ if inputError != none {
+ return (Command.Quit, none);
+ }
+ value := strings::trim_ascii(line.view());
+ if value.len() != 1 {
+ return (none, CommandError.InvalidCommand);
+ }
+ const char KEY = ascii::to_ascii_lowercase(value[0].cast<char>);
+ if KEY == 'w' {
+ return (Command.Up, none);
+ }
+ if KEY == 'a' {
+ return (Command.Left, none);
+ }
+ if KEY == 's' {
+ return (Command.Down, none);
+ }
+ if KEY == 'd' {
+ return (Command.Right, none);
+ }
+ if KEY == 'q' {
+ return (Command.Quit, none);
@@ -50 +115 @@
- stdoutln("#");
+ return (none, CommandError.InvalidCommand);
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/tests/frame.svg tutorial/checkpoints/03-movement/tests/frame.svg
--- tutorial/checkpoints/02-state/tests/frame.svg
+++ tutorial/checkpoints/03-movement/tests/frame.svg
@@ -2,2 +2,2 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="360" height="143" viewBox="0 0 360 143" role="img">
- <title>02-state representative frame</title>
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="124" viewBox="0 0 360 124" role="img">
+ <title>03-movement representative frame</title>
@@ -6,5 +6,4 @@
- <text x="24" y="37" class="terminal">ARROWFALL</text>
- <text x="24" y="56" class="terminal">#####</text>
- <text x="24" y="75" class="terminal">#@.&gt;#</text>
- <text x="24" y="94" class="terminal">#####</text>
- <text x="24" y="113" class="terminal">Player: (1, 1)</text>
+ <text x="24" y="37" class="terminal">ARROWFALL — open training floor</text>
+ <text x="24" y="56" class="terminal">.....</text>
+ <text x="24" y="75" class="terminal">.....</text>
+ <text x="24" y="94" class="terminal">..@..</text>
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/tests/frame.txt tutorial/checkpoints/03-movement/tests/frame.txt
--- tutorial/checkpoints/02-state/tests/frame.txt
+++ tutorial/checkpoints/03-movement/tests/frame.txt
@@ -1,5 +1,4 @@
-ARROWFALL
-#####
-#@.>#
-#####
-Player: (1, 1)
+ARROWFALL — open training floor
+.....
+.....
+..@..
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/tests/input.txt tutorial/checkpoints/03-movement/tests/input.txt
--- tutorial/checkpoints/02-state/tests/input.txt
+++ tutorial/checkpoints/03-movement/tests/input.txt
@@ -0,0 +1,3 @@
+d
+s
+q
diff -rN -U0 --exclude target tutorial/checkpoints/02-state/tests/output.txt tutorial/checkpoints/03-movement/tests/output.txt
--- tutorial/checkpoints/02-state/tests/output.txt
+++ tutorial/checkpoints/03-movement/tests/output.txt
@@ -1,5 +1,13 @@
-ARROWFALL
-#####
-#@.>#
-#####
-Player: (1, 1)
+ARROWFALL — open training floor
+.....
+.@...
+.....
+command>ARROWFALL — open training floor
+.....
+..@..
+.....
+command>ARROWFALL — open training floor
+.....
+.....
+..@..
+command>Run ended.
Comandos de check, teste, build e execução
Comandos de verificação
longbow check tutorial/checkpoints/03-movement
longbow test tutorial/checkpoints/03-movement
longbow build tutorial/checkpoints/03-movement
longbow run tutorial/checkpoints/03-movement < tutorial/checkpoints/03-movement/tests/input.txt

Versões deste tutorial

  1. 1. Renderize a primeira sala
  2. 2. Represente o estado do jogo
  3. 3. Leia comandos de movimento

Próxima versão

O movimento é limitado apenas pela borda retangular. A próxima versão adiciona células do mapa, colisão com paredes e uma saída.

Adicione o mapa e a colisão

Referências

Fonte: commit Arrowfall 56bb003, verificado com Flecha oficial 0.3.0. Código, entrada, saída, diff e frame desta página são lidos desse snapshot versionado.