Table of Contents [Show]
Drawing a Dynamic Board
Printing line by line is not too difficult for the player's hand, as it just required 3 nested loops. The first to loop the rows of the cards, the second to loop through the cards in the hand, and the third to loop thought the columns making up that row of that card. While it would have been possible to print the rows for each card as a string or substring it is slightly more complicated to do this because of the variable width of the cards.
"Variable with of the card?" you ask. Yes, all cards are the same width, but by default the one-quadrant terminal that I'd want to stick this thing in is only 87 columns wide, so not all cards can be displayed at full width. Much like you do in real life, the cards must be fanned out behind one another in the interest of space. The standard assumption is that a console is 80 columns wide and so this is what I worked with. Since I don't trust my basic algebra skills, I wrote a short program to tell me the maximum width that each card can be displayed at as well as the space required to fill the remaining columns on each side with consideration for the fact that the last card is always full-width. I plugged these values into 3 arrays with the values for the number of cards left in the hand; there is no sense in having the main program calculate constants.
Significantly harder than the player's hand was the middle of the board. This requires that a variable number of cards for the second and fourth hands be printed as well as a variable number of in-play cards and spaces between them. Having the AI's hands spread to take up as much space as possible like the players hand does would have required an extra layer of conditions that would not have yeilded any real value to the player so I didn't bother fighting with that. Instead I just made the hands steadily shrink in one direction. This allowed me to just check the line number as well as how many cards each player has left and print either the correct pattern or the equivalent blank space as the hand shirks. Right in the middle I could again check the line number, print blanks where no card exists and then duplicate the behaviour of the players hand for the in-play cards less the 3 spaces on each side that are populated by the AI's hands. The top hand was the easiest since I could just replicate the shrinking hand behaviour for consistency, and then put half of the spaces on each side. I could have done these as a constant as well, but since the card width was not variable, the calculation was easy enough to do inline.
Thus Ace of Spades = Ace of Hearts
Once the graphics are out of the way, the logic of the program slips together quite nicely, after all, this is where C excels. One neat aspect of dealing purely with numbers that I thought would be fun to point out is the ease of defining equivalency. As a consequence of C's number system starting with 0, it is simple to treat a range of numbers equally while still keeping them distinct (this is possible in 1 based languages but requires a tedious [x-1] call). What I mean by this, is that I can number the cards from 0-51 and divide by 4 on any given number to calculate its equivalency. C will chop the decimal points off of an interger and automatically round the result down. Therefore 0 could be the ace of spades and 3 the ace of hearts, but it is very easy for the system to see that they have the same actual value.
This brings me to what inspired me to document the writing of this program in the first place; the titular problem of dealing. There is a common problem in programming of trying to solve problems in the way that a human would, rather than thinking of how a computer might deal with the same problem. I fell victim to this when it came to the problem of dealing.
When a human deals cards, we go through a process of shuffling and dealing to players in sequence, just to have the players re-sort the cards. We don't think about how inefficient this is because it is essential to fair, random dealing. At first pass I did just this. I wrote a shuffle algorithm that picked from an array of 0-51 at random, then moved all numbers larger than the picked card left by one in the array. Because the higher numbers were all pushed left they would overwrite the selection so that it couldn't be picked twice, and leave a stray copy of the highest number at the end. It ran 52 times, generating a random selection from a range that shrank once per run to leave out the duplicate at the end. Plugging the selected number on each run into a seprate array at the position equivalent to the run number yeilds a shuffled array containing each number between 0-51 once. Dealing was then as simple as putting every forth card into an array for each player. But this leaves you with 4 shuffled arrays and is what eventually placed my palm firmly on my face.
Before I got to that point, however, I had already written a quick bubble sort. For those who don't know, bubble sorts are really easy but extremely inefficient. This is because a bubble sort requires that you compare each pair of side by side items in an array, swapping them if they are out of order. Because any item can only move up to one position in sequence each time this is done and it is possible that the highest or lowest item may be on the exact opposite side, this will have to be repeated as many times as the list is long, less one. This means a maximum of n less one comparisons per run, times n less one runs, or (n-1)^2 comparisons where n is the number of items in the set. Luckily 4 sets of 13 is a very managable 574 comparisons and would have been handled in a tiny fraction of a second.
The Real Problem
While setting up code to pass each players hand to the bubble sort function, I started thinking about how silly it was to have to sort cards immediately after unsorting them. It screamed redundancy. It was only then that I realized that the point of dealing was really only about random distribution, and has nothing to do with ordering except to keep humans from cheating. It would make way more sense to deal the already sorted deck into piles at random until each hand was full.
This isn't difficult task, nor is it an illogical solution to leap to, it is just not the way people that people behave. Imagine having a deck of cards in assending order. If I started dealing into four piles I could easily make sure that I deal myself every forth card to guarantee a straight flush; the last 13 cards to guarantee the highest cards; the last 4 cards to guarantee 4 aces; etc. etc. Humans go through multiple levels of inefficiency in order to ensure that they don't know which cards are landing in which pile. You're computer is not invested in you winning, so it can truly distribute the cards at random without shuffling them in the slightest.
Thinking Like a Computer
Upon this realization it becomes a really simple algorithm:
- Start counting the deck from 0 to 51, and set a counter for each hand starting at 0.
- Select a random player from 0 to 4
- If the player's hand is full (ie. their counter has reached 13), then repeat the previous step.
- Place the current value of the deck counter into the position of the counter for the selected player.
- Increment the counter for that player and the deck.
- Repeat from step 2.