Learn Python and build a game with an AI companion

A generative AI–powered assistant can help you learn programming while building small projects such as a text game.

Danilo Poccia
Amazon Employee
Published Jun 13, 2024
To practice and learn Python, let's build a simple text game. To follow along, you need an IDE with the Amazon Q Developer plugin. Follow these steps to download and install the plugin.
The suggestions you'll get will be different from mine, but you can provide your own feedback and get similar, or better, results. At any step, you can ask for more information on anything that is not clear.

Finding the right project

First, I create a new directory with an empty game.py file. In my setup, I have my IDE open and the Amazon Q chat open on one side of the editor.
In the Amazon Q chat, I ask for some ideas:
Tell me 5 simple text games that I can build in Python.
IDE with Amazon Q chat window and an empty Python project
IDE with Amazon Q chat window and an empty Python project
In the chat, I get options such as Guess the Number, Hangman, or a Text Adventure. I'd like to try with a word-guessing game like Hangman, but I prefer to start with something easier such as Rock, Paper, Scissors. So I enter my next instruction in the chat.
Give me the starting Python code for a Rock, Paper, Scissors game.
I copy the suggested code in the editor (there's an Insert at cursor button just after the code) and save it.
After the code, Amazon Q provides an explaination of how it works:
Here's how the code works:
  1. The get_choices() function prompts the user to enter their choice (rock, paper, or scissors) and randomly selects the computer's choice from the same options. It returns a dictionary containing the player's and computer's choices.
  2. The check_win() function takes the player's and computer's choices as arguments. It first prints out the choices made by both players. Then, it checks for a tie condition. If it's not a tie, it checks the player's choice and the computer's choice to determine the winner based on the rules of Rock, Paper, Scissors. It returns the appropriate result message.
  3. The choices dictionary is created by calling the get_choices() function.
  4. The result variable stores the result message returned by the check_win() function, passing in the player's and computer's choices.
  5. Finally, the result message is printed to the console.
This is a basic implementation of the Rock, Paper, Scissors game. You can further enhance it by adding features like keeping score, playing multiple rounds, or implementing a more user-friendly interface.
I read the description and then run it to see what happens.
The basics of the game are there but there's a lot to improve! For example, the program ends after the first match.
In the chat, I ask to continue the game after the first turn. I also suggest to refactor the code to have a play_game function handling the game loop.
Create a play_game function that loops until the player enters "bye".
I apply all suggestions and go through their description. I am not copying all info back here, you can reproduce these steps and get a similar answer. Here's the full code after these changes.
I replace the code in the editor with the new version in the chat, save it, and run it again. This is better, now I can have a proper game with multiple turns!

Refactoring the code

I don't like how the three possible options (rock, paper, scissors) are continuously repeated in the code. Let's put them in a list. First, I ask:
How can I create a constant in Python?
From the answer, I get that the convention is to use uppercase letters.
Refactor the code and put the three options (rock, paper, scissors) into a global list. Because that list is constant, use an UPPERCASE name such as CHOICES.
In the chat, I see the refactored code. An additional check (using recursion) has also been added in to ensure that the player's choice is a valid option.
In this case, I don't think recursion is the best option. I ask to simplify the code:
Replace recursion in the get_choices function with a loop.
In th eresponse, the recursive call has been replaced with a while True loop.
Now I want to improve the section that checks who wins. It's a lot of if/then clauses. With games, you usually want to find a data structure that can hold all the rules. In this way, you can apply the game rules in multiple part of the game without duplicating the same code.
Here are my instructions:
To check who wins, update the code to use a BEATS dictionary that, for any choice, returns the option that looses against that choice. Refactor the code to compute CHOICES from BEATS. Then, rename the check_win function as "who_wins" and make it return "player", "computer", or "tie".
As a consequence of the simplification of the output of the who_wins function, the now returns only one word (either player, computer, or tie), the play_game function now implements the formatting and the dsiplay of text.
Still, I don't like how the two choices, from the player and from the computer, are mixed together in the same function (get_choices). The player is getting input from the terminal, while the computer uses some strategy (currently randomness) to find the right choice. Because these are completely different procedures, I ask to separate the functions:
Separate get_choices in two functions, one for the player and one for the computer.
In the resulting code, there are a get_player_choice and a get_computer_choice functions. Here's the full code with the changes.
The get_player_choice function can be improved. For example, "bye" is repeated twice. I select the function in the editor and choose Amazon Q and Optimize from the contextual menu. I can use the same approach to Explain, Fix, or Refactor code, or use a custom prompt with the selected code to do something different, such as asking the code to be changed in a specific way.
Following the suggestions, I replace the function with this optimized version:

Keeping track of the score

Before improving the computer strategy, let's track the score for the players, so that at the end of a long game, we can know who won the most. Here are my instructions:
Update the code to store of the number of player and computer wins inside the play_game function. Print the scores at every turn. At the end, print who won more.
To avoid global variables in the response, I specified to store the scores inside the play_game function. Even if global variables can be ok in a small program, I prefer to avoid them because they can create problems when an application grows and becomes more complex.
I do a quick test to see how this works.
Now that scores are managed as expected, it is time to improve the computer strategy.

Improving the computer strategy

Instead of choosing what to do completely at random, let's make the computer use the previous choices of the player to understand their gameplay. To do so, I need a data structure that, when the computer anticipates the player choice, returns the best countermove.
Compute the opposite of the BEATS dictionary and call it BEATEN_BY.
Now, I try to improve the way the computer makes its choices using the player history:
Optimize the computer strategy with these changes. In the get_computer_choice function, pass in input a list of all player previous choices. Then, get a random element from the list and return the choice that beats that one (the value of BEATEN_BY[random element]). The list is updated after the computer makes its choice. If the list is empty, return a random choice.
I had to be very clear here on how to use some data structures because the BEATS and BEATEN_BY dictionaries can be confusing without context. Here's the suggested code:
This strategy is better than randomness but, for a long game, it can improved. In fact, it is better to not go too much in the past.
Optimize the computer startegy to only use the latest n entries in the player history, with n equal 10. Instead of n, call the variable MEMORY_LENGTH.
Here's the updated get_computer_choice function and the MEMORY_LENGTH constant.
We're almost there. The get_computer_choice function can be improved. For example, slicing an array in Python works even if there are less element than what is requested. I select the function and, using the contextual menu, I ask Amazon Q to optimize the code.
Here's the optimized function:
This version of the function is shorter and easier to understand. With these changes to strategy, the computer is definitely a better player.

Wrapping up and using classes

Let's ask for a final update to clean up the way the program starts.
Run the play_game function only if this is run and not imported as a module.
This changes the final part of the program to use a common Python convention.
I am satisfied with the current implementation of the game but I am wondering if I could have tight the different game functionalities together.
Let's try to use an object oriented approach. The whole game can be embedded into a single class. Then, each game is an instance of that class. Here's my ask:
Replace the BEATS, BEATEN_BY, and CHOICES data structures with a single RockPaperScissorsGame class that implementes the same functionalities. This class shoud have an embedded BEATS structure from which all other data is computed.
Here's my final code for reference. The result is not perfect but better than before.. If you followed along, you might have something similar but not exactly the same. You can continue to ask questions and pass instructions to add more features or refactor the code.
If you enjoyed this AI-assisted coding session, don't forget to give Amazon Q Developer a try. Next time, I'll try the same approach with a different project.
 

Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.

Comments