Capítulo 34 - Strategy e Command: Inimigos que Pensam
Neste capítulo você vai implementar dois padrões de design essenciais: Strategy para comportamentos intercambiáveis e Command para ações rastreáveis. Juntos, eles transformam inimigos estáticos em adversários inteligentes.
O Problema: Comportamento Rígido
Até agora, em um combate típico, todos os inimigos agem da mesma forma:
void atacarInimigo(Inimigo inimigo) {
int dano = calcularDano(heroi.arma, inimigo.defesa);
inimigo.hp -= dano;
// Inimigo sempre ataca de volta do mesmo jeito
int danoRetorno = calcularDano(inimigo.arma, heroi.defesa);
heroi.hp -= danoRetorno;
}
Todos fazem a mesma coisa. Um zumbi deveria andar aleatoriamente. Um lobo deveria perseguir você. Um dragão deveria mudar de tática conforme a luta avança. Sem um padrão, você acaba com centenas de if/else aninhados, e cada novo tipo de inimigo quebra a lógica anterior.
Strategy: Uma Mente para Cada Inimigo
O padrão Strategy encapsula um conjunto de algoritmos e os torna intercambiáveis. É como em Final Fantasy VII: muda de matéria, muda de poder. A classe do personagem fica a mesma, mas o comportamento muda.
Defina uma interface abstrata:
abstract class EstrategiaIa {
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa);
}
Uma estratégia recebe o inimigo, o alvo e o mapa, e retorna uma Acao (que veremos em breve). Agora implemente estratégias concretas.
IAAgressiva: Ataque Direto
Um lobo não hesita. Vê você, vai atrás.
A estratégia agressiva é simples mas efetiva: se o alvo está longe, ande em sua direção. Se está perto (1 tile), ataque. Se você não conseguir traçar uma linha reta até o alvo (há paredes no caminho), apenas ande aleatoriamente. Observe como a decisão retorna uma Acao (que veremos em breve); o padrão Command encapsula o que fazer.
class IAAgressiva implements EstrategiaIa {
@override
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa) {
int distancia = mapa.distancia(self.pos, alvo.pos);
if (distancia <= 1) {
return AcaoAtacar(self, alvo);
} else if (self.temLinhaDeVisao(alvo, mapa)) {
var proxPasso = mapa.caminhoParaPos(self.pos, alvo.pos);
return AcaoMover(self, proxPasso);
} else {
return AcaoMoverAleatorio(self, mapa);
}
}
}
Nota: O método
mapa.distancia()calcula a distância Manhattan entre duas posições no mapa, utilizada para determinar se um inimigo está próximo o suficiente para atacar (implementado emmapa.dart, Capítulo 12). O métodoself.temLinhaDeVisao()verifica se há linha de visão direta entre o inimigo e o alvo (sem paredes bloqueando), usando o algoritmo de Bresenham (implementado emcampo_visao.dart, Capítulo 19). O métodomapa.caminhoParaPos()retorna o próximo passo do caminho mais curto entre duas posições, usando busca em largura ou A* (implementado empathing.dart, Capítulo 20).
IACovardia: Retirada Estratégica
Um goblin covarde foge quando ferido:
Observe que IACovardia recebe um limiteHP (padrão 30%). Se o HP atual cai abaixo desse percentual, muda para fuga. Caso contrário, atua como agressivo. Isso permite criar variações: um goblin que foge em 30%, um orc que só foge em 10%, um dragão que nunca foge. Tudo com a mesma classe, apenas parâmetros diferentes.
class IACovardia implements EstrategiaIa {
final int limiteHP;
IACovardia({this.limiteHP = 30});
@override
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa) {
if (self.hp < (self.hpMax * limiteHP / 100)) {
var fuga = mapa.caminhoParaPos(
self.pos,
alvo.pos,
inverso: true
);
return AcaoMover(self, fuga);
}
int distancia = mapa.distancia(self.pos, alvo.pos);
if (distancia <= 1) {
return AcaoAtacar(self, alvo);
}
return AcaoMover(self, mapa.caminhoParaPos(self.pos, alvo.pos));
}
}
Nota: O método
mapa.caminhoParaPos()aceita um parâmetro nomeadoinverso: trueque inverte o algoritmo de pathfinding, retornando um passo na direção oposta ao alvo, útil para implementar comportamento de fuga (implementado empathing.dart, Capítulo 20).
IAPatrulha: Vigilância Constante
Um esqueleto segue uma rota. Se você aparecer no seu campo de visão, passa a atacar:
Patrulha é mais sofisticada. O inimigo caminha por uma rota predefinida. Se detecta o alvo, passa para combate direto. Note o emCombate: uma vez em combate, o inimigo permanece assim até a morte ou vitória. Sem isso, um inimigo poderia ficar alternando entre patrulha e perseguição infinitamente. Esse flag garante coerência no comportamento.
class IAPatrulha implements EstrategiaIa {
final List<Pos> rota;
int indiceRota = 0;
bool emCombate = false;
IAPatrulha(this.rota);
@override
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa) {
if (self.temLinhaDeVisao(alvo, mapa)) {
emCombate = true;
}
if (emCombate) {
int distancia = mapa.distancia(self.pos, alvo.pos);
if (distancia <= 1) {
return AcaoAtacar(self, alvo);
}
return AcaoMover(self, mapa.caminhoParaPos(self.pos, alvo.pos));
}
var proxAlvo = rota[indiceRota];
if (self.pos == proxAlvo) {
indiceRota = (indiceRota + 1) % rota.length;
proxAlvo = rota[indiceRota];
}
return AcaoMover(self, mapa.caminhoParaPos(self.pos, proxAlvo));
}
}
Nota: O método
self.temLinhaDeVisao()é usado aqui para detectar automaticamente quando um inimigo em patrulha avista o jogador, disparando o início do combate (implementado emcampo_visao.dart, Capítulo 19). Uma vez queemCombateé ativado, permanece assim até a morte, garantindo coerência no comportamento sem alternância errática entre patrulha e perseguição.
IAPassiva: Defesa Apenas
Um zumbi anda aleatoriamente e só ataca se for atacado primeiro:
Passiva é o oposto de agressiva. O inimigo ignora você até ser atacado. Depois, reage. Isso simula zumbis que estão dormindo ou distraídos, ou animais selvagens que fogem de humanos mas atacam se provocados. Note como foiAtacada é um flag que nunca volta a falso: uma vez despertado, o zumbi permanece hostil.
class IAPassiva implements EstrategiaIa {
bool foiAtacada = false;
@override
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa) {
if (!foiAtacada) {
return AcaoMoverAleatorio(self, mapa);
}
int distancia = mapa.distancia(self.pos, alvo.pos);
if (distancia <= 1) {
return AcaoAtacar(self, alvo);
}
return AcaoMover(self, mapa.caminhoParaPos(self.pos, alvo.pos));
}
}
Nota: A estratégia passiva depende do seu código de combate (não mostrado) para atualizar o flag
foiAtacada = truequando este inimigo sofre dano. Depois disso, funciona como a estratégia agressiva. Este é um exemplo de comunicação entre a IA e o sistema de combate através de estado mutável.
Integrando Strategy no Inimigo
Modifique a classe Inimigo para usar uma estratégia.
Quando você integra Strategy:
class Inimigo {
late Pos pos;
late String nome;
late int hp;
late int hpMax;
late Arma arma;
late Defesa defesa;
final EstrategiaIa estrategia;
Inimigo({
required this.nome,
required this.hp,
required this.arma,
required this.defesa,
required this.estrategia,
}) : hpMax = hp;
Acao obterProximaAcao(Jogador alvo, MapaMasmorra mapa) {
return estrategia.decidir(this, alvo, mapa);
}
}
Command: Ações Reversíveis
Strategy diz o quê fazer. Command diz como fazer de forma reversível e histórica. Command encapsula uma solicitação como um objeto, permitindo desfazer, refazer e manter histórico.
Defina a interface:
Em vez de métodos que executam diretamente, cada ação é um objeto que sabe como se executar, desfazer e descrever a si mesma. Isso permite construir um histórico de ações (útil para replay de combates, undo e logging). A interface é simples: executar() faz a coisa, desfazer() desfaz, e descricao descreve.
abstract class Acao {
void executar();
void desfazer();
String get descricao;
}
AcaoAtacar
A ação de ataque captura o estado antes (HP anterior) e depois (dano aplicado). Isso permite desfazer: basta restaurar hp para o valor anterior. Observe como dano é calculado e armazenado em executar(); é necessário porque em desfazer() você precisa saber quanto dano foi aplicado para poder reverter.
class AcaoAtacar implements Acao {
final Inimigo atacante;
final Character alvo;
late int dano;
late int hpAnterior;
AcaoAtacar(this.atacante, this.alvo);
@override
void executar() {
hpAnterior = alvo.hp;
dano = calcularDano(atacante.arma, alvo.defesa);
alvo.hp -= dano;
}
@override
void desfazer() {
alvo.hp = hpAnterior;
}
@override
String get descricao =>
"${atacante.nome} ataca ${alvo.nome} por $dano!";
}
AcaoMover
Movimento também é uma ação. Captura a posição original, move, permite desfazer restaurando a posição. Note que ePassavel() garante que você não anda através de paredes; se o destino é inválido, a ação não muda a posição. Importante: sempre validar antes de modificar estado.
class AcaoMover implements Acao {
final Inimigo self;
final Pos destino;
late Pos origem;
final MapaMasmorra mapa;
AcaoMover(this.self, this.destino, this.mapa);
@override
void executar() {
origem = self.pos;
if (mapa.ePassavel(destino)) {
self.pos = destino;
}
}
@override
void desfazer() {
self.pos = origem;
}
@override
String get descricao => "${self.nome} se move";
}
AcaoAguardar
Uma ação que não faz nada. Parece inútil, mas é essencial. Um inimigo pode decidir que a melhor ação este turno é aguardar: recarregar, regenerar, ou simplesmente deixar o alvo fazer a próxima ação. Sem AcaoAguardar, você precisaria de if (acao == null) em todo o código. Com ela, tudo é uniforme: sempre execute uma ação, mesmo que não faça nada.
class AcaoAguardar implements Acao {
final Character self;
AcaoAguardar(this.self);
@override
void executar() {}
@override
void desfazer() {}
@override
String get descricao => "${self.nome} aguarda";
}
Histórico de Ações e Undo
Uma das grandes vantagens de Command é manter um histórico completo:
O GerenciadorAcoes mantém uma lista de todas as ações já executadas. Quando você quer desfazer, volta um índice e chama desfazer() do comando anterior. Quer refazer? Avança o índice. Quer replay do combate inteiro? Itere o histórico. Quer ver o log? Obtenha as descrições. Tudo vem grátis dessa abstração simples.
class GerenciadorAcoes {
final List<Acao> historico = [];
int indiceAtual = -1;
void executar(Acao cmd) {
cmd.executar();
historico.removeRange(indiceAtual + 1, historico.length);
historico.add(cmd);
indiceAtual = historico.length - 1;
}
void desfazer() {
if (indiceAtual >= 0) {
historico[indiceAtual].desfazer();
indiceAtual--;
}
}
void refazer() {
if (indiceAtual < historico.length - 1) {
indiceAtual++;
historico[indiceAtual].executar();
}
}
List<String> obterHistorico() => historico
.sublist(0, indiceAtual + 1)
.map((cmd) => cmd.descricao)
.toList();
}
Turno de Combate Integrado
Agora um turno é limpo e legível:
Veja como executarTurnoInimigo é agora uma função simples e linear. O inimigo decide uma ação (via sua estratégia), a ação se executa, o log registra. Sem if/else aninhados, sem estado implícito, sem surpresas. A IA vem da estratégia, a reversibilidade vem do comando.
void executarTurnoInimigo(
Inimigo inimigo,
Jogador heroi,
MapaMasmorra mapa,
GerenciadorAcoes gerenciador,
) {
var acao = inimigo.obterProximaAcao(heroi, mapa);
gerenciador.executar(acao);
log.escrever(acao.descricao);
if (heroi.hp <= 0) {
log.escrever("${heroi.nome} caiu!");
}
}
Boss com Fases
Um padrão avançado: um chefe que muda de estratégia conforme seu HP cai:
Um boss não é apenas um inimigo forte. É um combate progredindo. Conforme o herói inflige dano, o chefe muda de tática. Assim como em Dark Souls, onde o boss fica desesperado quando está perto de morrer. Aqui, BossComFases muda a estratégia interna baseado no HP. O resto do código não precisa saber disso; para o jogo principal, é apenas mais uma EstrategiaIa.
class BossComFases implements EstrategiaIa {
late EstrategiaIa estrategiaAtual = IAAgressiva();
@override
Acao decidir(Inimigo self, Jogador alvo, MapaMasmorra mapa) {
if (self.hp < (self.hpMax * 50 / 100)) {
estrategiaAtual = IACovardia(limiteHP: 20);
} else if (self.hp < (self.hpMax * 25 / 100)) {
estrategiaAtual = IAAgressiva(); // Desesperado
}
return estrategiaAtual.decidir(self, alvo, mapa);
}
}
É como em Dragon Ball: conforme Goku fica mais ferido, ele muda de abordagem. Cada fase tem uma mente própria.
Antes vs. Depois
Antes: Indistinção
Zumbi: sempre anda aleatoriamente, sempre ataca se próximo
Lobo: sempre persegue, sempre ataca
Dragão: sempre ataca com força bruta
Depois: Singularidade
Zumbi (IAPassiva): anda lentamente até ser provocado
Lobo (IAAgressiva): persegue agressivamente, não desiste
Esqueleto (IAPatrulha): patrulha, mas quando vê você muda para combate
Dragão (BossComFases): adapta tática a cada fase, inteligente
Pergaminho do Capítulo
Neste capítulo, você aprendeu dois padrões de design que transformam inimigos estáticos em adversários inteligentes e comportamentos previsíveis. O padrão Strategy permite que cada inimigo tenha sua própria “mente” (uma agressiva persegue você ferozmente, outra patrulha e dispara quando detectada, outra foge quando ferida), tudo sem modificar a classe Inimigo. Você implementou cinco estratégias diferentes (IAAgressiva, IACovardia, IAPatrulha, IAPassiva e BossComFases), cada uma definindo como um inimigo decide agir em um turno. O padrão Command encapsula cada ação (ataque, movimento, espera) como um objeto reversível, permitindo que você construa um histórico completo, desfaça ações e implemente replay de combates. Juntos, Strategy e Command eliminam if/else aninhados, criam inimigos que “pensam” e proveem a base para sistemas de IA sofisticados que respeitam a elegância do código.
::: vocab Vocabulário do Dia
- Padrão Strategy - encapsula um algoritmo (a “tática” do inimigo) em uma classe; troca em runtime sem
if/switch. - Padrão Command - encapsula uma ação como objeto; habilita undo, replay e log de ações.
Strategy<T>(interface) - defineescolher(estado) -> Acao; cada inimigo recebe sua estratégia por composição.- Undo / redo - desfazer ou refazer ações navegando uma lista de
Commands; útil para roguelikes com retroceder. - Composição vs. herança - Strategy prefere composição (passar um objeto) à herança (estender uma classe); mais flexível. :::
Desafios da Masmorra
**Desafio 34.1. Implemente uma estratégia IASuicida que sempre avança em sua direção até ficar ao seu lado e então “explode”, causando dano em raio de 3 tiles em volta (afeta você e outros inimigos).
**Desafio 34.2. Implemente um comando AcaoLancarMagia que custa mana (novo atributo do Inimigo) e pode ser desfeito. Crie uma estratégia que lança magia se tiver mana suficiente.
**Desafio 34.3. Modifique IAPatrulha para suportar múltiplas rotas e escolha uma aleatoriamente quando criada ou quando termina a rota atual.
**Desafio 34.4. Implemente AcaoFuga que move o inimigo para uma posição segura; se não encontrar após 5 turnos, o inimigo muda para IAAgressiva.
**Boss Final 34.5. Crie um sistema de “Comportamento Adaptativo” onde um inimigo começa com IAPatrulha e, após sofrer 3 ataques consecutivos sem conseguir contra-atacar, muda para IAAgressiva. Use um contador interno que reseta quando consegue atacar.
**Desafio 34.6. Implemente uma estratégia IAMago para um inimigo mago que:
- Mantém distância mínima de 3 tiles do herói
- Lança um ataque mágico (
AcaoLancarMagia) a cada 2 turnos se o herói está em linha de visão - Se o herói se aproxima a menos de 3 tiles, recua (como
IACovardiamas com limiteHP = 100%) - A magia custará um novo atributo
mana, que regenera 5 pontos por turno quando não está em combate
**Desafio 34.7. Implemente um comando AcaoDesfazerMultiplo que desfaz as últimas N ações de uma vez. Modifique GerenciadorAcoes para suportar desfazerN(int n) e refazerN(int n), permitindo replay rápido de segmentos de combate durante debug.
**Desafio 34.8. Crie uma estratégia IACompostaAgressiva que combina múltiplas estratégias em sequência:
- Primeiros 3 turnos:
IAPatrulha(patrulha até detectar) - Próximos 5 turnos após detectar:
IAAgressiva(ataque direto) - Se HP < 50%:
IACovardia(foge) - Mantém um state machine interno que controla qual sub-estratégia usar em cada fase
O padrão Strategy transformou inimigos passivos em adversários inteligentes. Command permitiu que cada ação fosse registrada e revertida. Juntos, eles formam a base de IA sofisticada. A próxima fronteira é multiplicar esses inimigos inteligentes de forma eficiente e fazer o mundo todo reagir aos eventos do combate.
Próximo Capítulo
No Capítulo 35, você verá como usar Factory para criar centenas de inimigos variados de forma escalável (centralizada, orientada a dados) e Observer para fazer o mundo inteiro reagir aos eventos do combate sem acoplamento direto. Factory define que tipo de inimigo é criado e com que parâmetros; Observer faz cada sistema (log, XP, som, UI) reagir a eventos sem modificar o código de combate.