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.
@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.
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.
(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.
longbow run tutorial/checkpoints/03-movement < tutorial/checkpoints/03-movement/tests/input.txtEntrada padrão registrada
dsqA 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.
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
ARROWFALL — open training floor............@..Stdout registrada completa
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
[package]name = "arrowfall-step-03"version = "0.1.0"kind = "program"flecha = ">=0.3.0, <0.4.0"
[dependencies]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
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">#@.>#</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+qdiff -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
longbow check tutorial/checkpoints/03-movementlongbow test tutorial/checkpoints/03-movementlongbow build tutorial/checkpoints/03-movementlongbow run tutorial/checkpoints/03-movement < tutorial/checkpoints/03-movement/tests/input.txtVersões deste tutorial
- 1. Renderize a primeira sala
- 2. Represente o estado do jogo
- 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.
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.