Merge pull request #503 from jnellis/main

Java port of Craps. Updated Readme with detailed breakdown of the original BASIC code.
This commit is contained in:
Jeff Atwood
2022-01-14 09:20:54 -08:00
committed by GitHub
3 changed files with 357 additions and 0 deletions

View File

@@ -17,3 +17,211 @@ As published in Basic Computer Games (1978):
Downloaded from Vintage Basic at
http://www.vintage-basic.net/games.html
### Comments on the BASIC code for re-implementers.
15 LET R=0
`R` is a variable that tracks winnings and losings. Unlike other games that
start out with a lump sum of cash to spend this game assumes the user has as
much money as they want and we only track how much they lost or won.
21 LET T=1
22 PRINT "PICK A NUMBER AND INPUT TO ROLL DICE";
23 INPUT Z
24 LET X=(RND(0))
25 LET T =T+1
26 IF T<=Z THEN 24
This block of code does nothing other than try to scramble the random number
generator. Random number generation is not random, they are generated from the
previous generated number. Because of the slow speed of these systems back then,
gaming random number generators was a concern, mostly for gameplay quality.
If you could know the "seed value" to the generator then you could effectively
know how to get the exact same dice rolls to happen and change your bet to
maximize your winnings and minimize your losses.
The first reason this is an example of bad coding practice is the user is asked
to input a number but no clue is given as to the use of this number. This number
has no bearing on the game and as we'll see only has bearing on the internal
implementation of somehow trying to get an un-game-able seed for the random number
generator (since all future random numbers generated are based off this seed value.)
The `RND(1)` command generates a number from a seed value that is always
the same, everytime, from when the machine is booted up (old C64 behavior). In
order to avoid the same dice rolls being generated, a special call to `RND(-TI)`
would initialize the random generator with something else. But RND(-TI) is not
a valid command on all systems. So `RND(0)`, which generates a random number
from the system clock is used. But technically this could be gamed because the
system clock was driven by the bootup time, there wasn't a BIOS battery on these
systems that kept an internal real time clock going even when the system was
turned off, unlike your regular PC. Therefore, in order to ensure as true
randomness as possible, insert human reaction time by asking for human input.
But a human could just be holding down the enter key on bootup and that would
just skip any kind of multi-millisecond variance assigned by a natural human
reaction time. So, paranoia being a great motivator, a number is asked of the
user to avoid just holding down the enter key which negates the timing variance
of a human reaction.
What comes next is a bit of nonsense. The block of code loops a counter, recalling
the `RND(0)` function (and thus reseeding it with the system clock value)
and then comparing the counter to the user's number input
in order to bail out of the loop. Because the `RND(0)` function is based off the
system clock and the loop of code has no branching other than the bailout
condition, the loop also takes a fixed amount of time to execute, thus making
repeated calls to `RND(0)` predictive and this scheming to get a better random
number is pointless. Furthermore, the loop is based on the number the user inputs
so a huge number like ten million causes a very noticable delay and leaves the
user wondering if the program has errored. The author could have simply called
`RND(0)` once and used a prompt that made more sense like asking for the users
name and then using that name in the game's replies.
It is advised that you use whatever your languages' random number generator
provides and simply skip trying to recreate this bit of nonsense including
the user input.
27 PRINT"INPUT THE AMOUNT OF YOUR WAGER.";
28 INPUT F
30 PRINT "I WILL NOW THROW THE DICE"
40 LET E=INT(7*RND(1))
41 LET S=INT(7*RND(1))
42 LET X=E+S
.... a bit later ....
60 IF X=1 THEN 40
65 IF X=0 THEN 40
`F` is a variable that represents the users wager for this betting round.
`E` and `S` represent the two individual and random dice being rolled.
This code is actually wrong because it returns a value between 0 and 6.
`X` is the sum of these dice rolls. As you'll see though further down in the
code, if `X` is zero or one it re-rolls the dice to maintain a potential
outcome of the sum of two dice between 2 and 12. This skews the normal distribution
of dice values to favor lower numbers because it does not consider that `E`
could be zero and `S` could be 2 or higher. To show this skewing of values
you can run the `distribution.bas` program which creates a histogram of the
distribution of the bad dice throw code and proper dice throw code.
Here are the results:
DISTRIBUTION OF DICE ROLLS WITH INT(7*RND(1)) VS INT(6*RND(1)+1)
THE INT(7*RND(1)) DISTRIBUTION:
2 3 4 5 6 7 8 9 10 11 12
6483 8662 10772 13232 15254 13007 10746 8878 6486 4357 2123
THE INT(6*RND(1)+1) DISTRIBUTION
2 3 4 5 6 7 8 9 10 11 12
2788 5466 8363 11072 13947 16656 13884 11149 8324 5561 2790
If the dice rolls are fair then we should see the largest occurrence be a 7 and
the smallest should be 2 and 12. Furthermore the occurrences should be
symetrical meaning there should be roughly the same amount of 2's as 12's, the
same amount of 3's as 11's, 4's as 10's and so on until you reach the middle, 7.
But notice in the skewed dice roll, 6 is the most rolled number not 7, and the
rest of the numbers are not symetrical, there are many more 2's than 12's.
So the lesson is test your code.
The proper way to model a dice throw, in almost every language is
`INT(6*RND(1)+1)` or `INT(6*RND(1))+1`
SideNote: `X` was used already in the
previous code block discussed but its value was never used. This is another
poor coding practice: **Don't reuse variable names for different purposes.**
50 IF X=7 THEN 180
55 IF X=11 THEN 180
60 IF X=1 THEN 40
62 IF X=2 THEN 195
65 IF X=0 THEN 40
70 IF X=2 THEN 200
80 IF X=3 THEN 200
90 IF X=12 THEN 200
125 IF X=5 THEN 220
130 IF X =6 THEN 220
140 IF X=8 THEN 220
150 IF X=9 THEN 220
160 IF X =10 THEN 220
170 IF X=4 THEN 220
This bit of code determines the routing of where to go for payout, or loss.
Of course, line 60 and 65 are pointless as we've just shown and should be removed
as long as the correct dice algorithm is also changed.
62 IF X=2 THEN 195
....
70 IF X=2 THEN 200
The check for a 2 has already been made and the jump is done. Line 70 is
therefore redundant and can be left out. The purpose of line 62 is only to
print a special output, "SNAKE EYES!" which we'll see in the next block creates
duplicate code.
Lines 125-170 are also pointlessly checked because we know previous values have
been ruled out, only these last values must remain, and they are all going to
the same place, line 220. Line 125-170 could have simply been replaced with
`GOTO 220`
180 PRINT X "- NATURAL....A WINNER!!!!"
185 PRINT X"PAYS EVEN MONEY, YOU WIN"F"DOLLARS"
190 GOTO 210
195 PRINT X"- SNAKE EYES....YOU LOSE."
196 PRINT "YOU LOSE"F "DOLLARS."
197 LET F=0-F
198 GOTO 210
200 PRINT X " - CRAPS...YOU LOSE."
205 PRINT "YOU LOSE"F"DOLLARS."
206 LET F=0-F
210 LET R= R+F
211 GOTO 320
This bit of code manages instant wins or losses due to 7,11 or 2,3,12. As
mentioned previously, lines 196 and 197 are essentially the same as lines
205 and 206. A simpler code would be just to jump after printing the special
message of "SNAKE EYES!" to line 205.
Lines 197 and 206 just negate the wager by subtracting it from zero. Just saying
`F = -F` would have sufficed. Line 210 updates your running total of winnings
or losses with this bet.
220 PRINT X "IS THE POINT. I WILL ROLL AGAIN"
230 LET H=INT(7*RND(1))
231 LET Q=INT(7*RND(1))
232 LET O=H+Q
240 IF O=1 THEN 230
250 IF O=7 THEN 290
255 IF O=0 THEN 230
This code sets the point, the number you must re-roll to win without rolling
a 7, the most probable number to roll. Except in this case again, it has the
same incorrect dice rolling code and therefore 6 is the most probable number
to roll. The concept of DRY (don't repeat yourself) is a coding practice which
encourages non-duplication of code because if there is an error in the code, it
can be fixed in one place and not multiple places like in this code. The scenario
might be that a programmer sees some wrong code, fixes it, but neglects to
consider that there might be duplicates of the same wrong code elsewhere. If
you practice DRY then you never worry much about behaviors in your code diverging
due to duplicate code snippets.
260 IF O=X THEN 310
270 PRINT O " - NO POINT. I WILL ROLL AGAIN"
280 GOTO 230
290 PRINT O "- CRAPS. YOU LOSE."
291 PRINT "YOU LOSE $"F
292 F=0-F
293 GOTO 210
300 GOTO 320
310 PRINT X"- A WINNER.........CONGRATS!!!!!!!!"
311 PRINT X "AT 2 TO 1 ODDS PAYS YOU...LET ME SEE..."2*F"DOLLARS"
312 LET F=2*F
313 GOTO 210
This is the code to keep rolling until the point is made or a seven is rolled.
Again we see the negated `F` wager and lose message duplicated. This code could
have been reorganized using a subroutine, or in BASIC, the GOSUB command, but
in your language its most likely just known as a function or method. You can
do a `grep -r 'GOSUB'` from the root directory to see other BASIC programs in
this set that use GOSUB.
The rest of the code if fairly straight forward, replay the game or end with
a report of your winnings or losings.

View File

@@ -0,0 +1,24 @@
10 PRINT "DISTRIBUTION OF DICE ROLLS WITH INT(7*RND(1)) VS INT(6*RND(1)+1)"
20 DIM A(12)
30 DIM B(12)
100 FOR X = 1 TO 100000 : REM CHOOSE A LARGE NUMBER TO GET A FINER GRAINED HISTOGRAM
140 REM GET A NUMBER FROM 0 TO 6 INCLUSIVE WITH THE INTENT TO THROW AWAY ZEROES.
150 LET D1 = INT(7*RND(1))
155 LET D2 = INT(7*RND(1))
160 LET S1 = D1+D2
165 REM IF THIS SUM IS LESS THAN TWO THEN TRY AGAIN.
170 IF S1<2 THEN 150
199 REM GET A NUMBER FROM 0 TO 5 THEN ADD 1 TO IT TO MAKE IT 1 TO 6
200 LET D3 = INT(6*RND(1))+1
210 LET D4 = INT(6*RND(1))+1
220 LET S2 = D3+D4
245 REM USE OUR ARRAY AS A HISTOGRAM, COUNTING EACH OCCURRENCE OF DICE ROLL
250 A(S1) = A(S1) + 1
260 B(S2) = B(S2) + 1
290 NEXT X
300 PRINT "THE INT(7*RND(1)) DISTRIBUTION:"
310 FOR I = 2 TO 12 :PRINT I,:NEXT:PRINT
320 FOR I = 2 TO 12 :PRINT A(I),:NEXT:PRINT
325 PRINT "THE INT(6*RND(1)+1) DISTRIBUTION"
330 FOR I = 2 TO 12 :PRINT I,:NEXT:PRINT
340 FOR I = 2 TO 12 :PRINT B(I),:NEXT:PRINT

View File

@@ -0,0 +1,125 @@
import java.util.Random;
import java.util.Scanner;
/**
* Port of Craps from BASIC to Java 17.
*/
public class Craps {
public static final Random random = new Random();
public static void main(String[] args) {
System.out.println("""
CRAPS
CREATIVE COMPUTING MORRISTOWN, NEW JERSEY
2,3,12 ARE LOSERS; 4,5,6,8,9,10 ARE POINTS; 7,11 ARE NATURAL WINNERS.
""");
double winnings = 0.0;
do {
winnings = playCraps(winnings);
} while (stillInterested(winnings));
winningsReport(winnings);
}
public static double playCraps(double winnings) {
double wager = getWager();
System.out.println("I WILL NOW THROW THE DICE");
int roll = rollDice();
double payout = switch (roll) {
case 7, 11 -> naturalWin(roll, wager);
case 2, 3, 12 -> lose(roll, wager);
default -> setPoint(roll, wager);
};
return winnings + payout;
}
public static int rollDice() {
return random.nextInt(1, 7) + random.nextInt(1, 7);
}
private static double setPoint(int point, double wager) {
System.out.printf("%1$ d IS THE POINT. I WILL ROLL AGAIN%n",point);
return makePoint(point, wager);
}
private static double makePoint(int point, double wager) {
int roll = rollDice();
if (roll == 7)
return lose(roll, wager);
if (roll == point)
return win(roll, wager);
System.out.printf("%1$ d - NO POINT. I WILL ROLL AGAIN%n", roll);
return makePoint(point, wager); // recursive
}
private static double win(int roll, double wager) {
double payout = 2 * wager;
System.out.printf("%1$ d - A WINNER.........CONGRATS!!!!!!!!%n", roll);
System.out.printf("%1$ d AT 2 TO 1 ODDS PAYS YOU...LET ME SEE...$%2$3.2f%n",
roll, payout);
return payout;
}
private static double lose(int roll, double wager) {
String msg = roll == 2 ? "SNAKE EYES.":"CRAPS";
System.out.printf("%1$ d - %2$s...YOU LOSE.%n", roll, msg);
System.out.printf("YOU LOSE $%3.2f%n", wager);
return -wager;
}
public static double naturalWin(int roll, double wager) {
System.out.printf("%1$ d - NATURAL....A WINNER!!!!%n", roll);
System.out.printf("%1$ d PAYS EVEN MONEY, YOU WIN $%2$3.2f%n", roll, wager);
return wager;
}
public static void winningsUpdate(double winnings) {
System.out.println(switch ((int) Math.signum(winnings)) {
case 1 -> "YOU ARE NOW AHEAD $%3.2f".formatted(winnings);
case 0 -> "YOU ARE NOW EVEN AT 0";
default -> "YOU ARE NOW UNDER $%3.2f".formatted(-winnings);
});
}
public static void winningsReport(double winnings) {
System.out.println(
switch ((int) Math.signum(winnings)) {
case 1 -> "CONGRATULATIONS---YOU CAME OUT A WINNER. COME AGAIN!";
case 0 -> "CONGRATULATIONS---YOU CAME OUT EVEN, NOT BAD FOR AN AMATEUR";
default -> "TOO BAD, YOU ARE IN THE HOLE. COME AGAIN.";
}
);
}
public static boolean stillInterested(double winnings) {
System.out.print(" IF YOU WANT TO PLAY AGAIN PRINT 5 IF NOT PRINT 2 ");
int fiveOrTwo = (int)getInput();
winningsUpdate(winnings);
return fiveOrTwo == 5;
}
public static double getWager() {
System.out.print("INPUT THE AMOUNT OF YOUR WAGER. ");
return getInput();
}
public static double getInput() {
Scanner scanner = new Scanner(System.in);
System.out.print("> ");
while (true) {
try {
return scanner.nextDouble();
} catch (Exception ex) {
try {
scanner.nextLine(); // flush whatever this non number stuff is.
} catch (Exception ns_ex) { // received EOF (ctrl-d or ctrl-z if windows)
System.out.println("END OF INPUT, STOPPING PROGRAM.");
System.exit(1);
}
}
System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE");
System.out.print("> ");
}
}
}