diff --git a/29_Craps/README.md b/29_Craps/README.md index 7372b99f..3c45c62b 100644 --- a/29_Craps/README.md +++ b/29_Craps/README.md @@ -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. + + + diff --git a/29_Craps/distributions.bas b/29_Craps/distributions.bas new file mode 100644 index 00000000..a963b228 --- /dev/null +++ b/29_Craps/distributions.bas @@ -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 diff --git a/29_Craps/java/src/Craps.java b/29_Craps/java/src/Craps.java new file mode 100644 index 00000000..483c16a9 --- /dev/null +++ b/29_Craps/java/src/Craps.java @@ -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("> "); + } + } +}