Sumário
Nesse exercício foi analisado o código fonte do DemoSOAR no Netbeans a fim de entender seu funcionamento..
O DemoSoar possui uma classe chamada SimulationSOAR.java, que contém um método main. Dentro desse método é usada a classe NativeUtils para resolver o problema do gerenciamento do código JNI para diferentes versões de sistema operacional, conforme é apresentado na Figura 1.1.
Figura 1.1: Método principal do programa DemoSoar.
Como pode ser observado na Figura 1.1, na linha 1 do código fonte do DemoSoar é chamada a classe NativeUtils. Depois disso, no método main, essa classe é usada para carregar os arquivos necessários para o funcionamento do Soar, de acordo com o sistema operacional que está sendo usado pelo usuário. Na linha 35 é testado se o sistema é Windows, por meio da comparação com o valor armazenado na variável osName. Essa variável recebeu o valor que identifica o sistema operacional por meio do código da linha 29 (System.getProperties().getProperty("os.name")). Caso o sistema operacional seja Windows, na linha 36, verifica-se se a arquitetura do sistema é de 64 bits e, em caso positivo, nas linhas subsequentes ele carrega os arquivos referentes a esse sistema. Caso não seja 64 bits, ele carrega os arquivos por meio dos códigos das linhas 44, 45 e 46, que são referentes ao sistema de 32 bits. Na linha 48 ele verifica se o sistema operacional é o MAC e na linha 52, ele verifica se o
sistema é Linux. Para esse último, também é verificado se a arquitetura é 32 bits ou 64 bits. Para casa um dos dois sistemas, os arquivos correspondentes são carregados. Todos os arquivos referentes a cada sistema operacional estão encapsulados em um arquivo .jar que acompanha a aplicação.
Depois verificar o sistema operacional e carregar os arquivos referentes ao mesmo, a classe NativeUtils é utilizada para carregar o arquivo que contêm as regras SOAR que controlam o comportamento do robô do WorldServer3D, usando o código da linha 71. Depois, na linha 72 é informado o caminho do arquivo com as regras SOAR que controlam o robô do WorldServer3d e na linha 73 é informado o caminho do SoarDebugger. O valor informado é " " (vazio), porque o SoarDebugger deve estar no mesmo diretório da aplicação WorldServer3D. Já, na linha 74 é informada a porta de comunicação com o SoarDebugger.
Depois da inicialização da variáveis o programa entra no loop principal, que na Figura 1.1 está entre as linhas 87 e 91. Nesse loop, o método runSimulation() da classe simulationTask é executado enquanto a aplicação estiver sendo executada. Nesse método é feita uma comunicação com o SOAR e, por isso, para que haja tempo suficiente para a troca de informações, usou-se o código Thread.sleep(100); para retardar o início da próxima iteração do loop. A Figura 1.2 apresenta o código do método runSimulation().
Figura 1.2: Método runSimulation.
Conforme pode ser visto na Figura 1.2, a primeira tarefa no método runSimulation() é atualizar o estado da criatura, usando o código c.updateState();. Depois é armazenada a lista de objetos que estão no sensor de visão da criatura (linha 85). As informações obtidas na linha 85 da Figura 1.2 são preparadas para ser enviadas ao SOAR por meio do método prepareAndSetupCreatureToSimulation() que recebe como parâmetros a criatura e a lista de objetos, conforme pode ser visto na linha 95.
No método prepareAndSetupCreatureToSimulation() é criado o sensor visual da criatura e, em seguida, é passada a lista de objetos (variável things) para esse sensor, comforme é mostrado abaixo:
VisualSensor visualSensor = new VisualSensor();
visualSensor.setSensorReadings(things);
Depois, é criado o sensor de energia e é passado para esse sensor o estado atual da energia da criatura, obtido pelo comando creature.getFuel(), conforme é mostrado a seguir:
FuelSensor fuelSensor = new FuelSensor();
FuelSensorReadings fuelSensorReadings = new FuelSensorReadings();
fuelSensorReadings.setCurrentFuel(creature.getFuel());
fuelSensor.setSensorReadings(fuelSensorReadings);
O sensor visual (visualSensor) e o sensor de energia (fuelSensor), criados nos códigos mostrados anteriormente, são passados para a classe SimulationRobot por meio de uma variável chamada simulationCreature que é, posteriormente, passada como parâmetro do método setupStackHolder(), conforme é mostrado a seguir:
soarBridge.setupStackHolder(StakeholderType.CREATURE, simulationCreature);
Conforme pode ser visto acima, o método setupStackHolder() faz parte da classe soarBridge. Essa classe é a responsável por permitir o acesso ao SOAR. A classe setupStackHolder() desse método, recebe os dados do WorldServer3D e transmite esses dados para o SOAR. Por exemplo, esse método cria um sensor de energia para a criatura no SOAR e cria um elemento na memória de trabalho para armazenar o valor que representa o estado atual da energia usando os seguintes comandos:
Figura 1.3: Criando o sensor de energia e armazenando seu valor na memória de trabalho do SOAR.
Da mesma forma, ele também cria um sensor visual para a criatura no SOAR e cria elementos na memória de tabalho para armazenar os valores referentes aos objetos que estão sendo percebidos pela mesma:
Figura 1.4: Criando o sensor visual e armazenando seu valor na memória de trabalho do SOAR.
Conforme pode ser visto nas Figuras 1.3 e 1.4, para a criação dos sensores no SOAR e dos elementos da memória de trabalho é usada a classe Agent. Essa classe usa o pacote sml para manipular a memória de trabalho do agente no SOAR. As regras que usam esses elementos estão contidas no arquivo soar-rules.soar.
Voltando a Figura 1.2, após a execução do método prepareAndSetupCreatureToSimulation(), é executado o método runSimulation(), que também faz parte da classe soarBridge. Esse método é o responsável por executar o agente que foi criado e preparado pelo método prepareAndSetupCreatureToSimulation(). Para isso ele chama o método RunSelfTilOutput() que faz parte do pacote sml usado pela classe Agent.
Após a execução do agente, é chamado o método processResponseCommands(), conforme pode ser visto na linha 101 da Figura 1.2. Esse método é responsável por obter e processar os comandos esviados pelo SOAR. A primeiro tarefa executa nesse método é a chamada do método getReceivedCommands() que pertence a classe SoarBridge e retorna uma lista de comandos do SOAR. Os possíveis comandos retornados são MOVE, GET e EAT que estão no output-link do SOAR. O comando MOVE pode tem os seguintes argumentos: rightVelocity, leftVelocity, linearVelocity, xPosition e yPosition. Já o parâmetro GET tem como argumento o thingNameToGet. Por outro lado, o argumento do parâmetro EAT é o thingNameToEat.
Depois que a lista de comandos do SOAR é recebida pelo método processResponseCommands() ele executa o comando equivalente a cada tarefa. Então, por exemplo, se o comando é MOVE, ele executa esse comando chamando o método processMoveCommand que chama algum método da classe CommandUtility da biblioteca WS3DProxy para realizar o comando desejado, tal como ir para uma determinada posição ou girar para uma determinada posição. Se o comando for GET, é chamado o método processGetCommand que chama o método putInSack da classe creature da biblioteca WS3DProxy. Caso o comando seja EAT, é chamado o método processEatCommand que chama o método eatIt da classe creature da biblioteca WS3DProxy. Logo, fica claro o papel da biblioteca WS3DProxy, que recebe e executa os comandos retornados pelo SOAR.
Conforme foi mencionado na seção anterior, no método main é informado o nome e o diretório em que se encontram as regras do SOAR que serão usadas para controlar o agente do WorldServer3D. O arquivo usado nessa aplicação é o soar-rules.soar.
A primeira tarefa no arquivo soar-rules.soar está relacionada ao operador wander. Esse operador faz o agente se movimentar pelo mundo virtual com o propósito de explorá-lo e posteriormente encontrar objetos que possam ser usados em alguma outra tarefa. A primeira regra propõe esse operador e a segunda regra aplica o oeprador quando ele é selecionado, fazendo com que o agente se movimente pelo mundo virtual usando as seguintes configurações: ^Vel 0, ^VelR 2 e ^VelL 0. Já, a terceira regra relacionado a esse operador é usada para remover o comando depois que o movimento foi completado. Essas regras são apresentadas na Figura 1.5.
Figura 1.5: Regras relacionadas ao operador wander.
Outro operador proposto é o seeEntityWithMemoryCount. Este operador é usado para armazenar na memória as entidades que estão no sensor visual da criatura (JEWEL ou FOOD). Ele somente é selecionado quando o agente já possui entidades na memória de trabalho, conforme pode ser visto na Figura 1.6.
Figura 1.6: Operador seeEntityWithMemoryCount.
Quando não há nenhum objeto JEWEL ou FOOD no campo visual do agente, em vez de ser proposto o operador seeEntityWithMemoryCount, é proposto o seeEntityWithoutMemoryCount. Este operador tem a mesma função do operador anterior, pois cria os elementos necessários para armazenar informações referentes aos objetos JEWEL ou FOOD, conforme mostra a Figura 1.7.
Figura 1.7: Operador seeEntityWithoutMemoryCount.
Quando o agente detecta uma comida no seu campo visual é proposto e aplicado um operador que faz ele se movimentar até essa comida. Depois que o movimento é completado, o comando de movimento, bem como a comida são removidos da memória de trabalho do agente, usando as regras mostradas na Figura 1.8.
Figura 1.8: Operador moveFood.
Depois que o agente se movimenta para a direção de uma comida e está a uma distância menor que 30, é proposto o operador eatFood. Depois que esse operador é selecionado, então a regra que o aplica é ativada e, consequentemente, é gerado o comando que faz o agente comer o alimento. Depois que essa tarefa é completada, o comando é removido da memória de trabalho. Essas regras podem ser vistas na Figura 1.8.
Figura 1.9: Operador eatFood.
Outras regras importantes são as referentes às jóias (jewel). Há uma regra que propõe, aplica e remove o operador moveJewel. Essas regras não serão apresentadas pois são semelhantes às regras referentes ao operador moveFood. Há também uma regra que remove a entidade jewel da memória de trabalho, depois que o comando que move o agente até uma jóia é completado. Além disso, quando o agente se movimenta para a direção de uma jóia e está a uma distância menor 30, é proposto o operador getJewel. A regra que propõe e a que aplica esse operador são semelhantes as regras de proposição e aplicação do operador eatFood. Depois que o comando é completado com sucesso, a regra que exclui o comando da memória de trabalho é executada, de forma semelhante a regra que remove o comando eatFood.
Outra regra existente no arquivo soar-rules.soar é a que propõe o operador avoidBrick. Esse operador é selecionado quando o agente está a uma distância menor ou igual a 61 em relação a uma parede. Quando esse operador é selecionado, então é aplicada a regra que executa o comando MOVE a fim de fazer o agente se movimentar para uma direção que evite a parede. Depois que o comando de movimento é completado, a entidade do tipo BRICK é removida da memória de trabalho. Além disso, também é executada a regra que remove o comando MOVE. Veja essas regras na Figura 1.10.
Figura 1.10: Regra que propõe o operador avoidBrick, regra que aplica o operador e executa o comando MOVE e regras que removem a entidade BRICK e o comando MOVE da memória de trabalho.
Para que os operadores apresentados até o momento possam ser utilizados para controlar o agente é necessário evitar impasses na escolha dos operadores. Para isso, foram criadas regras que definem as preferências para cada operador. Por exemplo, os operadores seeEntityWithMemoryCount e seeEntityWithoutMemoryCount, que são responsáveis por armazenar as entidades do sensor visual do agente na memória de trabalho, têm preferências iguais (indiferentes) quando um dos dois pode ser selecionado, mas têm preferência maior que os operadores moveJewel, moveFood e avoidBrick. Para criar essas preferências foram usadas as regras apresentadas na Figura 1.11.
Figura 1.11: Preferências relacionadas aos operadores seeEntityWithMemoryCount e seeEntityWithoutMemoryCount.
Como foi explicado anteriormente, se o agente estiver a uma distância menor do que 30 de uma jóia, o operador getJewel é proposto. Nesse caso, ele tem preferência sobre os operadores moveJewel e moveFood. Além disso, se o outro operador proposto for o avoidBrick, o operador getJewel também tem preferência, pois ele pode mudar a direção com o objetivo de desviar da parede após obter o alimento. Por outro lado, se o agente tiver em seu sensor visual mais de uma jóia com uma distância menor que 30, mais de um operador getJewel será proposto, o que irá gerar um tie impasse. Nesse caso, deverá ser selecionado o operador que fará o agente obter a jóia que estiver mais próxima. Se no sensor visual do agente tiver mais de uma jóia a uma distância maior que 30, mais de um operador moveJewel será proposto e, como no caso anterior, o operador referente a jóia mais próxima deve ser selecionado. As regras que determinam essas preferências são
apresentadas na Figura 1.12.
Figura 1.12: Preferências entre os operadores relacionados ao objeto jóia (jewel).
As preferências entre os operadores relacionados ao objeto comida (food) são semelhantes ao que foi apresentado no caso anterior. Se o agente estiver a uma distância menor do que 30 de uma comida, o operador eatFood é proposto e tem preferência sobre os operadores moveFood, moveJewel e avoidBrick. Além disso, se o agente detectar através de seu sensor visual mais de uma comida com uma distância menor que 30, então, mais de um operador eatFood será proposto, o que irá gerar um tie impasse. Logo, deverá ser selecionado o operador que fará o agente consumir a comida mais próxima. Se no sensor visual do agente for detectada mais de uma jóia a uma distância maior que 30, mais de um operador moveFood será proposto e, dessa forma, o operador referente a comida mais próxima deve ser selecionado. As regras que definem essas preferências não serão apresentadas, pois são semelhantes às regras apresentas na Figura 1.12.
Se um tie impasse for provocado por serem propostos, ao mesmo tempo, os operadores moveFood e moveJewel, então a preferência para um dos operadores será atribuída da seguinte forma: se a energia do agente for menor do que um determinado limiar, então o operador moveFood tem maior preferência e, em caso oposto, o operador moveJewel tem preferência superior. As regras que definem a preferência entre os dois operadores são apresentadas na Figura 1.13.
Figura 1.13: Preferências entre os operadores moveFood e moveJewel.
Assim como ocorre com os objetos jóia e comida, quando mais de uma parede é detectada pelo agente através de seu sensor visual, mais de um operador avoidBrick é proposto para executar o comando MOVE e desviar o agente. Para resolver esse tie impasse, é atribuída maior preferência para o operador relacionado à parede que está mais próxima. Em outra situação, quando é proposto o operador moveJewel ou moveFood ao mesmo tempo que o operador avoidBrick, esse último tem maior preferência sobre os outros dois. Na Figura 1.14 são apresentadas as regras que definem essas preferências.
Figura 1.14: Preferências relacionadas a entidade brick.
Por fim, a última regra do arquivo soar.rules defini que o operador wander tem baixa preferência, conforme é mostrado na Figura 1.15.
Figura 1.15: Definição da preferência do operador wander.
Nessa seção serão apresentadas as modificações que foram feitas no programa DemoSOAR para que, no controle da criatura, os leaflets dela sejam levados em consideração. Leaflets são metas de obtenção de jóias que podem ser passadas para as criaturas do WorldServer3D.
Resumidamente, com as alterações que foram feitas, a criatura vai atrás apenas das jóias que estão no leafleft e das comidas. Caso, durante o percurso da criatura tenha uma jóia que não está em seu leaflet barrando seu caminho, ela pega essa jóia. Quando a criatura tem em seu campo de visão dois objetos iguais, ele dá preferência para o que estiver mais próximo e, caso sejam duas jóia, ela dá preferência para a que pertence ao maior leaflet. Caso tenha que escolher entre uma jóia e uma comida, ela dá preferência para a comida.
Uma das alterações no DemoSoar foi a criação de dois métodos na classe SimulationRobot: uma para definir a lista de leaflets e outro para obter a lesta de leaflets da criatura.
Figura 2.1: Métodos setLeafletList() e getLeafletList().
Os dois métodos apresentados acima foram chamados no método prepareAndSetupCreatureToSimulation da classe SimulationTask, conforme mostra a Figura 2.2.
Figura 2.3: Alteração no método prepareAndSetupCreatureToSimulation.
Também foi alterado o método setupStackHolder da classe SoarBridge para que ele passe para o SOAR, no input-link, a lista de leaflets da criatura. Nessa lista, as cores que não aparecem receberam o valor 0 para o número de jóias desejadas e recebidas, conforme mostra a Figura 2.3.
Figura 2.3: Alteração no método setupStackHolder.
Alterações no arquivo de regras do SOAR
Um dos problemas observados durante a execução do trabalho é que o programa WorldServed3D não atualiza a quantidade de jóias do leaflet que já foram coletadas. Então, para facilitar a atualização dos leaflets foi criada uma estrutura para armazenar todas as informações sobre cada um dos leaflets na memória de trabalho do SOAR, comforme mostra a Figura 2.4.
Figura 2.4: Armazenamento dos leaflets na memória de trabalho.
Foram criadas também, regras que verificam se existe alguma jóia na memória de trabalho, que esteja a menos a uma distância menor que 30 e que possa ser usada para preencher algum leaflet. Quando isso acontece, é proposto o operador relacionado a um dos leaflets que precisam dessa jóia. Depois que a jóia é capturada pela criatura, o comando GET é removido da memória de trabalho. A Figura 2.3 apresenta as regras relacionadas ao primeiro leaflet (leaflet0). As regras relacionadas aos outros leaflets não serão apresentadas pois são quase iguais as regras da Figura 2.6, mudando apenas o nome do operador.
Figura 2.6: Regras relacionadas ao operador getJewelLeaflet0.
Outro ponto importante para o bom funcionamento do programa é a definição das preferências. Uma das regras criadas defini que os operadores getJewelLeaflet0, getJewelLeaflet01 e getJewelLeaflet2, que são responsáveis por coletar jóias para preencher o leaflet, têm preferência sobre o operador moveJewel. Também foi criada uma regra que defini que os três operadores que capturam jóias para o leaflet têm preferência sobre o operador getJewel, que captura jóias que não estão no leaflet. Veja as duas regras na Figura 2.7.
Figura 2.7: Preferências dos operadores getJewelLeaflet0, getJewelLeaflet01 e getJewelLeaflet2 em relação aos operadores moveJewel e getJewel.
Em algumas ocasiões mais de leaflet precisa de um jóia de determinada cor. Nesses casos, mais de um operador será acionado para coletar a mesma jóia. Então, foram criadas regras que definem que terá preferência o operador relacinado ao leaflet com maior recompensa (payment) para a criatura, conforme é mostrado na Figura 2.8.
Figura 2.8: Regras que definem as preferências entre os operadores relacionados a cada leaflet.
Também foi criada uma regra que defini que os operadores relacionados ao leaflet tem maior prefência em relação ao operador avoidBrick que desvia a criatura da parede.
Figura 2.9: Definição da preferência entre o operador avoidBrick e os operadores que capturam jóias para o leaflet.
Por fim, a última regra defini que quando um mesmo operador for selecionado para capturar uma entre duas jóias diferentes para preencher um dos leaflets, gerando um tie impasse, deve-se capturar a jóia que estiver mais próxima. Veja na Figura 2.10.
Figura 2.10: Regra que dá preferência para a captura da jóia que estiver mais próxima.
Ao executar o programa, percebe-se que as regras funcionaram como esperado. Por exemplo, a Figura 2.11 mostra o resultado no SoarDebugger que confirma que as regras de preferência apresentadas na Figura 2.8 fazem com que o leafleft com maior recompensa receba a jóia, nas situações em que dois leaflets disputam a mesma jóia. Veja que na Figura abaixo, que o leaflet 0 gera menor recomensa para a criatura em relação ao leaflet2. Por isso, o operador getJewelLeaflet2 é selecionado.
Figura 2.11: Escolha de qual leaflet receberá uma jóia que está sendo disputada.
Outra aspecto da execução é apresentado na Figura 2.12, que mostra que a quantidade de jóias recebidas pelo leaflet é incrementado quando a criatura captura uma nova jóia.
Figura 2.12: Incremento das jóias do leaflet quando uma jóia é capturada.