Esta atividade corresponde à execução de todo o tutorial 3. As principais etapas estão descritas a seguir.
2.1. Criação da primeira versão do agente, capaz de se mover para posições desbloqueadas, virar, fazer meia-volta e ligar e desligar o radar.
Nesta primeira parte do tutorial 4 regras simples são propostas, permitindo que o tanque se mova pelo tabuleiro, ligando o radar toda vez que mudar de direção e desligando o radar quando não tiver detectado nada interessante à frente - algum recarregador, pacote de mísseis ou outro tanque.
- A primeira regra é a para mover à frente, caso não haja obstáculo.
- A segunda regra é para virar para esquerda ou direita (qual posição estiver livre) caso à frente haja um bloqueio. Esta regra ativa o radar com potência 13, que é a maior que faz sentido ligar: das 16 posições do tabuleiro, as extremidades estão sempre ocupadas por obstáculos e um pelo próprio tanque.
- A terceira regra é para fazer meia-volta, caso existam obstáculos tanto à frente quanto à esquerda e direita.
- A quarta regra é para desligar o radar caso ele não tenha detectado nenhum objeto "interessante": "energy", "health", "missiles" ou "tank".
Com essas regras básicas o tanque é capaz de explorar o tabuleiro, mas de uma forma pouco eficiente, fazendo movimentos cíclicos e só mudando de direção quando o movimento para frente está impedido. O uso do radar também é pouco eficiente, pois ele permanece ligado a cada movimento mesmo, por exemplo, quando o tanque está fazendo meia-volta.
2.2. Criação de submetas ou operadores de alto nível
É possível criar operadores de alto nível e agrupar, dentro deles, sub-operadores. Assim permite-se separar cada conjunto de sub-operadores, simplificando o tratamento entre eles, uma vez que deixa de existir a possibilidade de interferência mútua durante o processo de avaliação do Soar.
No caso deste jogo, operadores de alto nível podem ser os que determinam se o tanque deve:
- ANDAR (Wander) até avistar um tanque
- PERSEGUIR (Chase) um tanque avistado
- ATACAR (Attack) um tanque na linha de tiro
- RECUAR (Retreat) de um ataque ou de outro tanque
Para cada um desses operadores de alto nível, é possível então definir sub-operadores que, neste caso, corresponderiam a ações diretas dos tanques:
- Sub-operadores ANDAR: "Move", "Turn"
- Sub-operadores PERSEGUIR: "Move", "Turn"
- Sub-operadores ATACAR: "Move", "Turn", "Slide", "Fire"
- Sub-operadores RECUAR: "Move", "Wait"
Esquema ilustrativo dos operadores de alto nível propostos para TankSoar
A implementação de operadores de alto nível faz uso da capacidade do Soar de criação automática de sub-estado: toda vez que um impasse é encontrado, Soar gera um sub-estado onde um novo contexto é gerado e novos operadores podem ser selecionados e aplicados de forma a tentar solucionar o impasse.
Impasses no Soar podem acontecer quando:
- O operador proposto não é alterado - "operator no-change".
- Quando o estado atual não é alterado - "state no-change".
- Quando múltiplos operadores são propostos e não existe como decidir qual aplicar - "tie impasse".
- Quanto múltiplos operadores são propostos e as suas preferências são conflitantes - "conflict impasse".
É a exploração da primeira forma de impasse descrita acima - "operator no-change" - que corresponde à criação de sub-estados: inicialmente operadores de alto-nível podem ser propostos de acordo com condições específicas, mas sem haver regras diretas para a sua aplicação; assim, Soar irá criar um sub-estado para tentar resolver o impasse. Uma vez criado esse sub-estado, uma regra de elaboração - "default" para o uso de sub-estados - deve ser criada para associar ao nome desse sub-estado o nome do operador que havia sido proposto:
sp {elaborate*state*name
(state <s> ^superstate.operator.name <operator-name>)
-->
(<s> ^name <operator-name>)
}
O nome do sub-estado deve, então, ser usado para ser condição do conjunto de sub-operadores válidos para esse sub-estado, ou seja, para esse operador de alto-nível que foi selecionado; a seguir está a regra básica de mover à frente, no caso do operator de alto nível WANDER:
sp {wander*propose*move-forward
(state <s> ^name wander
^io.input-link.blocked.forward no)
-->
(<s> ^operator <o> +)
(<o> ^name move
^actions.move.direction forward)
}
É importante ressaltar que o estado original - ou super-estado do estado atual - é acessível a partir do seu sub-estado. Uma outra elaboração - também "default" no uso de sub-estados - deve ser criada para claramente indicar, dentro do sub-estado, qual é o estado incial da execução:
sp {elaborate*state*top-state
(state <s> ^superstate.top-state <ts>)
-->
(<s> ^top-state <ts>)
}
Além de serem acessíveis, os super-estados - considerando uma possível sequência de sub-estados aninhados - continuam ativos, ou seja, continuam sendo analizados e atualizados - por exemplo, na sua memória de trabalho - a cada ciclo de execução do Soar. De fato, isso é a essência do mecanismo de criação de sub-estados para resolução de impasses: assim que estes são solucionados, os sub-estados são automaticamente destruídos, uma vez que no seu super-estado (onde o impasse originou-se) o impasse deixa de existir.
Um sub-estado não possui extensão "io", apenas existente no estado inicial. Entretanto, entrada e saída também podem ser feitas em sub-estados, utilizando-se a extensão "io" do estado inicial, a qual continua válida e ativa. Assim, uma terceira extensão - também "default" nesse cenário - deve ser criara para estabelecer - e propagar - uma ligação com o "io" do estado original:
sp {elaborate*state*io
(state <s> ^superstate.io <io>)
-->
(<s> ^io <io>)
}
2.2.1. Persistência de elementos da memória de trabalho X subestados
Um ponto importante do uso de subestados é a persistência dos elementos criados na memória de trabalho durante a sua execução.
Para ser efetivo na resolução de impasses, o uso de subestados deve permitir a modificação do superestado de forma a impedir/resolver o impasse. Entretanto, a própria resolução do impasse faz com que o subestado seja automaticamente destruído, ou seja, removido da memória de trabalho o que, em teoria, provocaria a remoção de todos os elementos criados a partir do objeto do subestado.
Soar possui um mecanismo para resolver esse impasse, permitindo que certas extensões, criadas a partir de um subestado, sobrevivam à sua destruição. De forma similar à avalição de "o-support" e "i-support", Soar tem o mecanismo de justification, o qual extende até os elementos do superestado a verificação do suporte que será dado a um elemento criado num subestado.
Numa análise retroativa, Soar busca até chegar ao superestado todos os elementos da memória de trabalho que foram sucessivamente consultados até levar à criação - no subestado - da extensão analisada. Nesse percurso, caso chegue a conclusão que as bases dessa cadeia de elementos foi a aplicação de um operador, Soar determina que essa extensão terá "o-support" e, consequentemente, permanecerá na memória mesmo o subestado sendo destruído. A figura abaixo apresenta essa análise para o caso da elaboração que remove um som ouvido pelo tank (sensor "^sound") que foi armazenado por um certo tempo, permitindo que o tanque lembrasse a direção que o som fora inicialmente ouvido:
Essa análise é feita na memória de trabalho a partir do disparo da seguinte regra, que propõe a remoção do som antigo:
#
# Propose to remove expired sound direction saved in top-state
#
sp {all*propose*remove-sound
(state <s> ^name << wander chase retreat attack >>
^superstate.sound.expire-time <clock>
^io.input-link.clock > <clock>)
-->
(<s> ^operator <o> + =, >)
(<o> ^name remove-sound)
}
2.3. Observações importantes:
- Soar não permite elementos de memória com exatamente os mesmos valores duplicados; assim é possível ter 2 regras criando exatamente os mesmos elementos de memória, pois Soar irá criá-los apenas uma vez. É o que acontece com as regras de elaboração que permitem indicar o estado de baixo nível de energia ou de mísseis:
sp {elaborate*state*missiles*low
(state <s> ^name tanksoar
^io.input-link.missiles 0)
-->
(<s> ^missiles-energy low)
}
sp {elaborate*state*energy*low
(state <s> ^name tanksoar
^io.input-link.energy <= 200)
-->
(<s> ^missiles-energy low)
}
Essas regras irão disparar toda vez que a extensão "^name" ou "^missiles"/"^energy" forem alteradas; no entanto, apenas uma extensão "^missiles-energy low" será criada.
- Durante o tutorial, logo após a implementação inicial dos operadores de alto nível, foi acrescentada a seguinte regra para desligar o radar, seguindo a abordagem que havia sido indicada na seção anterior, quando ainda não eram utilizados os operadores de alto nível:
#
# Radar-off elaboration
#
sp {elaborate*radar-off
(state <s> ^operator.actions <a>
^io.input-link <il>)
(<il> ^radar-status on
-^radar.<< energy health missiles tank >>)
-(<a> ^radar.switch on)
-->
(<a> ^radar.switch off)
}
Assim como no caso do controle do escudo, esse comando é adicionado a um comando já existente, para mandar sempre todos os comandos juntos.
- Houve um problema de prioridade ("tie impasse") com as regras "remove-sound" e "fire" uma vez que ambas estavam definidas como "as mais prioritárias". A solução foi fazer com que as regras de ATTACK sejam priorizadas apenas entre elas, com demostrado na regra abaixo:
#
# Attack operators preference rules - to avoid conflict with external operator rules like "remove-sound".
#
sp {select*fire-against-others
(state <s> ^name attack
^operator <op1> +
^operator <op2> +)
(<op1> ^name fire)
(<op2> ^name <> fire)
-->
(<s> ^operator <op1> > <op2>)
}
De acordo com a regra acima, a operação "fire" tem prioridade sobre as operações "slide", "move-forward" e "turn-and-fire", todas do operador de alto nível ATTACK.
- Não existe regra para CHASE no caso do som vir de um lado que se está bloqueado. Outra situação encontrada é no caso do RETREAT onde não tem para onde fugir. Nesses casos o agente executa a operador de alto nível WAIT - criado como "fallback" - com uma certa frequência.
Para o caso do operador de alto nível CHASE, foi proposta a seguinte regra para executar o "slide", caso a direção do som esteja bloqueada:
#
# Propose to slide if sound direction is blocked
#
sp {chase*propose*slide
(state <s> ^name chase
^sound-direction <direction>
^io.input-link <il>
^superstate <ss>)
(<ss> ^side-direction.<direction> <side-direction>)
(<il> ^blocked <direction>
-^blocked <side-direction>)
-->
(<s> ^operator <o> + =)
(<o> ^name slide
^action.move.direction <side-direction>)
(write (crlf) | CHASE: Proposing slide | <side-direction>)
}
- Uma das regras para atualização do mapa gerou um problema, detectado durante a execução da ATIVIDADE 3, considerando as demais regras: marcando direto como OPEN a posição em que o tanque está removia os carregadores, caso um deles estivesse nessa posição; por algum motivo a regra de recriação dos carregadores - a partir do status de recarga em curso - não disparava em algumas situações. A regra foi então modificada para checar se a posição não era um recarregador:
#
# Positively mark the current square as open if it is not health/energy recharger
#
sp {elaborate*map*record-open*current-square
:o-support
(state <s> ^name tanksoar
^square <sq>)
(<sq> ^x <x>
^y <y>)
-{(<sq> ^<< open energy health >> *yes*)}
-->
(<sq> ^open *yes*)
(write (crlf) | MAP: mark current square (| <x> |, | <y> |) as open|)
}
- Um outro problema das regras de atualização dos mapas - de acordo com o sugerido no tutorial - foi o fato delas criarem numa mesma posição do mapa uma indicação de RECARREGADOR (tanto de ENERGIA quanto de SAÚDE) e uma indicação de OPEN. Isso acontecia se o tanque chegasse ao recarregador totalmente sem energia, ou seja, sem conseguir utilizar o radar para mapear a posição anteriormente: nesse cenário tanto a regra que marca a posição atual do tanque como OPEN quanto a regra que marca o RECARREGADOR cujo recarregamento estiver em curso disparam, criando ambas indicações para aquela posição. Essa dupla marcação criava impasses durante o procedimento de busca de caminho, desenvolvido na ATIVIDADE 3. Foi criada uma regra para remover uma marcação de OPEN caso a posição fosse indicada como RECARREGADOR:
#
# Remove open from current square if energy or health recharger
#
sp {elaborate*map*remove-open*energy-health
:o-support
(state <s> ^name tanksoar
^square <sq>)
(<sq> ^{<< health energy >> <what>} *yes*
^x <x>
^y <y>)
(<sq> ^open *yes*
^x <x>
^y <y>)
-->
(<sq> ^open *yes* -)
(write (crlf) | MAP: remove open mark from square (| <x> |, | <y> |) which is | <what> | recharger|)
}
2.4. Pontos importantes desse tutorial
- Introdução à utilização de subestados.
- Detalhamento sobre criação de regras com o-support / i-support, especialmente no caso de uso de subestados; utilização do operador ":o-support".
- Uso de estruturas na memória de trabalho para realização de cálculos - exemplo da criação dos mapas.