Step 1: Download python 3.12 at python.org/downloads
Step 2: Download this repo as a zip
Step 3: Double click the zip file in your downloads
Step 4: Open your terminal
Step 5: Type "cd " and then drag the folder into the terminal
Step 6: Type python3 main.py
In this project we're using the classic setup where in the root dir it's relatively minimalistic and main.py calls all the functions inside a src dir. Usually in this setup if this was more advanced we would have a data dir.
|--> main.py
|--> .gitignore
|--> IPOmodel.drawio.png
|--> journal.md
|--> requirements.txt
|--> readme.md
|--> src/Inside src it's once again relatively minimalistic. An init file to ensure no import issues, the logo code, and the games dir where the minigames and analysis reside.
|--> __init__.py
|--> logo.py
|--> games/We moved the utils file from inside src to inside games because it made imports much easier. The dir contains every group members minigames, the analysis code, and the utility code. Another __init__.py file was added here to prevent any import errors.
|--> __init__.py
|--> hangman.py
|--> flag_quiz.py
|--> hard_flag_quiz.py
|--> score_analysis.py
|--> utils.pyWe made three games and a code to analyze those games. The games where: hangman, a flag quiz, and a geography quiz. After the user is done playing games they can analyze their results. What starting letters where the easiest for them to remeber. What attributes did the countries they got wrong have in common. Etc.
In utils a variety of helpful functions are included that are used in most of the other files in this project. In utils the main functions used are ask_question and slow. Ask question calls a giant curses menu function that creates a beautiful interactive UI for the user to use.
def ask_question(prompt, options):
return curses.wrapper(lambda stdscr: menu(stdscr, options, header=prompt))The slow function prints out in an animated and cool way, information instead of a boring print statement
def slow(msg):
for c in msg:
print(c, end='', flush=True)
time.sleep(0.01)
print()
time.sleep(0.5)Also inside utils are two classes: a TurtleHandler class and a ResponseClassifcation class. TurtleHandler contains two variables that are used to globalize turtle so that it works when called from main.
def __init__(self):
self.screen = turtle.Screen()
self.pen = turtle.Turtle()For ResponseClassifcation there is a save_result function that processes inputs and saves them in the correct format that will be used in src/games/score_analysis.
def save_result(self, country, mode, result, question):
country.lower()
mode.lower()
result.lower()
question.capitalize()
self.scores[country][result][mode].append(question)To prevent errors in the init method I defined self.scores as:
self.scores = defaultdict(
lambda: {
"wrong": defaultdict(list),
"right": defaultdict(list),
}
)In the hangman.py there is a main function that takes a scoring system as an input. The scoring system tracks the global score of the user and related information about the question they answered. To begin the function initializes the variables wins, losses, and name. Once intialized, the main function calls playHangman.
The playHangman function welcomes the user and then has a main loop that it runs until the user decides they don't want to play another round. Once the user is finished playing, playHangman returns the wins and losses to display some quick stats in main.
playAgain = True
while playAgain:
# ... omited for brevity
result = runSingleRound(secretWord, maxMistakes, difficulty, score_system)
# ... omited for brevity
playAgain = askPlayAgain()
return wins, lossesThe user picks a difficulty through chooseDifficulty.
prompt = "Choose difficulty:"
options = ['Easy', 'Medium', 'Hard', 'Secret']
choice = ask_question(prompt, options)
return choice.lower()Once the user picks a difficulty the rest of the params for runSingleRound can be calculated.
difficulty = chooseDifficulty()
secretWord = pickWord(difficulty)
maxMistakes = getMaxMistakes(difficulty)The pickWord function has arrays of easyCountries, mediumCountries, and hardCountries. A country will randomly be selected from the array that corresponds to the difficulty.
easyCountries = [# E.g Canada]
mediumCountries = [# E.g "poland"]
hardCountries = [# E.g "luxembourg"]
if difficulty == "easy":
return random.choice(easyCountries)
if difficulty == "medium":
return random.choice(mediumCountries)
if difficulty == "hard":
return random.choice(hardCountries)
return "antidisestablishmentarianism"And the max mistakes is another chain of if statements.
if difficulty == "easy":
return 8
if difficulty == "medium":
return 7
if difficulty == "hard":
return 6
return 5Once all the params have been found runSingleRound is called. Inside it, the function iteratively prints the current hangman and gets the user input along with printing the basic info.
printHangman(mistakes)
displayCurrentWord(secretWord, correct)
# ... Prints the stats using the slow function. Ommited for brevity
# ... checks if guess is valid
# If all passed
if guess in secretWord:
correct.append(guess)
else:
wrong.append(guess)At the end of the input vetting and processing there are some checks at the end of the loop to see if the round is over.
if all(l in correct for l in secretWord):
printHangman(mistakes)
displayCurrentWord(secretWord, correct)
# ... print out Congrats
# ... save to score system
return "win"
if mistakes >= maxMistakes:
printHangman(mistakes)
# ... print You lost
# ... save to score system
# return "loss"printHangman is simply a function containing an array of hangman states where the state that corresponds with the num of mistakes is printed.
slow(stages[i])askPlayAgain simply uses the utils ask_question function and returns the mapped response.
prompt = "Play again?"
map = {'Yes': True, 'No': False}
options = ['Yes', 'No']
command = ask_question(prompt, options)
return map[command]Inside flag quiz, the structure originally was all functions but some of the code wasn't in functions. We shifted it to classes so that we could store varaibles cross functions easier.
def __init__(self, turtle_info, score_system):
self.flags = { # Name >> Method E.g: "France": self.draw_france}
self.screen = turtle_info.screen
self.pen = turtle_info.pen
self.score_system = score_system
# ... Do a bunch of turtle drawing stuffEach function gives turtle instructions to draw each flag.
def draw_belgium(self):
colors = ["black", "yellow", "red"]
for c in colors:
self.pen.color(c)
self.pen.begin_fill()
for _ in range(2):
self.pen.forward(100)
self.pen.right(90)
self.pen.forward(200)
self.pen.right(90)
self.pen.end_fill()
self.pen.forward(100)We have a play_flag_game function that initializes this class every time we want to play the flag game.
def play_flag_game(score_system, turtle_info):
Flags(turtle_info, score_system).main()We also have a main function to run everything and save the score results.
def main(self):
for _ in range(rounds):
# ... turtle stuff
country = random.choice(list(self.flags.keys()))
self.flags[country]()
# ... print question
# ... save result and display the result
# ... display infoIn Geography quiz the user picks between 3 options the in play function once called. The quiz_round function is called whenever once the user picks a difficulty. In quiz_round it loops through all the questions asking the user each time. For each correct answer, three random wrong answers are picked.
Whenever the user want some analytics on how they did, they can navigate to the Response Analysis options. There the file is pretty long and all of it is necessary so I'll keep this brief. There is a main function inside score_analysis that initalizes an analyzer class and then calls launch_explorer. In launch explorer, a navigator class is initalized and with curses it calles the run function in the navigation class. In the navigation class, the user can navigate through 15 options of different analytics for the questions they answered learning their weakneses and strengths and unique insights. Insights like captial letters etc. A breif example of how this works can be run when the file is not run from a sub-file.
if __name__ == "__main__":
from collections import defaultdict
# Create test data
rc = ResponseClassification()
rc.save_result("Canada","easy","right", "What is photosynthesis?")
rc.save_result("Canada", "hard", "wrong", "Explain quantum entanglement")
rc.save_result("USA", "medium", "right", "What is the capital of France?")
rc.save_result("USA", "easy", "wrong", "2 + 2 = ?")
rc.save_result("Brazil", "hard", "right", "Analyze the French Revolution")
rc.save_result("Brazil", "medium", "wrong", "Describe DNA structure")
# Initialize analyzer (AUTO-POPULATES all 16 categories!)
analyzer = ResponseAnalyzer(rc)
print("✅ ResponseAnalyzer initialized with auto-population!")
insights = analyzer.generate_insights()
print(f" Total insights generated: {len(insights)}")
print(f" Questions classified: {sum(len(items) for cat in analyzer.cap_letter_dist.values() for items in cat.values())}")
# Launch interactive explorer
launch_explorer(analyzer)Here we see data is saved to the analyzer and from that the insights are created and we can navigate through them through the launch explorer function.
Using curses users can navigate through windows picking actions they want to do. In main we do it like this
from src.games.utils import ask_question
prompt = r"You can't comprehend my maturity"
options = ['6', '7']
mapping = {'6': "Yes", '7': 'Ok?'}In src.utils there exists a basic common practice curses function that modulrizes this process.
def menu(stdscr, options, header=None):
# Logic that monitors the users inputs
# and updates the terminal accordingly
# till the enter key is pressedThe process of calling the function isn't very readible hence the sub-function ask_question
def ask_question(prompt, options):
return curses.wrapper(lambda stdscr: menu(stdscr, options, header=prompt))