After a good three months, the ChiPy Mentorship program’s current session has come to an end and while there is a lot more that can be done on this project, I feel like this is a good time for a detailed writeup on my project’s current state. It’s nowhere near what I wanted it to be, but I do have a frame in which to build additional details. Having gone with the repository name PlayingWithCards for so long, I think I’ll stick with that for my project’s name.
https://github.com/nickhattwick/PlayingWithCards
Overview
Currently, the program plays a version of Magic with forty card decks consisting of type-less lands and effect-less creature. With a simplified game structure, players summon creatures to attack and block, trying to take their opponents life points from twenty to zero while defending their own. Players can play against an autopilot with set moves or a learning computer player.
The next layer is the problem is the logging system which logs every move made, and parses the information keeping a growing record of moves (currently creatures summoned) and how often they led to a win or a loss. Using this log, the learning computer player calculates which cards to play during the game, while also updating the log and its strategy.
Rules of the Game
- Each player starts with a 40 card deck made up of lands and creatures, 20 life points, and an empty field, hand and discard pile.
- Both players shuffle and draw 7 cards from their respective decks.
- A player is randomly selected to go first and players take turns making moves
- At the beginning of each players turn, they draw a card and untap all cards they control. During each player’s turn they can play one land card and tap each untapped land they have to produce 1 Mana each.
- During their turn, players can summon creatures by spending Mana equal to the creature’s cost.
- Players can attack their opponent with the creatures they summon, and their opponent can block with one of their creatures. In that case, the creature with lower power will be destroyed (in case of a tie, both are destroyed).
- Attacking or blocking require creatures to become tapped, making them unable to attack or block again until they are untapped.
- If a creature’s attack is not blocked, the player it’s attacking loses life equal to its power.
- If a players life points hit zero, they lose.
Game Structure
Before getting into the specifics of how the game works, here’s a quick rundown of how the game is organized. Each player has a deck of cards, which is a list of Card objects. These decks are imported into a Board class which contains the game logic for one player and the functions needed to move cards around the players zones. Each Player (another class object) is assigned a Board and the Player class contains functions to control the board as well and to handle the attacking/blocking logic which require input from both players. The Player class is divided into subclasses that allow a human or computer to have different ways of inputting their moves. Now, onto a more detailed version of the game, starting at the end of the import chain with the cards, themselves.
As a card game, some of the most important factors are the cards themselves and how they’re programmed. At present, there are six different cards in the games, which are roughly the same as in the initial prototype of the game. These are five creatures with powers ranging from two to six and the land cards used to to play them. Initially these were represented as the string “l” and integers 2 through 6, with one number representing everything about the creature cards. While it created a playable game temporarily, if I wanted to represent specific cards, this wouldn’t work.
In order to better represent cards, I started with creating a “Card” class.

In its current version, all cards are represented with this one class. Every card will have a “kind”, which are either “land” or “creature” at the moment, and will get more detailed with subtypes as the game gets closer to Magic. Every card will also have an individual card name. At the moment, only creatures have cost and power, so presently, those values default to zero. This works presently, though when I add more kinds of cards, I will likely add subclasses to different types of cards to better match the values they do have.
The remaining three values in the _init_ function are different states a card can be in. Both lands and creatures have a tapped state, and the tap and untap methods allow for easy switching of the modes. Only creatures have attacked/blocked states, which are more references for the logging function rather than the game itself. Although as complicated as Magic can get, I’m fairly certain there are cards that will require me to keep those states. Finally, the _str_ and _repr_ function allow the game to print and return the objects as relevant information a person can understand rather than the object’s place in memory.
A deck is a list of these card objects, and currently all players use the same forty card deck.

