package org.drooms.impl.logic;
import org.drools.core.time.SessionPseudoClock;
import org.drooms.api.Action;
import org.drooms.api.Node;
import org.drooms.api.Node.Type;
import org.drooms.api.Player;
import org.drooms.api.Playground;
import org.drooms.impl.logic.events.*;
import org.drooms.impl.logic.facts.*;
import org.drooms.impl.util.GameProperties;
import org.kie.api.KieServices;
import org.kie.api.logger.KieRuntimeLogger;
import org.kie.api.runtime.Channel;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.KieSessionConfiguration;
import org.kie.api.runtime.conf.ClockTypeOption;
import org.kie.api.runtime.rule.EntryPoint;
import org.kie.api.runtime.rule.FactHandle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Represents a {@link Player}'s Strategy in action. This class holds
* and maintains Drools engine's state for each particular player.
*
* <p>
* When submitted to an {@link java.util.concurrent.Executor}, the strategy should make a decision on the next move,
* based on the current state of the working memory. This decision should be sent over the provided 'decision'
* channel. If not sent, it will default to STAY. See {@link Action} for the various types of decisions.
* </p>
* <p>
* This class enforces the following requirements on the strategies:
* </p>
*
* <ul>
* <li>'gameEvents' entry point must be declared, where the events not directly related to player actions will be sent.
* These events are {@link CollectibleAdditionEvent} and {@link CollectibleRemovalEvent}.</li>
* <li>'playerEvents' entry point must be declared, where the player-caused events will be sent. These events are
* {@link PlayerActionEvent} and {@link PlayerDeathEvent}.</li>
* <li>'rewardEvents' entry point must be declared, where the reward events will be sent. These events are
* {@link CollectibleRewardEvent} and {@link SurvivalRewardEvent}.</li>
* </ul>
*
* <p>
* This class provides the following Drools globals, if declared in the strategy:
* </p>
*
* <ul>
* <li>'logger' implementation of the {@link Logger} interface, to use for logging from within the rules.</li>
* <li>'tracker' instance of the {@link PathTracker}, to facilitate path-finding in the rules.</li>
* </ul>
*
* <p>
* Your strategies can be validated for all these - check {@link org.drooms.impl.util.DroomsStrategyValidator} and
* feel free to use it in your unit testing.
* </p>
*
* <p>
* The working memory will contain instances of the various helper fact types:
* </p>
*
* <ul>
* <li>{@link GameProperty}, many. Will never change or be removed.</li>
* <li>{@link CurrentPlayer}, once. Will never change or be removed.</li>
* <li>{@link CurrentTurn}, once. Will change with every turn.</li>
* <li>{@link Wall}, many. Will remain constant over the whole game.</li>
* <li>{@link Worm}, many. Will be added and removed as the worms will move, but never modified.</li>
* </ul>
*
*/
class DecisionMaker implements PlayerLogic, Channel, Callable<Action> {
private static final Logger LOGGER = LoggerFactory.getLogger(DecisionMaker.class);
private static void setGlobal(final KieSession session, final String global, final Object value) {
try {
session.setGlobal(global, value);
} catch (final RuntimeException ex) {
// do nothing, since the user has already been notified
}
}
private final FactHandle currentTurn;
private final EntryPoint gameEvents, playerEvents, rewardEvents;
private final Map<Player, Map<Node, FactHandle>> handles = new HashMap<>();
private final boolean isDisposed = false;
private final Player player;
private final PathTracker tracker;
private final KieSession session;
private final KieRuntimeLogger sessionAudit;
private Action latestDecision = null;
private Node currentHead = null;
public DecisionMaker(final Playground playground, final Player p, final GameProperties properties, final File
reportFolder) {
this.player = p;
final KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();
config.setOption(ClockTypeOption.get("pseudo"));
this.session = p.constructKieBase().newKieSession(config, null);
if (reportFolder != null) {
Path reportFile = Paths.get(reportFolder.getPath(), p.getName());
this.sessionAudit = KieServices.Factory.get().getLoggers().newFileLogger(session, reportFile.toString());
DecisionMaker.LOGGER.info("Auditing the Drools session is enabled.");
} else {
this.sessionAudit = null;
DecisionMaker.LOGGER.info("Auditing the Drools session is disabled.");
}
// this is where we listen for decisions
this.session.registerChannel("decision", this);
// this is where we will send events from the game
this.rewardEvents = this.session.getEntryPoint("rewardEvents");
this.gameEvents = this.session.getEntryPoint("gameEvents");
this.playerEvents = this.session.getEntryPoint("playerEvents");
// configure the globals for the session
this.tracker = new PathTracker(playground, p);
DecisionMaker.setGlobal(this.session, "tracker", tracker);
DecisionMaker.setGlobal(this.session, "logger",
LoggerFactory.getLogger("org.drooms.players." + p.getName()));
// insert playground walls
for (int x = -1; x <= playground.getWidth(); x++) {
for (int y = -1; y <= playground.getHeight(); y++) {
Node n = playground.getNodeAt(x, y);
if (n.getType() == Type.WALL) {
this.session.insert(new Wall(n));
}
}
}
// insert info about the game configuration
this.session.insert(new GameProperty(GameProperty.Name.MAX_TURNS, properties.getMaximumTurns()));
this.session
.insert(new GameProperty(GameProperty.Name.MAX_INACTIVE_TURNS, properties.getMaximumInactiveTurns()));
this.session.insert(new GameProperty(GameProperty.Name.DEAD_WORM_BONUS, properties.getDeadWormBonus()));
this.session.insert(new GameProperty(GameProperty.Name.TIMEOUT_IN_SECONDS, properties
.getStrategyTimeoutInSeconds()));
// insert info about the game status
this.currentTurn = this.session.insert(new CurrentTurn(GameProperties.FIRST_TURN_NUMBER - 1));
this.session.insert(new CurrentPlayer(p));
}
/**
* Stop the decision-making process, no matter where it currently is.
*/
public void halt() {
this.session.halt();
}
@Override
public void notifyOfCollectibleAddition(final CollectibleAdditionEvent evt) {
this.gameEvents.insert(evt);
}
@Override
public void notifyOfCollectibleRemoval(final CollectibleRemovalEvent evt) {
this.gameEvents.insert(evt);
}
@Override
public void notifyOfCollectibleReward(final CollectibleRewardEvent evt) {
this.rewardEvents.insert(evt);
}
@Override
public void notifyOfDeath(final PlayerDeathEvent evt) {
this.playerEvents.insert(evt);
// remove player from the WM
this.handles.remove(evt.getPlayer()).forEach((node, handle) -> this.session.delete(handle));
}
@Override
public void notifyOfPlayerMove(final PlayerActionEvent evt) {
final Player player = evt.getPlayer();
this.playerEvents.insert(evt);
// update player positions
if (!this.handles.containsKey(player)) {
this.handles.put(player, new HashMap<>());
}
final Map<Node, FactHandle> handles = this.handles.get(player);
// worm no longer occupies certain nodes
handles.keySet().stream().filter(n -> !evt.getNodes().contains(n)).collect(Collectors.toSet()).forEach(n ->
handles.remove(n));
// worm occupies a new node
evt.getNodes().stream().filter(n -> !handles.containsKey(n)).forEach(n -> handles.put(n, this.session.insert
(new Worm(player, n))));
// update head node
if (player == this.player) {
this.currentHead = evt.getHeadNode();
}
}
@Override
public void notifyOfSurvivalReward(final SurvivalRewardEvent evt) {
this.rewardEvents.insert(evt);
}
@Override
public void send(final Object object) {
this.validate();
if (object instanceof Action) {
if (this.latestDecision != null) {
DecisionMaker.LOGGER.debug("Player {} has changed the decision from {} to {}.", new Object[]{
this.player.getName(), this.latestDecision, object});
}
this.latestDecision = (Action) object;
} else {
DecisionMaker.LOGGER.warn("Player {} indicated an invalid move {}.", new Object[]{this.player.getName(),
this.latestDecision});
}
}
/**
* Clean up after this Drools instance. Will terminate the session and leave
* all the objects up for garbage collection. Only call once and then don't
* use this object anymore.
*
* @return False if already terminated.
*/
public boolean terminate() {
if (this.isDisposed) {
DecisionMaker.LOGGER.warn("Player {} already terminated.", new Object[]{this.player.getName()});
return false;
} else {
DecisionMaker.LOGGER.info("Terminating player {}.", new Object[]{this.player.getName()});
if (this.sessionAudit != null) {
this.sessionAudit.close();
}
this.halt();
this.session.dispose();
return true;
}
}
/**
* Signifies that this tracker has been notified of all events and that the immediately following action is
* {@link #call()}.
*/
public void commit() {
this.validate();
DecisionMaker.LOGGER.trace("Player {} updating path tracker. ", new Object[]{this.player.getName()});
Map<Player, Collection<Node>> positions = handles.keySet().stream().collect(Collectors.toMap(Function.identity
(), player -> handles.get(player).keySet()));
this.tracker.updatePlayerPositions(positions, this.currentHead);
DecisionMaker.LOGGER.trace("Player {} advancing time. ", new Object[]{this.player.getName()});
final SessionPseudoClock clock = this.session.getSessionClock();
clock.advanceTime(1, TimeUnit.MINUTES);
// increase turn number
final CurrentTurn turn = (CurrentTurn) this.session.getObject(this.currentTurn);
this.session.update(this.currentTurn, new CurrentTurn(turn.getNumber() + 1));
}
private void validate() {
if (this.isDisposed) {
throw new IllegalStateException("Player " + this.player.getName() + " already terminated!");
}
}
/**
* Call on the Drools engine to make the decision on worm's next move,
* according to the {@link Player}'s strategy.
*
* @return The move. STAY will be chosen when the strategy doesn't respond.
*/
@Override
public Action call() {
DecisionMaker.LOGGER.trace("Player {} deciding. ", new Object[]{this.player.getName()});
this.latestDecision = null;
this.session.fireAllRules();
if (this.latestDecision == null) {
DecisionMaker.LOGGER.info("Player {} didn't make a decision. STAY forced.", this.player.getName());
return Action.NOTHING;
} else {
DecisionMaker.LOGGER.info("Player {} final decision is {}. ", this.player.getName(), this.latestDecision);
return this.latestDecision;
}
}
}