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.
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 clicked2. 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=scissorsPaper 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"I leave the details as an exercise.
"Scissors cuts paper"
"Rock breaks scissors"
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);
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();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:
GameWgt ColorW = null;
if (hum != 2) tmp = myGame.ChoseRPS(HumanR,0);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 if (hum != com) ColorW = HumanR;
/// (you can do the other two)
else { // human won...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:
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
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).
<<Previous | ToC | Next >>
Revised: 2022 November 5