Overview
The following is an attempt at a Rock-Paper-Scissors (etc) game that has its business logic encapsulated in a rules engine. For the purposes of this exercise, I kept all of the business logic (eg what-beats-what and the "verbs") in a JBoss Drools engine.
This application doesn't have an "interface" per-se. It's merely the business logic and some utility classes.
Principles
The following were the principles I tried to follow:
- Extensibility. A business-y person should be able to extend the game without having to modify the code. (In this case, I considered the "rules" to be non-code.)
- Simple. If a business-y person needed to extend the code (see previous), they should only have to make minimal changes.
Implementation
As proof of concept, I implemented the rules of Rock-Paper-Scissors-Lizard-Spock, using the verbs supplied by the Sheldon Cooper quote:
"Scissors cuts paper, paper covers rock, rock crushes lizard, lizard poisons Spock, Spock smashes scissors, scissors decapitate lizard, lizard eats paper, paper disproves Spock, Spock vaporizes rock. And as it always has, rock crushes scissors."
-- Dr. Sheldon Cooper
Intended use
As can be seen by the code I've included below, there is no actual interface to the business logic. The eventual goal is to stick an HTTP interface on top of this logic and to allow POST requests to play a game of Rock-Paper-Scissors-Etc.
Technology
As mentioned, I used JBoss Drools to implement the business logic, using the mvel syntax. The supporting code, which consists of simple POJOs and a utility class for invoking the rules, is in Java. To validate existing behavior, I used the BDD testing library Cucumber and invoked it using JUnit. The project itself is a Maven project for ease of building.
The full code, as it stands, can be found on GitHub here: https://github.com/roddy/rock-paper-scissors-etc
Code
There are four Java classes: Player, ValidationOutcome, GameOutcome, and DrlUtilities. The first three are POJOs, the last is a utility class for invoking the rules.
Player.java
package net.roddy.rps.assets;
public class Player {
private String myMove;
public void setMove(String myMove) {
this.myMove = myMove;
}
public String getMove() {
return myMove;
}
}
ValidationOutcome.java
package net.roddy.rps.assets;
import java.util.ArrayList;
import java.util.List;
public class ValidationOutcome {
private boolean isValid;
private List<String> errors;
public ValidationOutcome() {
this.isValid = true;
this.errors = new ArrayList<>();
}
public void setValid(boolean isValid) {
this.isValid = isValid;
}
public boolean isValid() {
return this.isValid;
}
public void addValidationError(String error) {
this.errors.add(error);
}
public List<String> getValidationErrors() {
return new ArrayList<>(errors);
}
}
GameOutcome.java
package net.roddy.rps.assets;
public class GameOutcome {
private String winningThrow;
private String verb;
private String losingThrow;
public void setWinningThrow(String winningThrow) {
this.winningThrow = winningThrow;
}
public String getWinningThrow() {
return winningThrow;
}
public void setLosingThrow(String losingThrow) {
this.losingThrow = losingThrow;
}
public String getLosingThrow() {
return losingThrow;
}
public void setVerb(String verb) {
this.verb = verb;
}
public String getVerb() {
return verb;
}
}
DrlUtilities.java
package net.roddy.rps.utilities;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import java.util.List;
/**
* Utilty class for deploying DRL files and invoking rules.
*/
public class DrlUtilities {
/**
* Fires rules for the specified scope. Passes into the session the specified input objects, and adds a global with
* the specified name.
* @param scope the scope
* @param inputs the input objects
* @param globalName the global's name
* @param global the global
*/
public static void fireRulesForScope(String scope, List<Object> inputs, String globalName, Object global) {
KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
KieBase kBase = kContainer.getKieBase(scope);
KieSession session = kContainer.newKieSession(scope+"Session");
try {
for(Object input : inputs) {
session.insert(input);
}
session.setGlobal(globalName, global);
session.fireAllRules();
} finally {
session.dispose();
}
}
}
In addition to the Java code, there are two DRL files (Drools) and one XML file that defines the KIE Modules.
validation.drl
package rules.validation;
import net.roddy.rps.assets.Player;
import net.roddy.rps.assets.ValidationOutcome;
dialect "mvel"
global ValidationOutcome outcome;
rule "A player's move must be all lower case"
when
not(Player( move matches "[a-z]+"))
then
outcome.setValid(false);
outcome.addValidationError("Player move is not all lowercase.");
end
rule "A player's move must be one of the expected values"
when
Player( $move : move not in ( "rock", "paper", "scissors", "lizard", "spock" ))
then
outcome.setValid(false);
outcome.addValidationError("Player move is invalid: " + $move);
end
competition.drl
package rules.game;
import net.roddy.rps.assets.GameOutcome;
import net.roddy.rps.assets.Player;
dialect "mvel"
global GameOutcome outcome;
rule "Duplicate throws are a tie"
when
Player( $throw : move != null )
Player( move == $throw )
then
outcome.setWinningThrow($throw);
outcome.setLosingThrow($throw);
outcome.setVerb("ties");
end
rule "Scissors cut paper"
when
Player( move == "scissors" )
Player( move == "paper" )
then
outcome.setWinningThrow("scissors");
outcome.setVerb("cut");
outcome.setLosingThrow("paper");
end
rule "Paper covers rock"
when
Player( move == "paper" )
Player( move == "rock" )
then
outcome.setWinningThrow("paper");
outcome.setVerb("covers");
outcome.setLosingThrow("rock");
end
rule "Rock crushes lizard"
when
Player( move == "rock" )
Player( move == "lizard" )
then
outcome.setWinningThrow("rock");
outcome.setVerb("crushes");
outcome.setLosingThrow("lizard");
end
rule "Lizard poisons Spock"
when
Player( move == "lizard" )
Player( move == "spock" )
then
outcome.setWinningThrow("lizard");
outcome.setVerb("poisons");
outcome.setLosingThrow("spock");
end
rule "Spock smashes scissors"
when
Player( move == "spock" )
Player( move == "scissors" )
then
outcome.setWinningThrow("spock");
outcome.setVerb("smashes");
outcome.setLosingThrow("scissors");
end
rule "Scissors decapitate lizard"
when
Player( move == "scissors" )
Player( move == "lizard" )
then
outcome.setWinningThrow("scissors");
outcome.setVerb("decapitate");
outcome.setLosingThrow("lizard");
end
rule "Lizard eats paper"
when
Player( move == "lizard" )
Player( move == "paper" )
then
outcome.setWinningThrow("lizard");
outcome.setVerb("eats");
outcome.setLosingThrow("paper");
end
rule "Paper disproves Spock"
when
Player( move == "paper" )
Player( move == "spock" )
then
outcome.setWinningThrow("paper");
outcome.setVerb("disproves");
outcome.setLosingThrow("spock");
end
rule "Spock vaporizes rock"
when
Player( move == "spock" )
Player( move == "rock" )
then
outcome.setWinningThrow("spock");
outcome.setVerb("vaporizes");
outcome.setLosingThrow("rock");
end
rule "Rock crushes scissors"
when
Player( move == "rock" )
Player( move == "scissors" )
then
outcome.setWinningThrow("rock");
outcome.setVerb("crushes");
outcome.setLosingThrow("scissors");
end
kmodule.xml
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://jboss.org/kie/6.0.0/kmodule">
<kbase name="validation" packages="rules.validation">
<ksession name="validationSession" type="stateful" />
</kbase>
<kbase name="game" packages="rules.game">
<ksession name="gameSession" type="stateful" />
</kbase>
</kmodule>
Concerns
For the most part, the business logic is entirely encapsulated by the competition.drl rules. The only exception is the logic that validates that the move is acceptable. Since the validation logic is independent of the actual game logic, I had to include the list of actions in both places. So if we decide to add a new action -- "jedi" -- we not only have to update the competition.drl to include the appropriate interactions, but also the validation.drl to update the list of allowed actions.
Otherwise, please feel free to comment on anything else. The project was built on Java 8, but I haven't made use of any Java-8-specific features.
fireRulesForScope
method ? \$\endgroup\$