Rock-Paper-Scissors

(Scoring + Animation)

<<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 two pages of this construction, you might want to go back and do that.

How did your game play? What happened if you (the human, we do that kind of thing ) let the time run out? For example, at the end of the round, you could click ahead and it immediately started a new round. You probably saw that. What happened if you let its timer run out? A large part of programming is doing the best design we can, then building it, then discovering we forgot some initialization, or some edge cases, something that maybe doesn't happen very often or we just didn't think of it. It's called "debugging" and we spend far more time on this than we did in design. It should be the other way around, but me, I get impatient. There's a whole school of programming called "Agile" where they -- what's that line? -- promote the failure and call it a "feature." Today we are (ahem) agile. 

Me, I copied those initialization lines we had in cases 21-24, where we reset the widgets to the state they came out of the GameMaker, I copied all that to the initialization (whom<0) part of the program so it also happens when we restart.
 

Scoring

Perhaps you remember from the text-only version of RPS, there isn't a simple calculation to know who won this round, you just must slog through all nine different compares. Well, maybe not that bad, but it seems like it. We could copy and paste the same code in all three cases in our play switch, but this is where another subroutine -- I mean method -- would be helpful. I called mine "RipScore" and I pulled in also the decision of which widgets to hide and setting the computer's play. That way the three cases 16,17, and 18 collapse into one:
case 16:  case 17:  case 18: // human clicked
  tmp = rn.nextInt(3)+1; // random computer play..
  RipScore(whom,tmp); // also sets RipPhase
  break;


The subroutine RipScore now needs to do these six things:

1. Hide the (human) widgets not clicked

2. Show the computer's choice on its widget

3. Decide which side won, or it was a draw

4. Update the score if there's a winner

5. Update the message to the user as appropriate

6. Set the next state in RipPhase


Later on, when we do animation, RipScore can also set that up. This is pretty simple, I'd bet you could write this much on your own. You should try it before you look at what I did. You can start with this header line, before the DoStateEvt method:

public void RipScore(int hum, int com) { // hum is human play, com is computer's


Hiding two of the three widgets (1) is essentially three lines of code. You know which widtet was clicked, its reference number (1, 2, or 3) is in the parameter hum, so if (hum==1) you don't want to hide the paper widget, right? Invert the condition (hum!=1) to know that you can hide it. Can you do that for each widget?

The computer's choice (2) you similarly have in the parameter com, and assuming it's a valid number between 1=paper and 3=scissors, you can pass it directly to ChoseRPS:

tmp = myGame.ChoseRPS(CompuW,com);


Don't forget to declare your temporary variable. Java will tell you about it if you forget, but it's more professional not to have that happen (at least not very often).

Scoring (3) is a little tricky. You could look back at the text-only RPS program for inspiration, but I decided to just re-think it. I do that a lot, it's more fun than copying and pasting, and sometimes I get better code; and sometimes (hopefully not so often) I forget things and it comes out worse. Each player has a numeric code 1,2, or 3 for their play, and we might wonder if there is an easy ranking we can depend on. Let's see:

1=paper,  2=rock,  3=scissors
Paper covers rock, and rock breaks scissors, so apart from the scissors cutting the paper, the smaller number wins. That's easy to detect and fix. Paper+rock = 1+2=3, rock+scissors = 2+3=5, but paper+scissors = 1+3=4, it's unique. We are not always so lucky. Oh wait, rock+rock is also 4, but we can take those out first:
if (hum != com) { // not same play...
  if (hum+com==4) { // paper/scissors, but which?
    if (hum>com) hum = 0;
      else com = 0;} // now they will compare the same
  if (com<hum) { // computer won this round..
    tmp = Announce.PutTextLn("I won");
    Cscore++;}
  else { // human won...
    tmp = Announce.PutTextLn("You won");
    Hscore++;}
  tmp = CompuS.PutTextLn("Me  " + Cscore); // CompuS is the score widget
  tmp = HumanS.PutTextLn("You  " + Hscore);}
else tmp = Announce.PutTextLn("Tied, no score...");
RipPhase = 20;


Optional -- A more mathematical way to do it would recognize that these scores are a modulo-3 number system (+1), so if we take the difference and add +3 (so the difference is always positive) then remainder it by 3, then (depending on which way you subtract) the human always wins with a 1, the computer always wins with a 2, and 0 is always a tie. Try this on paper -- or program it up and see:

tmp = (com+3-hum)%3;


But that's pretty esoteric. Elegant, but way out there.

Optional -- Not so mathematical, but more coding effort, you can announce your win or loss with the traditional phrase, one of these three, whichever is appropriate:

"Paper covers rock"
"Scissors cuts paper"
"Rock breaks scissors"
I leave the details as an exercise.
 

Animation

Simple first, we can program the losing player's icon to blow up and disappear in a puff of smoke. In the case of the computer, there is only one icon, I called mine CompuW. If the computer lost (the human player won), then you can add this line, which starts the animation:
tmp = myGame.ChoseRPS(CompuW,0x86C);


That starts the animation immediately. Alternatively you can delay it about (not quite) two seconds with:

tmp = myGame.ChoseRPS(CompuW,0x8FC);
which puts some extra blank frames in front of the animation. Sorry, you only get 4 bits there (which in hexadecimal F=15 is the maximum), so any further delay you need to program using the frame counter. Less delay you can get by reducing the F to a smaller (hex) number. Pick something that looks good.

Anyway, the human has three icons, the one the player chose, and the other two that you are hiding. If the computer won this round, you would apply the animation to the remaining visible icon, the one that they played. The logic is already there, you just need to add one extra line to each of the three choices. I think you can do that.

Another cool animation effect is, while waiting through the synchronization delay, you could cycle the computer's icon through the three choices rapidly, one per frame. During each of the three timing cases (5, 10, and 15) you add a couple lines before the timeout test, here shown for the first of them:

case 5: // next timing event
  tmp = aFrameNum%3 +1;
  tmp = myGame.ChoseRPS(CompuW,tmp);
  if (aFrameNum<10) break; // wait ...


You can give your human player a little opportunity for skill by deleting the random number call and just leaving the last visible icon as the computer play. It's pretty hard to get the mouse click at just the right frame time, so there's some challenge to it. You also need to save off in a persistent variable what you set it to each time, or else extract the current visible icon using the API:

  tmp = myGame.ChoseRPS(CompuW,-1);

Color

Optional -- Somewhat more challenging to program, but very cool, you can have your blow-up sequence start with a small yellow fireball that dims to red as it grows and then fades to gray smoke as it blows up. This cannot be automated in GameEngine, you need to program each color change yourself. When I introduced the ChoseRPS API, I mentioned a color sequence I thought reasonable. Of course you can program anything you like, this is your game. It happens very fast, but you can slow the animation down by choosing a smaller first digit (instead of 8). During the animation, that number is added to a counter, and every time it goes over 16, the frame changes; 8 makes it change once every two frames (= 1/5th second, because 8 is half of 16 and there are ten frames each second), 4 would make it change once every four frames, = 2/5ths second, etc). I think 6 or 7 are about four frames per second, but not exactly (7 slightly faster, 6 slightly slower).