This next part creates a deck of six different cards that are improved versions of the origin one character cards. Each card is given a name such as Bear or Vampire and a cost to play and power corresponding to its number. While the cost and power were taken care of by one number in the previous version, as the game gets more complex, there will be cards with special effects and ones where the cost and power are different. The addition of names also helps make logging easier as well, as well the ability to move cards between game zones of check if a card is in a specific place. The find_by_name function allows the game to look up a card by its name, marking it to be used by another function and checking that it exists and that everything is running as it should. This function is very import and used all over the program.
In order to play creatures you tap lands to get Mana, a sort of currency used to play creatures. In its present state, Mana is a class with with an amount value that can be increased or decreased and a _repr_ function that represents it as its amount value. The state of mana.py is rather small right now since at the moment its just a number. The file will become more important in the future as the full game has different colors of Mana which are required to meet more specific costs. Additionally, types of Mana can have specific effects or restraints based on the effect of the card used to produce it. While it fills a very small role right now, it does set a class that can be expanded on to increase Mana capabilities later without messing much with the main functionality of the program.
Board.py is a program created to take care of all of the game logic in my program. It contains the code that imports the cards and Mana and controls how cards move around different zones and what state they are in. This is also the file that creates those zones.

The GameControl class represents all the different zones that cards can move between. Zones are represented by lists where the card objects are stored and consist of the hand, the field, the lands, the discard pile and the deck. The two main functions that move cards around are draw and move_card.

Draw takes care of moving unknown cards from the deck into the hand and making a player who tries to draw from an empty deck lose. Move_card on the other hand takes in three variables, the card its moving, where its starting point is, and where its supposed to end up. Then, as its name suggests, it moves the card. This function allows cards to move between any of the zones and is used in this file to play land by moving land from the hand to land zone

and to summon creatures, by moving them from the hand to the field.

The tap_for_mana function allows for lands to tap individually, although with the game being simplified, most of the the time, tap_all function is what will be called to produce all Mana possible at once. As the game improves, the tap functions will change as the game becomes closer to Magic.

I’ll get to that @jsonlog in a bit. It’s worth noting that there’s also an untap_all function in here, which does untaps all of the player’s cards.
Now, let’s talk about the “players” as they pertain to the program. Originally, the game was only playable between one human and one computer, but by containing most of the game logic in GameControl, it can exist without being tied to a player, allowing for a lot more flexibility. Since a GameControl object represents the board and logic for one specific player, GameControls can be given to players when the game begins based on who the players are. In order to represent them, a basic Player class was created and initiated with various values that are true for all players as well as references to some functions which exist in different forms across the different types of players .

As you can see, when a player is created it will be given its own GameControl object. While I mentioned that the board contained the game logic, there is an exception that wouldn’t fit nicely. As a GameControl represents one player’s zones and card movements, it can handle most logic except for parts where the two players interact. Because of this, the attacking and blocking logic is currently handled in the Player class.

These functions still call on the board to move the cards, but the logic of attacking and blocking relies on a lot of specifics from the player, due to how many choices need to be made in the process. If one player attacks and the other player blocks, a battle occurs, taken care of by the imported battle program from battle.py and determines which card or cards will be destroyed, calling back to board.py when resolved.

Again, this function is separated as when the game becomes more complete, the factors involved in battle will also become more complicated. One function to destroy a card which is made to reference one card card movement by passing specific arguments into board’s move_card argument, in this case from a players field to their discard pile. In a future version this function will likely be moved back to board as well as there will be more ways for cards to be destroyed than just the battle function. Additionally, the battle function will become more detailed, but for now it compares two creatures powers and destroys the one with less power. In case of a tie, both are destroyed.
Lastly, while the board object may actually play or tap cards, it’s up for the player to designate the cards that will be played or tapped. However, a single function to determine when and which arguments are passed into the board wouldn’t work because a human would interact differently with the game than a computer. For this, there are subclasses to represent the different types of players and there interactions.
The Human Player
The different players have different turn_prompt functions that will be called to determine how the player’s turn will be run. In the case of the human player, it needs print information based on the game state and give prompts for the player to respond to. After printing information, a “choice” variable will take an input from the player after asking what type of move they want to make.

There are four types of moves the human player will be able to choose from. “LAND” will play a land, “SUMMON” will prompt the player to input the card they want to summon calling the board’s summon function on that card, “TAP” will call tap_all for the player and tap all the player’s lands to generate Mana, and “ATTACK” prompts the player for to input which card they want to attack with. Until a player types “DONE”, the program will keep prompting them to inputting moves, with the board file handling the logic to make the move happen and check if a card is valid or not or if the player has already played their land for the turn. After “DONE” is called, the player’s turn will end, allowing the opponent to make their move.
During the opponent’s turn, the player’s block functions will handle everything related, passing in the blocker and making sure the block is valid, if not, passing back the question of whether or not to block. First, it checks whether they want to block the attacking creature, then which creature they will block with, and then lets the battle function take care of the battle, if it occurs.

