Pular para o conteúdo

4. Adicione o mapa e a colisão

Fonte atual: curriculum/arrowfall/checkpoints/04-map · Flecha 0.3.0 · 4/7.

O programa neste checkpoint

Até aqui: O checkpoint 03 aceita comandos e se move por um piso de treino aberto.

O checkpoint 03 comprova o loop de entrada, mas o piso aberto dá a todo comando a mesma consequência. A linguagem visual do checkpoint 01 ainda não tem autoridade: # não é sólido e > não encerra uma execução.

Este checkpoint transforma espaço em regra. Uma posição proposta é consultada no mapa fixo antes de Game confirmá-la, então parede, piso e saída podem produzir resultados diferentes.

Limitação atual

Toda direção funciona. Não há decisão espacial, colisão, vitória nem teste de regressão.

Próximo comportamento necessário: Usar # e > como regras: paredes rejeitam movimento sem gastar turno e entrar na saída vence.

Modelo e invariantes

Armazenar o mapa 9×7 em array fixo, expor um slice verificado e separar módulos de jogo, entrada, renderização e teste.

O array fixo dá ao mapa um tamanho conhecido em compilação. Um slice verificado expõe acesso limitado, enquanto módulos separados mantêm a E/S de terminal fora das regras.

Movimento agora tem duas fases. Position::moved calcula uma candidata somando um delta direcional; Game::apply inspeciona a candidata e só altera self.player depois que a célula a aceita. Movimento rejeitado não atualiza parte do estado.

O mapa é um array fixo 9×7 em ordem por linhas. tile_at o expõe como slice const verificado para tile_from_cells, que primeiro trata toda coordenada fora dos limites como parede e só então calcula y × WIDTH + x.

Tratar o exterior como # é um invariante que fecha a sala mesmo se um chamador propuser coordenada inválida. O acesso ao slice só ocorre depois das verificações com sinal, portanto o cast para usize não transforma coordenada negativa em índice.

Parede é no-op gratuito: o jogador não se move e turnCount não avança. Piso confirma a posição e custa um turno. A saída é verificada depois da confirmação, então frame vencedor e estado final concordam sobre a posição.

Game, input, render e test são separados porque a política do mapa pertence ao motor e o texto do terminal não. Inimigos e dano continuam fora desta etapa.

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.

  • Separar módulos game/input/render/main.
  • Adicionar mapa fixo e colisão.
  • Adicionar o primeiro teste de comportamento.

Movimento vira regra

Direction primeiro vira delta, e moved retorna uma nova Position em vez de alterar a original. Assim Game inspeciona next antes de decidir confirmá-la.

Trecho: src/game.fl:56–90. O arquivo completo aparece na seção de código abaixo.

src/game.fl:56-90 (excerpt)
Position Position::moved(&const self, Direction direction) {
Position delta = match direction {
Direction.Up => Position::at(0, -1),
Direction.Left => Position::at(-1, 0),
Direction.Down => Position::at(0, 1),
Direction.Right => Position::at(1, 0),
};
return Position::at(self->x + delta.x, self->y + delta.y);
}
Game Game::start() {
return build Game {
self.player = Position::at(1, 1);
self.turnCount = 0;
self.gameOutcome = Outcome.Playing;
};
}
Event Game::apply(&self, Command command) {
if command == Command.Quit {
self->gameOutcome = Outcome.Quit;
return Event.Quit;
}
next := self->player.moved(command_direction(command));
if tile_at(next) == '#' {
return Event.Wall;
}
self->player = next;
self->turnCount++;
if tile_at(self->player) == '>' {
self->gameOutcome = Outcome.Won;
return Event.Won;
}
return Event.Moved;
}

O ramo da parede retorna imediatamente, preservando player e turnCount. Somente a linha 83 atribui next à posição própria; incrementar o turno logo depois liga tempo a movimento bem-sucedido.

Testar > depois da atribuição faz da vitória uma propriedade de entrar na saída, não apenas olhar para ela. Quit é tratado separadamente e não precisa de direção.

Mapa fixo com acesso verificado

O literal do mapa contém exatamente WIDTH × HEIGHT células. tile_from_cells é a única conversão das coordenadas bidimensionais para o slice unidimensional verificado.

Trecho: src/game.fl:104–123. O arquivo completo aparece na seção de código abaixo.

