You are here

Atividade 2

Agente TankSoar Simples

 

    Este agente controlará o tank, movendo-o pelo seu ambiente e desviando-se de obstáculos. Os sensores do tank permitirão o desvio e a detecção de objetos no mapa do jogo. O principal sensor do tank que será usado é o seu radar, que localiza-se na frente do tank. Embora o tank possa se mover nas quatro direções, uma boa estratégia é mover o tank para frente e girar para desviar de obstáculos. O radar consome energia, portanto, seria interessante usá-lo apenas quando o tank gira, desligando-o logo em seguida. No entanto, é sempre bom detectar alguns objetos interessantes, tais como, outros tanks, mísseis e carregadores de energia e saúde. Deve-se também considerar a potência do radar, diretamente ligada ao consumo do mesmo, sendo que a maior distância alcançável pelo radar corresponde ao valor 13.

    Assim, define-se os três operadores que serão, primeiramente, aplicados para controlar o tank. Move, move o tank para frente se não houver obstáculos bloqueando o caminho. Turn, gira o tank e liga o radar na potência 13, se houver algum obstáculo impedindo o tank de se mover para a frente. Radar-off, se o radar está ligado e não há objetos visíveis, desliga o radar. Se o operador Radar-off foi proposto, então a sua preferência deverá ser maior que as preferências para movimentar o tank.

    O operador move assemelha-se aquele usado no Jogo Eaters. A regra que propõe este operador está descrita a seguir. A regra testa se não há obstáculo à frente do tank (^io.input-link.blocked.forward). Caso não haja obstáculos, o operador move será proposto (<o> ^name move) fazendo o tank se mover para frente (<o> ^actions.move.direction forward). Notar que está sendo usado o sensor Blocked para detectar a existência (ou ausência) de obstáculos à frente do tank.

    O operador turn (figura acima) é ligeiramente diferente. Este operador será proposto sempre que houver um obstáculo bloqueando o movimento do tank para frente (^io.input-link.blocked.forward yes). Assim sendo, falta ainda definir para qual direção o tank deve girar (direita ou esquerda). Isso será feito testando-se se alguma dessas direções não está, também, bloqueada (<< left right >> <dir>). Caso ambas direções estejam livres, serão propostos dois operadores (turn) de igual preferência de seleção. Outra ação desta regra, propose*turn, será ligar o radar do tank (^radar.switch on) e configurar sua potência (^radar-power.setting 13). A figura, a seguir, exibe outras duas regras para proposição de operadores:

    Pode ocorrer de haver obstáculos impedindo o movimento do tank em três direções (frente, direita e esquerda). Neste caso, o operador turn não poderá ser proposto. Para evitar do tank parar de se mover, uma nova regra, propose*turn*backward, irá propor um operador turn, ligeiramente alterado, quando esta situação for detectada (<b> ^forward yes ^left yes ^right yes). Este operador faz o tank girar, para esquerda (^actions.rotate.direction left), até que ocorra alguma situação que permita a proposição do operador move ou mesmo de turn.

    Outra regra, propose*radar-off (figura acima), testa se o radar está ligado (^radar-status on) e verifica se não há objetos interessantes à frente do tank. Esses objetos podem ser outro tank, mísseis ou carregadores de energia ou saúde (<r> ^ << energy health missiles tank >>). Caso sejam atendidas tais condições, esta regra propõe que o radar seja desligado (^actions.radar.switch off). Para aplicar, de fato, este operador, sua preferência deverá ser maior que as preferências para mover o tank. A regra select*radar-off*move, descrita abaixo, se encarrega de realizar esta alteração de preferências.

