Let's write a quick little version of Penney's Game using the clever little state machine engine, sam. Penney's Game is a fun little bar game you can con your friends into playing to get them to pay for the drinks.
I want to focus on the implementation here, so here's a link for you to find out about
Penney's Game (if you need it).
Before we start throwing code together, let's talk a bit about how we might put our state machine together. We have a few options. We could build a state for each pattern. If we limit the pattern length to 3, that would mean 17 states: an empty start state, single flip heads and tails states, 4 double flip states covering the possible combos there, 8 triple flip states to cover those combos, then a state for player 1 winning and a state for player 2 winning.
I'm lazy and that sounds like a lot of work. Plus, if we follow that route, we wouldn't be able to run a 4-flip game. At least, not without adding in
aw geez more states.
Another option is to just have states representing game loop flow, like one for flipping, another for checking victory conditions, and so like that. That feels nicer to me, so that's the approach I'm going with.
Here are the states I've come up with:
public enum PenneysState
{
ValidatePatterns,
Flip,
CheckVictory,
VictorFound
}
There are really only 2 working states, Flip and CheckVictory. But this is supposed to be a nice simple demo, so few is good.
For the payload, we need to track a few things:
- Player 1's victory pattern.
- Player 2's victory pattern.
- The history of flips for this game.
- Which player won.
We also have to have an id for the payload to fulfill the Payload contract. Our usage pattern for this game is a single execution, so the id is pretty much useless to us. Still, a contract is a contract and our honor is at stake. So, we'll just put a little something in for the id too.
Here's what I have for the payload:
public class PenneysPayload
implements Payload
{
public enum Flip
{
Heads,
Tails
}
public PenneysPayload (String playerOnePattern, String playerTwoPattern)
{
_playerPatterns = new String[] { playerOnePattern, playerTwoPattern };
_flips = new StringBuilder();
_id = System.currentTimeMillis();
}
public void addFlip (Flip flip)
{
_flips.append(flip.name().charAt(0));
}
public String getFlips ()
{
return _flips.toString();
}
@Override
public Long getId ()
{
return _id;
}
public String getPlayerOnePattern ()
{
return _playerPatterns[0];
}
public String getPlayerTwoPattern ()
{
return _playerPatterns[1];
}
public String[] getPlayerPatterns ()
{
return _playerPatterns;
}
public Integer getVictor ()
{
return _victor;
}
public void setVictor (Integer victor)
{
_victor = victor;
}
private Long _id;
private StringBuilder _flips;
private String[] _playerPatterns;
private Integer _victor;
}
The player patterns are immutable, so there are no setters for those. The flip history is only additive, so instead of a setter, we only have an adder for that. The victor is the only settable property.
I'm stashing the player patterns in an array of 2 so that in the validation step I can just walk through the list of patterns and validate each one. I could have kept them separate on the inside and provided a getAsList method or something like that, but that would complicate the code a bit more than I'm comfortable with. I kept getters for each individual pattern to emphasize the distinctness of those two patterns. The intent is that if you're accessing a single pattern, you use the specific accessor, and if you're accessing all patterns, you use the array accessor, but there's no way to enforce that behavior.
I'm tracking the flip history as a String, but only 'H' or 'T' is valid, so I set up an enum to enforce that addFlip is never called with anything other than a valid option. (Well, a clever coder could still shove null in there to great effect. Don't do that.)
Once we have the states determined and the payload laid out, it's really just a matter of walking through the steps to put the machine together. Let's break that guy down by step.
First the validations. We need to make sure the patterns input are valid, otherwise Strange Things might occur.
public PenneysState validatePatterns (PenneysPayload payload)
throws PenneysException
{
int i = 1;
for (String playerPattern: payload.getPlayerPatterns())
{
if (playerPattern == null)
{
throw new PenneysException("No pattern for player " + i);
}
if (_INVALID_PATTERN.matcher(playerPattern).find())
{
throw new PenneysException("Player " + i + "'s pattern contains invalid indicators. " +
"Are those Heads or Tails?");
}
i += 1;
}
if (payload.getPlayerOnePattern().length() != payload.getPlayerTwoPattern().length())
{
throw new PenneysException("The player's patterns are not the same length!");
}
return PenneysState.Flip;
}
You'll notice that I'm using an exception to indicate validation failures, rather than having an error state. Sam considers any Throwable to be an error state, and indicates the error condition in the completion notification.
We just check to make sure neither player submitted a null pattern. And we check to make sure the pattern only contains H's and T's. Ah, here's that regular expression being referenced:
static final private Pattern _INVALID_PATTERN = Pattern.compile("[^HT]");
Our final check is to make sure both players submitted the same number of flips in their patterns. We can't have a good, clean, fair game if one guy submits 4 flips and the other only 3, after all.
Sometimes people think I'm quirky because I do things like i += 1 instead of i++. In this case, it's because i++ is intended as an inline operative, whereas i += 1 is intended as an assignment operative. So, yeah, they're right, I am quirky.
The flip state code is really just a flip. That makes it nice and simple:
public PenneysState flip (PenneysPayload payload)
{
payload.addFlip((_RANDOM.nextInt() % 2 == 0 ? Flip.Heads : Flip.Tails));
return PenneysState.CheckVictory;
}
Grab a psuedo-random value, mod it down to odd or even, and translate that to heads or tails.
That leaves us with the victory checker.
public PenneysState checkVictory (PenneysPayload payload)
{
int i = 1;
for (String playerPattern: payload.getPlayerPatterns())
{
if (payload.getFlips().endsWith(playerPattern))
{
payload.setVictor(i);
return PenneysState.VictorFound;
}
i += 1;
}
return PenneysState.Flip;
}
To check for victory, we need to look at the tailing end of the flip history, that is, the most recent flips. The stock endsWith call takes care of that for us. So, if we find a match, we set the victor in the payload and jump to the end state. If we get to the end of the check, we know there was no victor, and we need to flip again.
Since we don't have annotations in sam yet, we still need to manually associate the states with their worker methods. We take care of that in the constructor.
protected PenneysMachine ()
throws NoSuchMethodException
{
super(PenneysState.class,PenneysPayload.class);
registerState(PenneysState.ValidatePatterns,"validatePatterns");
registerState(PenneysState.Flip,"flip");
registerState(PenneysState.CheckVictory,"checkVictory");
}
By the way, that call to super in the constructor is a must. Skipping that will cause breakages.
Also, remember that end states, such as VictorFound, don't get worker methods associated with them. That's how sam knows which states are end states. For those of you who are quirky in different ways than I, you could also use null as an end state. If any state worker returns null instead of a state, that payload will be considered to be in it's end state. But I like having a valid, indicated end state. It serves as a nice resolution for my brain.
The last piece we need to build is the user interface. We put the machine together so that we could really paint any type of user interface on this thing, a web service, a Swing application, even an FX application. For our little example, we'll keep it small and simple and just do a command-line interface with output spewing to standard out. After all, this is about the state machine, not some UI framework.
We will need the state machine to tell us the results of each execution. So, our main class will need to implement the CompletionListener.
public class PenneysGame
implements CompletionListener<PenneysState,PenneysPayload>
We'll need a few steps to set up the machine and kick it off before we try to process our game payload. We do that in the constructor so everything is ready to roll upon instantiation.
public PenneysGame ()
throws NoSuchMethodException
{
_machine = new PenneysMachine();
_machine.setProcessingQueue(new MemoryProcessingQueue<PenneysState,PenneysPayload>());
_machine.addCompletionListener(this);
_machine.start();
}
We set ourselves to be notified on completion, and start up the beastie.
To start a new game, we can have a little method called play.
public void play (String player1Pattern, String player2Pattern)
{
System.out.println("Player 1's pattern: " + player1Pattern);
System.out.println("Player 2's pattern: " + player2Pattern);
System.out.println("\nPlaying...\n");
_machine.process(new PenneysPayload(player1Pattern,player2Pattern));
}
Then, to handle the notifications, here's the notifyComplete part.
@Override
public void notifyComplete (PenneysPayload payload, PenneysState lastState, Exception error)
{
if (error != null)
{
System.out.println("ERROR: " + error.toString());
}
else
{
System.out.println("Flip pattern was: " + payload.getFlips());
System.out.println("Victor was: Player " + payload.getVictor());
}
_machine.requestStop();
}
This completion handles both happy path and error conditions. Since we're only playing a single round per execution, we shutdown the machine once we get our first notification, regardless of whether or not that payload ended in an error state.
There's one more little tool we'll need before we can build the main. We need to be able to have our UI thread pause and wait until the game completes. It may take any number of flips to complete; we have no way of knowing how long it will be. We could guess that it'll be less than a second (and we'd be right) and just have the main sleep for a second. But that's a pretty lousy and brittle solution. Any change to the system that changed how long that step takes could break it.
In general, it's best to avoid using timed waits for synchronization.
Instead, we could do a timed wait then check to see if the machine is still running. Something like this.
public void waitWhilePlaying ()
{
while (_machine.isAlive())
{
try
{
Thread.sleep(1000L);
}
catch (InterruptedException ignore)
{
}
}
}
But then, it would really be better if we could just tell our thread to pause for as long as the machine is active, and continue as soon as it's not. Good news, everybody. A machine is a thread, so you can join a machine with all the proud confidence you would join any old processing thread.
public void waitWhilePlaying ()
{
try
{
_machine.join(10000L);
}
catch (InterruptedException ignore)
{
}
}
Of course, in this case the join works because we're stopping the machine as soon as we get a completion notification. In a more complex example, you might need to ... well, do something more complex.
Finally, yes, finally, we're ready to throw our main together. We already have everything coded up, so the main just links all our nice code to the command-line itself.
static public void main (String[] args)
{
try
{
if (args.length < 2)
{
System.out.println("PenneysGame usage:");
System.out.println(" java -jar penneysgame.jar {player1pattern} {player2pattern}\n");
System.out.println("Each pattern can consist of any number of H's (for heads) or T's ");
System.out.println("(for tails), however each pattern must be the same length.");
}
else
{
PenneysGame game = new PenneysGame();
game.play(args[0],args[1]);
game.waitWhilePlaying();
}
}
catch (NoSuchMethodException e)
{
System.out.println("ERROR: " + e.toString());
}
}
We do a quick little check just to make sure the user isn't just hunting blindly for usage information, then kick up our PenneysGame and fire off a game.
Cool! Let's run it and see what happens! I'll run it with an extreme case, HHH and THH.
Player 1's pattern: HHH
Player 2's pattern: THH
Playing...
Flip pattern was: HTTHH
Victor was: Player 2
And that, ladies and gentlemen, is Penney's Game in sam.