My first terminal game
David Sobral
Posted on May 29, 2022
Hello, world!
This is my first blog post as an aspiring developer. I started this journey a month ago when I decided to change careers and join the IT world.
Not knowing where to start, I went to Codeacademy and joined their Computer Science course, hoping to get a general view into what it takes to start programming, and to get a broader scope into the possible paths I might take as I decide what to specialize into.
After finishing the first module, CS101, I was tasked with writing a simple terminal game using the knowledge they had taught me so far. And so, I decided to program a Blackjack terminal game.
The code is located at GitHub. I have decided I have learned all I can from it for now. Maybe I will come back to it in the future and rewrite it, if only to see how much I've learned since the time of this writing.
EDIT: I've rewritten the whole code from the ground up based on the lessons learned from this first foray. It's much slicker now. It also looks better when played on the terminal. It's coded to support infinite players and infinite hands per player. The link is here, if you want to check it out and compare the changes.
Learning to play Blackjack
First of all, I didn't even know how to play blackjack before taking this project. I remember having read (most) of Edward O. Thorp's 'A Man for All Markets' and not getting most of what he was talking about when he narrated his comings and goings (and sometimes getting kicked out) from Casinos as he counted cards and played the dealers before he gave up on all that and decided to turn to playing the markets.
I didn't know the rules back then, and I didn't care to learn them. That was not the reason why I was reading that book. Now that I know them, I get the feeling I might have read all the way through without getting bored if I had known them.
But now I decided to program a Blackjack game without knowing how to play Blackjack. I took on that challenge, therefore I had no choice but to get to know the game, in a short span of time no less.
First lines of code
There was no need to become an expert at the game before I started to code. In fact, I decided the way to go was to divide the project into tranches (Ed would be proud). That meant simply writing down the rules at the beginning of the file, and referring to them when needed as I laid out new Functions.
With that done, it was time to define the only two needed Classes for the game: class Dealer:
and class Player:
.
The first decision I had to make as I set out was whether I was writing a singleplayer game or a multiplayer game. Singleplayer would be easier, not as challenging, and boring. Multiplayer would be more of a challenge simply because I had not been taught how to define new objects dynamically from inside of a function.
Sure, I could just take the easy path and just leave three variables waiting to be defined into objects.
player1 = ''
player2 = ''
player3 = ''
Wouldn't that be boring? Further, isn't this a learning project? Might as well learn something from it, so I googled away:
How to make python create variables
One important thing is that I needed to write code that would create more than one Variable from a single Class dynamically.
Writing def create_var(): new_var = class() return new_var
is easy, I knew how to do that. But what I needed to do is to define multiple Player Objects as per the player's choice.
The first question a person gets when they run blackjack.py is 'how many players are we dealing with?'. The amount of people playing was up to the player. Leaving n empty variables just waiting for a definition would not only be counterproductive, it would make the code bulky and prone to errors, in my mind.
I needed a simple Function that would take the number given by the person answering that question and create the same amount of Player Objects like I described in previous paragraphs: player1, player2, player3...
What if the player wanted to play with 20 other people? As unlikely that is, I wanted to account for that in my code. (In the end I limited the game to 3 players for the sake of simplicity, but that can be changed by changing a number in a single line of code, and it wouldn't affect the rest of the code at all.)
I eventually ran into this stackoverflow thread where someone asked the very question I was asking myself. It took me some time to understand the answers this someone was given. I got it when I realized that Global Variables are simply Keys inside a bigger Dictionary called globals()
.
The second best voted answer for that thread suited my needs better. I needed a Function that would pass Global Variables that I could use anywhere without having to pass a Dictionary around. You will see that I ended up doing that anyway, but I only realized what I was doing halfway into the project, and I decided there was no need to fix that at that point.
You will see that I made a bit of a mess there. I inserted Objects into globals()
, like I wanted, but then I also put them into a list that I then passed around my Functions. I could have simply gone with creating a local Key and then inserting that Key into a dict_of_players
Dictionary with the player object as a Value.
Well, lesson learned. A few, in fact! I can insert new Variables straight into globals() when I need to create a bunch of Global Variables at once. Objects don't need to be defined into a variable for them to be used around my code. In fact, I didn't even need to create player1, player2 and so forth names for what I wanted to do. As you will see in my code, I used a List composed of Objects that I iterated through inside other Functions to perform the tasks needed for the game to work. In that case, I didn't even need to work with Dictionaries. Also, spamming Keys into globals() might not be the best idea, as I could accidentally overwrite another Variable.
There are many ways to do the same thing in programming. I've been told that many times, but I've seen that happen in practice only now.
With those lessons learned, time to move on.
Hit, stay, double down...
There isn't too much to say about these functions. Only that what took me the most time was figuring out that I needed to account even for the most unlikely situations possible in my code.
If you also don't know the rules for Blackjack, like I didn't, I'll give you a quick and simple rundown:
Blackjack is a game where the objective is to beat the dealer. You beat the dealer by scoring more than them at the end of the round. You do that by having a hand with a higher value than them (and by not going bust, e.g. scoring more than 21 points).
Each card in a standard 52 card deck has a value. Most cards have face value. A 2 of Diamonds is worth 2 points. A 10 of Spades is worth 10 points. Jacks, Kings and Queens are all also worth 10 points. As you can see, cards worth 10 points are the majority.
Now, what adds spice to the game are Aces. They are worth 1 point. They are also worth 11 points. Their value is determined by what other cards you have in your hand.
Say you have three cards in your hand. A King of Spades, a 9 of Diamonds and an Ace of Clubs. That hand is worth 20 points: 10 + 9 + 1)
Remove that King of Spades from your hand, and it's now worth 20 points as well: 9 + 11.
What if you had a card worth 10 and an ace? That hand is worth 21 points, and that's called a Blackjack. If you start out with a Blackjack, you win that turn right away, and the payout is bigger (3:2 or 6:5, depending on the rules).
Now, the simplest way to play Blackjack is with a single 52 card deck. There are 4 aces in such deck. If you're the first player being dealt a card, the chances of getting a single ace are slim, 1/52, that is less than 2%. Those chances go up as the game goes on without an ace coming out.
Getting 2 aces right away is even more unlikely. Rounding up, that is 0,04%. Imagine getting two aces, and then another one, and then another one? Those are astronomically low chances. Still, that IS possible.
The reason why I'm talking about this is because as I playtested my game, I realized it was bugging out almost every time I got an ace. When I fixed those bugs, I started getting bugs as two aces appeared in the game. I still haven't seen three aces appear in a single round, but I'm sure if I hadn't rewritten all my functions at a point in the development of my project, I'd have seen it bug out even after I patched them up to account for one ace or two aces.
At that point, I realized I'd have to rewrite most of my functions to account for the slightest chance that a player could have two, three or even four aces in hand.
That's when my code went from 200-300 hundred lines of code to over a thousand lines of code.
The hit()
Function, as seen above, is a single Function that appears twice in blackjack.py. It is used by the Dealer Class and by the Player Class to draw a new card from the deck, check it's value, save it to the hand value counter, append it to the player/dealer's hand list and print out which card has been drawn, how much the player/dealer's hand is now worth, and based on that, decide if the player/dealer is bust or not.
This Function is a few lines short of 200 lines, while before it was no longer than 15 or 20 lines, if I remember correctly. The reason for that is because it needs to make different decisions and calculations and print different things altogether if the player or dealer has zero, one, two, three or four aces in hand.
Those two appearances together account for nearly half of the whole file.
Modularization
On this matter, I think it's a good idea to talk about modularization.
As I worked on this project, I realized it was getting more and more convoluted. Some functions are exceptionally complicated, the hit()
function being the prime example of this.
Before writing this post, I had a short conversation about this project with someone that is orders of magnitude more experienced than me, and they gave me some very valuable tips about coding that pertain to the subjects of readability, testability and modularization.
One of the hardest things to test in my code was how my code would behave when aces come up. Think rerunning the code a million times until someone draws them from the deck. Even so, as I said before, I never got more than two aces in a single round. I still don't know if everything would run fine if I got three or four aces in a single hand. That pertains to testability.
When it comes to readability, this project doesn't fail, but doesn't score too high above the average, I would say. As I've been told, to understand how a function works, one would have to carefully read every single like of code.
My code is comprised of God Functions, as I've been told. Long and complicated functions that deal with much more than they should have to. That affects readability, and testability, most of all. But I imagine it also makes it much longer and perhaps heavier than it could have been if I had written it with more Classes that compartmentalized more efficiently each function that needs to be executed for things to work just the same.
Conclusion
A fine conclusion for this is to say that, although the code isn't perfect, it's working exactly as it was designed to work. The game is playable almost indefinitely, as long as the deck doesn't run out of cards. Maybe one last thing to add to the code to make the game rearrange and reshuffle the deck once it's run out.
Other than that, my goals for this project have been attained: to learn.
I've you've read all the way over here, and you've had the curiosity to check the code out, I would be more than glad to hear any commentary, critique and tips that you might have about it.
Posted on May 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.