Lab3 Pedro Cau

aluno: Pedro de Sousa Cau Ramos Salles
ano/sem: 2008/2o.
06/08/2008 (2)

Introdução

O objetivo deste laboratório foi desenvolver uma maquina de estados finita determinista. Tal maquina deve ser capaz de manejar vários estados, com transições entre eles. Tais transições são executadas através de eventos definidos. Esta maquina deve ter suporte a executar ações nas transições e nas chegadas a um estado. O evento também pode ter uma condição de guarda que se falso impede que a transição ocorra.

Desenvolvimento

A seguir temos o diagrama de classes da implementação da maquina de estados:

diagrama.PNG

Nesta implementação, temos que cada estado, cada transição e cada evento são objetos das classes State, Transition e Event, respectivamente.
Classe State armazena seu nome e uma lista com as transições possíveis de saída desse estado, e uma ação a ser executada na chegada a esse estado que é um objeto da classe StateCommand.
A classe Transition possui uma referencia ao estado de destino e ao evento que ocasiona essa transição. Ela não possui nome, uma vez que é definida pelo evento.
A classe Event armazena seu nome. Possui também uma condição de guarda (Classe EventGuard) e uma ação a ser executada na transição (Classe EventCommand) .
O funcionamento dessa maquina de estados é basicamente o seguinte:
A máquina de estados possuiu uma referencia para o seu estado atual.
Um estado armazena transições que possuem referencias a um destino. Quando um evento é disparado, procura-se qual transição do estado atual possui o evento chamado. Ao encontrar, esse objeto retorna o seu estado de destino que passa a ser referenciado pelo estado atual da maquina.
Na classe StateMachine, temos que o método fireEvent() delega a ação para o fireEvent() do estado atual:

public boolean fireEvent(String name) throws UndefinedTransitionException
{
        //verifico antes se o nome de evento eh valido:
        getEvent(name);
        String previousStateName = currentState.getName(); 
        currentState = currentState.fireEvent(name);//delegando o fireEvent para o estado atual
 
        getEvent(name).executeCommand(previousStateName); //executo a acao de transicao
        currentState.executeCommand(); //executo a acao de chegada a um estado
 
        notifyObservers();
        return true; //nao faço a menor ideia pra que serve esse boolean de retorno
}

Na classe State, temos o metodo fireEvent:

public State fireEvent(String eventName) throws UndefinedTransitionException
{
 
    for(Transition trans : transitions) //Busco nas transicoes
    {
        if(trans.getEventName().equals(eventName))//qual delas possui o evento chamado eventName
        {  //uma vez encontrado
            Event event = trans.getEvent();
            if(event.checkCondition()) //verifico a condicao de guarda
            {
                return trans.getDesination(); //e finalmente retorno o destino 
            }
            else throw new UndefinedTransitionException("Condition not satisfied"); //condicao nao foi satisfeita
        }
    }
    throw new UndefinedTransitionException("Not found: "+eventName);//o evento nao foi encontrado neste estado
}

Os comments explicam a idéia do código acima.
Esta então é a idéia principal desta implementação da maquina de estados.

Observação: o método fireEvent() da classe StateMachine retorna um boolean, de acordo com os protótipos dados, porem em nenhum lugar do roteiro fala quando que deve retornar true ou false. Na falta de idéia coloquei para retornar sempre true.

Sources

Temos a seguir o código completo da maquina de estados:

package stateMachine;
import java.util.*;
 
public class StateMachine
{
    ArrayList<State> states = new ArrayList<State>(); //armazena todos os estados
    ArrayList<Event> events = new ArrayList<Event>();  //armazena todos os eventos
    ArrayList<Observer> observers = new ArrayList<Observer>(); //observers
    State currentState; //guarda uma referencia ao estado atual da maquina
 
    public StateMachine()
    {
       //nao faz nada, mas devia receber e criar o estado inicial
    }
 