What you need to do is while you are waiting out your delay, you keep testing for particular frame times, then set the new color at exactly those times. I originally made separate cases for the different widgets being colorized, but I think it is easier if you declare another persistent widget variable, say ColorW at the class level and initialize it to null:

Random rn = new Random();
GameWgt ColorW = null;
Then when you decide which widget to animate, you set ColorW to be a reference to that widget, then do all your animating and colorizing on ColorW. That would start in the scoring program:
if (hum != 2) tmp = myGame.ChoseRPS(HumanR,0);
else if (hum != com) ColorW = HumanR;
/// (you can do the other two)
This sets it to the human widget no matter who lost, but later, if the computer lost, you will reset it to the computer's widget:
  else { // human won...
    tmp = Announce.PutTextLn("You won");
    ColorW = CompuW;
    Hscore++;}
...
else { // no score..
  tmp = Announce.PutTextLn("Tied, no score...");
  ColorW = null;}
if (ColorW != null) { // we have a loser to animate..
  tmp = myGame.ChoseRPS(CompuW,0x8FC); // start the animation
  tmp = myGame.ChoseRPS(CompuW,0xFFFF00);} // set color yellow
In the case of a tie (no score) you reset ColorW = null (maybe also as part of the initialization). Because the user click could happen at any time during that third sync frame, it might be helpful to reset the frame counter to a known value -- perhaps 55 -- so that your color timing is consistent:
case 16:  case 17:  case 18: // human clicked
  tmp = myGame.ChoseRPS(CompuW,-1); // play whatever is showing
  aFrameNum = 55; // stabilize the time reference
  RipScore(whom,tmp);
  RipPhase = 20;
  break;
case 20: // hold the result for three+ seconds more..
  if (aFrameNum<88) {
    if (ColorW==null) break;
    if (aFrameNum==75) // set color red-gray after one second..
      tmp = myGame.ChoseRPS(ColorW,0xCC9933);
    else if (aFrameNum==81) { // last visible frame..
      tmp = myGame.ChoseRPS(ColorW,4); // hide everything but cloud
      tmp = myGame.ChoseRPS(ColorW,0x42C); // run last frame slower
      tmp = myGame.ChoseRPS(ColorW,0xDDDDDD);}
    else if (aFrameNum==77) tmp = myGame.ChoseRPS(ColorW,0xAA7755);
    else if (aFrameNum==79) tmp = myGame.ChoseRPS(ColorW,0xCCAA88);
    break;}
  DoStateEvt(-4); // start over
  break;


That got pretty complicated. I had to fiddle with the numbers in the debugger, you probably will too. Good games are like that.

After you get your code working and want to compare it to what I did, or if you get stuck and think that looking at how I did it would help, you can look at my Rips. I added some console logging to make my debugging easier (you can ignore these) and a developmental level variable DevLev so the same code works without some of the options on this page (DevLev=0 or DevLev=1).
 

Next

Ready for another event-driven program?

<<Previous | ToC | Next >>

Revised: 2022 November 5