All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ode.ai.0.1.6.source-code.Planner Maven / Gradle / Ivy

There is a newer version: 0.1.7
Show newest version
package com.gracefulcode.ai;

import com.gracefulcode.ai.internal.GlobalState;
import com.gracefulcode.ai.internal.IllegalCloneException;
import com.gracefulcode.ai.internal.IllegalCostException;
import com.gracefulcode.ai.internal.IllegalPlanException;
import com.gracefulcode.ai.internal.Node;
import com.gracefulcode.ai.internal.State;

import java.util.ArrayList;

/**
 * Planner is the real meat and potatoes of this operation. Here's where all
 * the work gets done. It's also the place you shouldn't have to look. If you
 * do, it's my fault and I am truly sorry.
 *
 * @version 0.1
 * @since 0.1
 */
public class Planner<
	WS extends WorldState,
	G extends Goal,
	B extends Behavior,
	BP extends Iterable
> {
	public Planner() {

	}

	/**
	 * startPlanning consructs the initial state representing where the AI
	 * system begins. You must call stepState to step this state forward in
	 * planning.
	 *
	 * @param initialState The world state that this AI system begins at.
	 * @param goal The goal that we are ultimately trying to achieve.
	 * @param behaviorProvider The behaviors that we are allowed to use in our
	 *        plan.
	 *
	 * @return The initial State.
	 */
	public State startPlanning(
		WS initialState,
		G goal,
		BP behaviorProvider
	) {
		// TODO: Pool this?
		return new State(
			initialState,
			initialState,
			goal,
			behaviorProvider
		);
	}

	/**
	 * Gets an ArrayList of the behaviors that the AI system has come up with.
	 * 

* Note that these are in the reverse order that you may expect. * * @throws IllegalPlanException if the end state was not found within the * global state. * @throws IllegalCostException if a behavior returns a cost that is <= 0 * * @param endState The State on which you want to end. * * @return An ArrayList of behaviors to follow to get from the start to the * provided end State, in reverse order. */ public ArrayList getPlan(State endState) throws IllegalCostException, IllegalPlanException { ArrayList tmp = new ArrayList(); GlobalState globalState = endState.getGlobalState(); Node n = globalState.stateToNode.get(endState.getBestWorldState()); if (n == null) { throw new IllegalPlanException(globalState); } while (n.getParent() != null) { tmp.add(n.getBehavior()); n = n.getParent(); } return tmp; } /** * Stepping a state is a complicated thing. For sanity, we pulled the meat * into this function that operates on a single behavior at a time. * Modifies the State input variable. * * @throws IllegalCostException if a behavior returns a cost that is <= 0 * @throws IllegalCloneException if you world state clone operation returns * the same world state * * @param state Our current planne State. Note that this is not the * WorldState, it is something internal that allows partial * execution. * @param behavior The behavior that we are currently evaluating. * @param debugger The debugger, if the user provided one. */ private void stepStateWithBehavior( State state, B behavior, PlannerDebugger debugger ) throws IllegalCostException, IllegalCloneException { WS priorWorldState = state.getWorldState(); // If we cannot run this behavior, we don't have to do anything. if (!behavior.isRunnable(priorWorldState)) return; GlobalState globalState = state.getGlobalState(); Node previousNodeInstance = globalState.stateToNode.get(priorWorldState); @SuppressWarnings("unchecked") WS worldStateAfterBehavior = (WS)priorWorldState.clone(); if (worldStateAfterBehavior == priorWorldState) { throw new IllegalCloneException(priorWorldState); } behavior.modifyState(worldStateAfterBehavior); Node newNode = new Node(worldStateAfterBehavior, behavior, previousNodeInstance); if (globalState.bestSolution == null) { if (globalState.goal.isSatisfied(worldStateAfterBehavior)) { globalState.bestSolution = newNode; globalState.closedSet.add(worldStateAfterBehavior); if (!globalState.stateToNode.containsKey(worldStateAfterBehavior)) { globalState.stateToNode.put(worldStateAfterBehavior, newNode); } globalState.stateToNode.put(worldStateAfterBehavior, newNode); return; } } else { if (newNode.getCost() > globalState.bestSolution.getCost()) { if (!globalState.stateToNode.containsKey(worldStateAfterBehavior)) { globalState.stateToNode.put(worldStateAfterBehavior, newNode); } return; } if (globalState.goal.isSatisfied(worldStateAfterBehavior)) { if (newNode.getCost() < globalState.bestSolution.getCost()) { globalState.bestSolution = newNode; globalState.closedSet.add(worldStateAfterBehavior); if (!globalState.stateToNode.containsKey(worldStateAfterBehavior)) { globalState.stateToNode.put(worldStateAfterBehavior, newNode); return; } } } } if (globalState.stateToNode.containsKey(worldStateAfterBehavior)) { Node previousBestNodeInstance = globalState.stateToNode.get(worldStateAfterBehavior); float previousBestNodeCost = previousBestNodeInstance.getCost(); float previousNodeCost = previousNodeInstance.getCost() + behavior.getCost(priorWorldState); if (previousNodeCost < previousBestNodeCost) { previousBestNodeInstance.changeParent(previousNodeInstance, behavior); } if (globalState.openSet.contains(worldStateAfterBehavior)) { return; } if (globalState.closedSet.contains(worldStateAfterBehavior)) { return; } globalState.openSet.add(worldStateAfterBehavior); return; } // We haven't evaluated this before, make a new node. globalState.stateToNode.put(worldStateAfterBehavior, newNode); if (debugger != null) { debugger.didAddState(worldStateAfterBehavior); } globalState.openSet.add(worldStateAfterBehavior); } /** * Steps the provided State forward by one planning tick. In common cases, * you would call this once per frame in your game. You can call it more or * less often depending on your use case. *

* stepState will alter the passed in state to represent the new state of * the AI system. * * @throws IllegalCloneException if the world state clone is the same object. * @throws IllegalCostException if a behavior returns a cost that is <= 0 * * @param state The current state of this AI system. * * @return True if the AI system cannot proceed any more, otherwise False. */ public boolean stepState(State state) throws IllegalCostException, IllegalCloneException { return this.stepState(state, null); } /** * Steps the provided state forward by one planning tick, while also * providing a debugger. In common cases, you would call this once per * frame in your game. You can call it more or less often depending on your * use case. *

* stepState will alter the passed in state to represent the new state of * the AI system. * * @throws IllegalCostException if a behavior returns a cost that is <= 0 * @throws IllegalCloneException if your WorldState.clone() operation returns the same object * * @param state The current state of this AI system. * @param debugger The debugger you want to use in order to debug the AI * system. * * @return True if the AI system cannot proceed any more, otherwise False. */ public boolean stepState(State state, PlannerDebugger debugger) throws IllegalCostException, IllegalCloneException { GlobalState globalState = state.getGlobalState(); if (debugger != null) { debugger.didStartStep(); } // TODO: Check that we aren't being called with an already-closed // state. for (B b: globalState.behaviorProvider) { if (debugger != null) { debugger.startEvaluateBehavior(b); } this.stepStateWithBehavior(state, b, debugger); if (debugger != null) { debugger.endEvaluateBehavior(b); } } globalState.openSet.remove(state.getWorldState()); globalState.closedSet.add(state.getWorldState()); if (globalState.openSet.size() == 0) { if (debugger != null) { debugger.didEndStep(false); } state.setCurrentState(null); return false; } WS newState = globalState.openSet.poll(); if (newState == null) { return false; } state.setCurrentState(newState); if (debugger != null) { debugger.didEndStep(true); } return true; } }