sp {select*radar-off*move
   (state <s> ^operator <o1> +
              ^operator <o2> +)
   (<o1> ^name radar-off)
   (<o2> ^name move)
-->
   (<s> ^operator <o1> > <o2>)}

    A produção wander.soar reúne as regras discutidas acima. Após rodar o TankSoar com esta produção, a tela do SOAR Debugger (figura abaixo) exibe os resultados da aplicação desses operadores propostos. (Embora não haja figuras mostrando a situação do tank, no cenário do jogo).

     Inicialmente, o tank estava bloqueado por um obstáculo à sua frente. Então, foi proposto o operador turn, e o radar do tank foi ligado. Em seguida, a preferência maior foi pela seleção do operador radar-off, desligando o radar do tank. Agora, sem obstáculos à sua frente, o tank pode se mover (operador move proposto). Percebe-se que sempre que o tank gira (turn) sua próxima ação será desligar seu radar (radar-off). Porém, continuando a simulação, verifica-se que isso nem sempre é verdade. A próxima figura exibe essa situação:

    Foi detectado um objeto do tipo Health Charger (health) pelo radar. Desta vez, o tank girou (94: turn) mas continuou com seu radar ligado. Não foi selecionado o operador radar-off, em vez disso, foi selecionado o operador move (95 até 100). A figura abaixo ilustra essa situação. Pode-se perceber que o tank continua com seu radar ligado enquanto algum objeto interessante (neste caso, health e missiles) é detectado. O radar só foi desligado quando o tank passou sobre o pacote de mísseis (101: radar-off) recolhendo esses mísseis. No entanto, continuando a simulação vários passos além destes, chega uma situação em que o tank fica sem energia para usar seu radar. Neste caso, o operador turn não mais liga o radar e, portanto, o operador radar-off deixa de ser proposto. Essa situação vai mudar apenas quando o tank passar sobre um objeto do tipo Energy Charger.

    Durante a simulação, pode-se perceber uma peculiaridade do uso dessa produção simples (wander.soar). O tank move-se "cegamente" pelo seu ambiente. Seria interessante se o tank pudesse mapear seu ambiente, criando e posteriormente usando um mapa do terreno do jogo. Essa abordagem será proposta e analisada mais adiante. Assim, usando um mapa, o tank poderia "conhecer" a localização dos carregadores de energia e de saúde, além de poder "decidir" investigar as direções que estejam próximas de encruzilhadas. Dessa forma, o tank poderia tomar decisões mais inteligentes ao invés de simplesmente vagar, sem rumo, pelo seu ambiente.

Operador Wander - SubMetas

    As regras definidas anteriormente na produção wander.soar geram um comportamento muito simples do tank. O tank apenas vaga (wander) pelo seu ambiente. Seria interessante implementar outros comportamentos, tais como: perseguir (chase) outro tank, atacá-lo (attack) ou mesmo recuar (retreat) após ser atacado ou para evitar um ataque. Implementar tais comportamentos significaria alterar os operadores já propostos anteriormente. Esse tipo de solução é bem complexa visto que há a necessidade de se escrever várias regras para selecionar cada um dos comportamentos desejados, sem que um comportamento interfira no outro, além de que cada comportamento pode gerar a proposição de mais de um operador.

    O SOAR permite implementar comportamentos complexos, isto é, comportamentos que são compostos por comportamentos mais simples, de forma eficaz. Assim, pode-se "empacotar" alguns operadores, que vão gerar um comportamento complexo, dentro de um único operador de alto nível. As ações, do tank, de alto nível serão compostas por ações (operadores) de baixo nível. O tank usará seu conhecimento da situação atual (vindo de seus sensores) para selecionar entre alguns comportamentos, da mesma forma que seleciona operadores. Isso será realizado implementando-se operadores de alto nível abstratos em submetas. Assim, um novo estado será criado para permitir a seleção de operadores de baixo nível responsáveis pelas ações do tank que geram o comportamento complexo.

Operador wander

    O primeiro comportamento a ser implementado será definido pelo operador abstrato wander. Este operador será composto por regras que propõem outros operadores (sub-operadores) responsáveis por gerar o comportamento do tank. Neste caso, o tank apenas vaga, sem rumo, pelo seu ambiente. Sendo que os operadores que realizam tal tarefa são os operadores move e turn já apresentados anteriormente. Os demais comportamentos que serão implementados posteriormente são: perseguir (chase), atacar (attack) e recuar (retreat). O operador wander deve ser proposto quando nenhum desses outros operadores forem propostos. Ou seja, o tank não pode estar perseguindo e nem atacando outro tank; além do tank também não estar sofrendo ameaças (retreat).

    Assim, o operador wander será proposto sempre que nenhum outro tank for detectado e nenhuma ameaça (ataques do inimigo) for detectada. Os sensores que permitem detectar outro tank são Radar e Sound. Outros sensores poderiam ser usados, porém, seus resultados não seriam satisfatórios para informar corretamente a detecção de um tank adversário. O radar permite a correta identificação de um objeto como sendo um tank (embora possa haver obstáculos bloqueando-o). Um tank que esteja muito próximo fará um som (desde que esteja em movimento) indicando sua direção. Um míssil inimigo constitui uma ameaça ao tank. O sensor Incoming permite sua detecção. A regra que propõe o operador wander está descrita abaixo:

sp {propose*wander
   (state <s> ^name tanksoar
                   ^io.input-link <io>)
   (<io> ^sound silent
          -^radar.tank
          -^incoming.<dir> yes)
-->
   (<s> ^operator.name wander)}

    Conforme descrito, essa regra irá propor esse operador apenas quando nenhum míssil (-^incoming.<dir> yes) e nenhum tank (^sound silent e -^radar.tank) forem detectados.