   //comportamento de observável
    public void registerObserver(Observer observer)
    {
        observers.add(observer);
    }
    protected void notifyObservers()
    {
        for(Observer obs : observers)obs.update(this,currentState.getName());
 
    }
    //métodos para a definição da máquina
    public void createState(String name, StateCommand onEnter)
    {
        states.add(new State(name,onEnter));//adicionando um novo estado a lista
    }
    public void createState(String name)
    {
        states.add(new State(name, null)); //idem acima
    }
    public void setInitialState(String name)
    {
        currentState = getState(name); //define o valor inical de currentState
    }
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess) throws UndefinedStateException
    {
 
        Event newEvent = new Event(name,onSuccess); //crio um evento
 
        for(String fromOne : from) //itero pelas origens
        {
            getState(fromOne).addTransition(getState(to),newEvent); 
             //adiciono a transicao com o destino e o evento ao estado de origem
        }
         events.add(newEvent); //guardo o novo evento na lista de eventos
    }
    //Os outros overloads seguem a mesma ideia do createEvent acima
    public void createEvent(String name, String[] from, String to) throws UndefinedStateException
    {
        Event newEvent = new Event(name,null);
 
        for(String fromOne : from)
        {             
             getState(fromOne).addTransition(getState(to),newEvent);
        }
        events.add(newEvent);
    }
    public void createEvent(String name, String from, String to,  EventCommand onSuccess) throws UndefinedStateException
    {
        Event newEvent = new Event(name,onSuccess);
        getState(from).addTransition(getState(to),newEvent);
        events.add(newEvent);
    }
    public void createEvent(String name, String from, String to) throws UndefinedStateException
    {
        Event newEvent = new Event(name,null);
        getState(from).addTransition(getState(to),newEvent);
        events.add(newEvent);
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState) throws RuntimeException
    {
        getState(fromState).addTransition(getState(toState),getEvent(event)); //adciona uma transicao com um evento ja criado
    }
    public void appendTransitionToEvent(String event, String[] fromState, String toState) throws RuntimeException
    {
        for(String fromOne : fromState)
        {
            getState(fromOne).addTransition(getState(toState),getEvent(event));
        }
    }
    public void setGuardToEvent(String event,EventGuard g) throws UndefinedEventException
    {
        getEvent(event).setEventGuard(g); //define um evento de guarda a um evento
    }
    // métodos para a execução da máquina
    public boolean fireEvent(String name) throws UndefinedTransitionException
    {
        //verifico antes se o nome de evento eh valido:
        getEvent(name);
        String previousStateName = currentState.getName(); 
        currentState = currentState.fireEvent(name);//delegando o fireEvent para o estado atual
 
        getEvent(name).executeCommand(previousStateName); //executo a acao de transicao
        currentState.executeCommand(); //executo a acao de chegada a um estado
 
        notifyObservers();
        return true; //nao faço a menor ideia pra que serve esse boolean de retorno
    }
 
    public String getCurrentStateName()
    {
        return currentState.getName(); //retornando o nome do estado atual
    }
 
    private State getState(String name) throws UndefinedStateException
    {//funcao para buscar na lista um estado dado seu nome
        for(State st : states) 
        if(st.getName().equals(name)) return st;
 
        throw new UndefinedStateException("State" +name+ " not found");
    }
 
    private Event getEvent(String name) throws UndefinedEventException
    {//funcao para buscar na lista um evento dado seu nome
        for(Event evt : events)
        if(evt.getName().equals(name)) return evt;
 
        throw new UndefinedEventException("State" +name+ " not found");
    }
}

É importante ressaltar que foi necessário uma alteração em relação aos protótipos dos métodos dados , relativo aos métodos createEvent() e appendTransitionToEvent() e seus overloads, adicionando os throws exceptions, uma vez que na minha implementação não é possível criar uma transição com estados inválidos, então o exception deve ocorrer na criação do evento e não na execução do fireEvent(), como era proposto.

Classe State:

package stateMachine;
import java.util.*;
 
public class State
{
   String name;
   StateCommand myCommand;
   ArrayList<Transition> transitions = new ArrayList<Transition>(); //lista para armazenar as possiveis transições deste estado
 
   public State(String name, StateCommand sCommand)
   {
       this.name = name;
       myCommand = sCommand;
   }
 
   public String getName()
   {
       return name;
   }
 
   public void addTransition(State to,Event event)
   {//adicionar uma nova transição de saida desse estado
       transitions.add(new Transition(to,event));
   }
 
   public void executeCommand()
   {
       if(myCommand != null)myCommand.execute();//executa o comando de chegada a um estado
   }
 
   public State fireEvent(String eventName) throws UndefinedTransitionException
   {
 
       for(Transition trans : transitions) //Busco nas transicoes
       {
           if(trans.getEventName().equals(eventName))//qual delas possui o evento chamado eventName
           {  //uma vez encontrado
               Event event = trans.getEvent();
               if(event.checkCondition()) //verifico a condicao de guarda
               {
                   return trans.getDesination(); //e finalmente retorno o destino 
               }
               else throw new UndefinedTransitionException("Condition not satisfied"); //condicao nao foi satisfeita
           }
       }
       throw new UndefinedTransitionException("Not found: "+eventName);//o evento nao foi encontrado neste estado
   }
}

Classe Transition:

package stateMachine;
 
public class Transition
{
    Event event; //guarda seu evento
    State destination; //guarda o destino
 