src/game.fl:104-123 (excerpt)
export char tile_at(Position position) {
[63]char tiles = [
'#','#','#','#','#','#','#','#','#',
'#','.','.','.','.','.','.','.','#',
'#','.','.','#','#','#','.','.','#',
'#','.','.','.','.','.','.','.','#',
'#','.','.','#','#','#','.','.','#',
'#','>','.','.','.','.','.','.','#',
'#','#','#','#','#','#','#','#','#'
];
return tile_from_cells(tiles[:], position);
}
char tile_from_cells([]const char tiles, Position position) {
if position.x < 0 or position.x >= WIDTH or
position.y < 0 or position.y >= HEIGHT {
return '#';
}
return tiles[(position.y * WIDTH + position.x).cast<usize>];
}

O ramo de limites retorna # antes do acesso. Depois dele, y × WIDTH + x está em 0…62, então o cast e o slice referem-se a uma célula real.

Array fixo serve a um mapa cujas dimensões não mudam. Uma coleção expansível adicionaria alocação e tratamento de falhas sem atender a um requisito deste jogo.

Primeiro teste de regressão das regras

O primeiro teste de comportamento aciona Game diretamente. Ele não analisa teclas nem texto do terminal, portanto uma falha aponta para colisão, tempo ou vitória, não E/S.

Trecho: src/game_test.fl:1–21. O arquivo completo aparece na seção de código abaixo.

src/game_test.fl:1-21 (excerpt)
module game_test;
@imports
:: root::game
:: lib::std::tests::asserts
:: lib::std::tests::testing;
test walls_are_free_and_the_exit_wins(&testing::UnitTest t) {
state := game::Game::start();
wall := state.apply(game::Command.MoveUp);
state.apply(game::Command.MoveDown);
state.apply(game::Command.MoveDown);
state.apply(game::Command.MoveDown);
won := state.apply(game::Command.MoveDown);
asserts::is_true(
t,
wall == game::Event.Wall and won == game::Event.Won and
state.turn() == 4 and state.outcome() == game::Outcome.Won,
"walls are free while entering the exit wins"
);
}

MoveUp atinge a parede externa e não deve contar. Quatro MoveDown entram na saída; a asserção verifica eventos retornados, turno final e resultado final juntos.

O teste fixa o invariante da parede gratuita antes de um inimigo tornar o custo de turno mais importante no próximo checkpoint.

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/04-map < tutorial/checkpoints/04-map/tests/input.txt
Entrada padrão registrada
Entrada padrão
w
s
s
s
s

A entrada registrada começa com W. O diagnóstico relata parede e o frame seguinte ainda mostra Turn: 0. Quatro comandos S percorrem o corredor esquerdo e entram em > no turno 4.

Frame de terminal produzido pela versão 4: Adicione o mapa e a colisão
Frame representativo da execução registrada do checkpoint 4.

O frame representativo mostra o jogador na coordenada da saída. Ele é renderizado do estado confirmado, enquanto o evento de vitória explica por que o loop para.

Transcript acessível do frame
Transcript do frame
#########
#.......#
#..###..#
#.......#
#..###..#
#@......#
#########
Turn: 4
Stdout registrada completa
Stdout esperada
#########
#@......#
#..###..#
#.......#
#..###..#
#>......#
#########
Turn: 0
command>
A wall blocks your way. No turn spent.
#########
#@......#
#..###..#
#.......#
#..###..#
#>......#
#########
Turn: 0
command>
You move.
#########
#.......#
#@.###..#
#.......#
#..###..#
#>......#
#########
Turn: 1
command>
You move.
#########
#.......#
#..###..#
#@......#
#..###..#
#>......#
#########
Turn: 2
command>
You move.
#########
#.......#
#..###..#
#.......#
#@.###..#
#>......#
#########
Turn: 3
command>
You reach the exit.
#########
#.......#
#..###..#
#.......#
#..###..#
#@......#
#########
Turn: 4
You escaped Arrowfall!

O que este checkpoint estabelece

O movimento agora pede permissão ao mapa, e o motor possui os invariantes de colisão, turnos decorridos e vitória. A E/S do terminal permanece fora dessas regras.

A sala ainda é inofensiva: nada reage depois de um movimento. O próximo checkpoint dá uma resposta inimiga a cada ação efetiva e torna o tempo observável.

Exercício