Impasses e criação de estados

    A produção que propõe o operador wander não possui regras para aplicá-lo de fato. A seleção deste operador implicará na proposição e seleção de outros operadores (move e turn). Tentar rodar o TankSoar usando essa produção, com apenas uma regra de proposição de operador, criará um impasse: o SOAR detecta que não há regras que disparam quando o operador wander é selecionado. Isto constitui um impasse do tipo operator no-change, porque apesar de um operador ter sido proposto, nenhuma ação foi tomada. Existem outros três tipos de impasses. O impasse state no-change ocorre quando nenhum operador é proposto para o estado corrente. O impasse operator tie ocorre quando há muitos operadores propostos mas não há preferências suficientes para selecioná-los. O impasse operator conflict ocorre quando há múltiplos operadores propostos e um conflito entre suas preferências.

    Quando ocorre um impasse, o SOAR cria automaticamente um novo estado. Esse sub-estado criado permitirá a seleção e aplicação de novos operadores para resolver o impasse. Quando um novo operador for selecionado e aplicado, resolvendo o impasse, o sub-estado criado não será mais necessário e será removido da memória de trabalho. No entanto, os seus resultados (WME's criados) ainda podem persistir na memória de trabalho no super-estado. O super-estado é o estado original criado, onde ocorreu um impasse, criando um sub-estado neste (super) estado.

Operador wander: sub-operadores

    Ao propor o operador wander, um impasse será gerado e um novo sub-estado será criado para permitir a resolução deste impasse. Nesse momento, novos (sub) operadores deverão ser propostos para resolver o impasse e eliminar o sub-estado criado (voltando a propor novos operadores no super-estado inicial). Esses operadores que serão propostos são os mesmos operadores, move e turn, já definidos anteriormente. A figura abaixo ilustra suas regras de proposição:

    Pode-se notar que houve apenas uma pequena alteração. Logo no início de cada regra de proposição dos operadores, é realizado um novo teste. Esse teste (state <s> ^name wander) verifica se o nome do sub-estado criado é wander, ou seja, se este novo sub-estado foi criado pela proposição do operador wander no super-estado inicial. Para tornar este teste possível, houve a necessidade de se criar regras de elaboração de estado para gravar as informações do super-estado inicial no sub-estado criado. A figura abaixo ilustra isso:

    A regra elaborate*state*io grava as informações da estrutura de dados de entrada-saída (state <s> ^superstate.io <io>) do super-estado inicial para o sub-estado criado (<s> ^io <io>). Assim, o sub-estado que foi criado terá acesso às informações contidas no (super) estado anterior. A regra elaborate*state*name configura o nome do sub-estado criado (<s> ^name <name>) como sendo o nome do operador selecionado no super-estado (^superstate.operator.name <name>). As regras abaixo (figura) são responsáveis pela aplicação e remoção dos operadores selecionados:

Operadores Chase, Attack e Retreat

    Agora serão implementados os demais comportamentos que tornarão a operação do tank mais interessante. Lembrando-se que há quatro operadores responsáveis por implementar cada comportamento do tank. Os operadores, selecionados no super-estado, e suas possíveis ações estão listados a seguir:

Wander: Move, Turn

Chase: Move, Turn

Attack: Move, Turn, Slide, Fire

Retreat: Move, Wait

Operador chase

    O operador chase deverá ser proposto quando o tank detecta a aproximação de outro tank adversário, mas, não há informações suficientes sobre onde o adversário se encontra. O sensor Sound será usado porque este sensor acusa a proximidade de um tank (se movendo) numa dada direção. Caso haja algum sinal no radar do tank, indicando outro tank sendo detectado, não há a necessidade de perseguir o tank adversário. O tank pode atacar seu adversário. Há também a necessidade de verificar o arsenal de mísseis do tank. Não havendo mais mísseis, não faz sentido perseguir o tank adversário. Se a energia do tank também estiver baixa, o mesmo não poderá usar seu radar e nem seus escudos, devendo evitar aproximar-se do tank adversário. Essas condições estão resumidas na regra abaixo:

sp {propose*chase
   (state <s> ^name tanksoar
                   ^io.input-link <io>
                  -^missiles-energy low)
   (<io> ^sound <> silent
          -^radar.tank)
   -->
   (<s> ^operator.name chase)}

    Uma das condições dessa regra é que esse operador deve ser proposto apenas no super-estado inicial (state <s> ^name tanksoar). Essa mesma condição foi imposta ao operador wander definido anteriormente. Para se testar as condições de baixa energia e falta de mísseis (-^missiles-energy low) foi criada uma estrutura de dados na memória de trabalho para armazenar essas informações. A figura abaixo ilustra as regras responsáveis por isso:

    As regras elaborate*state*energy*low e elaborate*state*missiles*low tem a mesma ação, ou seja, indicar a falta de mísseis ou um nível baixo de energia (<s> ^missiles-energy low). Caso sejam disparadas ao mesmo tempo, o SOAR criará apenas uma estrutura na memória de trabalho. Essas regras também devem disparar apenas no super-estado (state <s> ^name tanksoar). A regra elaborate*task*tanksoar nomeia o super-estado (<s> ^name tanksoar) para permitir a identificação deste estado pelas regras que devem disparar nele.

    Após ser proposto, o operador chase é aplicado por meio da proposição e aplicação dos operadores move e turn. A figura abaixo exibe as regras de proposição desses operadores:

    Essas regras apenas disparam quando o sub-estado tiver sido criado pelo operador chase (state <s> ^name chase). As ações dessas regras são semelhantes às ações das regras que propunham os operadores move e turn do operador wander. A única alteração é que esses operadores testam a direção do som do tank adversário (^sound-direction). Assim, o operador move move o tank para frente, caso o som seja detectado à frente do tank. O operador turn gira o tank na direção do som, caso o som seja detectado nas laterais do tank. O operador backward gira o tanque para esquerda, caso o som seja detectado atrás do tank.

    O detecção da direção do som do tank adversário é possível devido à criação de um estrutura de dados na memória de trabalho. A regra chase*elaborate*state*sound-direction cria essa estrutura de dados (<s> ^sound-direction <sound>). Visto que o operador chase tem como objetivo perseguir e encontrar o tank adversário, é importante que o radar esteja ligado para permitir a visualização do tank adversário. A regra chase*elaborate*radar é responsável por isso. Essas regras são exibidas abaixo:

Operador attack

    O operador attack deve ser proposto quando o tank detectar outro tank adversário. Este operador fára o tank disparar mísseis no tank adversário. Porém, este operador não deverá ser aplicado caso o tank esteja com um nível baixo de energia e saúde. A regra que propõe este operador está descrita abaixo. Essa regra dispara apenas no super-estado (state <s> ^name tanksoar). Podem ser propostos mais de um operador (<s> ^operator <o> + =) se houver mais de um tank sendo detectado pelo radar.

sp {propose*attack
   (state <s> ^name tanksoar
                   ^io.input-link.radar.tank
                  -^missiles-energy low)
   -->
   (<s> ^operator <o> + =)
   (<o> ^name attack)}

    Os operadores que aplicam, de fato, o operador attack estão descritos nas figuras abaixo. Suas regras de proposição apenas disparam no sub-estado que tenha sido criado pela proposição do operador attack (state <s> ^name attack). Como pode haver mais de um tank sendo captado pelo radar, essas regras podem disparar mais de um operador ao mesmo tempo (<s> ^operator <o> + =).

    A regra attack*propose*fire-missile propõe atirar um míssil (^actions.fire.weapon missile), caso haja um tank no centro do radar (^radar.tank.position center). Essa regra também verifica se ainda há mísseis disponíveis no arsenal do tank (^missiles > 0). A regra attack*propose*slide detecta se não há um tank no centro do radar (-^tank.position center) e se há algum tank sendo detectado lateralmente (^tank.position { << left right >> <dir>). Caso sejam atendidas tais condições, esta regra propõe mover o tank na direção do tank detectado (^actions.move.direction <dir>). Essa regra também verifica se a direção proposta não está bloqueada (^blocked.<dir> no).

    A regra attack*propose*move verifica se o radar está detectando um tank que não esteja no centro do radar (-^tank.position center) e cuja direção (^position { << left right >> <dir> }) esteja bloqueada (^blocked.<dir> yes), impedindo o tank de perseguir o adversário. Caso sejam atendidas tais condições, essa regra propõe que o tank apenas se mova para frente (^actions.move.direction forward). Essa regra também verifica a distância do adversário (^distance <> 0), visto que o outro tank poderia estar do lado deste e, portanto, a ação correta seria se virar e atirar no adversário. A regra attack*propose*turn executa essa ação, ou seja, girar o tank na direção do adversário (^rotate.direction <dir>) e atirar um míssil (^fire.weapon missile). Essa regra verifica a distância do tank adversário (^distance 0) e sua direção (^position { << left right >> <dir> }).

Operador retreat

    O operador retreat será proposto em situações nas quais o tank encontra-se indefeso ou sob ataque. Ou seja, quando o tank estiver com pouca energia, não podendo usar seus escudos, e outro tank for detectado; ou ainda, quando o tank detecta a aproximação de mísseis adversários. As regras, descritas abaixo, que propõe este operador também só disparam no super-estado (state <s> ^name tanksoar).

    As regras propose*retreat*sound e propose*retreat*radar verificam a detecção de outro tank (^io.input-link.sound {<direction> <> silent} e ^io.input-link.radar.tank) e a condição de baixo nível de energia (^missiles-energy low). As regras propose*retreat*incoming e propose*retreat*incoming*not-sensed detectam a aproximação de um míssil (^io.input-link.incoming.<dir> yes). A diferença entre essas duas regras, propose*retreat*incoming e propose*retreat*incoming*not-sensed, é que a primeira verifica a condição de baixa energia (^missiles-energy low) e a segunda verifica a condição de não detecção do tank adversário (-^radar.tank e ^sound silent). Essa última regra serve para o caso do tank estar sob ataque mas não poder detectar seu adversário para atacá-lo (attack) ou perseguí-lo (chase).

    Antes de propor os operadores que aplicam, de fato, o operador retreat, é necessário recolher algumas informações importantes sobre o ambiente do jogo. As regras seguintes, descritas na figura abaixo, se encarregam de elaborar tais informações. Essas regras disparam apenas no sub-estado criado pela proposição do operador retreat (state <s> ^name retreat), criando estruturas de dados na memória de trabalho responsáveis por armazenar tais informações.

    A regra elaborate*retreat*sound*direction guarda todas as direções (<s> ^direction <direction>) onde foram detectadas a presença de outros tanks (^io.input-link.sound { <> silent <direction> }). A regra elaborate*retreat*radar*front informa que outro tank foi detectado (^io.input-link.radar.tank) logo à frente do tank (<s> ^direction forward). A regra elaborate*retreat*incoming*direction detecta a aproximação de mísseis adversários (^io.input-link.incoming.<dir> yes) e guarda as direções detectadas (<s> ^direction <dir>). A regra elaborate*retreat*radar*direction armazena as direções que devem ser evitadas (<s> ^avoid-direction <dir>), nas quais foram detectados tanks adversários pelo radar (^io.input-link.radar.tank.position { <dir> <> center }). Essa regra desconsidera tanks à frente do radar (<dir> <> center).

    Essas informações serão úteis para evitar mover o tank numa direção onde foi detectado um míssil ou outros tanks. As regras, acima, criarão estruturas de dados para armazenar as direções onde inimigos, tanks ou mísseis, foram detectados (^direction) e para armazenar as direções que devem ser evitadas (^avoid-direction), caso sejam detectados tanks fora da posição central do radar. Caso algumas dessas regras disparem ao mesmo tempo, tentando criar os mesmos tipos de WME's (com atributos de mesmo valor), apenas um WME será instanciado.

    A aplicação do operador retreat será feita propondo-se operadores do tipo move (retreat*propose*move) e, caso não seja possível propor tais operadores, um operador do tipo wait (retreat*propose*wait) será selecionado. A figura abaixo exibe as regras de proposição desses operadores. Essas regras apenas disparam no sub-estado criado pela proposição do operador retreat (state <s> ^name retreat).

    Os operadores move propostos devem mover o tank numa direção (^actions.move.direction <ndir>) que evite ameaças detectadas. O tank deve evitar as direções onde foram detectados outros tanks (-^avoid-direction <ndir>), direções em que foram detectados mísseis ou outros tanks (-^direction <ndir>), e evitar direções bloqueadas (^io.input-link.blocked.<ndir> no). O tank será movido lateralmente em relação às direções onde foram detectadas ameaças (^superstate.side-direction.<dir> <ndir>). Essas direções serão indicadas pela estrutura de dados ^side-direction criada na memória de trabalho pela regra seguinte:

sp {elaborate*sidestep-directions
   (state <s> ^name tanksoar)
   -->
   (<s> ^side-direction <sd>)
   (<sd> ^forward right left  
             ^backward right left
             ^right forward backward
             ^left forward backward)}

    A regra, acima, cria múltiplas opções de direções, para mover o tank, para cada direção considerada. Por exemplo, se a direção a ser evitada é forward, o tank poderá optar por mover-se lateralmente nas direções right ou left. Ambas direções serão propostas e um dos operadores (^actions.move.direction <ndir>) será selecionado. Essa regra dispara apenas no super-estado inicial criado (state <s> ^name tanksoar).

    O operador wait será sempre proposto (regra retreat*propose*wait, figura acima) com uma preferência ruim (<s> ^operator <o> + <), e apenas será proposto uma única vez (-^operator.name wait). Este operador será selecionado apenas quando não for possível propor operadores do tipo move. O operador wait sempre será proposto quando houver um impasse do tipo state no-change. Esse tipo de impasse ocorre quando nenhum operador pode ser proposto. Se não houvesse esse operador, wait, o SOAR geraria um cascata de impasses deste mesmo tipo. A figura abaixo exibe outras regras associadas a proposição e aplicação deste operador:

    A regra top-state*propose*wait testa se o estado tem os atributos ^attribute state e ^choices none. Caso esses atributos estejam assim definidos, a regra propõe o operador wait (<o> ^name wait). Esses atributos testados sempre são criados quando nenhum operador puder ser proposto (impasse state no-change). A regra top-state*apply*wait*random testa o nome do operador (<o> ^name wait) e aplica esse operador (<o> ^random elaboration).

    As outras regras, exibidas na figura acima, são responsáveis pela operação dos escudos. A regra elaborate*shields-on liga os escudos, se os mesmos estiverem desligados (^shield-status off), quando da aproximação de um míssil na direção do tank (^incoming.<dir> yes). A regra elaborate*shields-off desliga os escudos, se eles estiverem ligados (^shield-status on), sempre que nenhum míssil for detectado (-^incoming.<dir> yes). Essa estratégia é importante para evitar o consumo desnecessário de energia.

Produção simple-bot.soar

    Rodando o TankSoar com todas as regras descritas acima, na produção simple-bot.soar, agora o tank pode se mover, perseguir outros tanks, atacá-los e recuar para fugir de ameaças. A figura abaixo exibe a tela do SOAR Debugger. Curiosamente, percebe-se que quando o operador chase é proposto, nenhuma ação de perseguição é tomada, sendo que o tank decide retornar para o modo wander. Provavelmente esse comportamento foi causado porque o outro tank parou de se mover, não sendo possível detectá-lo devido à ausência de sons. No entanto, quando outro tank é detectado pelo radar, o tank dispara vários mísseis. Esse problema da ausência de sons será sanado adiante.

 

Imprementando a Detecção Sonora

    Quando um tank detecta outro e tenta perseguí-lo, ocorre que se o adversário parar de se mover, o tank não ouve mais seus sons e interrompe sua perseguição. Para sanar este problema, deve ser criada uma estrutura de dados na memória de trabalho para persistir a direção da qual veio o som do adversário. Serão propostos dois novos operadores. Um operador será responsável por gravar um novo som quando este for detectado pelo sensor Sound. O outro operador remove a estrutura de dados antiga quando a direção do som muda ou se o som gravado expira.

    A detecção de um som afeta a seleção dos operadores wander, chase e retreat. Portanto, a estrutura de dados que grava o som deverá ser criada no super-estado inicial. Os sons devem ser gravados durante a aplicação do operador wander, visto que a detecção de um som pode propor o operador chase. Os sons antigos devem ser removidos sempre que a direção do som mudar ou quando o som expira, independente de qual operador foi selecionado (wander, chase, attack, retreat).

    As regras abaixo (figura) são responsáveis pela proposição e aplicação do operador que grava um som captado pelos sensores. A regra wander*propose*record-sound propõe o operador (<o> ^name record-sound) quando o sub-estado tiver sido criado pela seleção do operador wander (state <s> ^name wander). Essa regra verifica se algum som foi detectado (^io.input-link.sound <> silent) e propõe o operador com uma melhor preferência (<s> ^operator <o> + >, =) para que seja imediatamente selecionado.

    A regra wander*apply*record-sound recolhe as informações sobre o super-estado (^superstate <ss>) para criar a estrutura que grava os sons (<ss> ^sound <sd>) nesse estado. Também são reunidas informações sobre o clock (^clock <clock>) para computar o tempo no qual o som expira (<sd> ^expire-time (+ <clock> 5)) e deve ser removido. Essa regra verifica a direção relativa do som (^sound <rel-sound>) em relação ao tank (forward, backward, left, right) e a direção para a qual o tank tende a se mover (^direction <direction>). Tais informações serão usadas para computar a direção absoluta (<ss> ^direction-map.<direction>.<rel-sound> <abs-sound>) de onde vem o som (<sd> ^absolute-direction <abs-sound>). A figura abaixo exibe a regra que cria a estrutura responsável pelo cômputo da direção absoluta do som (<s> ^direction-map <dm>) no super-estado inicial:

    Analisando a regra elaborate*directions, por exemplo, caso a direção absoluta do tank seja norte (<dm> ^north <north>) e a direção relativa do som seja esquerda (<north> ^left), a direção absoluta do som detectado será oeste (^left west). Porém, o operador chase necessita da direção relativa do som para poder perseguir o outro tank. A regra chase*elaborate*state*sound-direction (figura acima) calcula a direção relativa do som (<s> ^sound-direction <rel-sound>) a partir da direção absoluta do mesmo (^sound.absolute-direction <abs-sound>).

    A figura abaixo exibe as regras responsáveis por remover um antigo som gravado da memória de trabalho. A regra all*propose*remove-sound compara o tempo de expiração do som (^superstate.sound.expire-time <clock>) com o relógio atual (^io.input-link.clock > <clock>). Esta regra dispara nos sub-estados criados pela proposiçãos dos operadores wander, chase, attack, e retreat, propondo o operador remove-sound. A regra all*propose*remove-sound*changed-direction também propõe este mesmo operador. Essa regra dispara nos sub-estados relacionados aos operadores wander, chase, e retreat. A direção do som gravado é comparada com a direção do novo som detectado. Em ambas as regras anteriores, o operador é proposto com uma maior preferência de seleção (<s> ^operator <o> + =, >). A regra all*apply*remove-sound aplica o operador remove-sound, removendo o antigo som gravado da memória de trabalho.

    Finalizando, deve-se alterar as regras que propõe os operadores wander e chase (figura abaixo). Deve-se alterar as condições que verificavam a detecção (ou não) de um som, substituindo-as pelo teste da estrutura de dados que grava os sons (^sound). A regra de proposição do operador retreat também deve sofrer esta mesma alteração.

Produção simple-sound-bot.soar

    A figura abaixo exibe a tela do SOAR Debugger gerada pelo uso da produção simple-sound-bot.soar. Pode-se visualizar que foram propostos os operadores record-sound e remove-sound, responsáveis pela gravação de um novo som e remoção de sons antigos gravados. Desta vez, o operador chase foi selecionado e, de fato, aplicado. Após perseguir o tank adversário, o tank ataca-o (operador attack) com seus mísseis. O operador retreat também foi proposto, fazendo o tank recuar após o ataque.

 

Criando um Mapa

    Conforme o tank navega pelo seu ambiente, seria interessante se ele pudesse criar um mapa do ambiente para ser usado posteriormente. Esse mapa poderia ser usado para localizar os carregadores de energia e de saúde (conforme será proposto posteriormente) ou para melhor controlar o uso do seu radar. O mapa será continuamente criado, independente de quais operadores forem selecionados para controlar o comportamento do tank. O código responsável pela criação do mapa será construído sobre o código da produção simple-sound-bot.soar descrita anteriormente.

    O mapa será criado como uma estrutura de dados (^map) na memória de trabalho que será atualizada enquanto o tank move-se pelo seu ambiente. Essa estrutura é composta de 16x16 elementos (squares), ao invés de 14x14 células, porque também serão armazenadas as bordas do mapa do jogo. A regra propose*init-map, exibida na figura abaixo, propõe a seleção do operador (<o> ^name init-map) responsável pela inicialização do mapa. Esse operador será proposto com uma maior preferência (<s> ^operator <o> + >) sobre outros operadores também propostos. A regra apply*init-map (figura abaixo) cria e inicializa o mapa.

    Cada square é inicializado apenas com suas coordenadas X-Y. Outras informações adicionais serão acrescentadas, posteriormente, a cada elemento do mapa criado. Para facilitar o acesso às células adjacentes a cada célula do mapa, serão usadas outras duas regras para definir as células adjacentes ao sul e à leste de cada célula. Essas regras testam se o operador init-map já foi aplicado. A regra top-ps*apply*init-map*add-adjacencies*east cria um atributo, ^east-x, que armazena o valor X da célula ao leste de cada célula do mapa; este valor é igual ao valor de X da célula acrescido de 1 (<sq> ^east-x (+ <x> 1)). Da mesma forma, a regra top-ps*apply*init-map*add-adjacencies*north cria o atributo ^south-y que armazena o valor Y da célula ao sul de cada célula do mapa; isto é, o valor Y da célula acrescido de 1 (<sq> ^south-y (+ <y> 1)).

    As regras top-ps*apply*init-map*add-adjacent*link*east e top-ps*apply*init-map*add-adjacent*link*north criam links (norte, sul, leste, oeste) para as células adjacenstes a cada célula. Assim, isso facilitará a navegação nas direções adjacenstes a cada célula do mapa. Essas regras verificam os atributos ^east-x e ^south-y, criados para cada célula, e usam os valores das coordenadas X e Y desses atributos para definir quais células estão ao norte-sul (<sq1> ^south <sq2>, <sq2> ^north <sq1>) e quais estão ao leste-oeste (<sq1> ^east <sq2>, <sq2> ^west <sq1>) de cada célula.

    Outra tarefa importante (figura abaixo) é inicializar as células da borda do mapa. Essas células possuem obstáculos (<sq> ^obstacle *yes*) e devem, portanto, ter esse atributo definido. As regras da figura abaixo serão responsáveis pela definição desse atributo para as células da borda do mapa. Essas regras verificam as células que contêm as coordenadas X igual a 0 ou 15, e as células de coordenada Y igual a 0 ou 15.

    Após criar o mapa e inicializá-lo, agora é preciso escrever as regras para mantê-lo atualizado. O primeiro passo nessa tarefa é definir a célula na qual o tank encontra-se, ao ser instanciado no mapa. A regra all*map*curent-square verifica a posição do tank e busca no mapa a célula corespondente a tal posição. Então, uma variável ^square é criada para armazenar a célula corrente na qual o tank encontra-se. Essa regra dispara sempre que o tank mudar de posição, atualizando a sua posição em relação ao mapa. Abaixo, está descrita essa regra:

sp {all*map*curent-square
   (state <s> ^name tanksoar
                   ^io.input-link <il>
                   ^map.square <cs>)
   (<il> ^x <x>
          ^y <y>)
   (<cs> ^x <x>
            ^y <y>)
-->
   (<s> ^square <cs>)}

    O próximo passo é usar as informações do radar para indicar a posição dos objetos detectados e atualizar suas posições em relação ao mapa criado. No entanto, o radar não informa as posições absolutas (X-Y) de cada objeto detectado, isto é, o radar apenas informa a posição do objeto relativa ao tank. Para resolver esse inconveniente, será criada uma estrutura (^radar-map) na memória de trabalho que será responsável por fornecer os valores X-Y de cada leitura possível do radar. A regra elaborate*directions, figura abaixo, cria essa estrutura de dados. Cada direção (northp, southp, westp, eastp) possui atributos (^center, ^right, ^left) para casar com as informações (^direction, ^position) fornecidas pelo radar, além dos atributos ^sx e ^sy usados para cálculo da correta posição do objeto no radar.

    As regras map*mark-object e map*record-object, figura abaixo, são responsáveis por marcar um objeto no mapa e, em seguida, atualizar sua posição gravando-a no mapa. Para cada objeto detectado no radar, são verificados sua direção e posição (^direction <dir>, ^position <pos>), além do tipo do objeto (<type> << health energy obstacle open missiles tank >>). Então, essas informações serão casadas com a estrutura ^radar-map (^radar-map.<dir> <dirr>, <dirr> ^<pos> <pss>), fornecendo as informações corretas para o cálculo da posição absoluta do objeto detectado (<obs> ^x (+ (+ <x> (* <sx> <d>)) <dx>), ^y (+ (+ <y> (* <sy> <d>)) <dy>)). Será, então, criado um objeto temporário (<m> ^<type> <obs>) para armazenar o objeto detectado e sua posição absoluta. Posteriormente, esse objeto será casado com as células do mapa, gravando sua posição e atualizando o mapa (<sq> ^<type> *yes*). A notação :o-support indica que a estrutura do mapa criado (e atualizado) deve persistir na memória de trabalho.

    Alguns objetos detectados pelo radar podem mudar de posição ao longo do jogo. Pacotes de mísseis podem surgir em posições, e em tempos, aleatórios. Outros tanks estão sempre se movendo pelo mapa do jogo. Assim, suas posições devem ser atualizadas e sua antigas posições, removidas do mapa. As regras abaixo (figura) são responsáveis por isso. Essas regras verificam se um célula, no mapa, está marcada como ^missiles ou ^tank e se a célula (ao ser novamente detectada pelo radar) agora encontra-se vazia (^open). Caso sejam atendidas tais condições, o conteúdo da célula é atualizado no mapa, ou seja, seu valor antigo será removido do mapa.

    As regras exibidas na figura abaixo permitem que os tanks e mísseis, detectados e gravados no mapa, sejam removidos do mapa mesmo se estes não estiverem sendo detectados pelo radar. Eles serão removidos caso ocupem a mesma célula que está sendo ocupada pelo tank (^square <cs>). Ou seja, se o tank ocupa a mesma célula que um desses objetos é porque o outro tank se moveu (e não foi detectado) ou o pacote de mísseis foi recolhido por um dos tanks.

    Finalizando a atualização do mapa, as regras abaixo (figura) serão responsáveis por marcar as células que estão vazias ou que estejam ocupadas por um objeto Health Charger ou Energy Charger e que não tenham sido detectadas pelo radar. Novamente, o conteúdo dessas células será alterado caso o tank esteja sobre uma célula (^square <sq>) cujo conteúdo, gravado, não seja nenhum desses objetos (<sq> -^energy *yes*, <sq> -^health *yes*, <sq> -^open *yes*). No caso dos carregadores de energia e saúde, também será testado os atributos do tank responsáveis pela detecção desses objetos (^io.input-link.energyrecharger yes, ^io.input-link.healthrecharger yes) quando o tank está sobre um deles.

Produção mapping-bot.soar

    A figura abaixo exibe a tela do SOAR Debugger gerada pelo uso da produção mapping-bot.soar. Pode-se visualizar que foram disparadas várias regras map*mark-object. Uma dessas regras disparadas marcou, no mapa, a posição do carregador de saúde (M1 ^health O217 +) detectado pelo radar. Outra dessas regras disparadas marcou o pacote de mísseis (M1 ^missiles O220 +) que foi detectado no mapa do jogo.

    Pode-se visualizar, na figura abaixo, a detecção de um pacote de mísseis pelo tank, além da detecção do carregador de saúde. Analisando a figura anterior, nota-se que ambos os objetos possuem mesmo valor de Y. Pela figura abaixo, percebe-se que esses objetos estão alinhados horizontalmente no mapa do jogo.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer