Construindo um Eater Simples usando Regras
No exemplo do Problema Water Jug, estudado anteriormente, houve a necessidade de criar um operador de inicialização de estado para começar a solucionar o problema. No Jogo Eaters, isso não é necessário. O agente (criatura eater) começa o jogo obtendo as informações sobre sua situação a partir de uma estrutura chamada interface de entrada (input-link). Neste tópico será abordado o uso das interfaces de entrada e saída (output-link) de um programa SOAR, dentre outros conceitos úteis sobre programas e ferramentas do SOAR. Isso será demonstrado criando-se uma criatura eater o mais simples possível, para estudar a construção das regras que propõem e aplicam operadores.
Operador move-north
Este operador move a criatura um passo na direção norte. Para escrever a regra apply*move-north, é preciso entender como fazer para mover uma criatura pelo ambiente do jogo. Todas as ações externas da criatura são realizadas criando-se WME's que serão acoplados à interface de saída (output-link). O estado (s) contém um elemento io que contém o elemento output-link. Neste elemento, output-link, deverá ser criado um novo elemento do tipo move responsável por mover a criatura. Esse elemento, move, deverá conter um elemento do tipo direction que define a direção do movimento. Os valores possíveis deste elemento são: norte (north), sul (south), leste (east), oeste (west). A figura, a seguir, mostra as regras para o operador move-north:
A regra propose*move-north apenas propõe o uso do operador move-north (<o> ^name move-north). A regra apply*move-north testa a existência de um operador do tipo move-north (<o> ^name move-north) e localiza o objeto output-link (<io> ^output-link <ol>). Em seguida, cria nesse objeto (<ol>) um movimento (<ol> ^move <move>) para o norte (<move> ^direction north). Assim, ao aplicar este operador, a criatura se move uma célula na direção norte. A figura abaixo mostra o comportamento de uma criatura eater criada com esta regra de produção:
Segundo a figura acima, o operador move-north foi proposto (Firing propose*move-north) e, em seguida, selecionado ( 1: O: O1 (move-north)). E a criatura se moveu uma célula para o norte, no ambiente do jogo (não mostrado aqui). Porém, após isso, a criatura parou de se mover. E um novo estado foi criado (2: ==>S: S13 (operator no-change)). Isso, porque, esta produção não contém as regras suficientes para manter a criatura se movendo continuamente na direção norte. Isso será corrigido logo em seguida.
Shortcuts
Um problema da regra apply*move-north é que ela possui muitas variáveis (<o>, <io>, <move>) cujo único propósito é fazer a conexão entre atributos. A seguir, pode-se visualizar este problema descrito:
sp {apply*move-north
(state <s> ^operator <o>
^io <io>)
(<io> ^output-link <ol>)
(<o> ^name move-north)
-->
(<ol> ^move <move>)
(<move> ^direction north)}
Algumas variáveis se repetem, desnecessariamente, pelo código da regra. O SOAR disponibiliza um meio de se evitar isso, possibilitando a escrita de regras de forma mais simples e clara. O SOAR usa o conceito de shortcuts, conforme pode ser visto abaixo:
sp {apply*move-north
(state <s> ^operator.name move-north
^io.output-link <ol>)
-->
(<ol> ^move.direction north)}
A mesma regra foi re-escrita, mas, agora de forma mais simples e clara. Conforme pode-se perceber, alguns atributos foram unidos por meio da substituição do nome da variável pelo símbolo ".", separando os atributos.
Operador move-north: múltiplos movimentos
Será abordado o mecanismo de seleção de operadores do SOAR. Cada ação executada pela criatura eater poderia ser desempenhada pela instância de um operador independente. A instância de cada operador cria um WME na memória de trabalho, sendo o operador criado pelo disparo de uma regra de proposição de operadores. Cada operador instanciado só pode ser selecionado e aplicado apenas uma única vez.
Novas instâncias do operador move-north deveriam ser criadas na memória de trabalho para cada movimento novo da criatura. Para isso se tornar possível, a regra propose*move-north deve ser modificada para que dispare toda vez que a criatura executar um movimento. A versão anterior desta regra disparava apenas uma vez. Isso porque ela apenas testava se existia um estado (^type state) na memória de trabalho. Sendo que esse estado permanece, sempre, na memória de trabalho. Para a nova versão dessa regra funcionar adequadamente, a mesma deve testar elementos da memória de trabalho que se alteram sempre que a criatura executa um movimento. A figura abaixo ilustra essa nova versão da regra:
Agora, a regra propose*move-north testa as informações sobre a criatura eater que estão disponíveis na interface de entrada (^io.input-link.eater <e>). Essa regra testa a posição atual da criatura (<e> ^x <x> ^y <y>) que mudará sempre que a criatura se mover, disparando essa regra que propõe um novo operador do tipo move-north.
Antes de testar essa nova regra, ainda é preciso modificar a produção move-north, criando uma nova produção move-north-2, para sanar um outro problema gerado pela modificação da regra propose*move-north. O problema anterior da produção usada para mover a criatura, na direção norte, era que a criatura executava apenas um único movimento. Sanado este problema, agora a criatura executa múltiplos movimentos na direção norte. Porém, isso gera o seguinte problema:
(I3 ^move M3 ^move M2 ^move M4)
(M3 ^direction north ^status complete)
(M2 ^direction north ^status complete)
(M4 ^direction north ^status complete)
Pode-se observar que a estrutura da interface de saída (I3, output-link) é constantemente incrementada, conforme um novo movimento (M3, M2, M4) da criatura é executado. Isso é um problema, visto que pode levar a explosão da memória de trabalho do SOAR. A adição da seguinte regra, apply*move-north*remove-move, vai sanar este problema:
sp {apply*move-north*remove-move
(state <s> ^operator.name move-north
^io.output-link <ol>)
(<ol> ^move <move>)
(<move> ^status complete)
-->
(<ol> ^move <move> -)}
Essa regra remove os WME's responsáveis pelos comandos move aplicados anteriormente. A regra testa se o movimento terminou sua execução (<move> ^status complete) e remove-o da memória de trabalho (<ol> ^move <move> -). A figura abaixo mostra o resultado do uso da produção move-north-2:
Os símbolos "=>" e "<=" indicam, respectivamente, WME's que foram adicionados ou removidos da memória de trabalho. Também são exibidos alguns números (132 130 11 13) que são denominados timetags do WME's. Esses números são gerados quando um novo WME é criado.
A regra apply*move-north*remove-move teve que ser usada devido a uma característica do SOAR. A persistência de elementos da memória de trabalho. Regras que são parte da aplicação de um operador criam WME's que persistem na memória de trabalho. Por isso, estes devem ser removidos "manualmente". WME's persistentes são chamados o-supported (operator-supported), porque eles são criados por operadores. Alguns WME's persistem na memória de trabalho apenas durante o tempo de persistência da instância da regra que os criou. Estes WME's e preferências não persistentes são chamados i-supported (instantiation-supported). WME's e preferências não persistentes são criados pelas ações de regras que apenas testam o estado, ou que apenas testam ou elaboram operadores.
A regra apply*move-north gera ações o-supported (<ol> ^move.direction north). A regra propose*move-north gera ações i-supported (<s> ^operator <o> +). Regras que aplicam operadores não disparam durante a fase de proposição de operadores. Dessa forma, durante a fase de proposição de operadores, apenas regras que possuem ações do tipo i-supported disparam. Durante a fase de aplicação de operadores, ambas as regras i-supported e o-supported disparam.
SOAR Debugger: tracing
Pode-se usar o SOAR Debugger para acompanhar o processo de escolha e aplicação de operadores por meio de traces. Conforme a figura abaixo:
Pode-se observar que na fase de aplicação de operadores (apply phase) duas regras estão disparando ao mesmo tempo em paralelo: Firing apply*move-north e Firing apply*move-north*remove-move. A primeira está adicionando um novo comando move na interface de saída (I3, output-link) e a segunda está removendo o comando move anterior. Pode-se também observar que, na fase de proposição de operadores (propose phase), uma regra está disparando (Firing propose*move-north) e uma regra está retraindo (Retracting propose*move-north) ao mesmo tempo.
Operador move-to-food
As criaturas eaters podem sentir a presença de comida e paredes, ao seu redor, até dois passos em cada direção. Agora será criado um operador que move a criatura para uma das células vizinhas (norte, sul, leste, oeste) que contenha comida normal ou do tipo bônus. Pode haver comida em mais de uma célula vizinha, assim, mais de um operador poderá ser proposto. O SOAR não seleciona automaticamente e aleatóriamente entre um conjunto de operadores propostos. Neste caso, ocorre um impasse se há múltiplos operadores com mesma preferência de uso. Para evitar impasses, o SOAR possui preferências adicionais.
Serão necessárias quatro regras para o operador move-to-food. Uma regra que propõe o operador quando há comida normal na célula vizinha e uma regra que faz o mesmo quando há comida do tipo bônus. Para essas regras, o teste que será realizado verifica o conteúdo das células vizinhas, que mudam conforme a criatura se move. Assim, não ocorrerá o problema de o operador ser aplicado uma única vez, como acontecia com o operador move-north. As regras de proposição do operador também podem criar preferências indiferentes que levam a uma seleção aleatória dos operadores propostos. Uma terceira regra aplica o operador e move a criatura na direção correta. A outra regra vai remover o comando usado anteriormente.
Para selecionar aleatóriamente entre os operadores propostos, o SOAR possui uma preferência chamada indiferente, símbolo "=", que significa uma preferência indiferente pelo operador. O símbolo "+" significa uma preferência aceitável por esse operador. Ao usar esse tipo de preferência indiferente, em todos os operadores que podem ser propostos, o SOAR entende que a seleção de operadores deverá ser realizada aleatóriamente.
É interessante observar que uma célula possui um atributo content que indica o conteúdo da mesma (eater, wall, empty, normalfood, bonusfood). Cada célula também possui atributos north, easth, south, west que apontam para as células vizinhas desta. A figura abaixo mostra as regras (exceto a regra apply*move-to-food*remove-move) da produção move-to-food:
A regra propose*move-to-food testa o conteúdo de cada célula adjacente à celula ocupada pela criatura (^io.input-link.my-location.<dir>.content). Nesta regra, além do uso de shortcuts, também é usado um recurso interessante: a extensão de regras. Ao invés de escrever duas regras, conforme proposto anteriormente, para testar a existência dos dois tipos de comida disponíveis, foi escrita apenas uma regra de proposição de operadores. Para isso, usam-se os símbolos "<< >>" que definem os valores aceitáveis pela condição da regra (<< normalfood bonusfood >>). Essa regra também indica a preferência indiferente pelos operadores propostos (<s> ^operator <o> + =) definindo que a seleção de operadores deverá ser aleatória. A regra apply*move-to-food usa a variável <dir> para indicar a direção do movimento da criatura, definida pela regra que propos o operador. A regra apply*move-to-food*remove-move, que remove o último movimento realizado, está mostrada abaixo:
sp {apply*move-to-food*remove-move
(state <s> ^io.output-link <ol>
^operator.name move-to-food)
(<ol> ^move <move>)
(<move> ^status complete)
-->
(<ol> ^move <move> -)}
Depuração de Programas SOAR
A fim de se evitar erros, ao se escrever as regras das produções, são apresentadas técnicas para encontrar e corrigir possíveis erros em programas SOAR. Essas técnicas se baseiam na causa do erro. O VisualSoar verifica erros de sintaxe nas regras. Esses erros ocorrem quando as regras não foram bem escritas. O SOAR Debugger permite verificar os erros semânticos. Esses erros são causados por problemas com a lógica de funcionamento da regra.
Erros de Sintaxe
O VisualSoar permite verificar os erros de sintaxe das regras escritas. Para isso, durante o desenvolvimento do programa SOAR, basta selecionar a opção do menu Datamap -> Check All Productions for Syntax Errors. Um exemplo é exibido abaixo:
Pode-se ver a mensagem que foi exibida na Janela Feedback: "There were no errors detected in this project", ou seja, não foram encontrados erros sintáticos no projeto. O VisualSoar verifica todas as produções para certificar-se que elas estão de acordo com as regras sintáticas do SOAR. A figura a seguir mostra o que acontece quando um erro de sintaxe é encontrado:
A Janela Feedback agora exibe uma mensagem de erro. Essa mensagem contém o nome do arquivo onde o erro foi encontrado, e em qual linha (elaborations/top-state(5)). Clicando-se na mensagem de erro, o cursor é direcionado para o local do arquivo onde o erro foi encontrado, conforme pode ser visto na figura, em amarelo.
Erros Semânticos
O SOAR Debugger disponibiliza comandos, em tempo de execução, para analisar o funcionamento das regras das produções escritas. Alguns comandos úteis são descritos a seguir:
print
Este comando imprime estruturas da memória de trabalho. O parâmetro depth permite visualizar o conteúdo dos atributos de um identificador. Por exemplo, o comando print --depth 2 s1 imprime a seguinte mensagem:
(S1 ^epmem E1 ^io I1 ^operator O17 ^operator O19 + ^operator O17 +
^operator O18 + ^reward-link R1 ^smem S2 ^superstate nil ^type state)
(E1 ^command C1 ^present-id 1 ^result R2)
(I1 ^input-link I2 ^output-link I3)
(O17 ^direction west ^name move-to-food)
(O19 ^direction south ^name move-to-food)
(O18 ^direction north ^name move-to-food)
(S2 ^command C2 ^result R3)
wmes
Este comando imprime a timetag de um WME e suas informações. Por exemplo, o comando wmes o17 imprime a seguinte mensagem:
(410: O17 ^direction west)
(409: O17 ^name move-to-food)
Pode-se também usar timetags para imprimir informações sobre WME's:
wmes 420
(420: I3 ^move M9)
print 420
(I3 ^move M9)
matches
Este comando retorna uma lista das regras que já dispararam. Essa lista está separada por O Assertions (operadores aplicados), I Assertions (criam elementos i-supported) e Retractions (removem elementos i-supported). Por exemplo, o comando matches imprime a seguinte mensagem:
O Assertions:
apply*move-to-food*remove-move [S1]
apply*move-to-food [S1]
I Assertions:
Retractions:
O seguinte comando também pode ser usado, para verificar os WME's condizentes com as condições de uma regra disparada:
matches propose*move-to-food
1 (state <s> ^io <i*1>)
1 (<i*1> ^input-link <i*2>)
1 (<i*2> ^my-location <m*1>)
6 (<m*1> ^<dir> <d*1>)
2 (<d*1> ^content { << normalfood bonusfood >> <c*1> })
2 complete matches.
Os números, à esquerda, indicam quantos elementos satisfazem cada condição da regra.
preferences
Este comando imprime as preferências pela seleção de operadores. Por exemplo, o comando preferences imprime a seguinte mensagem:
Preferences for S1 ^operator:
acceptables:
O29 (move-to-food) + :I
O30 (move-to-food) + :I
unary indifferents:
O29 (move-to-food) = :I
O30 (move-to-food) = :I
selection probabilities:
O30 (move-to-food) + =0. :I (50.0%)
O29 (move-to-food) + =0. :I (50.0%)
O exemplo mostra que foram propostos dois operadores, O29 e O30, que são aceitáveis e que possuem preferências indiferentes. Também mostra as probabilidades de seleção destes operadores.
Os seguintes comandos também são interessantes:
preferences I3 move --name
Preferences for I3 ^move:
acceptables:
(I3 ^move M12) :O
From apply*move-to-food
Este comando indica o nome da produção que criou a preferência.
preferences I3 move --wmes
Preferences for I3 ^move:
acceptables:
(I3 ^move M12) :O
From apply*move-to-food
544 583 540 11 13
Este comando exibe quais WME's (544, ..., 13) casam com a regra apply*move-to-food.