O W inicial deve relatar parede enquanto Turn continua 0; quatro S devem vencer no turno 4.

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 (7 arquivos)

longbow.toml

longbow.toml
[package]
name = "arrowfall-step-04"
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_04",
"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/game.fl

src/game.fl
module game;
const i32 WIDTH = 9;
const i32 HEIGHT = 7;
export enum Direction {
default Up,
Left,
Down,
Right
}
export enum Command {
default MoveUp,
MoveLeft,
MoveDown,
MoveRight,
Quit
}
export enum Outcome {
default Playing,
Won,
Quit
}
export enum Event {
default Moved,
Wall,
Won,
Quit
}
export structure Position {
i32 x;
i32 y;
};
export structure Game {
private Position player;
private u32 turnCount;
private Outcome gameOutcome;
};
Position Position::at(i32 x, i32 y) {
return build Position {
self.x = x;
self.y = y;
};
}
bool Position::equals(&const self, &const Position other) {
return self->x == other->x and self->y == other->y;
}
Position Position::moved(&const self, Direction direction) {
Position delta = match direction {
Direction.Up => Position::at(0, -1),
Direction.Left => Position::at(-1, 0),
Direction.Down => Position::at(0, 1),
Direction.Right => Position::at(1, 0),
};
return Position::at(self->x + delta.x, self->y + delta.y);
}
Game Game::start() {
return build Game {
self.player = Position::at(1, 1);
self.turnCount = 0;
self.gameOutcome = Outcome.Playing;
};
}
Event Game::apply(&self, Command command) {
if command == Command.Quit {
self->gameOutcome = Outcome.Quit;
return Event.Quit;
}
next := self->player.moved(command_direction(command));
if tile_at(next) == '#' {
return Event.Wall;
}
self->player = next;
self->turnCount++;
if tile_at(self->player) == '>' {
self->gameOutcome = Outcome.Won;
return Event.Won;
}
return Event.Moved;
}
Position Game::playerPosition(&const self) {
return self->player;
}
u32 Game::turn(&const self) {
return self->turnCount;
}
Outcome Game::outcome(&const self) {
return self->gameOutcome;
}
export char tile_at(Position position) {
[63]char tiles = [
'#','#','#','#','#','#','#','#','#',
'#','.','.','.','.','.','.','.','#',
'#','.','.','#','#','#','.','.','#',
'#','.','.','.','.','.','.','.','#',
'#','.','.','#','#','#','.','.','#',
'#','>','.','.','.','.','.','.','#',
'#','#','#','#','#','#','#','#','#'
];
return tile_from_cells(tiles[:], position);
}
char tile_from_cells([]const char tiles, Position position) {
if position.x < 0 or position.x >= WIDTH or
position.y < 0 or position.y >= HEIGHT {
return '#';
}
return tiles[(position.y * WIDTH + position.x).cast<usize>];
}
Direction command_direction(Command command) {
return match command {
Command.MoveUp => Direction.Up,
Command.MoveLeft => Direction.Left,
Command.MoveDown => Direction.Down,
Command.MoveRight => Direction.Right,
Command.Quit => Direction.Up,
};
}

src/game_test.fl

src/game_test.fl
module game_test;
@imports
:: root::game
:: lib::std::tests::asserts
:: lib::std::tests::testing;
test walls_are_free_and_the_exit_wins(&testing::UnitTest t) {
state := game::Game::start();
wall := state.apply(game::Command.MoveUp);
state.apply(game::Command.MoveDown);
state.apply(game::Command.MoveDown);
state.apply(game::Command.MoveDown);
won := state.apply(game::Command.MoveDown);
asserts::is_true(
t,
wall == game::Event.Wall and won == game::Event.Won and
state.turn() == 4 and state.outcome() == game::Outcome.Won,
"walls are free while entering the exit wins"
);
}

src/input.fl