    public Transition(State to,Event event)
    {
 
        this.event = event;
        destination = to;
    }
 
    public State getDesination()
    {
        return destination; // retorna o destino desta transicao
    }
 
    public String getEventName()
    {
        return event.getName();
    }
    public Event getEvent()
    {
        return event; //retorna seu evento
    }
}

Classe Event:

package stateMachine;
 
class Event
{
        String nome;
        EventGuard myCondition;
        EventCommand myCommand;
        public Event(String nome,EventCommand command)
        {
            this.nome = nome;
            myCommand = command;
            myCondition = null;
        }
 
        public void setEventGuard(EventGuard condition)
        {
            myCondition = condition; //define um evento de guarda
        }
 
        public String getName(){return nome;}
 
        public boolean checkCondition()
        {
            if(myCondition != null)//se este evento tiver alguma condicao de guarda
                return myCondition.shouldPerform();//retorna seu resultado
            else return true; 
        }
 
        public void executeCommand(String previousState)
        {
            if(myCommand != null)
            myCommand.execute(previousState);//executa o comando de transicao 
        }
}

Exceptions:

Os exceptions são nada mais que subclasses de Exception e RuntimeException possuindo somente um construtor que passa um string como parâmetro para o construtor de suas respectivas superclasses.

Commands :

As classes EventCommand, StateCommand e EventGuard não sofreram alteração em relação ao roteiro do laboratório.

Testes

A classe de testes dada no roteiro teve de sofrer uma alteração para poder se adequar à minha implementação. A alteração foi necessária devido ao fato de minha maquina de estados gerar UndefinedStateException na hora de criar uma transição com nome de estado invalido, não somente na hora de executar o fireEvent(). O trecho em questão do código original da classe de testes esta mostrado a seguir:

public void testShouldReturnErrorIfStateDoesntExist() {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
 
        try {
            sm.fireEvent("jogar_bola");
            fail("deve gerar excessão");
        }catch(UndefinedStateError e) {
        }catch(UndefinedTransitionException e) {
        } 
    }

O trecho em questão foi alterado da seguinte forma:

public void testShouldReturnErrorIfStateDoesntExist() {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        try {
            sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
            sm.fireEvent("jogar_bola");
            fail("deve gerar excessão");
        }catch(UndefinedStateException e) {
        }catch(UndefinedTransitionException e) {
        }
    }

Como quem gera a exceção é o createEvent(), então simplesmente movi a linha que cria um evento com um estado invalido para dentro do try{}, para que a exceção possa ser tratada.
Mesmo que na minha implementação fosse possível uma transição ser criada entre estados inválidos, eu sugeriria essa alteração, uma vez que faz mais sentido gerar o erro na hora de criar a transição, do que na hora de executar a transição.

Ao aplicar os testes, foi obtido:

testes.PNG

A máquina de estados passou em todos os testes, o que mostra que ela está funcionando corretamente. O output no console também está correto:

output.PNG

Aplicação Professor e Turma:

A fim de testar a maquina de estados foi fornecida uma aplicação com o tema professor e turma onde as ações da turma influenciam no estado do professor. A fim de testar o funcionamento da maquina de estados nesta a aplicação, foi criado a classe ProfessorTurmaTest, mostrado a seguir:

public class ProfessorTurmaTest
{
 
    private void printState(WithSM a)
    {
        if(a instanceof Professor)System.out.println("Estado do professor: "+ a.getState());
        if(a instanceof Turma)System.out.println("Estado da turma: "+ a.getState());
    }
    private void printEvent(String nome, Turma turma)
    {
        System.out.println("Evento: " + nome);
        turma.fire(nome);
    }
    public void test()
    {
        Professor prof = new Professor();
        Turma turma = new Turma();
 
        turma.registerObserver(prof);
 
        printState(turma);
        printState(prof);
        printEvent("ser_atenta",turma);
        printState(turma);
        printState(prof);
        printEvent("usar_note",turma);
        printState(turma);
        printState(prof);
        printEvent("dormir",turma);
        printState(turma);
        printState(prof);
        printEvent("cancerizar",turma);
        printState(turma);
        printState(prof);
    }
}

Esta classe instancia um professor e uma turma e registra o professor como observador da turma, em seguida faz uma seqüência de disparos de eventos na turma. O resultado deste teste é impresso no console:

testePT.PNG

Podemos ver que a resposta aos eventos ocorreu como esperado.

Aplicação EnemyAi:

Para a aplicação da maquina de estados foi escolhido fazer um principio de inteligência artifical para jogos de ação . Para isso foi criado a classe EnemyAi, que pode ser descrito pelo seguinte diagrama:

smdiagram.PNG