Additional game details are printed throughout the program to let players know who wins or what the life totals are, although the above functions are everything specific to the human player.
The AutoPilot
The AutoPilot is the opposing player controlled by the computer, which has fixed logic for its moves. It is not the best strategy for the game, but is made to have a computer player that puts up resistance and which will win if it goes unchallenged. The human player can play against it, and it will give a learning computer player to learn if it wants to win. The leaning player’s turn_prompt is a list of different types of moves to call.

First it plays a land, then taps all its mana, and then runs its summon function and attacks with everything, before ending its turn.
The AutoPilot also has two other functions that make it work; auto_summon and all_attack. While all_attack is a “for loop” that just has all creatures the player controls attack one at a time, the AutoPilot’s auto_summon function has the decision making for which creature the player wants to summon. In this case, it’s a loop that plays the strongest card the player has the Mana to play.

While a better attack/block strategy will need to be implemented later, the all out attack strategy means that the computer will never block, so the computer’s will_block function will just call the player’s take_damage function.
The last of the players is called StillLearning, and is the computer player that uses an updating strategy. It presently uses AutoPilot as the parent class and changes the auto_summon function to value cards differently. But before getting into that, let’s talk about the logging system the game uses.
The Logging System
The structure of the current logging system is one file, jsonlog.py, is run along with the game, called through decorators, which keeps a log of every single move made through the course of a game and stores it and stores them in results.json. After a game is finished, running parsing.py will take all of the summons from results.json and create/update a dictionary for each card with the amount of times that card was played by the winning player and each time it was used by the losing player and store it in parsed.json. The last part of the logging cycle is the player that uses it. StillLearning will use the ratios in parsed.json to determine which cards it plays.
First, let’s talk about the initial logging file. Jsonlog is made to collect a log of everything in a game. Initially, the program creates some blank variables to be edited as games are played. The turn number will be increased as part of the program as a way of incorporating the turn number into the game.

The information that’s logged will also be separated into three different classes. One that creates moves, one that creates turns, and one that creates games.

The Moves class takes two arguments; a kind and a detail. Kind represents what category of move it is (land, tap, summon, attack, etc.) while the detail would be an optional variable if more is needed to describe the move. For instance, the card that was summoned or the card that attacked would be the detail for the “attack” or “summon” kinds, while the amount of Mana generated would be the detail for “tap”. The Game takes in a list of players in the game and a game number (currently not being implemented) as well as winner and loser values to be added after the game’s conclusion.
The largest of the class objects is the Turn object which starts with a player and a turn number, as well as the life totals, board states and knowledge of the player’s hand and number of cards in the opponent’s hand. The goal is for the turn log to log all information the player has access to, in order to see what the state the game was when a move was made. A list of moves is also added to the turn object as a way of sorting moves by player and turn.
Games and turns are created with help from the initiate_game and initiate_turn functions respectively.

By passing in the variables needed to make the object, these functions will update the current game or turn to reflect the turn in progress. Of course, the turn will need a way to add moves, which are done via different types of log functions. For instance, summon_log will add a Move object with “Summon” as its kind and the card name for its detail. The land log will just record “Land” as the move’s kind.

These log functions were one of the most eye-opening things for me during the mentorship program. Obviously, I’d heard that Python was an object oriented programming language, but before my mentor taught me about decorators, it had never occurred to me to think of functions as objects. These decorator functions take in another function as well as all of its arguments, and run the added decorator’s code along with the code of the original function. These functions are also called in a unique way which makes it very easy to tell which functions are being decorated.

