While this conversion will deal with programming and technical topics, the intent here is to have a good time, learn a little, but not necessarily generate the most perfect Javascript code in history. If you're willing to go along, please read on!!!
We start with the first game in the book, "Acey Ducey," with the original BASIC source here. Take note of that archive, because we'll be referencing it in frequently in this project. And, for those who are just too eager to see the result, feel free to jump to the SourceForge AceyDucey archive for a ready-to-run set of files.
This first effort is a monument to things not turning out as expected. When I first looked at "Acey Ducey," I saw a trivial BASIC program that I thought would roll to Javascript in practically no time.
I was wrong.
AceyDucey is about as simple a card game as they come; pick two cards, then place a bet on whether the next card will fall within the two just dealt. The BASIC source for this game is a little over 100 lines; my Javascript version is 175 lines, not even counting the separate code for the console display window. And the differences in how the programs operate just point out different a language Javascript is from other contemporary languages, like C#. Heck, a console executable C# port of AceyDucey would have been trivial!
I was wrong.
AceyDucey is about as simple a card game as they come; pick two cards, then place a bet on whether the next card will fall within the two just dealt. The BASIC source for this game is a little over 100 lines; my Javascript version is 175 lines, not even counting the separate code for the console display window. And the differences in how the programs operate just point out different a language Javascript is from other contemporary languages, like C#. Heck, a console executable C# port of AceyDucey would have been trivial!
The Big Lesson
From a lesson I learned writing the ConsoleWindow, you can't allow Javascript to block awaiting input. That lesson extends to the games themselves. Snagging console input has to be done by a custom event handler, but no handler can fire until Javascript's single-thread-of-control exits any currently executing method. As a result, the simple loops in the original BASIC code to control betting and replay logic, all driven by user inputs, just don't map one-for-one to Javascript. Yes, I could have added an HTML input box and a button on the form to force the issue, but doing so wouldn't have kept the "spirit of the console" I'm trying to retain from these old games.
This decision has an important consequence. Because we can't simply port sequential lines of code, the design of the ported game necessarily changes somewhat. We end up discovering that the best way to conceptualize or model the Javascript version of AceyDucey or any other input-dependent program is as a state machine. A state machine is just a way of modeling a system that moves from different configurations or "states", with the machine "moving" from state-to-state allowing the "edges" to represent the transitions between the states. For our model, our games move to different states, with the goal of identifying states requiring user input. We then use that state information to tie our input handler to a function that knows how to handle each possible input state. Simple, eh? Yeah, it really is - a lot simpler in practice than it is in words :)
This modeling concept allows us to block off "chunks" of program behavior into methods that roughly reflect the "edges" of our game machine, moving to user input states. At those states, the program references the console's input via a callback method that, in turn, routes the input to another method within the program, continuing execution appropriately. This state model allows the "external" input handler to jump "back" into our program and keep running.
The game itself
All this discourse about input handling hasn't even touched on the game itself, which borders on the trivial - and allows us even to visit a bit of object orientation along the way. So let's dive in.
Most of the original BASIC code deals with nothing more than printing out the value of the current card, or one of JACK, QUEEN, or KING for face cards (values greater than 11). In fact, AceyDucey repeats card generation and display logic three times; twice in lines 270-650, and again in lines 730-900. Note, too, that AceyDucey doesn't even draw from a "real" deck of 52 cards; each one chosen is a simple random number each time. The sequence is simple:
- Pick a random number from 2-14
- If that value is less than 11, print the raw value
- For values 11, 12, 13, and 14, print "JACK," "QUEEN," "KING," or "ACE", respectively.
function AceyDucey(gameConsole) { // other code snipped for now function Card(){ var value = Math.floor((Math.random()*13)+1); this.Text = function(){ if (value==1){ return "Ace"; } else if( value>10 ){ switch (value){ case 11: return "Jack"; break; case 12: return "Queen"; break; case 13: return "King"; break; } } else { return value; } }; } this.CardValue = function() { return value;}; }
States of Indecision
We talked earlier about modeling the game as a series of states, and dividing up code accordingly. The easiest chunk is the instruction display in lines 10-80, which we simply plop in a ShowInstructions() method. The player has a betting stake we initialize to $100. We then lay the foundation for playing the game, which amounts to generating two cards, displaying them (SetupRound), getting the user's bet (GetPlayerBet), the determining a win or a loss (PlayBet), and checking for the user going broke after losing (BASIC lines 900-1040).
Because we must implement a callback to receive a user's input, but also must know how to route that input in that callback, we define states in which the program has to handle user input. When AceyDucey needs a user's bet, we define that to be the WAITING_FOR_BET state; when we are confirming whether the user wants to restart the game, we're in a WAITING_FOR_REPLAY state. We track the game's state in a variable "gameState," and define handlers for both of those states (PlayBet() and ConfirmRestart()), tying them together in the CommandDispatcher() callback:
function CommandDispatcher(command){ if (gameState==ref.States.WAITING_FOR_BET){ setTimeout(ref.PlayBet(command),250); return; } if (gameState==ref.States.WAITING_FOR_REPLAY){ setTimeout(ref.ConfirmRestart(command),250); return; } };When the console's Readline method is fired, we send a reference to CommandDispatcher to receive the result. The Dispatcher then checks the program state to know which method should be fired to handle the specific command; because PlayBet or ConfirmRestart may, in turn, need more input, we must ensure they are not fired until CommandDispatcher() terminates; hence, we use Javascript's "setTimeout()" facility to defer execution of the handlers until an arbitrary 250ms after CommandDispatcher ends and freeing up the Javascript execution thread. This neatly ties together the need for input with the handlers needed to interpret it.
Input handling - Looking at GetPlayerBet()
After a round is set up by displaying two cards, we have to get the user's bet. This is handled in the GetPlayerBet() method, which displays a message, sets the WAITING_FOR_BET state, and fires the readLine method with the CommandDispatcher callback:
this.GetPlayerBet = function() { console.write("Enter your bet (Q to quit): "); gameState = ref.States.WAITING_FOR_BET; console.readLine(CommandDispatcher); };
The Game is Up
Input handling is the most esoteric part of this port; the rest of AceyDucey is fairly simple. We wrap a Play() method around the SetupInstructions(), SetupRound(), and GetPlayerBet() methods for the initial run. The only remaining logic is to compare the two cards generated in SetupRound(); that comparison is done in the IsBetween() method and represents a bit of logic departure from the BASIC source. In the original AceyDucey, the program logic forces the first card to be the lower-valued card in lines 270-330, storing the card values in variables "A" and "B." The "payoff" card value in "C" is then compared in lines 910-930. I chose not to force the lower-first-card forcing logic, just wrapping the comparison into a single IsBetween() method that takes three values, and determines if the first value is between the other two.
A perfectly reasonable if not preferable alternate design for IsBetween() would be a Card-object specific method, Compare(), accepting a Card object as an argument, and returning -1, 0, or 1 to indicate which card is lower or higher.
Validation
The original AceyDucey performs several input validation checks. One ensures the player doesn't bet more than he has. Another checks whether the user wants to start the game again if they go bust. I added a third validation to allow the user a quit option the original didn't support - the option to "quit while you're ahead" by typing "Q" for a bet amount. All the possible bet values are handled in the IsValidBet() method:
this.IsValidBet= function(text){ if (text=="Q"){ quitting=true; return true; } var amount = parseInt(text); if (isNaN(amount)){ console.writeLine("You have to enter a number to bet, dude..."); return false; } if (amount > playerStake){ console.writeLine("You only have $" + playerStake +" to bet, dude..."); return false; } if (amount < 0) { console.writeLine("Cute. You can't bet less than $0."); return false; } return true; };If the user chooses to end the game, betting "Q", we validate that input an return immediately; otherwise, we take the integer value of the string to get the bet value via Javascript's parseInt() method. If the user hasn't typed in a numeric value, parseInt assigns the special "NaN" value as the result; we test for this before any more numeric comparisons are made. Once a valid number has been verified, we compare that value to the current player's stake in the "playerStake" variable and invaliding the bet accordingly. Surviving those checks validates the input and returns true back to the caller.
Playing the Bet
Once the user's bet is validated, we play the game by selecting a new "payoff" card, and comparing its value to the first two dealt and stored in the card1 and card2 variables. We adjust the player's stake by virtue of the win or the loss, and start the process by calling Replay() to repeat the gameplay cycle:
this.PlayBet = function(bet){ if (ref.IsValidBet(bet)){ if (quitting){ ref.GameEnd("You're quitting this game."); return; } bet=parseInt(bet); payoffCard = new Card(); console.writeLine("NEXT CARD IS: " + payoffCard.Text()); if (ref.IsBetween(payoffCard.CardValue(),card1.CardValue(),card2.CardValue())) { console.writeLine("WINNER!"); playerStake += bet; } else { console.writeLine("SORRY, YOU LOSE!"); playerStake -= bet; } this.Replay(); } else { this.GetPlayerBet(); } };
That's a wrap!
With a few other methods that are self-explanatory, that wraps up this lengthy discussion over this simple BASIC game. This implementation is by no means perfect; a "pure" implementation would probably convert the card generation to occur from an actual deck of 52 cards, and the comparison could, as noted, be moved to a method off the Card object. We leave those as refinements for the reader. Here's a screen shot of Javascript AceyDucey in action:Until next week!! -David
No comments:
Post a Comment