Rock-Paper-Scissors

(Timing Events)

<<Previous | ToC | Next >>

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.
 

Timing in GameEngine

We have six states our game will step through:
0. Initialization, where it is waiting for the human to click

1. 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:

 
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
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:
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 up

0x323 -- 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);} //~DoOften

public 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