src/input.fl
module input;
@imports :: root::game :: lib::std::text::{ascii, strings};
export error CommandError {
InvalidCommand
}
export (game::Command, error) read_command() {
string line;
inputError := stdin("command>", &line);
if inputError != none {
return (game::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 (game::Command.MoveUp, none);
}
if KEY == 'a' {
return (game::Command.MoveLeft, none);
}
if KEY == 's' {
return (game::Command.MoveDown, none);
}
if KEY == 'd' {
return (game::Command.MoveRight, none);
}
if KEY == 'q' {
return (game::Command.Quit, none);
}
return (none, CommandError.InvalidCommand);
}

src/main.fl

src/main.fl
module main;
@imports :: root::game :: root::input :: root::render;
void main() {
state := game::Game::start();
while state.outcome() == game::Outcome.Playing {
frame, frameError := render::draw(&state);
if frameError != none {
stdoutln("Could not render the room.");
return;
}
stdoutln(frame);
command, commandError := input::read_command();
stdoutln();
if commandError != none {
stdoutln("Unknown command. No turn spent.");
proceed;
}
event := state.apply(command);
stdoutln(render::event_message(event));
}
finalFrame, finalError := render::draw(&state);
if finalError == none {
stdoutln(finalFrame);
}
if state.outcome() == game::Outcome.Won {
stdoutln("You escaped Arrowfall!");
}
}

src/render.fl

src/render.fl
module render;
@imports :: root::game :: lib::std::text::strings;
export (string, error) draw(&const game::Game state) {
canvas, allocationError := strings::StringBuilder::withCapacity(112);
if allocationError != none {
return (none, allocationError);
}
player := state->playerPosition();
loop i32 y = 0; y < 7; y++ {
loop i32 x = 0; x < 9; x++ {
position := game::Position::at(x, y);
char tile = game::tile_at(position);
if position.equals(&player) {
tile = '@';
}
if canvas.appendChar(tile) != none {
return (none, AllocationError.OutOfMemory);
}
}
if canvas.appendChar('\n') != none {
return (none, AllocationError.OutOfMemory);
}
}
if canvas.append("Turn: ") != none or
canvas.appendInteger(state->turn()) != none {
return (none, AllocationError.OutOfMemory);
}
return canvas.toString();
}
export stringv event_message(game::Event event) {
if event == game::Event.Wall {
return "A wall blocks your way. No turn spent.";
}
if event == game::Event.Won {
return "You reach the exit.";
}
if event == game::Event.Quit {
return "You leave the room.";
}
return "You move.";
}
Diff exato desde a versão anterior
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/README.md tutorial/checkpoints/04-map/README.md
--- tutorial/checkpoints/03-movement/README.md
+++ tutorial/checkpoints/04-map/README.md
@@ -1 +1 @@
-# 03 — The player moves
+# 04 — Walls make movement meaningful
@@ -5,2 +5,2 @@
-Checkpoint 02 derives the picture from a player position, but the position
-never changes.
+Checkpoint 03 moves on an open rectangle. Every in-bounds direction succeeds,
+so movement has no spatial decision.
@@ -10,2 +10,2 @@
-A game cannot be played without input. We first learn blocking stdin on an
-open floor, before walls add a second concern.
+The symbols introduced in checkpoint 01 should become rules. A real room needs
+solid walls, a reachable exit, and feedback when an action cannot happen.
@@ -15,2 +15,4 @@
-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.
+The fixed 9x7 map is stored as a checked slice over a fixed array. Walls reject
+movement without spending a turn; entering `>` wins. Engine, input, and render
+modules now separate rules from terminal I/O, and the first unit test protects
+collision and victory.
@@ -22,0 +25,3 @@
+- `src/game.fl`
+- `src/game_test.fl`
+- `src/input.fl`
@@ -23,0 +29 @@
+- `src/render.fl`
@@ -29 +35 @@
-See `../../diffs/03-movement.patch` for the exact predecessor diff.
+Exact changes: `../../diffs/04-map.patch`.
@@ -34,4 +40,4 @@
-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
+longbow check tutorial/checkpoints/04-map
+longbow test tutorial/checkpoints/04-map
+longbow build tutorial/checkpoints/04-map
+longbow run tutorial/checkpoints/04-map < tutorial/checkpoints/04-map/tests/input.txt
@@ -42,3 +48,4 @@
-1. A room appears — static terminal scene.
-2. The picture becomes state — `Game` owns `Position`.
-3. **The player moves** — blocking stdin drives mutation.
+1. A room appears — static scene.
+2. The picture becomes state — structs own position.
+3. The player moves — stdin drives an open floor.
+4. **Walls make movement meaningful** — map, collision, exit, modules, test.
@@ -48,2 +55,2 @@
-Run the recorded input. The last frame before `Run ended.` must place `@` at
-column 2, row 2; an unknown key must not move it.
+The recorded leading `W` must print the wall message while the next frame still
+shows `Turn: 0`; four `S` moves must then win on turn 4.
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/longbow.lock tutorial/checkpoints/04-map/longbow.lock
--- tutorial/checkpoints/03-movement/longbow.lock
+++ tutorial/checkpoints/04-map/longbow.lock
@@ -7 +7 @@
- "identity": "local/arrowfall_step_03",
+ "identity": "local/arrowfall_step_04",
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/longbow.toml tutorial/checkpoints/04-map/longbow.toml
--- tutorial/checkpoints/03-movement/longbow.toml
+++ tutorial/checkpoints/04-map/longbow.toml
@@ -2 +2 @@
-name = "arrowfall-step-03"
+name = "arrowfall-step-04"
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/src/game.fl tutorial/checkpoints/04-map/src/game.fl
--- tutorial/checkpoints/03-movement/src/game.fl
+++ tutorial/checkpoints/04-map/src/game.fl
@@ -0,0 +1,133 @@
+module game;
+
+const i32 WIDTH = 9;
+const i32 HEIGHT = 7;
+
+export enum Direction {
+ default Up,
+ Left,
+ Down,
+ Right
+}
+
+export enum Command {
+ default MoveUp,
+ MoveLeft,
+ MoveDown,
+ MoveRight,
+ Quit
+}
+
+export enum Outcome {
+ default Playing,
+ Won,
+ Quit
+}
+
+export enum Event {
+ default Moved,
+ Wall,
+ Won,
+ Quit
+}
+
+export structure Position {
+ i32 x;
+ i32 y;
+};
+
+export structure Game {
+ private Position player;
+ private u32 turnCount;
+ private Outcome gameOutcome;
+};
+
+Position Position::at(i32 x, i32 y) {
+ return build Position {
+ self.x = x;
+ self.y = y;
+ };
+}
+
+bool Position::equals(&const self, &const Position other) {
+ return self->x == other->x and self->y == other->y;
+}
+
+Position Position::moved(&const self, Direction direction) {
+ Position delta = match direction {
+ Direction.Up => Position::at(0, -1),
+ Direction.Left => Position::at(-1, 0),
+ Direction.Down => Position::at(0, 1),
+ Direction.Right => Position::at(1, 0),
+ };
+ return Position::at(self->x + delta.x, self->y + delta.y);
+}
+
+Game Game::start() {
+ return build Game {
+ self.player = Position::at(1, 1);
+ self.turnCount = 0;
+ self.gameOutcome = Outcome.Playing;
+ };
+}
+
+Event Game::apply(&self, Command command) {
+ if command == Command.Quit {
+ self->gameOutcome = Outcome.Quit;
+ return Event.Quit;
+ }
+ next := self->player.moved(command_direction(command));
+ if tile_at(next) == '#' {
+ return Event.Wall;
+ }
+ self->player = next;
+ self->turnCount++;
+ if tile_at(self->player) == '>' {
+ self->gameOutcome = Outcome.Won;
+ return Event.Won;
+ }
+ return Event.Moved;
+}
+
+Position Game::playerPosition(&const self) {
+ return self->player;
+}
+
+u32 Game::turn(&const self) {
+ return self->turnCount;
+}
+
+Outcome Game::outcome(&const self) {
+ return self->gameOutcome;
+}
+
+export char tile_at(Position position) {
+ [63]char tiles = [
+ '#','#','#','#','#','#','#','#','#',
+ '#','.','.','.','.','.','.','.','#',
+ '#','.','.','#','#','#','.','.','#',
+ '#','.','.','.','.','.','.','.','#',
+ '#','.','.','#','#','#','.','.','#',
+ '#','>','.','.','.','.','.','.','#',
+ '#','#','#','#','#','#','#','#','#'
+ ];
+ return tile_from_cells(tiles[:], position);
+}
+
+char tile_from_cells([]const char tiles, Position position) {
+ if position.x < 0 or position.x >= WIDTH or
+ position.y < 0 or position.y >= HEIGHT {
+ return '#';
+ }
+ return tiles[(position.y * WIDTH + position.x).cast<usize>];
+}
+
+Direction command_direction(Command command) {
+ return match command {
+ Command.MoveUp => Direction.Up,
+ Command.MoveLeft => Direction.Left,
+ Command.MoveDown => Direction.Down,
+ Command.MoveRight => Direction.Right,
+ Command.Quit => Direction.Up,
+ };
+}
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/src/game_test.fl tutorial/checkpoints/04-map/src/game_test.fl
--- tutorial/checkpoints/03-movement/src/game_test.fl
+++ tutorial/checkpoints/04-map/src/game_test.fl
@@ -0,0 +1,21 @@
+module game_test;
+
+@imports
+ :: root::game
+ :: lib::std::tests::asserts
+ :: lib::std::tests::testing;
+
+test walls_are_free_and_the_exit_wins(&testing::UnitTest t) {
+ state := game::Game::start();
+ wall := state.apply(game::Command.MoveUp);
+ state.apply(game::Command.MoveDown);
+ state.apply(game::Command.MoveDown);
+ state.apply(game::Command.MoveDown);
+ won := state.apply(game::Command.MoveDown);
+ asserts::is_true(
+ t,
+ wall == game::Event.Wall and won == game::Event.Won and
+ state.turn() == 4 and state.outcome() == game::Outcome.Won,
+ "walls are free while entering the exit wins"
+ );
+}
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/src/input.fl tutorial/checkpoints/04-map/src/input.fl
--- tutorial/checkpoints/03-movement/src/input.fl
+++ tutorial/checkpoints/04-map/src/input.fl
@@ -0,0 +1,36 @@
+module input;
+
+@imports :: root::game :: lib::std::text::{ascii, strings};
+
+export error CommandError {
+ InvalidCommand
+}
+
+export (game::Command, error) read_command() {
+ string line;
+ inputError := stdin("command>", &line);
+ if inputError != none {
+ return (game::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 (game::Command.MoveUp, none);
+ }
+ if KEY == 'a' {
+ return (game::Command.MoveLeft, none);
+ }
+ if KEY == 's' {
+ return (game::Command.MoveDown, none);
+ }
+ if KEY == 'd' {
+ return (game::Command.MoveRight, none);
+ }
+ if KEY == 'q' {
+ return (game::Command.Quit, none);
+ }
+ return (none, CommandError.InvalidCommand);
+}
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/src/main.fl tutorial/checkpoints/04-map/src/main.fl
--- tutorial/checkpoints/03-movement/src/main.fl
+++ tutorial/checkpoints/04-map/src/main.fl
@@ -3,51 +3 @@
-@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;
-}
+@imports :: root::game :: root::input :: root::render;
@@ -56,5 +6,10 @@
- state := Game::start();
- bool running = true;
- while running {
- draw(&state);
- command, commandError := read_command();
+ state := game::Game::start();
+ while state.outcome() == game::Outcome.Playing {
+ frame, frameError := render::draw(&state);
+ if frameError != none {
+ stdoutln("Could not render the room.");
+ return;
+ }
+ stdoutln(frame);
+ command, commandError := input::read_command();
+ stdoutln();
@@ -62 +17 @@
- stdoutln("Unknown command; use W/A/S/D or Q.");
+ stdoutln("Unknown command. No turn spent.");
@@ -65,43 +20,2 @@
- 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);
+ event := state.apply(command);
+ stdoutln(render::event_message(event));
@@ -109,2 +23,3 @@
- if KEY == 'd' {
- return (Command.Right, none);
+ finalFrame, finalError := render::draw(&state);
+ if finalError == none {
+ stdoutln(finalFrame);
@@ -112,2 +27,2 @@
- if KEY == 'q' {
- return (Command.Quit, none);
+ if state.outcome() == game::Outcome.Won {
+ stdoutln("You escaped Arrowfall!");
@@ -115 +29,0 @@
- return (none, CommandError.InvalidCommand);
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/src/render.fl tutorial/checkpoints/04-map/src/render.fl
--- tutorial/checkpoints/03-movement/src/render.fl
+++ tutorial/checkpoints/04-map/src/render.fl
@@ -0,0 +1,44 @@
+module render;
+
+@imports :: root::game :: lib::std::text::strings;
+
+export (string, error) draw(&const game::Game state) {
+ canvas, allocationError := strings::StringBuilder::withCapacity(112);
+ if allocationError != none {
+ return (none, allocationError);
+ }
+ player := state->playerPosition();
+ loop i32 y = 0; y < 7; y++ {
+ loop i32 x = 0; x < 9; x++ {
+ position := game::Position::at(x, y);
+ char tile = game::tile_at(position);
+ if position.equals(&player) {
+ tile = '@';
+ }
+ if canvas.appendChar(tile) != none {
+ return (none, AllocationError.OutOfMemory);
+ }
+ }
+ if canvas.appendChar('\n') != none {
+ return (none, AllocationError.OutOfMemory);
+ }
+ }
+ if canvas.append("Turn: ") != none or
+ canvas.appendInteger(state->turn()) != none {
+ return (none, AllocationError.OutOfMemory);
+ }
+ return canvas.toString();
+}
+
+export stringv event_message(game::Event event) {
+ if event == game::Event.Wall {
+ return "A wall blocks your way. No turn spent.";
+ }
+ if event == game::Event.Won {
+ return "You reach the exit.";
+ }
+ if event == game::Event.Quit {
+ return "You leave the room.";
+ }
+ return "You move.";
+}
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/tests/frame.svg tutorial/checkpoints/04-map/tests/frame.svg
--- tutorial/checkpoints/03-movement/tests/frame.svg
+++ tutorial/checkpoints/04-map/tests/frame.svg
@@ -2,2 +2,2 @@
-<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>
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200" role="img">
+ <title>04-map representative frame</title>
@@ -6,4 +6,8 @@
- <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>
+ <text x="24" y="37" class="terminal">#########</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">#..###..#</text>
+ <text x="24" y="132" class="terminal">#@......#</text>
+ <text x="24" y="151" class="terminal">#########</text>
+ <text x="24" y="170" class="terminal">Turn: 4</text>
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/tests/frame.txt tutorial/checkpoints/04-map/tests/frame.txt
--- tutorial/checkpoints/03-movement/tests/frame.txt
+++ tutorial/checkpoints/04-map/tests/frame.txt
@@ -1,4 +1,8 @@
-ARROWFALL — open training floor
-.....
-.....
-..@..
+#########
+#.......#
+#..###..#
+#.......#
+#..###..#
+#@......#
+#########
+Turn: 4
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/tests/input.txt tutorial/checkpoints/04-map/tests/input.txt
--- tutorial/checkpoints/03-movement/tests/input.txt
+++ tutorial/checkpoints/04-map/tests/input.txt
@@ -1 +1,4 @@
-d
+w
+s
+s
+s
@@ -3 +5,0 @@
-q
diff -rN -U0 --exclude target tutorial/checkpoints/03-movement/tests/output.txt tutorial/checkpoints/04-map/tests/output.txt
--- tutorial/checkpoints/03-movement/tests/output.txt
+++ tutorial/checkpoints/04-map/tests/output.txt
@@ -1,13 +1,59 @@
-ARROWFALL — open training floor
-.....
-.@...
-.....
-command>ARROWFALL — open training floor
-.....
-..@..
-.....
-command>ARROWFALL — open training floor
-.....
-.....
-..@..
-command>Run ended.
+#########
+#@......#
+#..###..#
+#.......#
+#..###..#
+#>......#
+#########
+Turn: 0
+command>
+A wall blocks your way. No turn spent.
+#########
+#@......#
+#..###..#
+#.......#
+#..###..#
+#>......#
+#########
+Turn: 0
+command>
+You move.
+#########
+#.......#
+#@.###..#
+#.......#
+#..###..#
+#>......#
+#########
+Turn: 1
+command>
+You move.
+#########
+#.......#
+#..###..#
+#@......#
+#..###..#
+#>......#
+#########
+Turn: 2
+command>
+You move.
+#########
+#.......#
+#..###..#
+#.......#
+#@.###..#
+#>......#
+#########
+Turn: 3
+command>
+You reach the exit.
+#########
+#.......#
+#..###..#
+#.......#
+#..###..#
+#@......#
+#########
+Turn: 4
+You escaped Arrowfall!
Comandos de check, teste, build e execução
Comandos de verificação
longbow check tutorial/checkpoints/04-map
longbow test tutorial/checkpoints/04-map
longbow build tutorial/checkpoints/04-map
longbow run tutorial/checkpoints/04-map < tutorial/checkpoints/04-map/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
  4. 4. Adicione o mapa e a colisão

Próxima versão

Colisão e vitória funcionam, mas nada responde ao movimento do jogador. A próxima versão adiciona resultados de turno e um goblin.

Adicione turnos e o goblin

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.