Capítulo 12 - Enums e o parser de comandos
Enums: tipos fechados e pequenos
Um enum (enumeração) é um tipo que só pode ter um conjunto predefinido de valores. É perfeito para coisas que não mudam: direções cardeais, dias da semana, fases da lua. Use enum para valores imutáveis:
// lib/direcao.dart
enum Direcao {
norte,
sul,
leste,
oeste,
}
Simples e poderoso:
void main() {
final direcao = Direcao.norte;
print(direcao);
}
Dart garante que direcao é uma das quatro opções. Nada de erros estranhos como direcao = 'septentriao' (typo). O compilador não permite valores inválidos.
Enums com membros (Dart 3+)
Em Dart 3, enum podem ter propriedades, construtores e métodos (use const no construtor):
// lib/direcao.dart
enum Direcao {
norte(simbolo: '↑', id: 'n'),
sul(simbolo: '↓', id: 's'),
leste(simbolo: '→', id: 'e'),
oeste(simbolo: '←', id: 'o');
final String simbolo;
final String id;
const Direcao({required this.simbolo, required this.id});
Direcao get oposta {
switch (this) {
case Direcao.norte:
return Direcao.sul;
case Direcao.sul:
return Direcao.norte;
case Direcao.leste:
return Direcao.oeste;
case Direcao.oeste:
return Direcao.leste;
}
}
static Direcao? deString(String s) {
switch (s.toLowerCase()) {
case 'n':
case 'norte':
return Direcao.norte;
case 's':
case 'sul':
return Direcao.sul;
case 'e':
case 'leste':
return Direcao.leste;
case 'o':
case 'oeste':
return Direcao.oeste;
default:
return null;
}
}
}
Uso:
void main() {
final dir = Direcao.norte;
print('Vou para ${dir.simbolo}');
final dirOposta = dir.oposta;
print('Oposta: ${dirOposta.simbolo}');
final dirDoInput = Direcao.deString('n');
print(dirDoInput);
}
Sealed classes: comandos com tipagem estrita
Agora vem o prato forte. sealed class são uma forma de Dart de dizer “esta hierarquia está fechada, apenas estas subclasses existem”. É perfeito para command (padrão de design), porque cada comando é diferente e tem argumentos diferentes. Dart também oferece extensions para adicionar métodos a tipos existentes sem herança e typedefs para nomear assinaturas de função complexas. Use sealed class para hierarquias fechadas:
// lib/comando_jogo.dart
import 'direcao.dart';
sealed class ComandoJogo {
const ComandoJogo();
String executar(); // Método abstrato
}
class ComandoMover extends ComandoJogo {
final Direcao direcao;
const ComandoMover(this.direcao);
@override
String executar() => 'Movendo para $direcao';
}
class ComandoAtacar extends ComandoJogo {
final String alvo;
const ComandoAtacar(this.alvo);
@override
String executar() => 'Atacando $alvo!';
}
class ComandoPegar extends ComandoJogo {
final String item;
const ComandoPegar(this.item);
@override
String executar() => 'Você pegou $item';
}
class ComandoInventario extends ComandoJogo {
const ComandoInventario();
@override
String executar() => 'Abrindo inventário...';
}
class ComandoOlhar extends ComandoJogo {
const ComandoOlhar();
@override
String executar() => 'Você observa ao seu redor...';
}
class ComandoStatus extends ComandoJogo {
const ComandoStatus();
@override
String executar() => 'Mostrando status...';
}
class ComandoAjuda extends ComandoJogo {
const ComandoAjuda();
@override
String executar() =>
'Comandos: norte/sul/leste/oeste, atacar, pegar, inv, '
'status, olhar, ajuda, sair';
}
class ComandoSair extends ComandoJogo {
const ComandoSair();
@override
String executar() => 'Até logo!';
}
class ComandoDesconhecido extends ComandoJogo {
final String entrada;
const ComandoDesconhecido(this.entrada);
@override
String executar() => 'Não entendo "$entrada". Tenta "ajuda".';
}
O parser: transformar texto em comandos
Agora o coração da magia: uma função que lê uma linha de texto e devolve um ComandoJogo tipado.
// lib/parser.dart
import 'comando_jogo.dart';
import 'direcao.dart';
ComandoJogo analisarLinha(String entrada) {
final linha = entrada.trim().toLowerCase();
if (linha.isEmpty) {
return const ComandoDesconhecido('(vazio)');
}
final palavras = linha.split(RegExp(r'\s+'));
final verbo = palavras[0];
final args = palavras.length > 1 ? palavras.sublist(1) : [];
switch (verbo) {
case 'n':
case 'norte':
return const ComandoMover(Direcao.norte);
case 's':
case 'sul':
return const ComandoMover(Direcao.sul);
case 'e':
case 'leste':
return const ComandoMover(Direcao.leste);
case 'o':
case 'oeste':
return const ComandoMover(Direcao.oeste);
case 'atacar':
case 'a':
if (args.isEmpty) {
return const ComandoDesconhecido('atacar o quê?');
}
final alvo = args.join(' ');
return ComandoAtacar(alvo);
case 'inv':
case 'inventario':
case 'i':
return const ComandoInventario();
case 'pegar':
case 'p':
if (args.isEmpty) {
return const ComandoDesconhecido('pegar o quê?');
}
final item = args.join(' ');
return ComandoPegar(item);
case 'status':
return const ComandoStatus();
case 'olhar':
case 'ver':
case 'l':
return const ComandoOlhar();
case 'ajuda':
case 'help':
case '?':
return const ComandoAjuda();
case 'sair':
case 'quit':
case 'exit':
return const ComandoSair();
default:
return ComandoDesconhecido(entrada);
}
}
Switch exaustivo com sealed classes
Isso é onde Dart brilha. Quando você faz um switch sobre uma sealed class, o compilador força-te a tratar todos os casos (switch exaustivo):
// lib/loop_jogo.dart
import 'comando_jogo.dart';
import 'parser.dart';
class LoopJogo {
void processarComando(ComandoJogo cmd) {
switch (cmd) {
case ComandoMover(:final direcao):
print('Movendo para $direcao...');
case ComandoAtacar(:final alvo):
print('Atacando $alvo...');
case ComandoPegar(:final item):
print('Pegando em $item...');
case ComandoInventario():
print('Mostrando inventário...');
case ComandoOlhar():
print('Observando...');
case ComandoStatus():
print('Mostrando status...');
case ComandoAjuda():
print('Mostrando ajuda...');
case ComandoSair():
print('Saindo do jogo...');
case ComandoDesconhecido(:final entrada):
print('Comando desconhecido: $entrada');
}
}
void mainLoop() {
while (true) {
print('> ');
final entrada = stdin.readLineSync() ?? '';
final cmd = analisarLinha(entrada);
processarComando(cmd);
}
}
}
Pattern matching com extração
Note a sintaxe especial: case ComandoAtacar(:final alvo). Isso é pattern matching. Extrai o campo alvo diretamente no case, tornando o código mais conciso:
// Sem pattern matching (mais verboso)
case ComandoAtacar cmd:
final alvo = cmd.alvo;
print('Atacando $alvo');
break;
// Com pattern matching (mais elegante)
case ComandoAtacar(:final alvo):
print('Atacando $alvo');
Integração completa: do input ao jogo
Veja como tudo flui. Note o import 'dart:io'; necessário para stdin e stdout:
// lib/main.dart
import 'dart:io';
import 'comando_jogo.dart';
import 'parser.dart';
import 'mundo_texto.dart';
import 'mundo_dados.dart';
void main() {
final mundo = criarMundoVila();
var salaAtual = 'praca';
print('=== MASMORRA ASCII ===');
print('Digite "ajuda" para ver comandos.\n');
while (true) {
final sala = mundo.obterSala(salaAtual);
print('\n[${sala!.nome}]');
print(sala.descricao);
if (sala.inimigoPresente != null) {
final ini = sala.inimigoPresente!;
print('Aqui está um ${ini.nome} (${ini.simbolo})!');
}
print('Saídas: ${sala.saidas.keys.join(", ")}');
stdout.write('> ');
final entrada = stdin.readLineSync() ?? '';
final cmd = analisarLinha(entrada);
switch (cmd) {
case ComandoMover(:final direcao):
final dirStr = direcao.id;
if (mundo.temSaida(salaAtual, dirStr)) {
salaAtual = mundo.irParaDirecao(salaAtual, dirStr)!;
print('Você se moveu para ${direcao.simbolo}');
} else {
print('Você não pode ir para $direcao.');
}
case ComandoAtacar(:final alvo):
final sala2 = mundo.obterSala(salaAtual);
if (sala2?.inimigoPresente != null) {
print('Você atacou ${sala2!.inimigoPresente!.nome}!');
} else {
print('Não há nada para atacar aqui.');
}
case ComandoPegar(:final item):
print('Você procurou por $item, mas não encontrou nada.');
case ComandoInventario():
print('Inventário vazio.');
case ComandoOlhar():
print('(você já vê isto)');
case ComandoAjuda():
print(cmd.executar());
case ComandoSair():
print('Até logo!');
return;
case ComandoDesconhecido(:final entrada):
print(cmd.executar());
}
}
}
Desafios da Masmorra
Desafio 12.1. Estender o enum Direcao (Direções diagonais). Adicione nordeste, noroeste, sudeste, sudoeste como novos membros ao enum. Cada um deve ter um símbolo apropriado ('↗', '↖', '↘', '↙') e id curto ('ne', 'nw', 'se', 'sw'). Atualize o método oposta() também.
Desafio 12.2. Novo comando ComandoEquipar. Crie uma sealed subclass ComandoEquipar com um campo arma: String. Adicione-a ao parser quando o jogador escreve “equipar espada” ou “eq lança”. Teste que o parser extrai o nome da arma corretamente.
Desafio 12.3. Sinonímia no parser (Abreviações). Adicione abreviações para direções: "u" (up) para norte, "d" (down) para sul, "l" para leste, "o" para oeste. Teste que analisarLinha("u") retorna ComandoMover(Direcao.norte) e analisarLinha("inv") retorna ComandoInventario().
Desafio 12.4. Sugestão de comando semelhante. Quando o jogador escreve um comando desconhecido, em vez de apenas retornar ComandoDesconhecido(entrada), verifique se é similar a um comando válido (ex.: “atlcar” ≈ “atacar”) e sugira: “Você quis dizer ‘atacar’? Tente novamente.”
Boss Final 12.5. Comando ComandoFala (Fala com argumento). Crie ComandoFala que aceita uma frase inteira (ex.: falar "Olá, mundo!"). Modifique o parser para capturar tudo após “falar” como argumento único (pode incluir múltiplas palavras e pontuação). Demonstre com uma frase completa.
Pergaminho do Capítulo
Neste capítulo você aprendeu:
enumdefinem tipos fechados com um conjunto finito de valores.enumcom membros (Dart 3+) podem ter propriedades, construtores e métodos.sealed classsão hierarquias fechadas, apenas as subclasses declaradas podem existir.- Parser transforma texto em objetos tipados, eliminando strings soltas e ambíguas.
- Pattern matching (
case ComandoAtacar(:final alvo)) extrai dadosinline. switchexaustivo força você a tratar todos os casos de umasealed class; o compilador avisa se você esquecer algum.
enum e sealed class são ferramentas poderosas para tornar o código mais seguro. Quando combinadas, garantem que cada comando é um tipo diferente (nenhuma confusão), cada comando tem os campos corretos (não há erros de acesso), e o código que despacha comandos trata todos os casos (compilador força isso).
Próximo Capítulo
No próximo capítulo, entramos na Parte III. O herói ganha ouro, armas e um inventário real. A masmorra começa a ter economia.