Nesta aplicação, as ações do EnemyAi serão apenas println()’s representando as falas do individuo. Numa real implementação,cada estado e evento seriam caracterizados por animações e ações. O código fonte desta classe está mostrado a seguir:

import stateMachine.*;
public class EnemyAi extends WithSM implements Observer
{
    private boolean berzerker = false;
    public EnemyAi()
    {
        super();
        setUpSM();
        sm.registerObserver(this);
    }
 
    private void setUpSM()
    {
        sm.createState("Idle");
        sm.createState("Searching");
        sm.createState("Attacking",new StateCommand(sm) {
            public void execute() {
                System.out.println("You will die!");
            }
        } );
        sm.createState("Evading",new StateCommand(sm) {
            public void execute() {
                System.out.println("Time to run!!");
            }
        } );
 
        sm.createState("Dying",new StateCommand(sm) {
            public void execute() {
                System.out.println("Arrgghhhh!!!!!");
            }
        } );
 
        sm.setInitialState("Idle");
 
        sm.createEvent("Hear sounds", "Idle","Searching", new EventCommand(sm) {
            public void execute(String previousState) {
                 System.out.println("I think I´ve heard something");
            }
        });
 
        String[] from = {"Idle","Searching"};
        sm.createEvent("Visual contact", from,"Attacking", new EventCommand(sm) {
            public void execute(String previousState) {
                if(previousState.equals("Idle"))System.out.println("How dare you come here?");
                if(previousState.equals("Searching"))System.out.println("I've found you!!");
            }
        } );
        sm.createEvent("Lost sight", "Attacking","Searching");
        sm.appendTransitionToEvent("Lost sight","Evading","Idle");
        String[] from2 = {"Idle","Searching","Attacking","Evading"};
        sm.createEvent("Enemy died", from,"Idle");
 
        String[] from3 = {"Attacking","Evading"};
        sm.createEvent("Hp low",from3,"Evading");
        sm.appendTransitionToEvent("Hp low","Idle","Idle");
        sm.appendTransitionToEvent("Hp low","Searching","Idle");
 
        String[] from4 = {"Idle","Searching","Attacking","Evading"};
        sm.createEvent("Hp zero",from4,"Dying");
 
         sm.setGuardToEvent("Hp low", new stateMachine.EventGuard() {
            public boolean shouldPerform(){
               return !berzerker; 
            }
        });
 
    }
 
    public void update(StateMachine sm, String newStateName)
    {
        System.out.println("Estado atual: "+ sm.getCurrentStateName());
    }
 
    public void setBerzerker(boolean set)
    {
        berzerker= set;
    }
 
    private void printEvent(String nome)
    {
        try{
        System.out.println("Evento: " + nome);
        sm.fireEvent(nome);
 
        }
        catch(UndefinedTransitionException e)
        {
            if(nome.equals("Hp low"))System.out.println("I´m furious!!");
            else System.out.println(e);
        }
    }
    public void test()
    {
        System.out.println("Estado atual: "+ sm.getCurrentStateName());
        printEvent("Hear sounds");
        printEvent("Visual contact");
        printEvent("Hp low");   
        printEvent("Lost sight");
        printEvent("Visual contact");
        printEvent("Hp zero");
    }
}

O método setUpSM() configura o state machine de acordo com o diagrama mostrado acima,e o método test() aplica uma sucessão de fireEvents() a fim de testar a aplicação. O método setBerserker() define o EnemyAi para o modo berserker, onde ele não irá fugir quando o Hp estiver baixo, atuando como guardEvent do evento Hp low. Para testar a aplicação, será executado o método test() para os modos normal e berserker. As saídas estão mostradas a seguir:

testeAi1.PNG

Com estes testes pudemos verificar que a aplicação está funcionando corretamente de acordo com o diagrama. Com esta aplicação foi possível ver que maquina de estados finita é útil para a implementação de inteligência artificial para jogos.

Conclusão

Esta pratica de laboratório foi muito importante, pois aplicou os conceitos de OO e padrões de projeto vistos até então numa aplicação muito útil em computação, pois são inúmeras as utilizações de maquina de estados. A implementação da máquina de estados não tão dificil quanto pareceu ser à primeira vista. Tal impreção talvez tenha sido causado pelo número de requisitos que a maquina de estados deveria atender, alem de ter sido deixada a sua implementação em aberto. Porém enquanto a implementação da máquina nao passou de 3 horas, a confecção do relatorio e dos testes cuidou de fazer o tempo total exceder 10 horas, o que ocasionou no atrazo para a entrega do mesmo. É possivel dizer que sem duvida este laboratório foi o mais produtivo dos três, e espero que as proximas práticas também abordem temas com bastante aplicações praticas.

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License