Simply putting “@” followed by the decorator function over the function being decorated. In this case, summon_log is made to take in the summon function from the board and edit it to give it logging abilities. What the decorator function does in this case is takes the name of the card the summon function is given and checks if it is on the field after running the function, and creates and adds the move to the current turn’s move log if so. The land_log function uses a similar method by checking whether the board’s playedland value for the turn changes from False to True, before logging the move.
In addition to the summon_log and land_log functions, there are also logs for tapping, attacking, and blocking, which work in the same way. All of these logs are placed using the the same decorator format, run a function and have a way of checking to see if the move happened by checking the status of the card. For the tap log, it checks whether a land switched from untapped to tapped. In the case of the attack and block logs, it checks if the creature’s attacked and blocked attributes change from false to true. In its present form it works pretty well, although I could see a scenario where it messes up due to multiple cards of the same name being played, so these functions are something I’ll want to look what I can change in the next version.
The remaining functions in jsonlog.py help to tie things together. First of, the end_turn function is called at the end of a players turn in the player file and takes care of archiving the current turn before the next turn starts and a new object is assigned to current_turn.

The next functions take the variables that have been stored and create a dictionary that’s organized to be stored in JSON. The turn_dict function deals with taking each turn in archived_turns and creating a dictionary to organize the turns. Each turn dictionary will have “number”, “player” and “moves” keys. While setting the values for the turn’s number and player values is pretty simple, organizing the moves is a bit more difficult. Initially the move will be set to a list, and then each move will have a dictionary created for it where the move’s kind is the key and the value is the detail. A list is created of each kind/detail combination and then appended to the turn_log’s “moves” list. Finally, the whole turn_log will be stored in turns_log, which will be a list of all turn dictionaries.

While turn_dict creates the dictionary for the turn, format_logging creates the final dictionary that will be stored. It’s a pretty simple function that wrap everything up, giving the game dictionary “number”, “winner”, and “loser” values and storing the turns_log in “turns.” These functions are run in order when the record_results function is called.

The record_results function is called in player.py when take_damage is called on a player resulting in their life points running out. Since it’s called by the losing player, their name is the one passed in and the winner is the opponent. After calling turn_dict and format_logging, the last function called is write_to_json which takes the dictionary and puts it in results.json.

This wraps up jsonlog.py’s functionality and most of the first log itself, but as much as I tried to completely separate the log from the game, some of the functions still ended up being tied together. Most of the functions so far have been shown without the program that actually runs them.
Rungame.py is the file that a person will run to kick off the game as well as the logging system. It imports the Player subclasses from player as well as a function called full_turn, which I’ll get to shortly. It uses the shuffle and choice functions from the random library and finally imports jsonlog.

The first thing it does it set two players from the different classes and calls the log’s initiate game function with those players. Currently it’s set to have StillLearning face off against the AutoPilot, though in order to play with the human, one of these would just have to be switched with HumanPlayer. Next, it shuffles the players’ decks and gives them each a 7-card starting hand and decides the player who will play first.

From there it uses the all_turns functions to generate the variables needed to create a turn object for the log. First it stores a turn_number variable then each time it’s called it switches between outputting the player going first and the player going second, along with the turn number. Finally, once both are called, it increases the turn number and loops back to returning the first player.
Next it makes a new variable called keep_playing, sets it to True and runs a “for loop” that will run through all_turns and alternate whose turn it is, switching players whenever keep_playing becomes False. For each player’s turn, it calls jsonlog’s initiate_turn function passing in the player and turn_number provided by all turns. Next it will call the full_turn for the player by setting the value of keep_playing equal to what full_turn is returning. Eventually full_turn will return False and the loop will move on.
Moving on to the full_turn function stored in prompts.py, this is what calls for players to run their turns and takes care of some of the logic for reseting turns. The first thing full_turn does is set the conditions for it to return False, which are that a player’s life points become zero or a player runs out of cards. Using another True/False switch to determine when it Full Turn stop, the program sets can_act equal to True and moves on to the functions that start a player’s turn.
First this will untap each of the player’s lands, set their playedland value to False so they can play a land again, draws a card for the player and untaps each creature and sets its attacked value (used to check for the log) back to False.

Then lastly, it sets can_act equal to the player’s turn_prompt, calling the turn_prompt and letting it run until it returns False. This system takes care of passing the turns back and forth between players. Overall, while most of the game logic is handled elsewhere, rungame.py and prompts.py have an import role of combining this logic into a runnable program.
So far, the code shown and talked about is what runs the entirety of my game and the initial logging of each game. Now let’s talk about the second log and the last player object that learns as it goes.
The two parts of my current learning player are parsing.py and the StillLearning player object. First let’s talk about the parsing file, which takes results.json and updates a dictionary of each creature summoned and its win to loss ratio. Parsing.py consists of two functions. The first is get_victory_key, which takes in a data set and a player and outputs a victory key of ‘W’ if the data has a winner value that’s the same as the player or ‘L’ if it has a loser value of the player.

