Here's what the RuleEngine interface looks like (note the use of generics for Move and State), along with the interfaces it relies on:
public interface RuleEngine<Move, State extends RuleEngineState<Move, State>> { State getInitialState(); int getNumRoles(); List<Role> getRoles(); boolean isTerminal(State state); int getGoal(State state, int roleIndex) throws GameDescriptionException; default ImmutableIntArray getGoals(State state) throws GameDescriptionException; List<Move> getLegalMoves(State state, int roleIndex) throws GameDescriptionException; State getNextState(State state, List<Move> jointMoves) throws GameDescriptionException; Translator<Move, State> getTranslator(); default State toNativeState(RuleEngineState<?, ?> otherState); default List<Move> getRandomJointMove(State state) throws GameDescriptionException; default State getRandomNextState(State state) throws GameDescriptionException; default List<List<Move>> getLegalMovesByRole(State state) throws GameDescriptionException; default List<List<GdlTerm>> getGdlLegalMovesByRole(State state) throws GameDescriptionException; default ImmutableIntArray doRandomPlayout(State state) throws GameDescriptionException; }
public interface RuleEngineState<Move, State extends RuleEngineState<Move, State>> { /** * It is recommended that every state return the same Translator instance * when this is called, so that there is exactly one Translator per * originating RuleEngine. RuleEngine#toNativeState relies on this to * operate efficiently. */ Translator<Move, State> getTranslator(); default Set<GdlSentence> toGdlState(); }
public interface Translator<M, S> { public GdlTerm getGdlMove(M move); public M getNativeMove(GdlTerm move); public Set<GdlSentence> getGdlState(S state); public S getNativeState(Set<GdlSentence> state); default List<GdlTerm> getGdlMoves(List<M> moves); default List<M> getNativeMoves(List<GdlTerm> moves); }Note that the implementations of the default methods are omitted for brevity.
What's different from StateMachine? The first change I made (and have done in my StateMachine implementations for a while) is to remove the initialize() method. Java concurrency is tricky, and usually the best way to deal with it is to use immutable objects wherever possible. In addition, an initialize() method introduces the possibility of initializing a type twice or not at all before trying to use it. Instead, I use static create() methods to do whatever processing is needed to create the internal state, and a RuleEngineFactory anywhere we need to specify a type of engine to be created. This lets most of the internals of RuleEngine implementations be immutable.
The method inputs are also different. Instead of requiring use of the Move, Role, and MachineState classes, we use inputs that don't have the associated overhead of extra classes. Role inputs become role indices, which most implementations use internally anyway, and are faster to convert into Roles than vice versa.
Moves and states use the RuleEngine's own preferred types. With MachineState as the input, methods would typically begin with an instanceof check to see if the state was the preferred subclass, and then either try to translate the state itself or throw an exception to tell the caller to do so. Now methods can jump into actually using the state.
A single GameDescriptionException type covers three different exception types used in the StateMachine. They all boil down to the same thing: the GDL did something bad.
Finally, to deal with the fact that we have multiple types of moves and states, we introduce a Translator object that is responsible for converting between native and standard representations for moves and states. The standard type for moves is GdlTerm; the standard type for states is Set<GdlSentence>. Each RuleEngine instance is expected to have its own Translator object, and each state a reference to that object, so that determining if a state is native to a RuleEngine is a simple instance equality check. (Unlike the MachineState/instanceof approach, this also lets us translate between multiple instances of the same class that have different encodings of state.)
One downside is that clients have to use parameterized methods to pass native moves and states into the engine, and these can be tricky to write if you're unfamiliar with the syntax. (This also causes Eclipse to disable many of its auto-fix features, like making a method signature for a missing method.)
No comments:
Post a Comment