This page continues the construction of a Rock-Paper-Scissors (RPS)
in GameEngine as part of a course on programming in Java (which starts
here).
If you have not yet worked through the first page of this construction,
you might want to go back and do that.
0. Initialization, where it is waiting for the human to click1. Synchronization One, holding "One..." on the message line for one second
2. Synchronization Two, holding "One... two..." on the message line for one second
3. Synchronization Three, holding "One... two... three..." on the message line for up to three seconds more
4. Announce the winner while animating the explosion of the losing play (about 2 seconds), or else
5. Announce the tie (about 2 seconds), then go back to state 0.
State 0 has no timeout, it just sits there indefinitely, waiting
for a click anywhere, and when we get a click in state 0, we advance to
state 1 and start the synchronization timer.
State 1 advances to state 2 with the larger message text, and that advances to state 3 even larger.
State 3 waits up to three seconds before giving up and returning to state 0. A click on one of the human choice icons in state 3 hides the other two icons, then calculates the score and advances to either state 4 or 5, which when it times out returns to state 0.
State 4 will eventually animate the explosion of the loser's icon, but the first cut will just sit there (like state 5).
You already knew all that.
At the least we will need a persistent (class) variable to remember what the current state is. Back in the program code window, still looking at the class variable declarations, I added one more integer variable I called RipPhase ("phase" is another word meaning "state", like "phase of the moon"). Do you remember the random number generator from when you did the text-only version of RPS? We will have the computer play randomly, so we need to import java.util.Random; at the very front of the class file (you can review random numbers in "Things You Need to Know"), and then declare and initialize a class variable to hold that reference:
int RipPhase = 0, Hscore = 0, Cscore = 0; // added instance variables
Random rn = new Random();
Most of the generated methods are "stubbed out" (comments only).
You will be using two of these, and you need to remove the double slants
from the front of their lines (so they are no longer comments, but real
code). The methods you don't need you can let the defaults do their thing,
and either leave them commented out, or else remove those methods entirely.
Don't remove any method that has a yellow box (executable code) in the
BlueJ editor window.
We need two methods defined in GameEngine to deal with the two events this game needs to respond to: timing (DoOften) and user clicks (ClickEvt). Both of them need to base their decisions on the combination of which event happened and what the current state is, so I decided to use shared code to make that decision, a single method that both events call. Clicks can happen in any of the three widgets representing the three choices the human player can choose from, so I numbered them in alphabetical order, 1=paper, 2=rock, and 3=scissors. Clicks can also happen in the background to start the synchronization, so I gave that a value 4. Timing events happen once every tenth of a second, rain or shine, so I gave them a reference number 0. To keep everything in one place, I also defined a startup parameter -1, which we can also use at the end of each play to start the next round. These are the parameter values sent to this new method. I called mine "TomzRips" but you could use a more descriptive name like "DoStateEvt" or anything that suits you.
Like many programming languages, Java has a special multi-way form of conditional to decide from any number of different choices (instead of a single boolean true or false in the IF-command). In Java it's called switch (see "Switch" in "Things You Need to Know") and it takes a single scalar (typically integer, but it could also be a character) index, like the state number or the event number. Alas, we want both values to participate, so the usual solution is to create a 2-dimensional table like this, and then to number the cells sequentially:
Now if you look at the numbers analytically, you can see that the sequential cell numbers can be calculated by multiplying the state (row) number times the number of different events (5) then adding the event (column) number. The cell number is now a single integer to switch on. We can eliminate the multiply step by numbering our states by fives -- basically the first column of the table, under event 0. This is generally the way we do a finite state machine (FSM). So this now is the basic form of the method that does our state processing:
St\Ev 0 1 2 3 4 <- Events 0 0 1 2 3 4 1 5 6 7 8 9 2 10 11 12 13 14 3 15 16 17 18 19 4 20 21 22 23 24 ^States
public void DoStateEvt(int whom) {
int tmp;
if (whom<0) { /// do initialization...
tmp = Announce.PutTextLn("Rock-Paper-Scissors, click to begin...");
RipPhase = 0;}
else switch(RipPhase+whom) {
case 0: // ignore timing in state 0
break;
case 1: // got any click...
case 2:
case 3:
case 4:
aFrameNum = 0; // start GameEngine's frame counter
tmp = Announce.PutTextLn("Synchronize: One...");
RipPhase = 5; // go to next state
break;
case 5: // next timing event
if (aFrameNum<10) break; // wait one full second, then...
tmp = Announce.PutTextLn("Synchronize: One... Two...");
RipPhase = 10; // go to next state
break;
case 6: // human clicked paper too soon (ignore it)
case 7: // human clicked rock too soon (ignore it)
case 8: // human clicked scissors too soon (ignore it)
break;
case 9: // human clicked somewhere else, start over
case 14: // human clicked somewhere else, start over
DoStateEvt(-2);
break;
case 10: // next timing event (in second second)
if (aFrameNum<20) break; // wait another full second, then...
tmp = Announce.PutTextLn("Synchronize: One... Two... Three...");
RipPhase = 15; // go to next state
break;
case 11: // human clicked paper too soon (ignore it)
case 12: // human clicked rock too soon (ignore it)
case 13: // human clicked scissors too soon (ignore it)
break;
...
default:
break;} // end of switch
} // end of method
Do you see how this works? I think it is very readable, almost like
a table representation. Notice that cases 1,2,3 and 4 all share the same
code. Any click on any of the hot widgets will have the same effect, starting
the synchronization phase. The variable aFrameNum is defined in
the super-class GameEvent, so it is visible to all the subclasses
(including yours). It gets incremented every frame (1/10th second), but
the interpretation (and initializatrion) is up to you. We can use it to
count off the seconds, which we start at zero when the user first clicks.
The really nice thing about doing a FSM with a
switch
is that you have a nice visual way to account for every possible combination
of state and input, nothing falls through the cracks. Mistakes are still
possible, but they are much easier to see. I left in cases 0, 6,7,8, and
11 through 14 for you to see, but you could eliminate them (and their break
lines) and let the default pick them up. Or, since it does nothing,
you could eliminate the default also and put the final switch
brace after the last casebreak. Putting them in or leaving them
out is exactly the same machine code generated by the compiler, it's just
whether you prefer seeing that they do nothing, and that you didn't accidentally
leave something out, versus having more executable code visible in one
screen (which is my personal preference, after efficiency).
The next five cases are where things start to happen. First we need
to understand the nature of the RPS widget, which has a special method
in the GameEngine that does all the heavy lifting for you. The basic form
of the method call is:
res = myGame.ChoseRPS(whom,doit);where whom is whatever RPS widget you want to do doit to, and doit is one of these choices:
0 -- Hide the widget
1 -- Show the widget as paper
2 -- Show the widget as rock
3 -- Show the widget as scissors
4 -- Show the widget as the final state in the explosion (same as hiding it)
6 -- Show all four images piled on top of each other
7 -- Show rock+paper+scissors only, but piled up0x323 -- Animate the scissors: snip, snip, approximately 1 second cycle
0x86C -- Exploding widget cloud, approximately 1.2 seconds total
0xCC9933 -- Change the cloud color to (for example) gray-red
The RPS sprite is actually a composite of four icon widgets, each
with its own animation capability (but only the scissors and explosion
icons have pixel frames for animation). The parameter you send to ChoseRPS
sets up one of several predefined settings. A single-digit parameter selects
one or all (or none) of the four icons to be visible.
A 6-digit hexadecimal number sets the color of the explosion icon.
I think the sequence of colors from 0xFFFF00 (yellow initial flame)
to 0xCC9933 (gray-red, as smoke overtakes the fading flame) to
0xAA7755
(grayer and less color) to 0xCCAA88 (brighter and less color)
to finally 0xDDDDDD (almost white, pure smoke) before it disappears.
You can choose other colors, but you need to set the colors at the right
time yourself, the animation controls only the shape of the cloudburst.
Optional, for geeks who want to know, (except for the color) the individual bits of the parameter you send to ChoseRPS are divided up into four components. The low three bits select one of the four icons (1/2/3/4). The next bit (+8) should be off except for the explosion sequence, it terminates the animation after one run. The next four bits (the second-last hex digit) are the icon frame number to choose, or for animation how many frames there are (two for scissors, six for the explosion, otherwise only one -- but if you told it "2" that icon would blink on and off).The next six bits up are the animation frame rate, in sixteenths of a 100ms (1/10th-second) display cycle. So if you give it a frame rate +0, then there is no animation, +0x100 (+256) is 1.6 seconds per animation step, +0x200 (+512) is 0.8 seconds per animation step (slightly faster than once per second), +0x300 (+768) is 0.63 seconds per animation step (a little slower than two steps per second), up to a maximum rate +0x1000 (+4096) which is one icon animation step every display frame (ten steps per second). Larger values up to +0x3F00 are possible, but they will skip animation steps to keep up.
Parameter values larger than 0x3FFF are considered to be a color (pure blue is not possible, you need to code it as 0x0100FF, which looks pretty pure to the human eye, and dark green less than 0x004000 will also need to add a hint of red; similarly black can be slightly reddish 0x010000, or else very dark gray 0x010101).
If you give it a negative parameter like -1, it will return a single-digit number 0 to 7, depending on who is showing. It returns something for positive parameters also, usually zero.
Disregarding animation for the moment, we need to hide the two widgets
the human player did not choose when they clicked on one of them, and we
need the computer to choose a play at random and display that. For now,
ignore the scoring and just hold the play for a couple seconds, then restart,
it gives us something to look at:
case 15: // during (and after) the third second..
if (aFrameNum<50) break; // wait up to three more full seconds, then...
DoStateEvt(-3); // give up and start over
break;
case 16: // human clicked paper
tmp = rn.nextInt(3)+1; // random computer play..
tmp = myGame.ChoseRPS(CompuW,tmp);
tmp = myGame.ChoseRPS(HumanR,0); // hide the rock
tmp = myGame.ChoseRPS(HumanX,0); // hide the scissors
tmp = Announce.PutTextLn("...");
RipPhase = 20; // go to next state
break;
case 17: // human clicked rock
tmp = rn.nextInt(3)+1; // random computer play..
tmp = myGame.ChoseRPS(CompuW,tmp);
tmp = myGame.ChoseRPS(HumanW,0); // hide the paper
tmp = myGame.ChoseRPS(HumanX,0); // hide the scissors
tmp = Announce.PutTextLn("...");
RipPhase = 20; // go to next state
break;
case 18: // human clicked scissors
tmp = rn.nextInt(3)+1; // random computer play..
tmp = myGame.ChoseRPS(CompuW,tmp);
tmp = myGame.ChoseRPS(HumanR,0); // hide the rock
tmp = myGame.ChoseRPS(HumanW,0); // hide the paper
tmp = Announce.PutTextLn("...");
RipPhase = 20; // go to next state
break;
case 19: // you decide what to do if the human clicks somewhere else
break;
case 20: // hold the result for three+ seconds more..
if (aFrameNum<80) break;
DoStateEvt(-4); // start over
break;
case 21: // got any click, just start the next round...
case 22:
case 23:
case 24:
aFrameNum = 0; // start GameEngine's frame counter
tmp = Announce.PutTextLn("Synchronize: One...");
tmp = myGame.ChoseRPS(HumanR,2); // show the rock
tmp = myGame.ChoseRPS(HumanW,1); // show the paper
tmp = myGame.ChoseRPS(HumanX,3); // show the scissors
tmp = myGame.ChoseRPS(CompuW,7); // show all three options for the computer
RipPhase = 5; // go to next state
break;
We still need to connect this method up to the events. At the end
of
StartUp, after the $ line, I inserted a call to the initialization:
/// [..$] End generated StartUp code [$] (do not modify this line)
DoStateEvt(-1);} //~StartUp
Then the two events, (DoOften) and (ClickEvt),
I made them call our event handling code:
public void DoOften() {
DoStateEvt(0);} //~DoOftenpublic boolean ClickEvt(GameWgt whom, int vert, int horz) {
if (whom==HumanX) DoStateEvt(3); // scissors
else if (whom==HumanR) DoStateEvt(2); // rock
else if (whom==HumanW) DoStateEvt(1); // paper
else DoStateEvt(4); // anywhere else
return false;} //~ClickEvt
It won't score and it won't animate, but it should be playable.
Try it.
In the next page we can do the scoring and animation for RPS.
<<Previous | ToC | Next >>
Revised: 2023 February 14