Next up, the function that uses get_victory_key is parse_log which opens results and creates a dictionary called summon_ratios. It then opens parsed.json and copies any existing dictionaries in it into summon_ratios.

After that, it looks through each turn in results.json and finds each summon that was made. It calls get_victory_key with the player who’s turn the summon was made during. Then for each card summoned, it adds one count to the value stored in the summon’s key for the corresponding victory_key or creates the key with a value of one if it doesn’t exist.

After going through each summon in the game and placing them accordingly, the program finally overwrites parsed.json with summon_ratios which has now combined the new files summon ratios with ones that were in parsed.json before.
Finally, the StillLearning player object takes in parsed.json and uses it to inform what moves it’s going to make. This new player inherits the AutoPilot’s function and brings in two new functions. First, it has a function called get_card_value which it uses to rank cards from the parsed.json file. Then it creates a new version of auto_summon to use this card value to determine which card it summons. Then it runs the same turn as AutoPilot, with a change in in the auto_summon function.

The get_card_value function is a relatively simple one, it takes in a card being summoned and a list of data. It takes in the existing count in the card’s “W” dictionary and subtracts the card’s “L” value from it and returns the value.
Next, auto summon starts by opening up parsed.json and loading it into summon data. From there, it sets empty current_card and current_card_value variables and creates a list of the creatures in the players hand. Looping through the creatures in the list, it sets the current creature as a choice. Then it proceeds if the card is one the player has enough Mana to summon. If the player doesn’t yet have a current_card then the first creature the player can possibly summon will be put in that slot and use get_card_value to get the current_card_value after searching for the card in summon_data.

If there’s already a current_card, then it will calculate the value of each possible summon and replace current_card with the new card and value if they are higher. Finally, once it’s thought about each creature in the player’s hand it will summon current_card (if there is one).
Currently StillLearning’s only learning function is its summon function with the rest of the turn working the same as the AutoPilot, but as the log changes, so will the values the player uses to determine which card to play.
That concludes my full description of the program. In order to run it with StillLearning against the AutoPilot, run rungame.py in Python3 to play a game and create the results log, then run parsing.py in order to update the parsed log that StillLearning uses. You can also run humangame.py in order to play a game using the keyboard to input moves.
Conclusion and Next Steps
Overall, I’m very pleased with how my project came out. As my very first programming project, I made a simplified yet working game along with an AI player that has an updating strategy. I’m also very happy with the organization of the program since it will be much easier to add new features to and try out different learning strategies since I won’t have to change much of the code to get new code to work.
So where would I like to go from here? I can see three different ways I’d like to move forward with the program. First, I want to make this game into the full version of Magic: the Gathering including a deck building program to build decks from an already existing JSON database of Magic cards. Second, I want to learn more about Machine Learning and algorithms. What I have right now is an updating strategy, but I haven’t come up with the best strategy. Lastly, at some point it would be neat to visualize the game for a human player so they can click and drag card objects.
With the code I’ve built so far, making the full game should be very doable. The hardest part will likely be converting the card’s effects into code the program can run and possibly making a program that knows enough to convert the card text for me. For future machine learning algorithms, I also know how to implement them, but the challenge will be learning more about machine learning and finding the right algorithms to use. The last step of actually visualizing the game will probably take longer depending on what I aim to learn first, as I’d likely look at learning another language in order to do that. Nevertheless, there are lots of things I’d like to learn about, and it’s definitely good to have a base program to build on as I learn things that could help.
Thank You
Lastly, I’d like to thank my mentor, Nikhil Sharma as well as Ray Berg and everyone who helped run the ChiPy Mentorship Program. This is the first program I’ve made and I learned a ton through the program. Every time I meat with my mentor he’d show me different ways of using Python that I wasn’t previously familiar with. I now know how importing from different files works, how to read/write files with Python as well as about decorators and using polymorphism with class objects. All in all, I’ve learned a lot about Python and I had a great mentor helping me out the whole way. Thanks again for everything.




Leave a Reply