-"
- gops = "class=\"discount_original_price\">"
- gps = "class=\"discount_final_price\">"
- end = "
"
-
- self.games = []
- self.database = database
- info = requests.get(link).text
-
- self.gather_games(info, gns, gds, gps, gops, end)
- self.write_info()
-
- def gather_games(self, info, gns, gds, gps, gops, end):
- """
- This method adds game objects to the scansteampage
- object's list self.games
- """
- position = 0
- consecutive_fails = 0
-
- while consecutive_fails < 2:
- a_game = game(info, position, gns, gds, gps, gops, end)
- position = a_game.endloc
- if a_game.isvalid:
- self.games.append(a_game)
- consecutive_fails = 0
- else:
- consecutive_fails +=1
-
- def write_info(self):
- """
- This method makes writes to the database.
-
- The database's keys will be games' titles
- The database's values will be strings of the following
- format:
- (name) is on sale for (price), which is a (discount percent)
- discount from its original price of (original price)
- """
- gameshelf = shelve.open(self.database)
- if len(gameshelf.keys()) > 0:
- gameshelf.clear()
- for game in self.games:
- gameshelf[game.name] = ("%s is on sale for %s, which is a %s" %
- (game.name, game.price, game.discount) +
- " discount from its original price of %s" % game.ogprice)
- gameshelf.close()
-
-
-# comment out the below line after running it once
-ourgamesshelf = scansteampage()
-
-# The above code creates a shelf called gameshelf
-# First, create a list to store the values
-# Next, write "Current Sales\n" on a blank text file.
-# Finally, write the values followed by 2 newlines to The
-# text file.
-# Your end result should look like below:
-# Current Sales
-# Something is on sale for $1000.00, which is a 50% discount from
-# its original price of 2000.00
-
-# write your code here.
diff --git a/3_advanced/chapter20/practice/txt_write_practice.py b/3_advanced/chapter20/practice/txt_write_practice.py
new file mode 100644
index 00000000..0d26d17c
--- /dev/null
+++ b/3_advanced/chapter20/practice/txt_write_practice.py
@@ -0,0 +1,136 @@
+import requests
+import shelve
+
+
+class game:
+ def __init__(self, pageinfo, bsl: int, gns, gds, gps, gops, end):
+ """
+ Arguments:
+ bsl is the beginning search location. It should be an integer
+ pageinfo is the html of a website converted to a string
+ gns is the 'game's name start'; it is what to look for directly
+ before a game's name
+ gds is the 'game's discount start'; it is what to look for directly
+ before a game's discount
+ gps is the 'game's price start'; it is what to look for directly
+ before a game's discounted price
+ gops is the 'game original price start'; it is what to look for
+ directly before a game's original price
+
+ """
+ self.string = pageinfo
+ self.isvalid = True
+ self.begin = bsl
+ self.endloc = 0
+
+ self.discount = self.find(self.string, gds, end, cb=True)
+ self.price = self.find(self.string, gps, end)
+ self.ogprice = self.find(self.string, gops, end)
+ self.name = self.find(self.string, gns, end, cwe=True)
+
+ def find(self, string, start: str, end: str, cb=False, cwe=False):
+ """
+ Arguments
+ string is the string where the substring you are looking for
+ is located
+ start is the substring directly before the substring you are
+ looking for
+ end is the substring directly after the substring you are
+ looking for
+ cb is whether or not to change the beginning point for the
+ searches to the endloc
+ cwe is whether to compare the endloc with self.begin and
+ check whether the difference is withing the acceptable range
+ """
+ try:
+ startloc = string.index(start, self.begin)
+ endloc = string.index(end, startloc)
+ except Exception:
+ self.endloc = self.begin + 1
+ self.isvalid = False
+ return
+
+ if cb:
+ self.begin = endloc
+ if cwe:
+ # check if the end location is too far away from the
+ # beginning to be a valid name
+ if endloc - self.begin > 300:
+ self.isvalid = False
+ self.endloc = endloc
+
+ return string[startloc:endloc].lstrip(start).rstrip(end)
+
+
+class scansteampage:
+ def __init__(self, database="gameshelf"):
+ """
+ See game's explanation for the abbreviations
+ """
+ link = "https://store.steampowered.com/"
+ gns = '
'
+ gds = '
-'
+ gops = 'class="discount_original_price">'
+ gps = 'class="discount_final_price">'
+ end = "
"
+
+ self.games = []
+ self.database = database
+ info = requests.get(link).text
+
+ self.gather_games(info, gns, gds, gps, gops, end)
+ self.write_info()
+
+ def gather_games(self, info, gns, gds, gps, gops, end):
+ """
+ This method adds game objects to the scansteampage
+ object's list self.games
+ """
+ position = 0
+ consecutive_fails = 0
+
+ while consecutive_fails < 2:
+ a_game = game(info, position, gns, gds, gps, gops, end)
+ position = a_game.endloc
+ if a_game.isvalid:
+ self.games.append(a_game)
+ consecutive_fails = 0
+ else:
+ consecutive_fails += 1
+
+ def write_info(self):
+ """
+ This method makes writes to the database.
+
+ The database's keys will be games' titles
+ The database's values will be strings of the following
+ format:
+ (name) is on sale for (price), which is a (discount percent)
+ discount from its original price of (original price)
+ """
+ gameshelf = shelve.open(self.database)
+ if len(gameshelf.keys()) > 0:
+ gameshelf.clear()
+ for game in self.games:
+ gameshelf[game.name] = (
+ "%s is on sale for %s, which is a %s"
+ % (game.name, game.price, game.discount)
+ + " discount from its original price of %s" % game.ogprice
+ )
+ gameshelf.close()
+
+
+# comment out the below line after running it once
+ourgamesshelf = scansteampage()
+
+# The above code creates a shelf called gameshelf
+# First, create a list to store the values
+# Next, write "Current Sales\n" on a blank text file.
+# Finally, write the values followed by 2 newlines to The
+# text file.
+# Your end result should look like below:
+# Current Sales
+# Something is on sale for $1000.00, which is a 50% discount from
+# its original price of 2000.00
+
+# write your code here.
diff --git a/3_advanced/chapter20/solutions/json_practice_1.py b/3_advanced/chapter20/solutions/json_practice_1.py
index 7c8947e8..ed6de397 100644
--- a/3_advanced/chapter20/solutions/json_practice_1.py
+++ b/3_advanced/chapter20/solutions/json_practice_1.py
@@ -1,8 +1,9 @@
# use the "favorite_foods.json"
# in that json file, there will be a dictionary called "favorite_foods"
# print all the unique favorite foods, which will be the values.
-# Save all the names into a list. Add that list to the top dictionary and write
-# the top dictionary into the json file
+# Save all the names into a list. Add that list to the dictionary
+# ('names' should be the key and the names list should be the value)
+# and write the dictionary into the json file
import json
diff --git a/README.md b/README.md
index 003c6248..e2dfa533 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,14 @@ Source code from Code For Tomorrow's Python course
/badge.svg)
## Difficulty Level
-Source code is organized under 3 difficulty levels:
+Source code is organized under 4 categories:
1. `1_beginner`
2. `2_intermediate`
3. `3_advanced`
+4. `games`
## Chapters
-Under each of the 3 packages, source code is further divided by chapter.
+Under each of the 4 packages, source code is further divided by chapter.
### Beginner
1. `chapter1` Intro to Python
2. `chapter2` Data
@@ -38,6 +39,13 @@ Under each of the 3 packages, source code is further divided by chapter.
19. `chapter19` Exception Handling
20. `chapter20` File I/O
+### Games
+1. `chapter1` Console Games
+2. `chapter2` Pygame Basics
+3. `chapter3` Pygame Events
+4. `chapter4` OOP + Pygame
+5. `chapter5` Pygame Sounds
+
## Category
Under each chapter, source code is further divided by category:
1. `examples` - example code to demo certain programming concepts
diff --git a/dsa/chapter1/examples/recursion.py b/dsa/chapter1/examples/recursion.py
new file mode 100644
index 00000000..15e6c106
--- /dev/null
+++ b/dsa/chapter1/examples/recursion.py
@@ -0,0 +1,35 @@
+# Code to figure out how many of a factor a number has
+
+
+def number_factor(number, factor, factor_counter=0):
+ """
+ Parameters:
+ 1) number is the number in which we are finding the number of
+ factors of. EX: 24
+ 2) factor is the factor in which we are finding the number of
+ in the parameter number. EX: 2
+ Output: The number of times the parameter number can be divisible
+ by the parameter factor. This number is also the parameter
+ factor_counter right before it is returned. EX: 3
+ """
+
+ if number % factor != 0: # Base Case
+ return factor_counter
+ else: # Recursive Case
+ return number_factor(number / factor, factor, factor_counter + 1)
+
+
+print(number_factor(24, 2))
+
+
+def countdown(n, arr=[]):
+ if n < 0: # base case 1
+ return "out of bounds"
+ if n == 0: # base case 2
+ return arr
+ # recursive case
+ arr.append(n)
+ return countdown(n - 1, arr)
+
+
+print(countdown(5))
diff --git a/dsa/chapter1/practice/time_complexity.py b/dsa/chapter1/practice/time_complexity.py
new file mode 100644
index 00000000..e3acacf8
--- /dev/null
+++ b/dsa/chapter1/practice/time_complexity.py
@@ -0,0 +1,25 @@
+"""
+For each of the following time complexities, create
+a function that has that time complexity.
+"""
+
+# time complexity: O(1)
+# your code here
+
+
+# time complexity: O(n)
+# your code here
+
+
+# time complexity: O(n^2)
+# your code here
+
+
+# time complexity: O(log(n))
+# your code here
+
+# time complexity: O(n * log(n))
+# your code here
+
+# time complexity: O(2**n)
+# your code here
diff --git a/dsa/chapter1/practice/time_complexity_questions.py b/dsa/chapter1/practice/time_complexity_questions.py
new file mode 100644
index 00000000..f272e328
--- /dev/null
+++ b/dsa/chapter1/practice/time_complexity_questions.py
@@ -0,0 +1,74 @@
+"""
+Classify the following code examples with their
+runtimes. Write your responses as comments.
+"""
+
+
+def do_something():
+ # runtime for do_something() is O(1)
+ pass
+
+
+# what is the runtime for example 1?
+def example_one(n):
+ for i in range(n):
+ do_something()
+
+
+# what is the runtime for example 2?
+def example_two(n):
+ do_something()
+
+
+# what is the runtime for example 3?
+def example_three(n):
+ for i in range(n):
+ for x in range(i):
+ do_something()
+
+
+# what is the runtime for example 4?
+def example_four(n):
+ for i in range(n // 2):
+ do_something()
+
+
+# what is the runtime for example 5?
+def example_five(n):
+ i = 0
+ while i < n:
+ do_something()
+ i *= 2
+
+
+# what is the runtime for example 6?
+def example_six(n):
+ for i in range(10):
+ do_something()
+
+
+# what is the runtime for example 7?
+def example_seven(n):
+ for i in range(2**n):
+ do_something()
+
+
+# what is the runtime for example 8?
+def example_eight(n):
+ for i in range(n):
+ for x in range(7):
+ do_something()
+
+
+# what is the runtime for example 9?
+def example_nine(n):
+ for i in range(n):
+ example_one(n)
+
+
+# what is the runtime for example 10?
+def example_ten(n):
+ i = 0
+ while i < n:
+ do_something()
+ i += 2
diff --git a/dsa/chapter1/solutions/time_complexity.py b/dsa/chapter1/solutions/time_complexity.py
new file mode 100644
index 00000000..d006e0f8
--- /dev/null
+++ b/dsa/chapter1/solutions/time_complexity.py
@@ -0,0 +1,99 @@
+"""
+For each of the following time complexities, create
+a function that has that time complexity. The following solutions
+are examples and not the only ways to have done this problem.
+"""
+
+
+# time complexity: O(1)
+def double_my_number(number):
+ x = number
+ x *= 2
+ return x
+
+
+# time complexity: O(n)
+def sum_till_n(n):
+ total = 0
+ for i in range(n):
+ total += i
+
+ return total
+
+
+# time complexity: O(n^2)
+def print_triangle(n):
+ for row in range(n):
+ for column in range(row):
+ print("* ", end="")
+ print()
+
+
+# time complexity: O(log(n))
+def sum_powers_of_two(max_number):
+ power_of_two = 1
+ total = 0
+
+ while power_of_two < max_number:
+ total += power_of_two
+ power_of_two *= 2
+
+ return total
+
+
+# time complexity: O(n * log(n))
+def sum_many_powers_of_two(number_of_times):
+ total = 0
+ for i in range(number_of_times):
+ # since sum_powers_of_two is O(log(n))and this for loop is O(n),
+ # the resulting time complexity is O(n * log(n))
+ total += sum_powers_of_two(i)
+
+ return total
+
+
+# time complexity: O(2**n)
+def get_binary_combinations(number_of_digits):
+ """
+ Gets the combinations of binary numbers with number_of_digits digits
+ For example, get_binary_combinations(2) should give
+ ["00", "01", "10", "11"].
+
+ """
+ cur_options = ["0", "1"]
+ next_options = []
+
+ operations = 0
+
+ # In total, this is O(2**n). It may be a bit confusing, but
+ # this is O(2**n) because of the fact that the current options
+ # doubles each time we go through the for loop, so it has to
+ # spend twice as long each time.
+ for i in range(number_of_digits - 1):
+ for option in cur_options:
+ next_options.append(option + "0")
+ next_options.append(option + "1")
+ operations += 1
+ cur_options = next_options
+ next_options = []
+
+ print(f"took {operations} operations")
+ return cur_options
+
+
+# for comparison, here's a very easy to understand
+# function with O(2**n) runtime.
+def regular_o_2_to_the_n(n):
+ operations = 0
+ for i in range(2**n):
+ operations += 1
+ print(f"took {operations} operations")
+
+
+# if you actually don't believe that get_binary_combinations is O(2**n),
+# try running the below.
+# as you can see, they have similar operational cost,
+# meaning that get_binary_combinations really is O(2**n)
+# times = 20
+# get_binary_combinations(times)
+# regular_o_2_to_the_n(times)
diff --git a/dsa/chapter1/solutions/time_complexity_questions.py b/dsa/chapter1/solutions/time_complexity_questions.py
new file mode 100644
index 00000000..96945dc9
--- /dev/null
+++ b/dsa/chapter1/solutions/time_complexity_questions.py
@@ -0,0 +1,84 @@
+"""
+Classify the following code examples with their
+runtimes. Write your responses as comments.
+"""
+
+
+def do_something():
+ # runtime for do_something() is O(1)
+ pass
+
+
+# what is the runtime for example 1?
+# runtime is O(n)
+def example_one(n):
+ for i in range(n):
+ do_something()
+
+
+# what is the runtime for example 2?
+# runtime is O(1)
+def example_two(n):
+ do_something()
+
+
+# what is the runtime for example 3?
+# runtime is O(n^2)
+def example_three(n):
+ for i in range(n):
+ for x in range(i):
+ do_something()
+
+
+# what is the runtime for example 4?
+# runtime is O(n)
+def example_four(n):
+ for i in range(n // 2):
+ do_something()
+
+
+# what is the runtime for example 5?
+# runtime is O(log(n))
+def example_five(n):
+ i = 0
+ while i < n:
+ do_something()
+ i *= 2
+
+
+# what is the runtime for example 6?
+# runtime is O(1)
+def example_six(n):
+ for i in range(10):
+ do_something()
+
+
+# what is the runtime for example 7?
+# runtime is O(2**n)
+def example_seven(n):
+ for i in range(2**n):
+ do_something()
+
+
+# what is the runtime for example 8?
+# runtime is O(n)
+def example_eight(n):
+ for i in range(n):
+ for x in range(7):
+ do_something()
+
+
+# what is the runtime for example 9?
+# runtime is O(n^2)
+def example_nine(n):
+ for i in range(n):
+ example_one(n)
+
+
+# what is the runtime for example 10?
+# runtime is O(n)
+def example_ten(n):
+ i = 0
+ while i < n:
+ do_something()
+ i += 2
diff --git a/dsa/chapter2/examples/bst.py b/dsa/chapter2/examples/bst.py
new file mode 100644
index 00000000..f3d8c4d5
--- /dev/null
+++ b/dsa/chapter2/examples/bst.py
@@ -0,0 +1,215 @@
+class Node:
+ def __init__(self, key, value) -> None:
+ # set key/value
+ self.key = key
+ self.value = value
+
+ # set children to None
+ self.left: Node = None
+ self.right: Node = None
+
+ def __str__(self) -> str:
+ return str(self.key) + ": " + str(self.value)
+
+ def __repr__(self) -> str:
+ return str(self)
+
+
+class BinaryTree:
+ def __init__(self) -> None:
+ self.root = None
+
+ def search(self, key):
+ """
+ Searches the binary tree for a node with the given key.
+ Takes advantage of the fact that, in a binary tree,
+ keys with lesser values go on the left and keys with greater
+ values go on the right
+ """
+ current = self.root
+ while current is not None:
+ if key == current.key:
+ return current.value
+ elif key < current.key:
+ current = current.left
+ else:
+ current = current.right
+ raise Exception("KEY NOT FOUND")
+
+ def get_height(self):
+ """
+ Gets the height of the binary tree.
+ """
+ if self.root is None:
+ return 0
+
+ height = 0
+ next = [self.root]
+ while len(next) != 0:
+ temp_next = []
+ height += 1
+ for node in next:
+ if node.left is not None:
+ temp_next.append(node.left)
+ if node.right is not None:
+ temp_next.append(node.right)
+ next = temp_next
+ return height
+
+ def insert_node(self, node: Node) -> None:
+ """
+ Tries to insert a node into the tree.
+ If a node with the same key is already found,
+ sets the value of that node to the value of
+ the provided node
+ """
+ # case where root is None
+ if self.root is None:
+ self.root = node
+ return
+
+ # go through the nodes
+ current = self.root
+ while current is not None:
+ if node.key == current.key:
+ current.value = node.value
+ return
+ elif node.key < current.key:
+ if current.left is None:
+ current.left = node
+ return
+ else:
+ current = current.left
+ else:
+ if current.right is None:
+ current.right = node
+ return
+ else:
+ current = current.right
+
+ def remove_node(self, parent, right_or_left="right"):
+ """
+ Helper method to remove a node.
+ Notice how we have to set `parent.xxx` to something.
+ This is because, in order to remove a node from a binary
+ tree, what you are really doing is getting rid of all
+ references to that node. So, we make sure to change
+ the value stored in `parent.xxx` to a different node
+ (or `None`) so that we remove the node we're trying to get rid of
+ """
+ if right_or_left == "right":
+ node = parent.right
+ else:
+ node = parent.left
+
+ if node.right is not None:
+ temp = node.left
+ if right_or_left == "right":
+ parent.right = node.right
+ else:
+ parent.left = node.right
+
+ if temp is not None:
+ self.insert_node(temp)
+ elif node.left is not None:
+ temp = node.right
+ if right_or_left == "right":
+ parent.right = node.left
+ else:
+ parent.left = node.left
+ if temp is not None:
+ self.insert_node(temp)
+ else:
+ if right_or_left == "right":
+ parent.right = None
+ else:
+ parent.left = None
+
+ def __getitem__(self, key):
+ return self.search(key)
+
+ def __setitem__(self, key, value):
+ self.insert_node(Node(key, value))
+
+ def __delitem__(self, key):
+ """
+ Deletes an entry from the binary tree
+ """
+ # case where key to delete is the root
+ if self.root is not None and self.root.key == key:
+ if self.root.right is not None:
+ temp = self.root.left
+ self.root = self.root.right
+ if temp is not None:
+ self.insert_node(temp)
+ elif self.root.left is not None:
+ temp = self.root.right
+ self.root = self.root.left
+ if temp is not None:
+ self.insert_node(temp)
+ else:
+ self.root = None
+
+ # regular cases
+ current = self.root
+ while current is not None:
+ if current.left is not None and current.left.key == key:
+ self.remove_node(current, "left")
+ break
+ if current.right is not None and current.right.key == key:
+ self.remove_node(current, "right")
+ break
+
+ if key < current.key:
+ current = current.left
+ if key > current.key:
+ current = current.right
+
+ def print_structure(self) -> None:
+ """
+ Prints out what the binary tree looks like
+ """
+ if self.root is None:
+ print("{}")
+ return
+
+ height = self.get_height()
+ spacing = 6
+ total_width = spacing * (2**height)
+
+ # print top divider
+ print("-" * total_width)
+
+ current_generation = [self.root]
+ next_generation = []
+ for i in range(1, height + 1):
+ margin_between = int(
+ (total_width - spacing * (2 ** (i - 1))) / ((2 ** (i - 1)) + 1)
+ )
+ for node in current_generation:
+ print(" " * margin_between, end="")
+ if node is None:
+ print(" " * spacing, end="")
+ next_generation.extend([None] * 2)
+ else:
+ print(node, end="")
+ next_generation.extend([node.left, node.right])
+ # print a newline
+ print()
+
+ current_generation = next_generation
+ next_generation = []
+
+ # print bottom divider
+ print("-" * total_width)
+
+
+myBinaryTree = BinaryTree()
+myBinaryTree[33] = 22
+myBinaryTree[22] = 11
+myBinaryTree[44] = 33
+myBinaryTree[55] = 22
+del myBinaryTree[33]
+
+# to see how it internally arranges data
+myBinaryTree.print_structure()
diff --git a/dsa/chapter2/examples/graph.py b/dsa/chapter2/examples/graph.py
new file mode 100644
index 00000000..189040aa
--- /dev/null
+++ b/dsa/chapter2/examples/graph.py
@@ -0,0 +1,68 @@
+# simple graph
+my_graph = {
+ "A": {"B", "C", "E"},
+ "B": {"A", "D"},
+ "C": {"A"},
+ "D": {"B", "E"},
+ "E": {"D", "A"},
+}
+
+
+# graph data structure
+class Node:
+ def __init__(self, val: str, neighbors: list = None):
+ self.val = val
+ if neighbors:
+ self.neighbors = neighbors
+ else:
+ self.neighbors = set()
+
+ def addNeighbor(self, n):
+ self.neighbors.add(n)
+
+
+class Graph:
+ def __init__(self, connections: list = None):
+ self.nodes = {}
+ if connections:
+ self.parse(connections)
+
+ def createNode(self, values: list):
+ for value in values:
+ self.nodes[value] = Node(value)
+
+ def createEdge(self, n1: str, n2: str):
+ self.nodes[n1].addNeighbor(self.nodes[n2])
+ self.nodes[n2].addNeighbor(self.nodes[n1])
+
+ def parse(self, connections: list):
+ for connection in connections:
+ if connection[0] not in self.nodes:
+ self.createNode([connection[0]])
+ if connection[1] not in self.nodes:
+ self.createNode([connection[1]])
+
+ self.createEdge(connection[0], connection[1])
+
+ def __repr__(self):
+ s = "{\n"
+ for value in self.nodes:
+ node = self.nodes[value]
+ s += f"\t{value}: {[n.val for n in node.neighbors]}\n"
+ s += "}"
+ return s
+
+
+# Takes in a list of tuples representing connections
+my_graph = Graph(
+ [
+ ("A", "B"),
+ ("B", "E"),
+ ("E", "D"),
+ ("D", "F"),
+ ("D", "A"),
+ ("A", "C"),
+ ("C", "B"),
+ ]
+)
+print(my_graph)
diff --git a/dsa/chapter2/examples/linked_list.py b/dsa/chapter2/examples/linked_list.py
new file mode 100644
index 00000000..371c6131
--- /dev/null
+++ b/dsa/chapter2/examples/linked_list.py
@@ -0,0 +1,216 @@
+from datetime import datetime as d
+
+
+class Node:
+ def __init__(self, value, prev=None, next=None) -> None:
+ self.value = value
+ self.prev = prev
+ self.next = next
+
+
+class DoublyLinkedList:
+ def __init__(self) -> None:
+ self.head = Node(None)
+ self.head.next = Node(None, self.head)
+ self.tail = self.head.next
+ self.size = 0
+
+ def add_current(self, value, current) -> bool:
+ """
+ Helper method
+
+ O(1) operation
+
+ Arguments:
+ @param value - the value to insert
+
+ @param current - the node to insert the value in front of
+
+ @returns bool - True on success
+ """
+ if current.next is None:
+ current.next = Node(value, current)
+ else:
+ current.next = Node(value, current, current.next)
+ if current.next.next:
+ current.next.next.prev = current.next
+ self.size += 1
+ return True
+
+ def add_front(self, value) -> bool:
+ """
+ O(1) operation
+ """
+ return self.add_current(value, self.head)
+
+ def add_back(self, value) -> bool:
+ """
+ O(1) operation
+ """
+ return self.add_current(value, self.tail.prev)
+
+ def add(self, value, idx) -> bool:
+ """
+ O(N) operation since it has to iterate to idx
+ """
+ if idx > self.size:
+ return False
+ current = self.head
+ for i in range(idx):
+ current = current.next
+ self.add_current(value, current)
+
+ def set(self, value, idx) -> bool:
+ """
+ O(N) operation since it has to iterate to idx
+ """
+ if idx >= self.size:
+ return False
+ current = self.head.next
+ for i in range(idx):
+ current = current.next
+ current.value = value
+ return True
+
+ def set_front(self, value) -> bool:
+ """
+ O(1) operation
+ """
+ if self.size == 0:
+ return False
+ self.head.next.value = value
+ return True
+
+ def set_back(self, value) -> bool:
+ """
+ O(1) operation
+ """
+ if self.size == 0:
+ return False
+ self.tail.prev.value = value
+ return True
+
+ def remove_current(self, current) -> bool:
+ """
+ Helper method (O(1) operation)
+
+ Arguments:
+ @param current - the node to be removed
+
+ @returns bool - True on success
+ """
+ current.prev.next = current.next
+ if current.prev.next:
+ current.prev.next.prev = current.prev
+ self.size -= 1
+ return True
+
+ def remove_value(self, value) -> bool:
+ """
+ Attempts to remove the first occurrence of value from the list.
+
+ O(N) operation since it has to iterate through the list to find the value
+
+ @returns bool - True on success (value found and removed),
+ False on failure to find the value
+ """
+ current = self.head.next
+
+ # advance the cursor until either we've reached the end
+ # of the list or we've reached the value
+ while current != self.tail and current.value != value:
+ current = current.next
+
+ # found the item, time to remove
+ if current != self.tail and current.value == value:
+ self.remove_current(current)
+ return True
+
+ # didn't find the item
+ return False
+
+ def remove_front(self) -> bool:
+ if self.size == 0:
+ return True
+ return self.remove_current(self.head.next)
+
+ def remove_back(self) -> bool:
+ if self.size == 0:
+ return True
+ return self.remove_current(self.tail.prev)
+
+ def remove_idx(self, idx) -> bool:
+ """
+ O(N) operation since it has to iterate to idx
+ """
+ if idx >= self.size:
+ return False
+ current = self.head.next
+ for i in range(idx):
+ current = current.next
+ return self.remove_current(current)
+
+ def clear(self) -> bool:
+ """
+ O(1) operation
+ """
+ self.head.next = self.tail
+ self.tail.prev = self.head
+ self.size = 0
+
+ def __str__(self) -> str:
+ ret = "["
+ current = self.head.next
+ while current.next is not None:
+ ret += str(current.value)
+ current = current.next
+ if current.next is not None:
+ ret += ", "
+ ret += "]"
+ return ret
+
+ def print_from_front(self) -> None:
+ print(self)
+
+ def print_from_back(self) -> None:
+ current = self.tail.prev
+ print("[", end="")
+ while current.prev is not None:
+ print(current.value, end="")
+ current = current.prev
+ if current.prev is not None:
+ print(", ", end="")
+ print("]")
+
+
+my_double = DoublyLinkedList()
+my_regular = []
+
+TEST_SIZE = 100000
+
+start = d.now()
+for i in range(TEST_SIZE):
+ my_regular.insert(0, i)
+end = d.now()
+print(f"it took {(end-start).total_seconds()} seconds to do that regularly")
+
+start = d.now()
+for i in range(TEST_SIZE):
+ my_double.add_front(i)
+end = d.now()
+print(f"it took {(end-start).total_seconds()} seconds to do that doubly")
+
+start = d.now()
+for i in range(TEST_SIZE - 1):
+ my_regular.pop(0)
+end = d.now()
+print(f"it took {(end-start).total_seconds()} seconds to do that regularly")
+
+start = d.now()
+for i in range(TEST_SIZE - 1):
+ my_double.remove_front()
+end = d.now()
+print(f"it took {(end-start).total_seconds()} seconds to do that doubly")
+
+print(my_regular)
+print(my_double)
diff --git a/dsa/chapter2/examples/queue.py b/dsa/chapter2/examples/queue.py
new file mode 100644
index 00000000..386fa5bd
--- /dev/null
+++ b/dsa/chapter2/examples/queue.py
@@ -0,0 +1,32 @@
+class Queue:
+ def __init__(self):
+ # Make List
+ self.queue_list = []
+
+ def appending(self, item):
+ # Checks to see if there are any duplicates in list
+ if item in self.queue_list:
+ # If so it returns error
+ return "Value Already Exists"
+ else:
+ self.queue_list.append(item)
+
+ def pops(self):
+ # Checks to see if list is empty
+ if len(self.queue_list) != 0:
+ # if it isn’t empty it removes first value
+ return self.queue_list.pop(0)
+ else:
+ return "List is Empty"
+
+
+Check_Queue = Queue()
+
+# Should add value to list
+Check_Queue.appending(100)
+Check_Queue.appending(200)
+Check_Queue.appending(300)
+
+# Should print 300 and then 200
+print(Check_Queue.pops())
+print(Check_Queue.pops())
diff --git a/dsa/chapter2/examples/stack.py b/dsa/chapter2/examples/stack.py
new file mode 100644
index 00000000..bc2e00a3
--- /dev/null
+++ b/dsa/chapter2/examples/stack.py
@@ -0,0 +1,32 @@
+class Stack:
+ def __init__(self):
+ # Make List
+ self.stack_list = []
+
+ def appending(self, item):
+ # Checks to see if there are any duplicates in list
+ if item in self.stack_list:
+ # If so it returns error
+ return "Value Already Exists"
+ else:
+ self.stack_list.append(item)
+
+ def pops(self):
+ # Checks to see if list is empty
+ if len(self.stack_list) != 0:
+ # if it isn’t empty it removes last value
+ return self.stack_list.pop()
+ else:
+ return "List is Empty"
+
+
+Check_Stack = Stack()
+
+# Should add value to list
+Check_Stack.appending(100)
+Check_Stack.appending(200)
+Check_Stack.appending(300)
+
+# Should print 300 and then 200
+print(Check_Stack.pops())
+print(Check_Stack.pops())
diff --git a/dsa/chapter2/practice/basic_bst.py b/dsa/chapter2/practice/basic_bst.py
new file mode 100644
index 00000000..5f6469fa
--- /dev/null
+++ b/dsa/chapter2/practice/basic_bst.py
@@ -0,0 +1,112 @@
+"""
+Let's create a very basic version of a BST that only
+has insertion capabilities. Your task will be to fill
+in the node class and the BST class. The structure is somewhat
+different from the BST that was given as an example, but
+the logic is the same.
+"""
+
+
+class BSTNode:
+ def __init__(self, key, value):
+ """
+ set self.key to key
+ set self.value to the value
+ create a left neighbor/child and a right neighbor/child, each of which
+ start as None
+ """
+
+ # your code here
+ pass
+
+ def add_recursively(self, other_node):
+ """
+ Adds the other node to this node or this node's children.
+ If other_node's key is equal to this node's key, do nothing.
+ If other_node's key is less than this node's key, then:
+ - if this node's left child is None, set this node's left child
+ to that other node
+ - if this node's left child is not None, then add this node
+ recursively to the left child
+ If other_node's key is greater than this node's key, then:
+ - if this node's right child is None, set this node's right child
+ to that other node
+ - if this node's right child is not None, then add this node
+ recursively to the right child
+ """
+
+ # your code here
+ pass
+
+ def get_value(self, key):
+ """
+ Tries to return the value of the node whose key matches `key`.
+ If this node's key matches `key`, return this node's value.
+ If the key is less than this node's key:
+ - if left child is None, return 0
+ - else, get the value recursively
+ If the key is greater than this node's key:
+ - if right child is None, return 0
+ - else, get the value recursively
+ """
+
+ # your code here
+ pass
+
+ def __str__(self):
+ """
+ Creates and returns a string that looks like:
+ left_child, self value, right_child
+ However, if left child or right_child is None, don't add them
+ to the string.
+ """
+
+ # your code here
+ pass
+
+
+class BST:
+ def __init__(self):
+ # set a root node to None
+
+ # your code here
+ pass
+
+ def __setitem__(self, item, value):
+ """
+ create a new node whose key is item and whose value is value
+ then, if root is None, set root to that node.
+ else, add that node recursively.
+ """
+
+ # your code here
+ pass
+
+ def __getitem__(self, item):
+ """
+ Try to find the node with key that matches item.
+ If no match is found, return 0
+ """
+
+ # your code here
+ pass
+
+ def __repr__(self):
+ """
+ Returns a string representation of the root
+ """
+ # your code here
+ pass
+
+
+my_bst = BST()
+my_bst[50] = 30
+my_bst[40] = 31
+my_bst[60] = 32
+my_bst[30] = 33
+my_bst[70] = 34
+my_bst[20] = 35
+my_bst[80] = 36
+my_bst[10] = 37
+my_bst[90] = 38
+print(my_bst) # 37, 35, 33, 31, 10, 32, 34, 36, 38
diff --git a/dsa/chapter2/practice/basic_linked_list.py b/dsa/chapter2/practice/basic_linked_list.py
new file mode 100644
index 00000000..08ffd30d
--- /dev/null
+++ b/dsa/chapter2/practice/basic_linked_list.py
@@ -0,0 +1,136 @@
+"""
+In this problem, you will create a basic Doubly Linked List.
+The goal is to be able to have nodes connected all the way
+to 100. All you have to do is fill in the add_front and
+add_back methods.
+"""
+
+
+class DoublyLinkedListNode:
+ def __init__(self, value) -> None:
+ self.value = value
+
+ self.next = None
+ self.prev = None
+
+ def __repr__(self):
+ return f"{self.value}"
+
+
+class DoublyLinkedList:
+ def __init__(self) -> None:
+ """
+ Creates a head and tail node. For convenience,
+ set the head to a node with the value `None`
+ and the tail to a node with the value `None`.
+ Sets head's next node as tail, and tail's previous
+ node as head.
+ Sets the size of the list to 0 as well.
+
+ This way, the list will be "empty" when
+ the head's next node is the tail (and the tail's
+ previous node is the head). The purpose of these
+ nodes is to make insertion and deletion much faster.
+ They will not store any value besides `None` and can
+ be thought of as placeholders for the beginning and
+ end of the list
+ """
+ # create the nodes
+ self.head = DoublyLinkedListNode(None)
+ self.tail = DoublyLinkedListNode(None)
+
+ # set head's next to tail, and tail's prev to head
+ self.head.next, self.tail.prev = self.tail, self.head
+
+ # set the size of the list to 0
+ self.size = 0
+
+ def add_front(self, value):
+ """
+ Adds a node with the provided value to the front
+ of the Doubly Linked List. Increases size by 1 as well.
+
+ By "front of the Doubly Linked List," we mean that it
+ should be the node right after the placeholder head node.
+
+ Ex: if you had nodes A, B, C, D, and you inserted node E,
+ then you would have A, E, B, C, D. A's next node would be
+ E, E's next node would be B, B's prev node would be E,
+ and E's prev node would be A.
+ """
+ # create a node with the provided value
+ # add it to the front of the Doubly Linked List
+ # make sure to correctly set the prev/next nodes
+ # your code here
+
+ # increase size by 1
+ self.size += 1
+
+ def add_back(self, value):
+ """
+ Adds a node with the provided value to the back
+ of the Doubly Linked List. Increases size by 1 as well.
+
+ By "back of the Doubly Linked List," we mean that it should
+ be the node right before the placeholder tail node.
+
+ Ex: if you had nodes A, B, C, D, and you inserted node E,
+ then you would have A, B, C, E, D. C's next node would be
+ E, E's next node would be D, D's prev node would be E,
+ and E's prev node would be C.
+ """
+ # create a node with the provided value
+ # add it to the back of the Doubly Linked List
+ # make sure to correctly set the prev/next nodes
+
+ # increase size by 1
+ self.size += 1
+
+ def print_forward(self):
+ """
+ Iterates through and prints all the nodes.
+ This should start at the head and end at the tail.
+ """
+ # ignore self.head since self.head is a "placeholder"
+ cur_node = self.head.next
+
+ while cur_node.next is not None:
+ print(cur_node, end=", ")
+ cur_node = cur_node.next
+ print()
+
+ def print_backward(self):
+ """
+ Iterates through and prints all the nodes.
+ This should start at the tail and end at the tail
+ """
+ # ignore self.tail since self.tail is a "placeholder"
+ cur_node = self.tail.prev
+
+ while cur_node.prev is not None:
+ print(cur_node, end=", ")
+ cur_node = cur_node.prev
+ print()
+
+ def __len__(self):
+ """
+ Returns the length of the list
+ """
+ return self.size
+
+
+our_doubly_linked_list = DoublyLinkedList()
+
+# add the numbers 50-99
+for i in range(50, 100):
+ our_doubly_linked_list.add_back(i)
+
+# add numbers 49 - 0
+for i in range(49, -1, -1):
+ our_doubly_linked_list.add_front(i)
+
+# print our list forward (0 -> 99)
+our_doubly_linked_list.print_forward()
+
+# print our list backward (99 -> 9)
+our_doubly_linked_list.print_backward()
diff --git a/dsa/chapter2/practice/nodes_to_10.py b/dsa/chapter2/practice/nodes_to_10.py
new file mode 100644
index 00000000..e8af4d1f
--- /dev/null
+++ b/dsa/chapter2/practice/nodes_to_10.py
@@ -0,0 +1,35 @@
+"""
+Nodes to 10
+
+Fill in the node class, then create nodes so that
+printing start_node will print the numbers from 0 to 10.
+"""
+
+
+class Node:
+ def __init__(self, value):
+ self.value = value
+
+ # create a neighbors list
+ # your code here
+
+ def __repr__(self) -> str:
+ # start with just the node's value
+ ret = f"{self.value}, "
+
+ # add all of the neighbors' values (recursively) to the string
+ for node in self.neighbors:
+ ret += f"{node}"
+
+ return ret
+
+
+start_node = Node(0)
+
+# add code that creates nodes and adds them as neighbors so that
+# start_node is connected to 1, 1 is connected to 2, 2 is connected to 3,
+# 3 is connected to 4, etc. If done correctly, printing start_node will
+# print 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+# your code here
+
+print(start_node)
diff --git a/dsa/chapter2/practice/restaurant_queue.py b/dsa/chapter2/practice/restaurant_queue.py
new file mode 100644
index 00000000..1aea151a
--- /dev/null
+++ b/dsa/chapter2/practice/restaurant_queue.py
@@ -0,0 +1,56 @@
+"""
+Cook cook cook, orders all day
+
+As a chef in a restaurant, you cook a bunch of dishes
+You can only take one order at a time, and you're tired of having
+people complain at you when you don't do their order first. So you decide to
+set up a system where you accumulate a "list" of orders and cook one order
+-- the first order that was put into the "list" -- at a time.
+
+Your job is to implement this "list" as an OrderQueue.
+You should be able to add new orders into your OrderQueue
+and remove finished orders from your OrderQueue.
+Starter code is provided.
+"""
+
+
+class OrderQueue:
+ def __init__(self) -> None:
+ self.orders = []
+
+ def dequeue(self) -> str:
+ """
+ Removes the first order in the OrderQueue
+ and returns it
+ """
+ # your code here
+ pass
+
+ def enqueue(self, order: str) -> None:
+ """
+ Inserts the order into the OrderQueue
+
+ Args:
+ order: str - the order to be inserted into the OrderQueue
+ """
+ # your code here
+ pass
+
+ def __len__(self):
+ return len(self.orders)
+
+
+# test code
+uncooked_orders = OrderQueue()
+
+# 3 customers ordered at the same time
+uncooked_orders.enqueue("a medium rare steak")
+uncooked_orders.enqueue("six gyoza")
+uncooked_orders.enqueue("two enchiladas")
+
+# now you cook them one by one
+for order in range(len(uncooked_orders)):
+ # this should print
+ # the medium rare steak first, then the gyoza, and finally the enchiladas
+ print(f"finished cooking {uncooked_orders.dequeue()}")
+print(f"done! (exactly {len(uncooked_orders)} orders left)")
diff --git a/dsa/chapter2/practice/word_reversal.py b/dsa/chapter2/practice/word_reversal.py
new file mode 100644
index 00000000..dfd89765
--- /dev/null
+++ b/dsa/chapter2/practice/word_reversal.py
@@ -0,0 +1,66 @@
+"""
+Ever wanted to reverse a word a harder way?
+Well, look no further than this problem that puts your
+knowledge of Stacks to the test in order to solve a problem
+that is already solveable by python builtins!
+
+Your job is to reverse a string by using a stack,
+adding every letter in the string (starting from the beginning of the string)
+into the stack, and then popping every letter from the stack.
+If done correctly, this will result in a reversed version of the string.
+
+Starter code is given.
+"""
+
+
+class Stack:
+ def __init__(self) -> None:
+ """
+ Initializes the stack. The back of the list will be
+ the top of the stack (so self.items[-1] is the first item
+ in the stack)
+ """
+ self.items = []
+
+ def push(self, letter: str) -> None:
+ """
+ Adds the letter to the stack. The letter should end up
+ on the *top* of the stack (the back of the list)
+ """
+ # your code here
+ pass
+
+ def pop(self) -> str:
+ """
+ Removes the top letter from the stack. Returns this letter.
+ """
+ # your code here
+ pass
+
+ def __len__(self) -> int:
+ return len(self.items)
+
+
+def reverse_word(word: str) -> str:
+ letter_stack = Stack()
+
+ # push every letter in the word (starting from the beginning of the word)
+ # into the stack
+ for letter in word:
+ # your code here
+ pass
+
+ reversed_word = ""
+ # pop every letter from the stack and add it to our reversed word
+ for i in range(len(letter_stack)):
+ # your code here
+ pass
+
+ return reversed_word
+
+
+# test code
+print(reverse_word("boj doog"))
+print(reverse_word("racecar"))
+print(reverse_word("a man a plan a canal panama"))
+print(reverse_word("read kool"))
diff --git a/dsa/chapter2/solutions/basic_bst.py b/dsa/chapter2/solutions/basic_bst.py
new file mode 100644
index 00000000..5186e4e3
--- /dev/null
+++ b/dsa/chapter2/solutions/basic_bst.py
@@ -0,0 +1,151 @@
+"""
+Let's create a very basic version of a BST that only
+has insertion capabilities. Your task will be to fill
+in the node class and the BST class. The structure is somewhat
+different from the BST that was given as an example, but
+the logic is the same.
+"""
+
+
+class BSTNode:
+ def __init__(self, key, value):
+ """
+ set self.key to key
+ set self.value to the value
+ create a left neighbor/child and a right neighbor/child, each of which
+ start as None
+ """
+
+ # set key, value
+ self.key, self.value = key, value
+
+ # set left child, right child
+ self.left_child: BSTNode = None
+ self.right_child: BSTNode = None
+
+ def add_recursively(self, other_node):
+ """
+ Adds the other node to this node or this node's children.
+ If other_node's key is equal to this node's key, do nothing.
+ If other_node's key is less than this node's key, then:
+ - if this node's left child is None, set this node's left child
+ to that other node
+ - if this node's left child is not None, then add this node
+ recursively to the left child
+ If other_node's key is greater than this node's key, then:
+ - if this node's right child is None, set this node's right child
+ to that other node
+ - if this node's right child is not None, then add this node
+ recursively to the right child
+ """
+ if other_node.key == self.key:
+ return # do nothing
+
+ if other_node.key < self.key:
+ if self.left_child is None:
+ self.left_child = other_node
+ else:
+ self.left_child.add_recursively(other_node)
+
+ if other_node.key > self.key:
+ if self.right_child is None:
+ self.right_child = other_node
+ else:
+ self.right_child.add_recursively(other_node)
+
+ def get_value(self, key):
+ """
+ Tries to return the value of the node whose key matches `key`.
+ If this node's key matches `key`, return this node's value.
+ If the key is less than this node's key:
+ - if left child is None, return 0
+ - else, get the value recursively
+ If the key is greater than this node's key:
+ - if right child is None, return 0
+ - else, get the value recursively
+ """
+ if self.key == key:
+ return self.value
+
+ if key < self.key:
+ if self.left_child is None:
+ return 0
+ else:
+ return self.left_child.get_value(key)
+
+ if key > self.key:
+ if self.right_child is None:
+ return 0
+ else:
+ return self.right_child.get_value(key)
+
+ def __str__(self):
+ """
+ Creates and returns a string that looks like:
+ left_child, self value, right_child
+ However, if left child or right_child is None, don't add them
+ to the string.
+ """
+ ret = ""
+
+ # add left child to the string if it isn't None
+ if self.left_child is not None:
+ ret += str(self.left_child) + ", "
+
+ # add self.value to the string
+ ret += str(self.value)
+
+ # add right child to the string if it isn't None
+ if self.right_child is not None:
+ ret += ", " + str(self.right_child)
+
+ return ret
+
+
+class BST:
+ def __init__(self):
+ # set a root node to None
+ self.root = None
+
+ def __setitem__(self, item, value):
+ """
+ create a new node whose key is item and whose value is value
+ then, if root is None, set root to that node.
+ else, add that node recursively.
+ """
+ # create the new node
+ new_node = BSTNode(item, value)
+
+ # either add it recursively or set it as the root
+ if self.root is None:
+ self.root = new_node
+ else:
+ self.root.add_recursively(new_node)
+
+ def __getitem__(self, item):
+ """
+ Try to find the node with key that matches item.
+ If root is None or no match is found return 0
+ """
+ if self.root is None:
+ return 0
+ return self.root.get_value(item)
+
+ def __repr__(self):
+ """
+ Returns a string representation of the root
+ """
+ return str(self.root)
+
+
+my_bst = BST()
+my_bst[50] = 30
+my_bst[40] = 31
+my_bst[60] = 32
+my_bst[30] = 33
+my_bst[70] = 34
+my_bst[20] = 35
+my_bst[80] = 36
+my_bst[10] = 37
+my_bst[90] = 38
+print(my_bst)
diff --git a/dsa/chapter2/solutions/basic_linked_list.py b/dsa/chapter2/solutions/basic_linked_list.py
new file mode 100644
index 00000000..0719dea7
--- /dev/null
+++ b/dsa/chapter2/solutions/basic_linked_list.py
@@ -0,0 +1,147 @@
+"""
+In this problem, you will create a basic Doubly Linked List.
+The goal is to be able to have nodes connected all the way
+to 100. All you have to do is fill in the add_front and
+add_back methods.
+"""
+
+
+class DoublyLinkedListNode:
+ def __init__(self, value) -> None:
+ self.value = value
+
+ self.next = None
+ self.prev = None
+
+ def __repr__(self):
+ return f"{self.value}"
+
+
+class DoublyLinkedList:
+ def __init__(self) -> None:
+ """
+ Creates a head and tail node. For convenience,
+ set the head to a node with the value `None`
+ and the tail to a node with the value `None`.
+ Sets head's next node as tail, and tail's previous
+ node as head.
+ Sets the size of the list to 0 as well.
+
+ This way, the list will be "empty" when
+ the head's next node is the tail (and the tail's
+ previous node is the head). The purpose of these
+ nodes is to make insertion and deletion much faster.
+ They will not store any value besides `None` and can
+ be thought of as placeholders for the beginning and
+ end of the list
+ """
+ # create the nodes
+ self.head = DoublyLinkedListNode(None)
+ self.tail = DoublyLinkedListNode(None)
+
+ # set head's next to tail, and tail's prev to head
+ self.head.next, self.tail.prev = self.tail, self.head
+
+ # set the size of the list to 0
+ self.size = 0
+
+ def add_front(self, value):
+ """
+ Adds a node with the provided value to the front
+ of the Doubly Linked List. Increases size by 1 as well.
+
+ By "front of the Doubly Linked List," we mean that it
+ should be the node right after the placeholder head node.
+
+ Ex: if you had nodes A, B, C, D, and you inserted node E,
+ then you would have A, E, B, C, D. A's next node would be
+ E, E's next node would be B, B's prev node would be E,
+ and E's prev node would be A.
+ """
+ # create a node with the provided value
+ new_node = DoublyLinkedListNode(value)
+
+ # get the old second-to-front node
+ old_second_to_front_node = self.head.next
+
+ # change the orders
+ old_second_to_front_node.prev, new_node.prev = new_node, self.head
+ self.head.next, new_node.next = new_node, old_second_to_front_node
+
+ # increase size by 1
+ self.size += 1
+
+ def add_back(self, value):
+ """
+ Adds a node with the provided value to the back
+ of the Doubly Linked List. Increases size by 1 as well.
+
+ By "back of the Doubly Linked List," we mean that it should
+ be the node right before the placeholder tail node.
+
+ Ex: if you had nodes A, B, C, D, and you inserted node E,
+ then you would have A, B, C, E, D. C's next node would be
+ E, E's next node would be D, D's prev node would be E,
+ and E's prev node would be C.
+ """
+ # create a node with the provided value
+ new_node = DoublyLinkedListNode(value)
+
+ # get the old second-to-last node
+ old_second_to_last_node = self.tail.prev
+
+ # change the orders
+ old_second_to_last_node.next, new_node.next = new_node, self.tail
+ self.tail.prev, new_node.prev = new_node, old_second_to_last_node
+
+ # increase size by 1
+ self.size += 1
+
+ def print_forward(self):
+ """
+ Iterates through and prints all the nodes.
+ This should start at the head and end at the tail.
+ """
+ # ignore self.head since self.head is a "placeholder"
+ cur_node = self.head.next
+
+ while cur_node.next is not None:
+ print(cur_node, end=", ")
+ cur_node = cur_node.next
+ print()
+
+ def print_backward(self):
+ """
+ Iterates through and prints all the nodes.
+ This should start at the tail and end at the tail
+ """
+ # ignore self.tail since self.tail is a "placeholder"
+ cur_node = self.tail.prev
+
+ while cur_node.prev is not None:
+ print(cur_node, end=", ")
+ cur_node = cur_node.prev
+ print()
+
+ def __len__(self):
+ """
+ Returns the length of the list
+ """
+ return self.size
+
+
+our_doubly_linked_list = DoublyLinkedList()
+
+# add the numbers 50-99
+for i in range(50, 100):
+ our_doubly_linked_list.add_back(i)
+
+# add numbers 49 - 0
+for i in range(49, -1, -1):
+ our_doubly_linked_list.add_front(i)
+
+# print our list forward (0 -> 99)
+our_doubly_linked_list.print_forward()
+
+# print our list backward (99 -> 9)
+our_doubly_linked_list.print_backward()
diff --git a/dsa/chapter2/solutions/nodes_to_10.py b/dsa/chapter2/solutions/nodes_to_10.py
new file mode 100644
index 00000000..aef5a221
--- /dev/null
+++ b/dsa/chapter2/solutions/nodes_to_10.py
@@ -0,0 +1,39 @@
+"""
+Nodes to 10
+
+Fill in the node class, then create nodes so that
+printing start_node will print the numbers from 0 to 10.
+"""
+
+
+class Node:
+ def __init__(self, value):
+ self.value = value
+
+ # create a neighbors list
+ self.neighbors = []
+
+ def __repr__(self) -> str:
+ # start with just the node's value
+ ret = f"{self.value}, "
+
+ # add all of the neighbors' values (recursively) to the string
+ for node in self.neighbors:
+ ret += f"{node}"
+
+ return ret
+
+
+start_node = Node(0)
+
+# add code that creates nodes and adds them as neighbors so that
+# start_node is connected to 1, 1 is connected to 2, 2 is connected to 3,
+# 3 is connected to 4, etc. If done correctly, printing start_node will
+# print 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+previous_node = start_node
+for value in range(1, 11):
+ cur_node = Node(value)
+ previous_node.neighbors.append(cur_node)
+ previous_node = cur_node
+
+print(start_node)
diff --git a/dsa/chapter2/solutions/restaurant_queue.py b/dsa/chapter2/solutions/restaurant_queue.py
new file mode 100644
index 00000000..2eb55e07
--- /dev/null
+++ b/dsa/chapter2/solutions/restaurant_queue.py
@@ -0,0 +1,54 @@
+"""
+Cook cook cook, orders all day
+
+As a chef in a restaurant, you cook a bunch of dishes
+You can only take one order at a time, and you're tired of having
+people complain at you when you don't do their order first. So you decide to
+set up a system where you accumulate a "list" of orders and cook one order
+-- the first order that was put into the "list" -- at a time.
+
+Your job is to implement this "list" as an OrderQueue.
+You should be able to add new orders into your OrderQueue
+and remove finished orders from your OrderQueue.
+Starter code is provided.
+"""
+
+
+class OrderQueue:
+ def __init__(self) -> None:
+ self.orders = []
+
+ def dequeue(self) -> str:
+ """
+ Removes the first order in the OrderQueue
+ and returns it
+ """
+ return self.orders.pop(0)
+
+ def enqueue(self, order: str) -> None:
+ """
+ Inserts the order into the OrderQueue
+
+ Args:
+ order: str - the order to be inserted into the OrderQueue
+ """
+ self.orders.append(order)
+
+ def __len__(self):
+ return len(self.orders)
+
+
+# test code
+uncooked_orders = OrderQueue()
+
+# 3 customers ordered at the same time
+uncooked_orders.enqueue("a medium rare steak")
+uncooked_orders.enqueue("six gyoza")
+uncooked_orders.enqueue("two enchiladas")
+
+# now you cook them one by one
+for order in range(len(uncooked_orders)):
+ # this should print
+ # the medium rare steak first, then the gyoza, and finally the enchiladas
+ print(f"finished cooking {uncooked_orders.dequeue()}")
+print(f"done! (exactly {len(uncooked_orders)} orders left)")
diff --git a/dsa/chapter2/solutions/word_reversal.py b/dsa/chapter2/solutions/word_reversal.py
new file mode 100644
index 00000000..e8372fcb
--- /dev/null
+++ b/dsa/chapter2/solutions/word_reversal.py
@@ -0,0 +1,62 @@
+"""
+Ever wanted to reverse a word a harder way?
+Well, look no further than this problem that puts your
+knowledge of Stacks to the test in order to solve a problem
+that is already solveable by python builtins!
+
+Your job is to reverse a string by using a stack,
+adding every letter in the string (starting from the beginning of the string)
+into the stack, and then popping every letter from the stack.
+If done correctly, this will result in a reversed version of the string.
+
+Starter code is given.
+"""
+
+
+class Stack:
+ def __init__(self) -> None:
+ """
+ Initializes the stack. The back of the list will be
+ the top of the stack (so self.items[-1] is the first item
+ in the stack)
+ """
+ self.items = []
+
+ def push(self, letter: str) -> None:
+ """
+ Adds the letter to the stack. The letter should end up
+ on the *top* of the stack (the back of the list)
+ """
+ self.items.append(letter)
+
+ def pop(self) -> str:
+ """
+ Removes the top letter from the stack. Returns this letter.
+ """
+ return self.items.pop()
+
+ def __len__(self) -> int:
+ return len(self.items)
+
+
+def reverse_word(word: str) -> str:
+ letter_stack = Stack()
+
+ # push every letter in the word (starting from the beginning of the word)
+ # into the stack
+ for letter in word:
+ letter_stack.push(letter)
+
+ reversed_word = ""
+ # pop every letter from the stack and add it to our reversed word
+ for i in range(len(letter_stack)):
+ reversed_word += letter_stack.pop()
+
+ return reversed_word
+
+
+# test code
+print(reverse_word("boj doog"))
+print(reverse_word("racecar"))
+print(reverse_word("a man a plan a canal panama"))
+print(reverse_word("read kool"))
diff --git a/dsa/chapter3/examples/a_star.py b/dsa/chapter3/examples/a_star.py
new file mode 100644
index 00000000..254b2337
--- /dev/null
+++ b/dsa/chapter3/examples/a_star.py
@@ -0,0 +1,84 @@
+from queue import PriorityQueue
+
+
+def heuristic(cell, end):
+ # assuming (100, 100) is end
+ x1, y1 = cell
+ x2, y2 = end
+ return abs(x2 - x1) + abs(y2 - y1)
+
+
+def reconstruct(path, end):
+ final = []
+ curr = end
+ while curr in path:
+ final.append(curr)
+ curr = path[curr]
+
+ return reversed(final)
+
+
+def get_neighbors(cell, start=(0, 0), end=(100, 100)):
+ def between(a, b, c):
+ return (b <= a and a <= c) or (b >= a and a >= c)
+
+ x1, y1 = cell
+ return [
+ (x + x1, y + y1)
+ for x in range(-1, 2)
+ for y in range(-1, 2)
+ if (
+ between(x + x1, start[0], end[0])
+ and between(y + y1, start[1], end[1])
+ )
+ ]
+
+
+def a_star(end: tuple = (100, 100), start: tuple = (0, 0)):
+ count = 0
+ open = PriorityQueue()
+ open.put((0, count, start))
+ path = {}
+ g_score = {
+ (x, y): float("inf")
+ for x in range(end[0] + 1)
+ for y in range(end[1] + 1)
+ }
+ g_score[start] = 0
+
+ f_score = {
+ (x, y): float("inf") for x in range(end[0]) for y in range(end[1])
+ }
+ f_score[0] = heuristic(start, end)
+
+ while not open.empty():
+ curr = open.get()[2]
+
+ temp_g = g_score[curr] + 1
+ for n in get_neighbors(curr, start, end):
+ if n == end:
+ path[n] = curr
+ return reconstruct(path, end)
+ if temp_g < g_score[n]:
+ path[n] = curr
+ g_score[n] = temp_g
+ f_score[n] = temp_g + heuristic(n, end)
+ if not any(n == item[2] for item in open.queue):
+ count += 1
+ open.put((f_score[n], count, n))
+
+ # path not found
+ return None
+
+
+path = [coord for coord in a_star((50, 10))]
+path.insert(0, (0, 0))
+
+for y in range(0, 11):
+ lst = []
+ for x in range(0, 51):
+ if (x, y) not in path:
+ lst.append("x")
+ else:
+ lst.append("o")
+ print(lst)
diff --git a/dsa/chapter3/examples/bfs.py b/dsa/chapter3/examples/bfs.py
new file mode 100644
index 00000000..5fe04c0a
--- /dev/null
+++ b/dsa/chapter3/examples/bfs.py
@@ -0,0 +1,19 @@
+from queue import Queue
+
+
+def BFS(start_node):
+ visited = set(start_node)
+ current_depth_nodes = Queue()
+ current_depth_nodes.put(start_node)
+
+ while len(current_depth_nodes) != 0: # this depth is not empty
+ # returns and deletes the first element
+ current_node = current_depth_nodes.get()
+
+ # add each neighbor to this depth
+ for neighbor in current_node.neighbors:
+ if neighbor not in visited:
+ # adds element to end
+ visited.add(neighbor)
+ # adds element to end
+ current_depth_nodes.put(neighbor)
diff --git a/dsa/chapter3/examples/binary_search.py b/dsa/chapter3/examples/binary_search.py
new file mode 100644
index 00000000..8869c353
--- /dev/null
+++ b/dsa/chapter3/examples/binary_search.py
@@ -0,0 +1,27 @@
+def binary_search(lst, item):
+ """
+ Arguments:
+ lst - a list sorted in ascending order
+ item - the item that we want to find
+ Returns:
+ the idx of the item or -1 if not found
+ """
+
+ low_bound = 0
+ upper_bound = len(lst) - 1
+
+ # take the average, but make sure it's an integer
+ cur_idx = (low_bound + upper_bound) // 2
+
+ while low_bound <= upper_bound:
+ if lst[cur_idx] == item:
+ return cur_idx
+ if lst[cur_idx] < item:
+ # it was an undershot, so set this as the new lower bound
+ low_bound = cur_idx + 1
+ else: # lst[cur_idx] > item)
+ # it was an overshot, so set this as the new upper bound
+ upper_bound = cur_idx - 1
+ # update cur_idx
+ cur_idx = (low_bound + upper_bound) // 2
+ return -1
diff --git a/dsa/chapter3/examples/dfs.py b/dsa/chapter3/examples/dfs.py
new file mode 100644
index 00000000..7de583cb
--- /dev/null
+++ b/dsa/chapter3/examples/dfs.py
@@ -0,0 +1,20 @@
+def recursive_dfs(visited, graph, node):
+ if node not in visited:
+ print(node)
+ visited.add(node)
+ for neighbour in graph[node]:
+ recursive_dfs(visited, graph, neighbour)
+
+
+def iterative_dfs(graph, start):
+ stack, path = [start], []
+
+ while stack:
+ node = stack.pop()
+ if node in path:
+ continue
+ path.append(node)
+ for neighbor in graph[node]:
+ stack.append(neighbor)
+
+ return path
diff --git a/dsa/chapter3/examples/linear_search.py b/dsa/chapter3/examples/linear_search.py
new file mode 100644
index 00000000..e8821336
--- /dev/null
+++ b/dsa/chapter3/examples/linear_search.py
@@ -0,0 +1,14 @@
+def linear_search(arr, val) -> int:
+ """
+ Search the provided array for the provided value
+ and get the index, if found
+ Arguments:
+ arr - the array to search in
+ val - the value to search for
+ Returns:
+ int - the index of the value if it was found, else -1
+ """
+ for i in range(len(arr)):
+ if arr[i] == val:
+ return i
+ return -1
diff --git a/dsa/chapter3/examples/mergesort.py b/dsa/chapter3/examples/mergesort.py
new file mode 100644
index 00000000..235d46c9
--- /dev/null
+++ b/dsa/chapter3/examples/mergesort.py
@@ -0,0 +1,42 @@
+def mergelists(lst1, lst2):
+ idx1 = 0
+ idx2 = 0
+ ret = []
+
+ # while either one has unused items
+ while idx1 < len(lst1) or idx2 < len(lst2):
+ # both lists still have items
+ if idx1 < len(lst1) and idx2 < len(lst2):
+ if lst1[idx1] < lst2[idx2]:
+ ret.append(lst1[idx1]) # add the item from lst1
+ idx1 += 1 # increment our idx in lst1
+ else: # lst2[idx2] <= lst1[idx1]
+ ret.append(lst2[idx2]) # add the item from lst2
+ idx2 += 1 # increment our idx in lst2
+
+ # only one list still has items
+ elif idx1 < len(lst1): # if only lst1 still has items
+ ret.extend(lst1[idx1:]) # add the rest of this list
+ idx1 = len(lst1)
+ elif idx2 < len(lst2): # if only lst2 still has items
+ ret.extend(lst2[idx2:]) # add the rest of this list
+ idx2 = len(lst2)
+
+ return ret
+
+
+def mergesort(lst):
+ # "base case" where the list is just 0 or 1 item(s).
+ # In this case, we can say it is already sorted and just return it.
+ if len(lst) <= 1:
+ return lst
+
+ # if it's not just 1 or 0 item(s), then follow mergesort logic
+ middle_idx = len(lst) // 2 # we want an integer, so use //
+ first_half = mergesort(lst[:middle_idx]) # sort the first half
+ second_half = mergesort(lst[middle_idx:]) # sort the second half
+ return mergelists(first_half, second_half) # merge the two sorted halves
+
+
+print(mergesort([5, 4, 3, 2, 1]))
+print(mergesort([i for i in range(99, -1, -1)]))
diff --git a/dsa/chapter3/examples/quicksort.py b/dsa/chapter3/examples/quicksort.py
new file mode 100644
index 00000000..13c41aac
--- /dev/null
+++ b/dsa/chapter3/examples/quicksort.py
@@ -0,0 +1,71 @@
+def partitionv1(arr, pi):
+ """
+ partitionv1 takes some pivot index (pi), and puts all of the items
+ smaller than pivot to the left, and all of the items larger than
+ pivot to the right,
+
+ in doing so we put pivot in the same spot as if the entire list was
+ sorted
+
+ note: this is only one way of doing it
+ """
+ # moves pivot to the end of the list so it doesn't get in the way
+ arr[-1], arr[pi] = arr[pi], arr[-1]
+
+ i = 0 # i is initialized to be the left side of our list
+
+ for j in range(len(arr)):
+ # if j is smaller than the pivot, arr[j] is smaller than the pivot,
+ # so we want to move it to the left
+ if arr[j] < arr[-1]:
+ # swaps arr[j] and arr[i], so arr[j] is at the left side of the list
+ arr[i], arr[j] = arr[j], arr[i]
+ i += 1
+ # move the pivot from the end to the correct location
+ arr[i], arr[-1] = arr[-1], arr[i]
+
+
+def partitionv2(arr, low, high):
+ """
+ partitionv2 takes an array, a low, and a high and partitions
+ the section of the array between low and high (inclusive).
+
+ partitionv2 always partitions with the last element in the
+ section as the pivot
+ """
+
+ i = low # i is initialized to be the left side of our list
+
+ for j in range(low, high):
+ # if j is smaller than the pivot, arr[j] is smaller than the pivot,
+ # so we want to move it to the left
+ if arr[j] < arr[high]:
+ # swaps arr[j] and arr[i], so arr[j] is at the left side of the list
+ arr[i], arr[j] = arr[j], arr[i]
+ i += 1
+ # move the pivot from the end to the correct location
+ arr[i], arr[high] = arr[high], arr[i]
+
+
+def quicksort(arr, low, high):
+ """
+ quicksort that recursively partitions the left nd right side
+ of the pivot
+
+ This implementation always partitions with the last element as the pivot
+ """
+
+ i = low # i is initialized to be the left side of our list
+
+ for j in range(low, high):
+ # if j is smaller than the pivot, arr[j] is smaller than the pivot,
+ # so we want to move it to the left
+ if arr[j] < arr[high]:
+ # swaps arr[j] and arr[i], so arr[j] is at the left side of the list
+ arr[i], arr[j] = arr[j], arr[i]
+ i += 1
+ # move the pivot from the end to the correct location
+ arr[i], arr[high] = arr[high], arr[i]
+
+ quicksort(arr, low, i - 1) # left side of the pivot
+ quicksort(arr, i + 1, high) # right side of the pivot
diff --git a/dsa/chapter3/examples/selection_sort.py b/dsa/chapter3/examples/selection_sort.py
new file mode 100644
index 00000000..b8731de5
--- /dev/null
+++ b/dsa/chapter3/examples/selection_sort.py
@@ -0,0 +1,27 @@
+def selection_sort(lst):
+ for i in range(len(lst)):
+ min_value = lst[i]
+ min_val_idx = i
+
+ # find the new minimum value and its idx
+ for x in range(i, len(lst)):
+ if lst[x] < min_value:
+ min_value = lst[x]
+ min_val_idx = x
+
+ # swap the minimum value with the value at the current idx
+ lst[i], lst[min_val_idx] = min_value, lst[i]
+ return lst
+
+
+test1 = [3, 12, 7, 2, 0, 3]
+test1 = selection_sort(test1)
+print(test1) # [0, 2, 3, 3, 7, 12]
+
+test2 = [-23, 0, 72, -33, 11, 6, 2, -5, -9, 10, -1]
+test2 = selection_sort(test2)
+print(test2) # [-33, -23, -9, -5, -1, 0, 2, 6, 10, 11, 72]
+
+test3 = [i for i in range(1000, -1, -1)]
+test3 = selection_sort(test3) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
+print(test3)
diff --git a/dsa/chapter3/practice/a_star.py b/dsa/chapter3/practice/a_star.py
new file mode 100644
index 00000000..eb2b598a
--- /dev/null
+++ b/dsa/chapter3/practice/a_star.py
@@ -0,0 +1,186 @@
+"""
+A Star Practice
+
+In this practice problem, you get to fill in some a_star code
+as well as see the effects of using different heuristics on
+a_star's execution time.
+
+Heuristics and helper functions are given. Your job is to fill
+in sections of the A* algorithm where it says 'your code here'
+"""
+
+from queue import PriorityQueue
+import math
+import random
+
+
+class Point:
+ def __init__(self, x: int, y: int) -> None:
+ self.x: int = x
+ self.y: int = y
+
+ def get_neighbors(self, start, end):
+ """
+ This function returns a list of neighboring points
+ using the fact that neighboring points will be the
+ following (p = neighboring, c = current)
+
+ ```
+ p p p
+ p c p
+ p p p
+ ```
+ """
+
+ def between(a, b, c):
+ return (b <= a and a <= c) or (b >= a and a >= c)
+
+ return [
+ Point(x + self.x, y + self.y)
+ for x in range(-1, 2)
+ for y in range(-1, 2)
+ if (
+ between(x + self.x, start.x, end.x)
+ and between(y + self.y, start.y, end.y)
+ )
+ ]
+
+ def __eq__(self, __o: object) -> bool:
+ return self.x == __o.x and self.y == __o.y
+
+ def __hash__(self) -> int:
+ return hash((self.x, self.y))
+
+ def __str__(self):
+ return f"({self.x}, {self.y})"
+
+ def __repr__(self) -> str:
+ return str(self)
+
+
+def adding_heuristic(cur: Point, end: Point):
+ """
+ this heuristic returns a value
+ that looks like the following:
+ abs(x-x1) + abs(y-y1).
+ """
+ return abs(cur.x - end.x) + abs(cur.y - end.y)
+
+
+def triangle_heuristic(cur: Point, end: Point):
+ """
+ this heuristic will return a value
+ based off the pythagorean theorem
+ that looks like the following
+ sqrt((x-x1)^2 + (y-y1)^2)
+ """
+ return math.sqrt((cur.x - end.x) ** 2 + (cur.y - end.y) ** 2)
+
+
+def bad_heuristic(cur: Point, end: Point):
+ """
+ This heuristic will return a value
+ that is the opposite of the distance,
+ meaning that the closer cur is to end,
+ the worse (higher) score this will give it
+ """
+ return -(abs(cur.x - end.x) + abs(cur.y - end.y))
+
+
+def random_heuristic(cur: Point, end: Point):
+ """
+ Returns a totally random number.
+ """
+ return random.randint(cur.x, end.x) + random.randint(cur.y, end.y)
+
+
+def reconstruct_path(path: dict, start, end):
+ backwards_path = []
+ curr = end # we know that we start at the end
+ while curr in path:
+ # add the current node to the backwards_path
+ backwards_path.append(curr)
+
+ # since path is a dictionary of node : how to get there,
+ # we get the previous node in the path by doing path[curr]
+ curr = path[curr]
+
+ # this will be the first item after we reverse the list
+ backwards_path.append(start)
+
+ return reversed(backwards_path)
+
+
+def a_star(start: Point, end: Point, heuristic):
+ # min_x, max_x = min(start.x, end.x), max(start.x, end.x) # uncomment this
+ # min_y, max_y = min(start.y, end.y), max(start.y, end.y) # uncomment this
+ """
+ initialize f_scores (final scores) to infinity for every
+ point between the [min_x, min_y] and [max_x, max_y]
+
+ initialize g_scores (distance to get there) to infinity for every
+ point between [min_x, min_y] and [max_x, max_y]
+
+ Since it takes 0 steps to get to the start, initialize that g score to 0
+ """
+ # your code here
+
+ # initialize our unexplored queue and add
+ # insert the start node.
+ # format for inserting nodes: (f_score, count, node)
+ count = 0
+ unexplored = PriorityQueue()
+ unexplored.put((0, count, start))
+
+ # this is a dictionary that stores entries in the format
+ # node: how to get there
+ # this means that path[(1, 1)] might equal (0, 0)
+ # since maybe the path goes from (0, 0) to (1, 1)
+ # we use this variable to help us reconstruct the path that
+ # a star found
+ path = {}
+
+ # allows us to see how many executions it really took
+ num_executions = 0
+
+ while not unexplored.empty():
+ current: Point = unexplored.get()[2] # just get the Point
+
+ # it takes 1 more step to get to any neighbor, so their g_scores will
+ # be one more than the current g score
+ for node in current.get_neighbors(start, end):
+ if node == end:
+ # the way to get to the end is from the current node
+ path[node] = current
+ print(f"finished after {num_executions} executions")
+ return reconstruct_path(path, start, end)
+ else:
+ """
+ if either we haven't explored this node yet
+ (meaning g_scores[node] = infinity) or this
+ is a shorter path to get to this node
+ (g_score[current] + 1 < g_scores[node]), then:
+
+ * update our path
+ * update our f and g scores:
+ * remember, f score = g score + heuristic
+ * if it wasn't already in unexplored:
+ * update our count
+ * add the unexplored node w/ its score and count to
+ unexplored
+ """
+ # your code here
+
+ num_executions += 1
+ print(f"no solution found after {num_executions} executions")
+ return None # no path found
+
+
+# you can try changing the heuristic and seeing how that affects the path taken
+# as well as the number of executions it took
+path = a_star(Point(0, 0), Point(10, 15), adding_heuristic)
+path_len = 0
+for i in path:
+ print(i)
+ path_len += 1
+print(f"path length was {path_len}")
diff --git a/dsa/chapter3/practice/bfs_dfs.py b/dsa/chapter3/practice/bfs_dfs.py
new file mode 100644
index 00000000..7480d097
--- /dev/null
+++ b/dsa/chapter3/practice/bfs_dfs.py
@@ -0,0 +1,101 @@
+"""
+In this practice problem, we practice implementing
+breadth first search and depth first search and see them
+in action
+"""
+
+
+class Node:
+ def __init__(self, value: int) -> None:
+ self.value: int = value
+ self.children = []
+
+ def __str__(self) -> str:
+ return str(self.value)
+
+ def __repr__(self) -> str:
+ return str(self)
+
+
+def BFS(start_node: Node):
+ """
+ Implement a breadth-first search algorithm,
+ that prints the nodes that you visit as you go.
+
+ Remember, a breadth-first search algorithm works by
+ visiting all the children in a certain depth before
+ advancing to the next depth
+
+ Note that your function will be slightly different from
+ the one in the example since the nodes in this file
+ have children and not neighbors
+ """
+ # first, initialize an empty list of all visited nodes
+ # next, initialize a list of all the nodes at the current depth
+ # and have it contain start_node
+ # lastly, create an empty list of all the nodes at the next depth.
+ # your code here
+
+ # iterate until the the list of nodes at the current depth is empty
+ # in each iteration, go through all the nodes at the current depth
+ # and, if it isn't in the list of visited nodes:
+ # print it
+ # add it to visited nodes
+ # add its children to the list of nodes at the next depth
+ # once done iterating through all the nodes at the current depth,
+ # set the list of nodes at the current depth equal to the
+ # list of nodes at the next depth
+ # and set the next depth to an empty list
+ # your code here
+
+
+def DFS(start_node: Node, visited: list = []):
+ """
+ Implement a recursive depth-first search algorithm,
+ that prints the nodes that you visit as you go.
+
+ Remember, a depth-first search algorithm goes all the way
+ down to the last child before working its way up and visiting
+ neighbors
+
+ Note that your function will be slightly different from
+ the one in the example since the nodes in this file
+ have children and not neighbors
+ """
+ # check if the start node is in visited
+ # if it isn't, then print the node
+ # add it to the list of visited,
+ # and then use DFS on each of its children
+ # (make sure to pass the list of visited as an argument)
+
+
+if __name__ == "__main__":
+ # make a graph that looks like the following
+ # / 5
+ # 2 - 6 - 10
+ # /
+ # 1 - 3 - 7 - 11 - 12
+ # \
+ # 4 - 8
+ # \ 9
+ start_node = Node(1)
+ for i in range(3):
+ start_node.children.append(Node(i + 2))
+ start_node.children[0].children.append(Node(5))
+ start_node.children[0].children.append(Node(6))
+ start_node.children[0].children[1].children.append(Node(10))
+
+ start_node.children[1].children.append(Node(7))
+ start_node.children[1].children[0].children.append(Node(11))
+ start_node.children[1].children[0].children[0].children.append(Node(12))
+
+ start_node.children[2].children.append(Node(8))
+ start_node.children[2].children.append(Node(9))
+
+ print("with BFS")
+ BFS(start_node) # 1 2 3 4 5 6 7 8 9 10 11 12
+ print()
+
+ print("with DFS")
+ DFS(start_node) # 1 2 5 6 10 3 7 11 12 4 8 9
+ print()
diff --git a/dsa/chapter3/practice/mergesort.py b/dsa/chapter3/practice/mergesort.py
new file mode 100644
index 00000000..11d85596
--- /dev/null
+++ b/dsa/chapter3/practice/mergesort.py
@@ -0,0 +1,69 @@
+def mergesort(lst: list) -> list:
+ """
+ Let's implement mergesort,
+ First let's create a base case where if the list is 0 or 1 elements long,
+ return it
+ """
+ # your code here
+
+ """
+ Now that we have handled the base case, if the list is any longer, we can
+ go into typical mergesort logic,
+
+ We need to split the list into 2 halves, so lets first find the middle
+ index value. Use // instead of / because we want an integer
+ """
+ # your code here
+
+ """
+ Now we can run mergesort on the first and second halves of lst. Create a
+ variable first_half which is the result of calling mergesort on the first
+ half of lst. Repeat for the second half of the list, creating the variable
+ second_half. Use list splicing for this.
+ """
+ # your code here
+
+ """
+ Now we need to merge the two sorted halves. In order to do this, we will
+ implement a mergelists helper function. Return the result of mergelists
+ with first_half and second_half as the two parameters.
+ """
+ return # your code here
+
+
+def mergelists(lst1: list, lst2: list) -> list:
+ idx1 = 0
+ idx2 = 0
+ ret = []
+
+ """
+ Let's create a while loop that runs for as long as idx1 or idx2 is less
+ than the len of lst1 or lst2.
+ """
+ while idx1 < len(lst1) or idx2 < len(lst2):
+ # If both lists have items, we need to compare the first items of the
+ # lst1 and lst2, and append whichever item is smaller to ret. Then, we
+ # increment the idx1 or idx2 variable respectively
+ if idx1 < len(lst1) and idx2 < len(lst2):
+ # your code here
+ pass
+
+ elif idx1 < len(lst1):
+ # if only lst1 has items left, append the remaining items to the
+ # end of ret, and set idx1 to len(lst1)
+
+ # your code here
+ pass
+ elif idx2 < len(lst2):
+ # if only lst2 has items left, append the remaining items to the
+ # end of ret, and set idx2 to len(lst2)
+
+ # your code here
+ pass
+ return ret
+
+
+if __name__ == "__main__":
+ lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+ mergesort(lst)
+ print(lst)
diff --git a/dsa/chapter3/practice/quicksort.py b/dsa/chapter3/practice/quicksort.py
new file mode 100644
index 00000000..a5b76662
--- /dev/null
+++ b/dsa/chapter3/practice/quicksort.py
@@ -0,0 +1,89 @@
+lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+
+
+def quicksort(arr: list):
+ """
+ quicksort_recursive takes in the list you are sorting, the first index of
+ the sublist you want to sort, and the last index of the sublist you want
+ to sort, in that order
+
+ For the first call to quicksort_recursive, the first index and last index
+ should be 0 and the index to the last item of the list respectively
+
+ Make a call to quicksort_recursive with the appropriate arguments below
+ """
+ # your code here
+
+
+def quicksort_recursive(arr, low, high):
+ """
+ Arguments:
+ arr: list, the entire list we are sorting,
+ low: int, the first index of the sublist we are sorting
+ high: int, the last index of the sublist we are sorting
+ """
+ # base case
+ if low >= high:
+ return
+
+ # partition the sublist and return the pivot_index
+ pivot_index = partition(arr, low, high) # NOQA
+
+ # recursive calls
+ """
+ After the list has been partitioned around the pivot_index, we need to
+ call quicksort_recursive on the two sublists: the one to the left of the
+ pivot_index, and the one to the right
+
+ We do this on the right side by setting the high index to one less than
+ pivot_index, and on the left side by setting the low index to one higher
+ than pivot_index
+
+ Make calls to quicksort_recursive with the appropriate arguments below
+ """
+ # your code here
+
+
+def partition(arr, low, high):
+ """
+ Partition takes a pivot (in our case, arr[high]), and accomplishes the
+ following:
+ All of the elements between low and high that are SMALLER than the
+ pivot are placed to the LEFT of the pivot.
+ Conversely, all elements between low and high that are LARGER than
+ the pivot are placed to the RIGHT of the pivot
+ This has the side effect that the location of pivot after the partition has
+ taken place is the same as if the list was sorted. Of course, the areas to
+ the left and right of the pivot are not yet sorted.
+ """
+ i = low # initialize i to the left side of what we are sorting
+
+ """
+ Create a for loop that creates an index j, and loops through indexes low
+ (inclusive) to high (exclusive)
+ """
+ # your code here
+ """
+ Inside our loop, we are trying to find items (arr[j]) that are less than
+ our pivot (arr[high]). If we find one, we want to swap our item
+ (arr[j]) with arr[i], an item thats to the left side of our
+ sublist. Then, we will increment i by one, so we don't continuously
+ swap with same arr[i] over and over again.
+
+ Create an if statement to do this below
+ """
+ # your code here
+
+ """
+ Our pivot (arr[high]) is still on the right side of our sublist. Let's swap
+ it with arr[i] so it moves to the right spot.
+ """
+ # your code here
+
+ # return the pivot_index
+ return i
+
+
+if __name__ == "__main__":
+ quicksort(lst, 0, len(lst) - 1)
+ print(lst)
diff --git a/dsa/chapter3/practice/searching.py b/dsa/chapter3/practice/searching.py
new file mode 100644
index 00000000..fb3bb591
--- /dev/null
+++ b/dsa/chapter3/practice/searching.py
@@ -0,0 +1,124 @@
+"""
+Let's see the difference between linear and binary searches!
+Some of the algorithm is already done for you, but you
+will have to fill in some areas.
+
+Then, run the code and you can see the results
+"""
+
+import random
+from datetime import datetime as d
+
+
+def linear_search(arr, val) -> int:
+ """
+ Linear Search - iterates through all the items in the array and checks
+ equality with the provided value. If the value matches, returns
+ the index of that value. Else, returns -1
+ Arguments:
+ arr - the array to search
+ val - the value to search for
+ Returns:
+ int - index of the value on success, -1 on failure
+ """
+ for i in range(len(arr)):
+ # your code here
+ pass
+ return -1
+
+
+def binary_search(arr, val) -> int:
+ """
+ Binary Search - checks the list for a value using a binary search.
+ Only works on sorted lists since it assumes that all the values
+ in indexes greater than i are greater and all the values in
+ indexes less than i are less.
+ Arguments:
+ arr - the array to search
+ val - the value to search for
+ Returns:
+ int - index of the value on success, -1 on failure
+ """
+ low = 0
+ high = len(arr) - 1
+ while low <= high:
+ current = (low + high) // 2
+ if val == arr[current]:
+ # your code here
+ pass
+ elif val < arr[current]:
+ # your code here
+ pass
+ else: # val > arr[current]
+ # your code here
+ pass
+ return -1
+
+
+# example 1 - sorted list
+# the below demonstrates the binary search is faster than
+# linear search on sorted lists
+size = 100000
+lst_1 = [i for i in range(size)]
+
+tests = 3
+for i in range(tests):
+ print(f"sorted test #{i+1}:")
+ print("searching linearly")
+ target = random.randint(0, size)
+ linear_start = d.now()
+ linear_result = linear_search(lst_1, target)
+ linear_end = d.now()
+ print(
+ "finished searching linearly in "
+ + f"{(linear_end - linear_start).total_seconds()} seconds "
+ + f"and got the {'right' if linear_result == target else 'wrong'} result"
+ + f" ({linear_result})"
+ )
+
+ print("searching binarily")
+ binary_start = d.now()
+ binary_result = binary_search(lst_1, target)
+ binary_end = d.now()
+ print(
+ "finished searching binarily in "
+ + f"{(binary_end - binary_start).total_seconds()} seconds "
+ + f"and got the {'right' if binary_result == target else 'wrong'} result"
+ + f" ({binary_result})"
+ )
+ print()
+
+# example 2 - unsorted list
+# the below demonstrates that binary search doesn't work on unsorted
+# lists, but linear search does
+size = 100000
+lst_2 = [i for i in range(size)]
+random.shuffle(lst_2)
+
+tests = 3
+for i in range(tests):
+ print(f"unsorted test #{i+1}:")
+ print("searching linearly")
+ idx = random.randint(0, size)
+ target = lst_2[idx]
+ linear_start = d.now()
+ linear_result = linear_search(lst_2, target)
+ linear_end = d.now()
+ print(
+ "finished searching linearly in "
+ + f"{(linear_end - linear_start).total_seconds()} seconds "
+ + f"and got the {'right' if linear_result == idx else 'wrong'} result"
+ + f" ({linear_result})"
+ )
+
+ print("searching binarily")
+ binary_start = d.now()
+ binary_result = binary_search(lst_2, target)
+ binary_end = d.now()
+ print(
+ "finished searching binarily in "
+ + f"{(binary_end - binary_start).total_seconds()} seconds "
+ + f"and got the {'right' if binary_result == idx else 'wrong'} result"
+ + f" ({binary_result})"
+ )
+ print()
diff --git a/dsa/chapter3/practice/selectionsort.py b/dsa/chapter3/practice/selectionsort.py
new file mode 100644
index 00000000..a1ac9f51
--- /dev/null
+++ b/dsa/chapter3/practice/selectionsort.py
@@ -0,0 +1,31 @@
+def selectionsort(arr: list):
+ """
+ Let's implement selection sort! There are 4 easy steps to follow in order
+ to implement it.
+ 1. Create a loop through iterate through the list.
+ 2. Create an inner loop that iterates from the outer index + 1 to the
+ end of the list.
+ 3. Compare the element at the outer index to the element at the inner
+ index.
+ 4. If the element at the outer index is larger than at the inner index,
+ swap the 2 elements.
+ """
+
+ # Step 1, create an outer loop that iterates through the whole list. Let's
+ # name the outer index "i"
+
+ # Step 2, create an inner loop that iterates from i+1 to the end of the
+ # list, let's name inner index "j"
+
+ # Step 3, check if the element at index i is larger than the element at
+ # index j
+
+ # Step 4, swap the element at the outer index with the element at the
+ # inner index
+ pass
+
+
+if __name__ == "__main__":
+ lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+ selectionsort(lst)
+ print(lst)
diff --git a/dsa/chapter3/solutions/a_star.py b/dsa/chapter3/solutions/a_star.py
new file mode 100644
index 00000000..759f28a3
--- /dev/null
+++ b/dsa/chapter3/solutions/a_star.py
@@ -0,0 +1,195 @@
+"""
+A Star Practice
+
+In this practice problem, you get to fill in some a_star code
+as well as see the effects of using different heuristics on
+a_star's execution time.
+
+Heuristics and helper functions are given. Your job is to fill
+in sections of the A* algorithm where it says 'your code here'
+"""
+
+from queue import PriorityQueue
+import math
+import random
+
+
+class Point:
+ def __init__(self, x: int, y: int) -> None:
+ self.x: int = x
+ self.y: int = y
+
+ def get_neighbors(self, start, end):
+ """
+ This function returns a list of neighboring points
+ using the fact that neighboring points will be the
+ following (p = neighboring, c = current)
+
+ ```
+ p p p
+ p c p
+ p p p
+ ```
+ """
+
+ def between(a, b, c):
+ return (b <= a and a <= c) or (b >= a and a >= c)
+
+ return [
+ Point(x + self.x, y + self.y)
+ for x in range(-1, 2)
+ for y in range(-1, 2)
+ if (
+ between(x + self.x, start.x, end.x)
+ and between(y + self.y, start.y, end.y)
+ )
+ ]
+
+ def __eq__(self, __o: object) -> bool:
+ return self.x == __o.x and self.y == __o.y
+
+ def __hash__(self) -> int:
+ return hash((self.x, self.y))
+
+ def __str__(self):
+ return f"({self.x}, {self.y})"
+
+ def __repr__(self) -> str:
+ return str(self)
+
+
+def adding_heuristic(cur: Point, end: Point):
+ """
+ this heuristic returns a value
+ that looks like the following:
+ abs(x-x1) + abs(y-y1).
+ """
+ return abs(cur.x - end.x) + abs(cur.y - end.y)
+
+
+def triangle_heuristic(cur: Point, end: Point):
+ """
+ this heuristic will return a value
+ based off the pythagorean theorem
+ that looks like the following
+ sqrt((x-x1)^2 + (y-y1)^2)
+ """
+ return math.sqrt((cur.x - end.x) ** 2 + (cur.y - end.y) ** 2)
+
+
+def bad_heuristic(cur: Point, end: Point):
+ """
+ This heuristic will return a value
+ that is the opposite of the distance,
+ meaning that the closer cur is to end,
+ the worse (higher) score this will give it
+ """
+ return -(abs(cur.x - end.x) + abs(cur.y - end.y))
+
+
+def random_heuristic(cur: Point, end: Point):
+ """
+ Returns a totally random number.
+ """
+ return random.randint(cur.x, end.x) + random.randint(cur.y, end.y)
+
+
+def reconstruct_path(path: dict, start, end):
+ backwards_path = []
+ curr = end # we know that we start at the end
+ while curr in path:
+ # add the current node to the backwards_path
+ backwards_path.append(curr)
+
+ # since path is a dictionary of node : how to get there,
+ # we get the previous node in the path by doing path[curr]
+ curr = path[curr]
+
+ # this will be the first item after we reverse the list
+ backwards_path.append(start)
+
+ return reversed(backwards_path)
+
+
+def a_star(start: Point, end: Point, heuristic):
+ min_x, max_x = min(start.x, end.x), max(start.x, end.x)
+ min_y, max_y = min(start.y, end.y), max(start.y, end.y)
+
+ # initialize f scores (final scores) to infinity for every
+ # point between the (min_x, min_y) and (max_x, max_y)
+ f_scores = {
+ Point(x, y): float("inf")
+ for x in range(min_x, max_x + 1)
+ for y in range(min_y, max_y + 1)
+ }
+
+ # initialize g scores (distance to get there) to infinity for every
+ # point between (min_x, min_y) and (max_x, max_y)
+ g_scores = {
+ Point(x, y): float("inf")
+ for x in range(min_x, max_x + 1)
+ for y in range(min_y, max_y + 1)
+ }
+ # it takes 0 steps to get to the start, so initialize that g score to 0
+ g_scores[start] = 0
+
+ # this will be how many nodes we have added
+ # because priorityqueue sorts things, this is added as a backup measure
+ # when putting items into the queue to say that, if their f scores are
+ # the same, then just explore the one that we found first
+ count = 0
+ unexplored = PriorityQueue()
+ unexplored.put((0, count, start))
+
+ # this is a dictionary that stores node: how to get there
+ # this means that path[(1, 1)] might equal (0, 0)
+ # we use this variable to help us reconstruct the path that
+ # a star found
+ path = {}
+
+ # allows us to see how many executions it really took
+ num_executions = 0
+
+ while not unexplored.empty():
+ current: Point = unexplored.get()[2] # just get the Point
+
+ # it takes 1 more step to get to any neighbor, so their g_scores will be
+ # one more than the current g score
+ temp_g_score = g_scores[current] + 1
+ for node in current.get_neighbors(start, end):
+ if node == end:
+ # the way to get to the end is from the current node
+ path[node] = current
+ print(f"finished after {num_executions} executions")
+ return reconstruct_path(path, start, end)
+ else:
+ # if either we haven't explored this node yet
+ # (meaning g_scores[node] = infinity) or this is a shorter path to
+ # get to this node, then
+ if temp_g_score < g_scores[node]:
+ # update our path that way now the shortest way to get to this node
+ # is through the current node
+ path[node] = current
+
+ # update our f and g scores
+ g_scores[node] = temp_g_score
+ f_scores[node] = temp_g_score + heuristic(node, end)
+
+ # add the node to unexplored if it wasn't already in unexplored
+ if not any(node == item[2] for item in unexplored.queue):
+ # update our count and add the unexplored node w/ its score
+ count += 1
+ unexplored.put((f_scores[node], count, node))
+ num_executions += 1
+ print(f"no solution found after {num_executions} executions")
+ return None # no path found
+
+
+# you can try changing the heuristic and seeing how that affects the path taken,
+# as well as the number of executions it took
+path = a_star(Point(0, 0), Point(15, 33), adding_heuristic)
+path_len = 0
+for i in path:
+ print(i)
+ path_len += 1
+print(f"path length was {path_len}")
diff --git a/dsa/chapter3/solutions/bfs_dfs.py b/dsa/chapter3/solutions/bfs_dfs.py
new file mode 100644
index 00000000..acd6fe8a
--- /dev/null
+++ b/dsa/chapter3/solutions/bfs_dfs.py
@@ -0,0 +1,87 @@
+class Node:
+ def __init__(self, value: int) -> None:
+ self.value: int = value
+ self.children = []
+
+ def __str__(self) -> str:
+ return str(self.value)
+
+ def __repr__(self) -> str:
+ return str(self)
+
+
+def BFS(start_node: Node):
+ """
+ Implement a breadth-first search algorithm,
+ but print the nodes that you visit as you go.
+
+ Remember, a breadth-first search algorithm works by
+ visiting all the children in a certain depth before
+ advancing to the next depth
+
+ Note that your function will be slightly different from
+ the one in the example since the nodes in this file
+ have children and not neighbors
+ """
+
+ # initialize our lists
+ visited_nodes = []
+ current_depth_nodes = [start_node]
+ next_depth_nodes = []
+
+ # iterate until there are no nodes at the current depth
+ while len(current_depth_nodes) != 0:
+ for node in current_depth_nodes:
+ if node not in visited_nodes:
+ print(node, end=" ")
+
+ # add the node to visited
+ # and add its children to the list of nodes at the next depth
+ visited_nodes.append(node)
+ next_depth_nodes.extend(node.children)
+
+ # "go to the next depth level" by setting
+ # current_depth_nodes = next_depth_nodes
+ current_depth_nodes = next_depth_nodes
+ next_depth_nodes = []
+
+
+def DFS(start_node: Node, visited: list = []):
+ if start_node not in visited:
+ print(start_node, end=" ")
+
+ visited.append(start_node)
+ for node in start_node.children:
+ DFS(node, visited)
+
+
+if __name__ == "__main__":
+ # make a graph that looks like the following
+ # / 5
+ # 2 - 6 - 10
+ # /
+ # 1 - 3 - 7 - 11 - 12
+ # \
+ # 4 - 8
+ # \ 9
+ start_node = Node(1)
+ for i in range(3):
+ start_node.children.append(Node(i + 2))
+ start_node.children[0].children.append(Node(5))
+ start_node.children[0].children.append(Node(6))
+ start_node.children[0].children[1].children.append(Node(10))
+
+ start_node.children[1].children.append(Node(7))
+ start_node.children[1].children[0].children.append(Node(11))
+ start_node.children[1].children[0].children[0].children.append(Node(12))
+
+ start_node.children[2].children.append(Node(8))
+ start_node.children[2].children.append(Node(9))
+
+ print("with BFS")
+ BFS(start_node) # 1 2 3 4 5 6 7 8 9 10 11 12
+ print()
+
+ print("with DFS")
+ DFS(start_node) # 1 2 5 6 10 3 7 11 12 4 8 9
+ print()
diff --git a/dsa/chapter3/solutions/mergesort.py b/dsa/chapter3/solutions/mergesort.py
new file mode 100644
index 00000000..93deb682
--- /dev/null
+++ b/dsa/chapter3/solutions/mergesort.py
@@ -0,0 +1,74 @@
+def mergesort(lst: list) -> list:
+ """
+ Let's implement mergesort,
+ First let's create a base case where if the list is 0 or 1 elements long,
+ return it
+ """
+ if len(lst) <= 1:
+ return lst
+
+ """
+ Now that we have handled the base case, if the list is any longer, we can
+ go into typical mergesort logic,
+
+ We need to split the list into 2 halves, so lets first find the middle
+ index value. Use // instead of / because we want an integer
+ """
+ middle_idx = len(lst) // 2
+
+ """
+ Now we can run mergesort on the first and second halves of lst. Create a
+ variable first_half which is the result of calling mergesort on the first
+ half of lst. Repeat for the second half of the list, creating the variable
+ second_half. Use list splicing for this.
+ """
+ first_half = mergesort(lst[:middle_idx]) # sort the first half
+ second_half = mergesort(lst[middle_idx:]) # sort the second half
+
+ """
+ Now we need to merge the two sorted halves. In order to do this, we will
+ implement a mergelists helper function. Return the result of mergelists
+ with first_half and second_half as the two parameters.
+ """
+ return mergelists(first_half, second_half) # merge the two sorted halves
+
+
+def mergelists(lst1: list, lst2: list) -> list:
+ idx1 = 0
+ idx2 = 0
+ ret = []
+
+ """
+ Let's create a while loop that runs for as long as idx1 or idx2 is less
+ than the len of lst1 or lst2.
+ """
+ while idx1 < len(lst1) or idx2 < len(lst2):
+ # If both lists have items, we need to compare the first item of the
+ # lst1 and lst2, and append whichever item is smaller to ret. Then, we
+ # increment the idx1 or idx2 variable respectively
+ if idx1 < len(lst1) and idx2 < len(lst2):
+ if lst1[idx1] < lst2[idx2]:
+ ret.append(lst1[idx1]) # add the item from lst1
+ idx1 += 1 # increment our idx in lst1
+ else: # lst2[idx2] <= lst1[idx1]
+ ret.append(lst2[idx2]) # add the item from lst2
+ idx2 += 1 # increment our idx in lst2
+
+ elif idx1 < len(lst1):
+ # if only lst1 has items left, append the remaining items to the
+ # end of ret, and set idx1 to len(lst1)
+ ret.extend(lst1[idx1:])
+ idx1 = len(lst1)
+ elif idx2 < len(lst2):
+ # if only lst2 has items left, append the remaining items to the
+ # end of ret, and set idx2 to len(lst2)
+ ret.extend(lst2[idx2:])
+ idx2 = len(lst2)
+
+ return ret
+
+
+if __name__ == "__main__":
+ lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+ mergesort(lst)
+ print(lst)
diff --git a/dsa/chapter3/solutions/quicksort.py b/dsa/chapter3/solutions/quicksort.py
new file mode 100644
index 00000000..45d0bd90
--- /dev/null
+++ b/dsa/chapter3/solutions/quicksort.py
@@ -0,0 +1,89 @@
+def quicksort(arr: list):
+ """
+ quicksort_recursive takes in the list you are sorting, the first index of
+ the sublist you want to sort, and the last index of the sublist you want
+ to sort, in that order
+
+ For the first call to quicksort_recursive, the first index and last index
+ should be 0 and the index to the last item of the list respectively
+
+ Make a call to quicksort_recursive with the appropriate arguments below
+ """
+ quicksort_recursive(arr, 0, len(arr) - 1)
+
+
+def quicksort_recursive(arr, low, high):
+ """
+ Arguments:
+ arr: list, the entire list we are sorting,
+ low: int, the first index of the sublist we are sorting
+ high: int, the last index of the sublist we are sorting
+ """
+ # base case
+ if low >= high:
+ return
+
+ pivot_index = partition(arr, low, high)
+
+ # recursive calls
+ """
+ After the list has been partitioned around the pivot_index, we need to
+ call quicksort_recursive on the two sublists: the one to the left of the
+ pivot_index, and the one to the right
+
+ We do this on the right side by setting the high index to one less than
+ pivot_index, and on the left side by setting the low index to one higher
+ than pivot_index
+
+ Make calls to quicksort_recursive with the appropriate arguments below
+ """
+ quicksort_recursive(arr, low, pivot_index - 1) # right side
+ quicksort_recursive(arr, pivot_index + 1, high) # left side
+
+
+def partition(arr, low, high):
+ """
+ Partition takes a pivot (in our case, arr[high]), and accomplishes the
+ following:
+ All of the elements between low and high that are SMALLER than the
+ pivot are placed to the LEFT of the pivot.
+ Conversely, all elements between low and high that are LARGER than
+ the pivot are placed to the RIGHT of the pivot
+ This has the side effect that the location of pivot after the partition has
+ taken place is the same as if the list was sorted. Of course, the areas to
+ the left and right of the pivot are not yet sorted.
+ """
+ i = low # initialize i to the left side of what we are sorting
+
+ """
+ Create a for loop that creates an index j, and loops through indexes low
+ (inclusive) to high (exclusive)
+ """
+ for j in range(low, high): # iterate through the list with arr[j]
+ """
+ In our loop, we are trying to find items (arr[j]) that are less than
+ our pivot (arr[high]). If we find one, we want to swap our item
+ (arr[j]) with arr[i], an item thats to the left side of our
+ sublist. Then, we will increment i by one, so we don't continuously
+ swap with same arr[i] over and over again.
+
+ Create an if statement to do this below
+ """
+ if arr[j] < arr[high]:
+ # swap arr[j] with arr[i] so arr[j] is at the left side
+ arr[i], arr[j] = arr[j], arr[i]
+ i += 1
+ """
+ Our pivot (arr[high]) is still on the right side of our sublist. Let's swap
+ it with arr[i] so it moves to the right spot.
+ """
+ arr[i], arr[high] = arr[high], arr[i]
+
+ # return the pivot_index
+ return i
+
+
+if __name__ == "__main__":
+ lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+ quicksort(lst)
+ print(lst)
diff --git a/dsa/chapter3/solutions/searching.py b/dsa/chapter3/solutions/searching.py
new file mode 100644
index 00000000..8b6d332f
--- /dev/null
+++ b/dsa/chapter3/solutions/searching.py
@@ -0,0 +1,121 @@
+"""
+Let's see the difference between linear and binary searches!
+Some of the algorithm is already done for you, but you
+will have to fill in some areas.
+
+Then, run the code and you can see the results
+"""
+
+import random
+from datetime import datetime as d
+
+
+def linear_search(arr, val) -> int:
+ """
+ Linear Search - iterates through all the items in the array and checks
+ equality with the provided value. If the value matches, returns
+ the index of that value. Else, returns -1
+ Arguments:
+ arr - the array to search
+ val - the value to search for
+ Returns:
+ int - index of the value on success, -1 on failure
+ """
+ for i in range(len(arr)):
+ if arr[i] == val:
+ return i
+ return -1
+
+
+def binary_search(arr, val) -> int:
+ """
+ Binary Search - checks the list for a value using a binary search.
+ Only works on sorted lists since it assumes that all the values
+ in indexes greater than i are greater and all the values in
+ indexes less than i are less.
+ Arguments:
+ arr - the array to search
+ val - the value to search for
+ Returns:
+ int - index of the value on success, -1 on failure
+ """
+ low = 0
+ high = len(arr) - 1
+ while low <= high:
+ current = (low + high) // 2
+ if val == arr[current]:
+ return current
+ elif val < arr[current]:
+ high = current - 1
+ else: # val > arr[current]
+ low = current + 1
+ return -1
+
+
+# example 1 - sorted list
+# the below demonstrates the binary search is faster than
+# linear search on sorted lists
+size = 100000
+lst_1 = [i for i in range(size)]
+
+tests = 3
+for i in range(tests):
+ print(f"sorted test #{i+1}:")
+ print("searching linearly")
+ target = random.randint(0, size)
+ linear_start = d.now()
+ linear_result = linear_search(lst_1, target)
+ linear_end = d.now()
+ print(
+ "finished searching linearly in "
+ + f"{(linear_end - linear_start).total_seconds()} seconds "
+ + f"and got the {'right' if linear_result == target else 'wrong'} result"
+ + f" ({linear_result})"
+ )
+
+ print("searching binarily")
+ binary_start = d.now()
+ binary_result = binary_search(lst_1, target)
+ binary_end = d.now()
+ print(
+ "finished searching binarily in "
+ + f"{(binary_end - binary_start).total_seconds()} seconds "
+ + f"and got the {'right' if binary_result == target else 'wrong'} result"
+ + f" ({binary_result})"
+ )
+ print()
+
+# example 2 - unsorted list
+# the below demonstrates that binary search doesn't work on unsorted
+# lists, but linear search does
+size = 100000
+lst_2 = [i for i in range(size)]
+random.shuffle(lst_2)
+
+tests = 3
+for i in range(tests):
+ print(f"unsorted test #{i+1}:")
+ print("searching linearly")
+ idx = random.randint(0, size)
+ target = lst_2[idx]
+ linear_start = d.now()
+ linear_result = linear_search(lst_2, target)
+ linear_end = d.now()
+ print(
+ "finished searching linearly in "
+ + f"{(linear_end - linear_start).total_seconds()} seconds "
+ + f"and got the {'right' if linear_result == idx else 'wrong'} result"
+ + f" ({linear_result})"
+ )
+
+ print("searching binarily")
+ binary_start = d.now()
+ binary_result = binary_search(lst_2, target)
+ binary_end = d.now()
+ print(
+ "finished searching binarily in "
+ + f"{(binary_end - binary_start).total_seconds()} seconds "
+ + f"and got the {'right' if binary_result == idx else 'wrong'} result"
+ + f" ({binary_result})"
+ )
+ print()
diff --git a/dsa/chapter3/solutions/selectionsort.py b/dsa/chapter3/solutions/selectionsort.py
new file mode 100644
index 00000000..22cb686c
--- /dev/null
+++ b/dsa/chapter3/solutions/selectionsort.py
@@ -0,0 +1,31 @@
+def selectionsort(arr: list):
+ """
+ Let's implement selection sort! There are 4 easy steps to follow in order
+ to implement it.
+ 1. Create a loop through iterate through the list.
+ 2. Create an inner loop that iterates from the outer index + 1 to the
+ end of the list.
+ 3. Compare the element at the outer index to the element at the inner
+ index.
+ 4. If the element at the outer index is larger than at the inner index,
+ swap the 2 elements.
+ """
+
+ # Step 1, create an outer loop that iterates through the whole list. Let's
+ # name the outer index "i"
+ for i in range(len(arr)):
+ # Step 2, create an inner loop that iterates from i+1 to the end of the
+ # list, let's name inner index "j"
+ for j in range(i + 1, len(arr)):
+ # Step 3, check if the element at index i is larger than the
+ # element at index j
+ if arr[i] > arr[j]:
+ # Step 4, swap the element at the outer index with the element
+ # at the inner index
+ arr[i], arr[j] = arr[j], arr[i]
+
+
+if __name__ == "__main__":
+ lst = [-3, 5, -10, 18, 74, 22, 1, -40]
+ selectionsort(lst)
+ print(lst)
diff --git a/games/chapter1/examples/guessthepassword.py b/games/chapter1/examples/guessthepassword.py
new file mode 100644
index 00000000..7961131b
--- /dev/null
+++ b/games/chapter1/examples/guessthepassword.py
@@ -0,0 +1,20 @@
+# Directions: The goal of this exercise is to create
+# a game where the user has to guess a certain password that
+# you set and see how many tries it takes for that user to guess correctly
+
+# start with assigning the password to some variable
+pas = "password"
+
+# set an input so it will appear in the console and ask the user
+guess = input("Enter the password:")
+
+# set a counter to count the number of guesses
+counter = 1
+
+# set a while loop to check if the user guess correctly and count the number of guesses
+while guess != pas:
+ guess = input("Incorrect Password. Try Again:")
+ counter += 1
+
+# print the results
+print(f"Nice Job. Unlocked. It took you {str(counter)} tries")
diff --git a/games/chapter1/examples/hangman.py b/games/chapter1/examples/hangman.py
new file mode 100644
index 00000000..380707ef
--- /dev/null
+++ b/games/chapter1/examples/hangman.py
@@ -0,0 +1,60 @@
+# Directions: Lets Play Hangman. In the code, create a function that
+# takes as a paramater the word that the user has to guess.
+# The user should have 15 'lives'.
+# Similar to the original game of hangman, if the user guesses an incorrect
+# letter, then their lives goes down. If they guess a correct letter, they
+# don't lose a life.
+
+
+score = 0
+
+
+def hangman(endword: str):
+ global score
+ wordSet = set(endword)
+ print(
+ "Welcome to Hangman! You have 15 lives to "
+ + "figure out the correct word. Good Luck!"
+ )
+
+ lives = 15
+ correctguesses = []
+
+ # mainloop
+ for i in range(15):
+ # take user input
+ guess = input(f"Guess a letter! You have {lives} lives left: ")
+
+ # win condition
+ if guess == endword:
+ print(f"Nice, the word is '{endword}'")
+ score += 1
+ break
+
+ if guess in endword:
+ correctguesses.append(guess)
+
+ # 'draw screen' phase
+ for i in range(len(endword)):
+ if endword[i] in correctguesses:
+ print(endword[i], end="")
+ else:
+ print("_ ", end="")
+ print()
+
+ if guess not in wordSet:
+ lives -= 1
+
+ # update game state
+ # game over condition
+ if lives == 0:
+ print(f"You ran out of lives. The correct word is '{endword}'")
+
+ # win condition
+ if set(correctguesses) == wordSet:
+ print(f"Nice, the word is '{endword}'")
+ score += 1
+ break
+
+
+hangman("hangman")
diff --git a/games/chapter1/practice/blackjack.py b/games/chapter1/practice/blackjack.py
new file mode 100644
index 00000000..af9644b1
--- /dev/null
+++ b/games/chapter1/practice/blackjack.py
@@ -0,0 +1,11 @@
+# Directions: The goal of blackjack is to be the first player
+# to get to 21. Each player will draw randomly and the
+# sum of the cards will add to 21. If the cards of a player go
+# over 21, that person automatically loses.
+
+# Add your imports here
+
+print("Welcome to the game of BlackJack. ")
+print("")
+
+# Add your code here
diff --git a/games/chapter1/practice/poker.py b/games/chapter1/practice/poker.py
new file mode 100644
index 00000000..931a9a9f
--- /dev/null
+++ b/games/chapter1/practice/poker.py
@@ -0,0 +1,370 @@
+from random import choice, randint
+
+# how poker actually works:
+# Every player is dealt two cards (face down)
+# The number of cards in the middle (face up) is initially 3 and
+# is increased one per round. Players decide if they want to bet on the round
+# or fold before the next card is revealed. If a player bets, then all other
+# players must 'call' (put in the same # of chips)
+# once there are 5 cards in the middle, then the players see
+# who can make the best match with their 2 cards and
+# the 5 cards in the middle the player that makes the best match wins
+
+# check the code in the area that says "--CODE AREA--"
+# THERE ARE 4 INSTRUCTIONS; if you fill them out, then the program should work.
+# Note that, in our version of poker, the game ends once any player has less
+# than 7 chips.
+
+suites = ["Clubs", "Diamonds", "Hearts", "Spades"]
+face_cards = {11: "Jack", 12: "Queen", 13: "King", 14: "Ace"}
+rankings = {
+ 0: "Royal Flush",
+ 1: "Straight Flush",
+ 2: "Four of a kind",
+ 3: "Full House",
+ 4: "Flush",
+ 5: "Straight",
+ 6: "Three of a Kind",
+ 7: "Two pairs",
+ 8: "Pair",
+ 9: "High Card",
+}
+deck = []
+
+
+# --- SUPPORTING CODE ---
+class card:
+ def __init__(self, value: int, suite: str, name: str = None):
+ self.name = name if name else str(value)
+ self.value = value
+ self.suite = suite
+
+ def __str__(self) -> str:
+ return f"A(n) {self.name} of {self.suite}"
+
+ def __eq__(self, o) -> bool:
+ return str(self) == str(o)
+
+ def __sub__(self, o) -> bool:
+ return self.value - o.value
+
+
+class hand_results:
+ """
+ A class for easy comparing of results of a hand
+ Note that a hand_result is considered "less than" another
+ hand_result if the hand_result's priority has a lower value
+ than the other hand_result's priority (meaning that
+ the first hand_result has a higher priority). Vice versa for
+ gt
+ """
+
+ def __init__(self, results: list):
+ self.results = results
+ self.priority = (
+ results.index(True) if True in results else len(results)
+ )
+
+ def __lt__(self, o) -> bool:
+ return self.priority > o.priority
+
+ def __le__(self, o) -> bool:
+ return self.priority >= o.priority
+
+ def __gt__(self, o) -> bool:
+ return self.priority < o.priority
+
+ def __ge__(self, o) -> bool:
+ return self.priority <= o.priority
+
+ def __eq__(self, o) -> bool:
+ return self.priority == o.priority
+
+
+class hand:
+ """
+ This class represents one person's hand (or the river)
+ """
+
+ def __init__(self):
+ self.cards = []
+
+ def add_card(self, card: card) -> None:
+ self.cards.append(card)
+
+ def __str__(self) -> str:
+ msg = ""
+ for card in self.cards:
+ msg += str(card) + ", "
+ return msg
+
+ def __len__(self) -> int:
+ return len(self.cards)
+
+ def union(self, o) -> None:
+ for card in o.cards:
+ self.cards.append(card)
+
+ def does_val_card_exist(
+ self, val: int, cards_not_equal_to: list = []
+ ) -> tuple:
+ for card in self.cards:
+ if card.value == val and card not in cards_not_equal_to:
+ return (True, card)
+ return (False, None)
+
+ def find_matches(
+ self, num_matches: int, cards_to_exclude: list = []
+ ) -> tuple:
+ for card in self.cards:
+ temp = []
+ for oth in cards_to_exclude:
+ temp.append(oth)
+ if card not in temp:
+ temp.append(card)
+
+ for i in range(num_matches - 1):
+ bool_val, potential_card = self.does_val_card_exist(
+ card.value, temp
+ )
+ if not bool_val:
+ break
+ temp.append(potential_card)
+ else:
+ card_matches = temp
+ for oth_card in cards_to_exclude:
+ card_matches.remove(oth_card)
+ return (True, card_matches)
+ return (False, [])
+
+ def check_straight_flush(self, card_start: card):
+ potential_card = card_start
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val or potential_card.suite != card_start.suite:
+ break
+ else: # for loop finished fine
+ return True
+ return False
+
+ def check_straight(self, card_start: card) -> bool:
+ potential_card = card_start
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val:
+ break
+ else: # for loop finished fine
+ return True
+ return False
+
+ def get_best_hand(self) -> hand_results:
+ # try to get a 5 card flush:
+ flush_possible = False
+ for card in self.cards:
+ same_suite = 0
+ for other_card in self.cards:
+ if not card == other_card and card.suite == other_card.suite:
+ same_suite += 1
+ if same_suite >= 5:
+ flush_possible = True
+
+ # try to get a 5 card straight
+ straight_possible = False
+ for card in self.cards:
+ potential_card = card
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val:
+ break
+ else: # for loop finished fine
+ straight_possible = True
+
+ # try to get a straight flush
+ straight_flush_possible = False
+ if straight_possible and flush_possible:
+ for card in self.cards:
+ if not straight_flush_possible:
+ straight_flush_possible = self.check_straight_flush(card)
+
+ # royal flush possible
+ royal_flush_possible = False
+ if self.does_val_card_exist(10)[0]:
+ royal_flush_possible = self.check_straight(
+ self.does_val_card_exist(10)[1]
+ )
+
+ # try to get a pair (2 cards of same val)
+ pair_possible = self.find_matches(2)[0]
+
+ # try to get a 3 of a kind
+ three_possible = self.find_matches(3)[0]
+
+ four_possible = self.find_matches(4)[0]
+
+ # try to get a full house
+ full_house_possible = False
+ for card in self.cards:
+ bool_val, cards = self.find_matches(3)
+ if (
+ bool_val and not full_house_possible
+ ): # was able to find 3 of a kind (2 other cards of same value)
+ # use exclude and try to find a pair
+ full_house_possible = self.find_matches(2, cards)[0]
+
+ two_pair_possible = False
+ for card in self.cards:
+ bool_val, cards = self.find_matches(2) # find a pair
+ if bool_val and not two_pair_possible:
+ two_pair_possible = self.find_matches(2, cards)[0]
+
+ return hand_results(
+ [
+ royal_flush_possible,
+ straight_flush_possible,
+ four_possible,
+ full_house_possible,
+ flush_possible,
+ straight_possible,
+ three_possible,
+ two_pair_possible,
+ pair_possible,
+ ]
+ )
+
+
+def initialize_deck():
+ global deck
+
+ deck = [
+ card(value, suite, face_cards[value])
+ if value >= 11
+ else card(value, suite)
+ for value in range(2, 15)
+ for suite in suites
+ ]
+
+
+def take_card() -> card:
+ global deck
+ c = choice(deck)
+ deck.remove(c)
+ return c
+
+
+# -- CODE AREA --
+# -- Your code will go here --
+
+dealer_chips = 20
+player_chips = 20
+
+
+def play_poker():
+ global dealer_chips, player_chips, deck
+
+ round_num = 0
+ player_inp = ""
+
+ # INSTRUCTION
+ # while both players have more than 7 chips
+ while """YOUR CONDITION HERE""":
+ initialize_deck()
+
+ # inicialize hands to randomized ones each round
+ player = hand()
+ dealer = hand()
+ river = hand()
+ for i in range(2): # two cards initially
+ dealer.add_card(take_card())
+ player.add_card(take_card())
+ # initialize the pool in the middle
+ for i in range(3):
+ river.add_card(take_card())
+
+ chips_at_stake = 0
+ winner = ""
+
+ round_num += 1
+ print(f"round number {round_num}")
+ # do one individual round
+ while len(river) < 5:
+ print(f"your hand right now is {player}")
+ print(f"the river is currently {river}")
+ # dealer bet
+ dealerbet = min(
+ randint(1, 5), dealer_chips
+ ) # that way the dealer doesn't go into negative chips
+ dealer_chips -= dealerbet
+ chips_at_stake += dealerbet
+
+ # player either calls or folds
+ print(f"dealer bet {dealerbet}")
+ player_inp = input(
+ "call (bet that much) or fold (abandon this round) or STOP? "
+ )
+
+ # INSTRUCTION
+ # handle input
+ # if the input is 'STOP', then quit the program
+ # if the input is 'call', then the player bets the same number
+ # of chips that the dealer bet (player chips will decrease
+ # and chips_at_stake will increase)
+ # lastly, if the input is 'fold', then set winner to True
+ # and break out of the round (use the break keyword)
+ if player_inp == "STOP":
+ pass
+ if player_inp == "call":
+ pass
+ if player_inp == "fold":
+ pass
+
+ # update the river
+ river.add_card(take_card())
+
+ print(f"currently, dealer has {dealer_chips} chips")
+ print(f"currently, you have {player_chips} chips")
+
+ print()
+
+ print(f"The river ended up as {river}")
+ print()
+ # no winner yet (meaning the round ended normally)
+ if winner == "":
+ # compare hands
+ dealer.union(river)
+ player.union(river)
+
+ dealer_result = dealer.get_best_hand()
+ player_result = player.get_best_hand()
+ print(
+ "It was your",
+ rankings[player_result.priority],
+ "vs the dealer's",
+ rankings[dealer_result.priority],
+ )
+
+ # INSTRUCTION
+ # if player_result is greater than or equal to
+ # dealer_result, then the player won that round
+ # if not, then the dealer won that round.
+ # make sure to update the variable winner
+
+ # INSTRUCTION
+ # if the dealer won, then
+ # print "The dealer won that round"
+ # The dealer then gets the chips that were in chips_at_stake
+ # if you won, then
+ # print "You won that round"
+ # the player gets the chips that were in chips_at_stake
+ # chips_at_stake will be 0 again no matter what
+ # Also, make sure to
+ # print how many chips each player has
+ print() # used to make it look prettier since adds extra line
+
+
+play_poker()
diff --git a/games/chapter1/solution/blackjack.py b/games/chapter1/solution/blackjack.py
new file mode 100644
index 00000000..8cf43ec2
--- /dev/null
+++ b/games/chapter1/solution/blackjack.py
@@ -0,0 +1,86 @@
+# Directions: The goal of blackjack is to be the first player
+# to get to 21. Each player will draw randomly and the
+# sum of the cards will add to 21. If the cards of a player go
+# over 21, that person automatically loses.
+
+import random
+
+
+print("Welcome to the game of BlackJack. ")
+print("")
+
+# Create the lists of the two players
+# the dealer is the console and the player is the user
+dealerList = []
+userList = []
+
+# append two random cards to start the game
+for i in range(2):
+ dealerList.append(random.randint(2, 11))
+ userList.append(random.randint(2, 11))
+
+# print the first two cards
+print("Here is the dealer's cards:" + str(dealerList))
+print("Here is the user's cards:" + str(userList))
+if sum(userList) == 21:
+ print("User Won")
+ exit()
+if sum(dealerList) == 21:
+ print("Dealer won")
+ exit()
+
+# ask hit or stay... write a functionn for hit and stay...
+# conditional for the typed in key hit means to take another
+# card and stay means to play with already drawn cards
+
+# print the instructions
+ask = input("Type in H to hit and S to stay:")
+
+
+# write a function for hit to use in multiple scenarios
+def hit(cards):
+ cards.append(random.randint(2, 11))
+
+
+# while loop will keep checking the two players' cards to see if they reached 21 or not
+while ask == "H" or ask == "h":
+ hit(userList)
+ print("Here is your hand:" + str(userList))
+
+ if sum(userList) > 21:
+ print("User is Busted")
+ exit()
+ if sum(userList) == 21:
+ print("User Won")
+ exit()
+ ask = input("Type in H to hit and S to stay:")
+
+# if statement for "stay"
+if ask == "S" or ask == "s":
+ print("Here is your hand:" + str(userList))
+
+
+# when hit- append a anotehr random number into the list of the dealer/userList
+# when stay- just go to next play and print out the list
+# while dealer less than 17 append new cards to the list
+
+while sum(dealerList) < 17:
+ hit(dealerList)
+ print("Here is dealer's hand:" + str(dealerList))
+
+# compare cards for win
+if sum(dealerList) > 21:
+ print("Dealer is Busted")
+
+ exit()
+if sum(dealerList) == 21 and sum(userList) == 21:
+ print("It is a tie")
+ exit()
+if sum(dealerList) == 21:
+ print("Dealer Won")
+ exit()
+
+if 21 - sum(dealerList) > 21 - sum(userList):
+ print("User is closer")
+if 21 - sum(dealerList) < 21 - sum(userList):
+ print("Dealer is closer")
diff --git a/games/chapter1/solution/poker.py b/games/chapter1/solution/poker.py
new file mode 100644
index 00000000..21ea1f02
--- /dev/null
+++ b/games/chapter1/solution/poker.py
@@ -0,0 +1,362 @@
+from random import choice, randint
+
+# how poker actually works:
+# Every player is dealt two cards (face down)
+# The number of cards in the middle (face up) is initially 3 and
+# is increased one per round. Players decide if they want to bet on the round
+# or fold before the next card is revealed. If a player bets, then all other
+# players must 'call' (put in the same # of chips)
+# once there are 5 cards in the middle, then the players see
+# who can make the best match with their 2 cards and
+# the 5 cards in the middle the player that makes the best match wins
+
+# check the code in the area that says "--CODE AREA--"
+suites = ["Clubs", "Diamonds", "Hearts", "Spades"]
+face_cards = {11: "Jack", 12: "Queen", 13: "King", 14: "Ace"}
+rankings = {
+ 0: "Royal Flush",
+ 1: "Straight Flush",
+ 2: "Four of a kind",
+ 3: "Full House",
+ 4: "Flush",
+ 5: "Straight",
+ 6: "Three of a Kind",
+ 7: "Two pairs",
+ 8: "Pair",
+ 9: "High Card",
+}
+deck = []
+
+
+# --- SUPPORTING CODE ---
+class card:
+ def __init__(self, value: int, suite: str, name: str = None):
+ self.name = name if name else str(value)
+ self.value = value
+ self.suite = suite
+
+ def __str__(self) -> str:
+ return f"A(n) {self.name} of {self.suite}"
+
+ def __eq__(self, o) -> bool:
+ return str(self) == str(o)
+
+ def __sub__(self, o) -> bool:
+ return self.value - o.value
+
+
+class hand_results:
+ """
+ A class for easy comparing of results of a hand
+ Note that a hand_result is considered "less than" another
+ hand_result if the hand_result's priority has a lower value
+ than the other hand_result's priority (meaning that
+ the first hand_result has a higher priority). Vice versa for
+ gt
+ """
+
+ def __init__(self, results: list):
+ self.results = results
+ self.priority = (
+ results.index(True) if True in results else len(results)
+ )
+
+ def __lt__(self, o) -> bool:
+ return self.priority > o.priority
+
+ def __le__(self, o) -> bool:
+ return self.priority >= o.priority
+
+ def __gt__(self, o) -> bool:
+ return self.priority < o.priority
+
+ def __ge__(self, o) -> bool:
+ return self.priority <= o.priority
+
+ def __eq__(self, o) -> bool:
+ return self.priority == o.priority
+
+
+class hand:
+ """
+ This class represents one person's hand (or the river)
+ """
+
+ def __init__(self):
+ self.cards = []
+
+ def add_card(self, card: card) -> None:
+ self.cards.append(card)
+
+ def __str__(self) -> str:
+ msg = ""
+ for card in self.cards:
+ msg += str(card) + ", "
+ return msg
+
+ def __len__(self) -> int:
+ return len(self.cards)
+
+ def union(self, o) -> None:
+ for card in o.cards:
+ self.cards.append(card)
+
+ def does_val_card_exist(
+ self, val: int, cards_not_equal_to: list = []
+ ) -> tuple:
+ for card in self.cards:
+ if card.value == val and card not in cards_not_equal_to:
+ return (True, card)
+ return (False, None)
+
+ def find_matches(
+ self, num_matches: int, cards_to_exclude: list = []
+ ) -> tuple:
+ for card in self.cards:
+ temp = []
+ for oth in cards_to_exclude:
+ temp.append(oth)
+ if card not in temp:
+ temp.append(card)
+
+ for i in range(num_matches - 1):
+ bool_val, potential_card = self.does_val_card_exist(
+ card.value, temp
+ )
+ if not bool_val:
+ break
+ temp.append(potential_card)
+ else:
+ card_matches = temp
+ for oth_card in cards_to_exclude:
+ card_matches.remove(oth_card)
+ return (True, card_matches)
+ return (False, [])
+
+ def check_straight_flush(self, card_start: card):
+ potential_card = card_start
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val or potential_card.suite != card_start.suite:
+ break
+ else: # for loop finished fine
+ return True
+ return False
+
+ def check_straight(self, card_start: card) -> bool:
+ potential_card = card_start
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val:
+ break
+ else: # for loop finished fine
+ return True
+ return False
+
+ def get_best_hand(self) -> hand_results:
+ # try to get a 5 card flush:
+ flush_possible = False
+ for card in self.cards:
+ same_suite = 0
+ for other_card in self.cards:
+ if not card == other_card and card.suite == other_card.suite:
+ same_suite += 1
+ if same_suite >= 5:
+ flush_possible = True
+
+ # try to get a 5 card straight
+ straight_possible = False
+ for card in self.cards:
+ potential_card = card
+ for i in range(4): # there need to be 4 cards higher than it
+ bool_val, potential_card = self.does_val_card_exist(
+ potential_card.value + 1
+ )
+ if not bool_val:
+ break
+ else: # for loop finished fine
+ straight_possible = True
+
+ # try to get a straight flush
+ straight_flush_possible = False
+ if straight_possible and flush_possible:
+ for card in self.cards:
+ if not straight_flush_possible:
+ straight_flush_possible = self.check_straight_flush(card)
+
+ # royal flush possible
+ royal_flush_possible = False
+ if self.does_val_card_exist(10)[0]:
+ royal_flush_possible = self.check_straight(
+ self.does_val_card_exist(10)[1]
+ )
+
+ # try to get a pair (2 cards of same val)
+ pair_possible = self.find_matches(2)[0]
+
+ # try to get a 3 of a kind
+ three_possible = self.find_matches(3)[0]
+
+ four_possible = self.find_matches(4)[0]
+
+ # try to get a full house
+ full_house_possible = False
+ for card in self.cards:
+ bool_val, cards = self.find_matches(3)
+ if (
+ bool_val and not full_house_possible
+ ): # was able to find 3 of a kind (2 other cards of same value)
+ # use exclude and try to find a pair
+ full_house_possible = self.find_matches(2, cards)[0]
+
+ two_pair_possible = False
+ for card in self.cards:
+ bool_val, cards = self.find_matches(2) # find a pair
+ if bool_val and not two_pair_possible:
+ two_pair_possible = self.find_matches(2, cards)[0]
+
+ return hand_results(
+ [
+ royal_flush_possible,
+ straight_flush_possible,
+ four_possible,
+ full_house_possible,
+ flush_possible,
+ straight_possible,
+ three_possible,
+ two_pair_possible,
+ pair_possible,
+ ]
+ )
+
+
+def initialize_deck():
+ global deck
+
+ deck = [
+ card(value, suite, face_cards[value])
+ if value >= 11
+ else card(value, suite)
+ for value in range(2, 15)
+ for suite in suites
+ ]
+
+
+def take_card() -> card:
+ global deck
+ c = choice(deck)
+ deck.remove(c)
+ return c
+
+
+# -- CODE AREA --
+# -- Your code will go here --
+
+# initialize two variables
+# one will be the dealer's chips, the other will be the player's chips
+dealer_chips = 20
+player_chips = 20
+
+
+def play_poker():
+ global dealer_chips, player_chips, deck
+
+ round_num = 0
+ player_inp = ""
+ while player_inp != "STOP" and (dealer_chips > 7 and player_chips > 7):
+ initialize_deck()
+
+ # inicialize hands to randomized ones each round
+ player = hand()
+ dealer = hand()
+ river = hand()
+ for i in range(2): # two cards initially
+ dealer.add_card(take_card())
+ player.add_card(take_card())
+ # initialize the pool in the middle
+ for i in range(3):
+ river.add_card(take_card())
+
+ chips_at_stake = 0
+ winner = ""
+
+ round_num += 1
+ print(f"round number {round_num}")
+ # do one individual round
+ while len(river) < 5:
+ print(f"your hand right now is {player}")
+ print(f"the river is currently {river}")
+ # dealer bet
+ dealerbet = min(
+ randint(1, 5), dealer_chips
+ ) # that way the dealer doesn't go into negative chips
+ dealer_chips -= dealerbet
+ chips_at_stake += dealerbet
+
+ # player either calls or folds
+ print(f"dealer bet {dealerbet}")
+ player_inp = input(
+ "call (bet that much) or fold (abandon this round) or STOP? "
+ )
+
+ # handle input
+ if player_inp == "STOP":
+ return # just get out of the function
+ if player_inp == "call":
+ chips_at_stake += dealerbet
+ player_chips -= dealerbet
+ # if betting dealerbet chips would put them in debt
+ if player_chips < 0:
+ print("Sorry, you lose")
+ return
+ if player_inp == "fold":
+ winner = "dealer"
+ break
+
+ # update the river
+ river.add_card(take_card())
+
+ print(f"currently, dealer has {dealer_chips} chips")
+ print(f"currently, you have {player_chips} chips")
+
+ print()
+
+ print(f"The river ended up as {river}")
+ print()
+ # no winner yet
+ if winner == "":
+ # compare hands
+ dealer.union(river)
+ player.union(river)
+
+ dealer_result = dealer.get_best_hand()
+ player_result = player.get_best_hand()
+ print(
+ "It was your",
+ rankings[player_result.priority],
+ "vs the dealer's",
+ rankings[dealer_result.priority],
+ )
+ if player_result >= dealer_result:
+ winner = "player"
+ else:
+ winner = "dealer"
+ if winner == "dealer":
+ print("dealer won that round")
+ dealer_chips += chips_at_stake
+ chips_at_stake = 0
+ else: # winner == "player"
+ print("you won that round")
+ player_chips += chips_at_stake
+ chips_at_stake = 0
+
+ print(f"currently, dealer has {dealer_chips} chips")
+ print(f"currently, you have {player_chips} chips")
+ print()
+
+
+play_poker()
diff --git a/games/chapter2/examples/basic_window.py b/games/chapter2/examples/basic_window.py
new file mode 100644
index 00000000..f2cb1116
--- /dev/null
+++ b/games/chapter2/examples/basic_window.py
@@ -0,0 +1,25 @@
+import pygame # imports the module
+
+# RESIZABLE is only needed if you want a resizable window
+from pygame.locals import RESIZABLE
+
+# initializes imported pygame modules
+pygame.init()
+
+# creates resizable pygame window that is 500 pixels wide and 400 high
+# sets the caption of the window to "My first pygame app!"
+flag = RESIZABLE
+window = pygame.display.set_mode((500, 400), flag)
+pygame.display.set_caption("My first pygame app!")
+
+# this is where the game loop begins
+run = True
+while run:
+ for event in pygame.event.get():
+ # checks if the close button is pressed
+ # if so, exit the game loop
+ if event.type == pygame.QUIT:
+ run = False
+
+# deactivates pygame modules, opposite of pygame.init()
+pygame.quit()
diff --git a/games/chapter2/examples/comprehensive_example.py b/games/chapter2/examples/comprehensive_example.py
new file mode 100644
index 00000000..ed1942df
--- /dev/null
+++ b/games/chapter2/examples/comprehensive_example.py
@@ -0,0 +1,37 @@
+import pygame
+
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+x = y = 0
+width = 100
+height = 50
+
+# initializes imported pygame modules
+pygame.init()
+
+# creates pygame window that is 500 pixels wide and 400 high
+# sets the caption of the window to "My first pygame app!"
+window = pygame.display.set_mode((500, 400))
+pygame.display.set_caption("My first pygame app!")
+
+# this is where the game loop begins
+run = True
+while run:
+ # change the coordinates
+ x, y = x + 1, y + 1
+
+ # draw a black screen over the previous frame
+ window.fill(BLACK)
+
+ # draw a new rectangle and update the screen
+ pygame.draw.rect(window, RED, (x, y, width, height))
+ pygame.display.update()
+
+ for event in pygame.event.get():
+ # checks if the close button is pressed
+ # if so, exit the game loop
+ if event.type == pygame.QUIT:
+ run = False
+
+# deactivate pygame modules, opposite of pygame.init()
+pygame.quit()
diff --git a/games/chapter2/examples/draw_objects.py b/games/chapter2/examples/draw_objects.py
new file mode 100644
index 00000000..fdc45209
--- /dev/null
+++ b/games/chapter2/examples/draw_objects.py
@@ -0,0 +1,54 @@
+import pygame
+
+pygame.init()
+
+window = pygame.display.set_mode((800, 800))
+pygame.display.set_caption("Drawing and Moving Objects")
+
+BLACK = (0, 0, 0) # background color
+RED = (255, 0, 0)
+GREEN = (0, 255, 0)
+
+# make a rectangle without the pygame.Rect class
+x = 100 # top-left x value
+y = 400 # top-left y value
+width = 100 # width of the rectangle
+height = 50 # height of the rectangle
+
+# make a pygame.Rect rectangle
+# the syntax is `myvar = pygame.Rect(top-left x, top-left y, width, height)`
+# with 0 as top-left x value, 100 as top-left y value,
+# width = 50, height = 100
+green_rectangle = pygame.Rect(0, 100, 50, 100)
+
+run = True
+while run:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ run = False
+
+ # move a rectangle that isn't a pygame.Rect object
+ x += 1 # move to the right 1 px
+ y += 1 # move down 1 px
+
+ # move a rectangle that is a pygame.Rect object
+ green_rectangle.move_ip(1, 2) # moves 1 to the right, 2 down
+ # this is equivalent to green_rectangle = green_rectangle.move(1, 2)
+
+ # erase the previous frame
+ window.fill(BLACK)
+
+ # draw a rectangle that isn't a pygame.Rect object
+ pygame.draw.rect(window, RED, (x, y, width, height))
+
+ # draw a rectangle that is a pygame.Rect object
+ pygame.draw.rect(window, GREEN, green_rectangle)
+
+ # update the screen
+ pygame.display.update()
+
+ # sometimes you need to limit frame rate or your objects
+ # will seem to move too fast
+ pygame.time.wait(30) # wait 30 milliseconds between frame
+
+pygame.quit() # close pygame after finishing
diff --git a/games/chapter2/examples/text.py b/games/chapter2/examples/text.py
new file mode 100644
index 00000000..9e19a3c7
--- /dev/null
+++ b/games/chapter2/examples/text.py
@@ -0,0 +1,40 @@
+import pygame
+from pygame.locals import RESIZABLE
+
+pygame.init()
+flag = RESIZABLE
+window = pygame.display.set_mode((500, 400), flag)
+pygame.display.set_caption("Text!")
+WHITE = (255, 255, 255)
+
+run = True
+while run:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ run = False
+
+ # step 1 to writing: load the font with pygame.font.SysFont(font, size)
+ # For font - You can either use the default font
+ # (pygame.font.get_default_font()) or use a font name
+ # (like Comic Sans or Arial).
+ # For size - a positive integer representing the font size.
+ font = pygame.font.SysFont("Arial", 32)
+
+ # step 2 - render the font with
+ # (font variable name).render(
+ # text: string, antialias: bool, color: tuple, background=None
+ # )
+ # In this case, we render the text "Hello World!", pass True as antiaalias
+ # and have the color of the text be WHITE
+ text = font.render("Hello World!", True, WHITE)
+
+ # step 3 - blit to the screen
+ # You can either blit the text to a rectangle on the screen or a specified
+ # coordinate
+ # In this case, we blit (draw) the text with a top-left value of (0, 0)
+ window.blit(text, (100, 100))
+
+ # update the screen; just like with moving/displaying rectangles
+ pygame.display.update()
+
+pygame.quit()
diff --git a/games/chapter2/practice/add_text.py b/games/chapter2/practice/add_text.py
new file mode 100644
index 00000000..d6b1f142
--- /dev/null
+++ b/games/chapter2/practice/add_text.py
@@ -0,0 +1,54 @@
+# Add some text to your game
+
+# This problem builds off of bouncingrect.py
+
+# Add some text to the screen. You can either:
+# - draw the text to a specified coordinate OR
+# - blit the text onto the bouncing rectangle.
+
+import pygame
+import time # not necessary, but used for frame cap
+
+pygame.init() # initialize pygame module
+
+SCREEN_SIZE = (600, 400)
+RECT_SIZE = (100, 100)
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+momentum = [1, 1] # (down and right)
+
+window = pygame.display.set_mode(SCREEN_SIZE)
+running = True
+
+# start the rectangle in the middle of the screen
+x = SCREEN_SIZE[0] // 2
+y = SCREEN_SIZE[1] // 2
+
+while running:
+ # if the rectangle collided with the left or right side
+ # of the screen
+ if x + RECT_SIZE[0] >= SCREEN_SIZE[0] or x <= 0:
+ momentum[0] = -momentum[0]
+ # if the rectangle collided with the top or bottom
+ # of the screen
+ if y + RECT_SIZE[1] >= SCREEN_SIZE[1] or y <= 0:
+ momentum[1] = -momentum[1]
+
+ # add the speed to the current x and y to get the
+ # new x and y
+ x += momentum[0]
+ y += momentum[1]
+
+ window.fill(BLACK) # 'erase' the previous frame
+ pygame.draw.rect(window, RED, (x, y, RECT_SIZE[0], RECT_SIZE[1]))
+
+ # Your code here.
+
+ pygame.display.update() # update the display
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ running = False
+ time.sleep(0.01) # frame cap to make the rectangle more visible
+
+pygame.quit() # deactivate the pygame module
diff --git a/games/chapter2/practice/bouncing_rect.py b/games/chapter2/practice/bouncing_rect.py
new file mode 100644
index 00000000..e14e4f4d
--- /dev/null
+++ b/games/chapter2/practice/bouncing_rect.py
@@ -0,0 +1,25 @@
+# Make a “bouncing rectangle!”
+
+# For this, please use the given screen and rectangle
+# width and height.
+
+# The rectangle should start in (or close to) the middle of the
+# screen. It should be moving down and right. If it collides
+# with the screen’s lower or upper boundary, it should reverse
+# its vertical direction. If it collides with the screen’s left
+# or right boundary, it should reverse its horizontal direction.
+
+# Note: you can import the time module as well and use
+# time.sleep(0.01)
+# in your mainloop to act as a frame cap to make your rectangle
+# more visible
+
+# put imports here
+
+SCREEN_SIZE = (600, 400)
+RECT_SIZE = (100, 100)
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+momentum = [1, 1] # (down and right)
+
+# add code here
diff --git a/games/chapter2/practice/moving_text.py b/games/chapter2/practice/moving_text.py
new file mode 100644
index 00000000..27a67359
--- /dev/null
+++ b/games/chapter2/practice/moving_text.py
@@ -0,0 +1,23 @@
+# move some text!
+
+# The text should start in the upper corner and be moving
+# down and to the right. You can move the text using coordinates
+# or blitz the text on to a moving rectangle. Feel free to be
+# creative with colors, fonts, and font sizes. However, if
+# applicable, make the rectangle proportional to the text, and
+# everything smaller than the screen
+
+
+# Note: you can import a time module in your loop
+# to make it more clear
+
+
+# imports!
+
+SCREEN_SIZE = (800, 800)
+BLACK = (0, 0, 0)
+BLUE = (0, 0, 255)
+WHITE = (255, 255, 255)
+momentum = (2, 2) # down and right
+
+# add code here
diff --git a/games/chapter2/practice/reset_position.py b/games/chapter2/practice/reset_position.py
new file mode 100644
index 00000000..8599a1af
--- /dev/null
+++ b/games/chapter2/practice/reset_position.py
@@ -0,0 +1,27 @@
+# Reset the moving rectangle's position if it leaves the screen!
+# The rectangle that will be moving is already provided
+# it is `red_rectangle`. Your job is to move it across the screen
+# at a speed of 5px down and 5px right per frame. Then, if the
+# bottom of the rectangle is greater than the screen height or the
+# right of the rectangle is greater than the screen width, reset
+# the rectangle's x and y to 0 and 0.
+
+import pygame
+
+pygame.init()
+
+SCREEN_HEIGHT = 600
+SCREEN_WIDTH = 600
+
+window = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
+pygame.display.set_caption("Reset Position")
+
+# color constants
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+
+# makes a pygame.Rect rectangle
+# the syntax is `myvar = pygame.Rect(top-left x, top-left y, width, height)`
+red_rectangle = pygame.Rect(0, 0, 100, 100)
+
+# add code here
diff --git a/games/chapter2/solutions/add_text.py b/games/chapter2/solutions/add_text.py
new file mode 100644
index 00000000..38838645
--- /dev/null
+++ b/games/chapter2/solutions/add_text.py
@@ -0,0 +1,61 @@
+# Add some text to your game
+
+# This problem builds off of bouncingrect.py
+
+# Add some text to the screen. You can either:
+# - draw the text to a specified coordinate OR
+# - blit the text onto the bouncing rectangle.
+
+import pygame
+import time # not necessary, but used for frame cap
+
+pygame.init() # initialize pygame module
+
+SCREEN_SIZE = (600, 400)
+RECT_SIZE = (100, 100)
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+WHITE = (255, 255, 255)
+momentum = [1, 1] # (down and right)
+
+window = pygame.display.set_mode(SCREEN_SIZE)
+running = True
+
+# start the rectangle in the middle of the screen
+x = SCREEN_SIZE[0] // 2
+y = SCREEN_SIZE[1] // 2
+
+while running:
+ # if the rectangle collided with the left or right side
+ # of the screen
+ if x + RECT_SIZE[0] >= SCREEN_SIZE[0] or x <= 0:
+ momentum[0] = -momentum[0]
+ # if the rectangle collided with the top or bottom
+ # of the screen
+ if y + RECT_SIZE[1] >= SCREEN_SIZE[1] or y <= 0:
+ momentum[1] = -momentum[1]
+
+ # add the speed to the current x and y to get the
+ # new x and y
+ x += momentum[0]
+ y += momentum[1]
+
+ window.fill(BLACK) # 'erase' the previous frame
+ pygame.draw.rect(window, RED, (x, y, RECT_SIZE[0], RECT_SIZE[1]))
+
+ font = pygame.font.SysFont("Calibri", 16)
+
+ bouncetext = font.render("This Bounces!", True, WHITE)
+ stationarytext = font.render("This doesn't bounce", True, WHITE)
+
+ window.blit(bouncetext, (x, y, RECT_SIZE[0], RECT_SIZE[1]))
+ window.blit(stationarytext, (100, 100))
+
+ pygame.display.update() # update the display
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ running = False
+ time.sleep(0.01) # frame cap to make the rectangle more visible
+
+pygame.quit() # deactivate the pygame module
diff --git a/games/chapter2/solutions/bouncing_rect.py b/games/chapter2/solutions/bouncing_rect.py
new file mode 100644
index 00000000..36a9d81b
--- /dev/null
+++ b/games/chapter2/solutions/bouncing_rect.py
@@ -0,0 +1,59 @@
+# Make a “bouncing rectangle!”
+
+# For this, please use the given screen and rectangle
+# width and height.
+
+# The rectangle should start in (or close to) the middle of the
+# screen. It should be moving down and right. If it collides
+# with the screen’s lower or upper boundary, it should reverse
+# its vertical direction. If it collides with the screen’s left
+# or right boundary, it should reverse its horizontal direction.
+
+# Note: you can import the time module as well and use
+# time.sleep(0.01)
+# in your mainloop to act as a frame cap to make your rectangle
+# more visible
+
+import pygame
+import time # not necessary, but used for frame cap
+
+pygame.init() # initialize pygame module
+
+SCREEN_SIZE = (600, 400)
+RECT_SIZE = (100, 100)
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+momentum = [1, 1] # (down and right)
+
+window = pygame.display.set_mode(SCREEN_SIZE)
+running = True
+
+# start the rectangle in the middle of the screen
+x = SCREEN_SIZE[0] // 2
+y = SCREEN_SIZE[1] // 2
+
+while running:
+ # if the rectangle collided with the left or right side
+ # of the screen
+ if x + RECT_SIZE[0] >= SCREEN_SIZE[0] or x <= 0:
+ momentum[0] = -momentum[0]
+ # if the rectangle collided with the top or bottom
+ # of the screen
+ if y + RECT_SIZE[1] >= SCREEN_SIZE[1] or y <= 0:
+ momentum[1] = -momentum[1]
+
+ # add the speed to the current x and y to get the
+ # new x and y
+ x += momentum[0]
+ y += momentum[1]
+
+ window.fill(BLACK) # 'erase' the previous frame
+ pygame.draw.rect(window, RED, (x, y, RECT_SIZE[0], RECT_SIZE[1]))
+ pygame.display.update() # update the display
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ running = False
+ time.sleep(0.01) # frame cap to make the rectangle more visible
+
+pygame.quit() # deactivate the pygame module
diff --git a/games/chapter2/solutions/moving_text.py b/games/chapter2/solutions/moving_text.py
new file mode 100644
index 00000000..4c92e455
--- /dev/null
+++ b/games/chapter2/solutions/moving_text.py
@@ -0,0 +1,58 @@
+import pygame
+
+pygame.init() # initializes pygame module
+
+SCREEN_SIZE = (800, 800)
+BLACK = (0, 0, 0) # background color
+BLUE = (0, 0, 255) # color of font
+WHITE = (255, 255, 255) # color of rectangle
+momentum = (2, 2)
+
+window = pygame.display.set_mode(SCREEN_SIZE)
+pygame.display.set_caption("Moving-Text")
+
+
+# make a pygame.Rect rectangle
+white_rectangle = pygame.Rect(0, 100, 130, 40)
+
+# move the text with coordinates instead of the rectangle
+x = 0 # x-coordinate of the top-left pixel of text
+y = 400 # y-coordinate of the top-left pixel of text
+
+# sets a font and font size
+font = pygame.font.SysFont("Times New Roman", 40)
+
+# create a piece of text
+text = font.render("HELLO", False, BLUE)
+
+run = True
+while run:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ run = False
+
+ # moves a pygame.Rect rectangle relative to its position
+ white_rectangle.move_ip(momentum) # moves 2 to the right, 2 down
+
+ # move the text by coordinates
+ x += 1 # move right by one pixel
+ y += 1 # move down by one pixel
+
+ # erase the previous frame
+ window.fill(BLACK)
+
+ # draw a rectangle that is a pygame.Rect object
+ pygame.draw.rect(window, WHITE, white_rectangle)
+
+ # draws text onto the rectangle
+ window.blit(text, white_rectangle)
+ # you can also use coordinates in the form of a tuple
+ # the coordinates would place the top left pixel
+ # Syntax: window.blit(text, (x,y))
+
+ # update the screen
+ pygame.display.update()
+
+ pygame.time.wait(30) # adds a 30 millisecond delay
+
+pygame.quit() # close pygame after finishing
diff --git a/games/chapter2/solutions/reset_position.py b/games/chapter2/solutions/reset_position.py
new file mode 100644
index 00000000..fe5cbc5f
--- /dev/null
+++ b/games/chapter2/solutions/reset_position.py
@@ -0,0 +1,55 @@
+# Reset the moving rectangle's position if it leaves the screen!
+# The rectangle that will be moving is already provided
+# it is `red_rectangle`. Your job is to move it across the screen
+# at a speed of 5px down and 5px right per frame. Then, if the
+# bottom of the rectangle is greater than the screen height or the
+# right of the rectangle is greater than the screen width, reset
+# the rectangle's x and y to 0 and 0.
+
+import pygame
+
+pygame.init()
+
+SCREEN_HEIGHT = 600
+SCREEN_WIDTH = 600
+
+window = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
+pygame.display.set_caption("Reset Position")
+
+# color constants
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+
+# makes a pygame.Rect rectangle
+# the syntax is `myvar = pygame.Rect(top-left x, top-left y, width, height)`
+red_rectangle = pygame.Rect(0, 0, 100, 100)
+
+run = True
+while run:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ run = False
+
+ # move the rectangle 5 units right and 5 units down each frame
+ red_rectangle.move_ip(5, 5)
+
+ # erase the previous frame
+ window.fill(BLACK)
+
+ # reset the rectangle if its right is past the screen width or
+ # its bottom is below the screen height
+ if (
+ red_rectangle.right > SCREEN_WIDTH
+ or red_rectangle.bottom > SCREEN_HEIGHT
+ ):
+ red_rectangle.x, red_rectangle.y = 0, 0
+
+ # draw the rectangle in red
+ pygame.draw.rect(window, RED, red_rectangle)
+
+ # update the screen
+ pygame.display.update()
+
+ pygame.time.wait(50) # wait 50 milliseconds between frame
+
+pygame.quit()
diff --git a/games/chapter3/examples/comprehensive_example.py b/games/chapter3/examples/comprehensive_example.py
new file mode 100644
index 00000000..d978eff4
--- /dev/null
+++ b/games/chapter3/examples/comprehensive_example.py
@@ -0,0 +1,23 @@
+import pygame
+
+pygame.init()
+
+flag = pygame.locals.RESIZABLE
+window = pygame.display.set_mode((500, 400), flag)
+
+pygame.event.set_blocked(pygame.KEYDOWN)
+
+run = True
+while run:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ # if event is QUIT
+ run = False
+ if event.type == pygame.KEYUP:
+ # if event is KEYUP
+ print("Up up up!")
+ if event.type == pygame.KEYDOWN:
+ # will never happen because KEYDOWN is blocked
+ print("Down down down!")
+
+pygame.quit()
diff --git a/games/chapter3/practice/MoveRectProblem.py b/games/chapter3/practice/MoveRectProblem.py
new file mode 100644
index 00000000..22d180ad
--- /dev/null
+++ b/games/chapter3/practice/MoveRectProblem.py
@@ -0,0 +1,11 @@
+# Build off of your previous code (the code from QuitPygameProblem.py)
+# Draw the provided rectangle onto the screen.
+# Move the object up when either the W or up arrow key is pressed;
+# right when either the D or right arrow is pressed; etc.
+
+# rect[0] = rect color
+# rect[1] = x-coord
+# rect[2] = y-coord
+# rect[3] = width
+# rect[4] = height
+rectangle = [(255, 0, 0), 20, 20, 20, 20]
diff --git a/games/chapter3/practice/QuitPygameProblem.py b/games/chapter3/practice/QuitPygameProblem.py
new file mode 100644
index 00000000..f18c99c8
--- /dev/null
+++ b/games/chapter3/practice/QuitPygameProblem.py
@@ -0,0 +1,8 @@
+# Create a pygame window.
+# Close the pygame window when either the quit event occurs
+# or the escape key is pressed.
+# Use the provided width and height. Fill the screen with white
+
+width = 500
+height = 500
+white = (255, 255, 255)
diff --git a/games/chapter3/practice/SpaceCounter.py b/games/chapter3/practice/SpaceCounter.py
new file mode 100644
index 00000000..da33176d
--- /dev/null
+++ b/games/chapter3/practice/SpaceCounter.py
@@ -0,0 +1,7 @@
+# Create a program that increments a counter every time the space bar is
+# pressed. This counter should be displayed as text on the pygame window.
+
+import pygame
+
+pygame.init()
+screen = pygame.display.set_mode((400, 400))
diff --git a/games/chapter3/practice/ticking_counter.py b/games/chapter3/practice/ticking_counter.py
new file mode 100644
index 00000000..4befdd84
--- /dev/null
+++ b/games/chapter3/practice/ticking_counter.py
@@ -0,0 +1,22 @@
+# make a time bomb!
+
+# Create a counter that starts at a number, such
+# 10 and goes down everytime the user presses
+# the keyboard. However, this is a time bomb!
+# create some text to warn the user, and when the
+# number gets low, switch the message. Then, when
+# the number hits zero, switch the message again
+# to show that they've blown up, and exit the program.
+# Wait a little before exiting so that the last message
+# is readable.
+# Make sure to use some of the methods featured in 3.4!
+
+import pygame # add more imports if needed
+
+screen = pygame.display.set_mode((400, 400))
+
+# feel free to change these values
+fonts = pygame.font.SysFont("arial", 20)
+font = pygame.font.SysFont("arial", 70)
+text = "DON'T PRESS A KEY"
+counter = 10
diff --git a/games/chapter3/solutions/MoveRectProblem.py b/games/chapter3/solutions/MoveRectProblem.py
new file mode 100644
index 00000000..14056966
--- /dev/null
+++ b/games/chapter3/solutions/MoveRectProblem.py
@@ -0,0 +1,57 @@
+# Build off of your previous code (the code from QuitPygameProblem.py)
+# Draw the provided rectangle onto the screen.
+# Move the object up when either the W or up arrow key is pressed;
+# right when either the D or right arrow is pressed; etc.
+
+import pygame
+
+pygame.init()
+
+run = True
+width = 500
+height = 500
+white = (255, 255, 255)
+screen = pygame.display.set_mode((width, height))
+screen.fill(white)
+
+# rect[0] = rect color
+# rect[1] = x-coord
+# rect[2] = y-coord
+# rect[3] = width
+# rect[4] = height
+rectangle = [(255, 0, 0), 20, 20, 20, 20]
+
+# pygame main loop
+while run:
+ pygame.time.delay(50)
+ # clear the screen by filling it white
+ screen.fill(white)
+ # draw rect
+ pygame.draw.rect(
+ screen,
+ rectangle[0],
+ pygame.Rect(rectangle[1], rectangle[2], rectangle[3], rectangle[4]),
+ )
+ # Check events
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ # there are a couple of ways of stopping the pygame
+ # loop. One way is to set run = false. Or you can
+ # import sys and use sys.exit() to stop your program.
+ run = False
+ if event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_ESCAPE:
+ run = False
+ # get states of keys
+ keysPressed = pygame.key.get_pressed()
+ # recall that y coord decreases as you go up the window
+ # the origin is at the top left corner
+ if keysPressed[pygame.K_UP] or keysPressed[pygame.K_w]:
+ rectangle[2] -= 5 if rectangle[2] >= 5 else 0
+ if keysPressed[pygame.K_s] or keysPressed[pygame.K_DOWN]:
+ rectangle[2] += 5 if rectangle[2] <= 475 else 0
+ if keysPressed[pygame.K_d] or keysPressed[pygame.K_RIGHT]:
+ rectangle[1] += 5 if rectangle[1] <= 475 else 0
+ if keysPressed[pygame.K_a] or keysPressed[pygame.K_LEFT]:
+ rectangle[1] -= 5 if rectangle[1] >= 5 else 0
+ pygame.display.update()
diff --git a/games/chapter3/solutions/QuitPygameProblem.py b/games/chapter3/solutions/QuitPygameProblem.py
new file mode 100644
index 00000000..2ab243e3
--- /dev/null
+++ b/games/chapter3/solutions/QuitPygameProblem.py
@@ -0,0 +1,29 @@
+# Create a pygame window.
+# Close the pygame window when either the quit event occurs
+# or the escape key is pressed.
+# Use the provided width and height. Fill the screen with white
+
+import pygame
+
+pygame.init()
+
+run = True
+width = 500
+height = 500
+white = (255, 255, 255)
+screen = pygame.display.set_mode((width, height))
+screen.fill(white)
+
+# pygame main loop
+while run:
+ pygame.time.delay(50)
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ # there are a couple of ways of stopping the pygame
+ # loop. One way is to set run = false. Or you can
+ # import sys and use sys.exit() to stop your program.
+ run = False
+ if event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_ESCAPE:
+ run = False
+ pygame.display.update()
diff --git a/games/chapter3/solutions/SpaceCounter.py b/games/chapter3/solutions/SpaceCounter.py
new file mode 100644
index 00000000..bb742f0f
--- /dev/null
+++ b/games/chapter3/solutions/SpaceCounter.py
@@ -0,0 +1,39 @@
+# Create a program that increments a counter every time the space bar is
+# pressed. This counter should be displayed as text on the pygame window.
+
+import pygame
+
+pygame.init()
+screen = pygame.display.set_mode((400, 400))
+
+font = pygame.font.SysFont("arial", 70)
+
+display_counter = 0
+
+run = True
+
+while run:
+ # Render the "display_counter" to the screen
+ show_counter = font.render(str(display_counter), True, (255, 192, 203))
+
+ # Makes Screen Black
+ screen.fill((0, 0, 0))
+
+ # Prints Data on Screen
+ screen.blit(show_counter, (30, 30))
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ run = False
+
+ # Checks to see if key is pressed
+ if event.type == pygame.KEYDOWN:
+ # Checks to see if the space is pressed
+ if event.key == pygame.K_SPACE:
+ # Adds one to the counter
+ display_counter += 1
+
+ # Updates the data
+ pygame.display.update()
+
+pygame.quit()
diff --git a/games/chapter3/solutions/ticking_counter.py b/games/chapter3/solutions/ticking_counter.py
new file mode 100644
index 00000000..13e8478b
--- /dev/null
+++ b/games/chapter3/solutions/ticking_counter.py
@@ -0,0 +1,38 @@
+import pygame
+import time # use this to show text
+
+pygame.init()
+screen = pygame.display.set_mode((400, 400)) # set frame
+
+fonts = pygame.font.SysFont("arial", 20) # size of message
+font = pygame.font.SysFont("arial", 70) # size of counter
+text = "DON'T PRESS A KEY" # text
+counter = 10 # sets the counter
+
+run = True
+
+while run:
+ if pygame.event.peek(pygame.KEYDOWN): # checks queue for keydown
+ counter -= 1 # decreases counter by one
+ pygame.event.clear(pygame.KEYDOWN) # clears keydown from queue
+ if counter == 1: # changes text
+ text = "PLEASE YOU'll BLOW US UP!"
+ if counter == 0: # changes text and ends program
+ text = "YOU BLEW US UP D:"
+ run = False
+ # the if statements are before because when
+ # run is false, they will still run
+ # one last time, showing the last message
+ show_message = fonts.render(
+ text, True, (255, 102, 253)
+ ) # sets the message
+ show_counter = font.render(
+ str(counter), True, (255, 230, 102)
+ ) # sets the counter
+ screen.fill((0, 0, 0)) # refreshes every frame
+ screen.blit(show_counter, (200, 200)) # shows counter
+ screen.blit(show_message, (50, 100)) # shows message
+ pygame.display.update() # updates the frame
+
+time.sleep(1) # makes the last value of text readable
+pygame.quit()
diff --git a/games/chapter4/examples/OOP_game.py b/games/chapter4/examples/OOP_game.py
new file mode 100644
index 00000000..ab3bf7c7
--- /dev/null
+++ b/games/chapter4/examples/OOP_game.py
@@ -0,0 +1,473 @@
+"""
+This is a tank game made with pygame and original images.
+
+Brief description of classes within this file:
+ Game_obj - the abstract base class for all the objects that appear
+ on-screen, including the Tank class, the Bullet class, and
+ the Target class
+ Bullet - inherits from Game_obj.
+ Target - inherits from Game_obj. It is always stationary.
+ Tank - inherits from Game_obj. Takes keyboard input (W, A, S, and D)
+ to control the tank's movement.
+ App - the abstract base class for the actual Tank_game class. It's
+ purpose is to define a structure for the game.
+ Tank_game - the functional class that inherits from App. It creates
+ a bullet whenever the mouse is clicked. It handles the collisions
+ (if a bullet hits a target, both are deleted. If the tank runs into
+ the target, the target is deleted.)
+"""
+
+import pygame
+
+from pygame.locals import (
+ K_w,
+ K_s,
+ K_a,
+ K_d,
+ KEYDOWN,
+ KEYUP,
+ QUIT,
+ RESIZABLE,
+ MOUSEBUTTONDOWN,
+)
+import time
+import math
+import random
+
+BULLET_IMG_PATH = "./bullet.png"
+TARGET_IMG_PATH = "./target.png"
+TANK_IMG_PATH = "./completetank.png"
+
+BLACK = (255, 255, 255)
+DIRTBROWN = (168, 95, 0)
+SANDBROWN = (237, 201, 175)
+
+TANKSPEED = [2, 2] # speed x and speed y
+BULLETSPEED = [8, 8]
+
+
+class Game_obj:
+ def __init__(self, picture: str, **kwargs) -> None:
+ """
+ A basic game object class. It handles collisions,
+ the basic drawing method, the move and moveto methods,
+ and the check_out_of_screen method.
+
+ Arguments:
+ picture:str - the location of the picture that will be displayed on
+ the screen for this object
+ Valid keyword arguments:
+ "size":tuple(x,y) - a specific size that you want to have the object be.
+ The picture will be scaled to that size and the hitbox
+ will be updated accordingly.
+ "position":tuple(x,y) - the tuple at which the top left of the object
+ should be positioned at
+ "speed":tuple(x,y) - the tuple that represents the object's speed.
+ """
+ self.name = ""
+
+ # self.image will be a pygame.Surface class
+ self.image = pygame.image.load(picture)
+ self.image = (
+ pygame.transform.scale(
+ self.image, (kwargs["size"][0], kwargs["size"][1])
+ )
+ if "size" in kwargs
+ else self.image
+ )
+
+ self.rect = (
+ self.image.get_rect()
+ ) # self.rect will be of pygame.Rect class
+ self.size = self.rect.size # will be a tuple of (sizex, sizey)
+
+ if "position" in kwargs:
+ self.moveto(kwargs["position"])
+
+ self.speed = (
+ {"x": kwargs["speed"][0], "y": kwargs["speed"][1]}
+ if "speed" in kwargs
+ else {"x": 0, "y": 0}
+ )
+
+ def check_collision(self, other: object) -> bool:
+ if not isinstance(other, Game_obj):
+ raise TypeError(
+ "Invalid type; need a game_obj or a child class of game_obj"
+ )
+ # the rect class's colliderect method returns 1 if there is
+ # a collision and 0 if there isn't a collision
+ return self.rect.colliderect(other.rect) == 1
+
+ def draw(self, screen: pygame.Surface, color: tuple) -> None:
+ pygame.draw.rect(screen, color, self.rect, 0)
+ screen.blit(self.image, self.rect)
+
+ def move(self) -> None:
+ """
+ Moves the object according to it's current speed.
+ """
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+ # self.draw(screen, color)
+
+ def set_speed(self, new_speed: tuple) -> None:
+ """
+ Sets the object's speed to the provided tuple
+ Arguments:
+ new_speed (tuple(x,y)) - a tuple containing the desired speed for
+ the object to have.
+ """
+ self.speed["x"], self.speed["y"] = new_speed[0], new_speed[1]
+
+ def moveto(self, position: tuple) -> None:
+ """
+ A helper function that moves the rectangle to the desired position.
+
+ Arguments:
+ position (tuple) - the x and y coordinates of where you want the rectangle's
+ top left to be moved to.
+ """
+ self.rect = self.rect.move(
+ position[0] - self.rect.topleft[0],
+ position[1] - self.rect.topleft[1],
+ )
+
+ def check_out_of_screen(self, screen_size: tuple) -> bool:
+ """
+ Checks whether or not the object is completely outside of the screen.
+ Returns True or False accordingly.
+ Arguments:
+ screen_size (tuple) - the size of the screen (x,y)
+ """
+ if (
+ self.rect.bottom > screen_size[1]
+ or self.rect.top < 0
+ or self.rect.left < 0
+ or self.rect.right > screen_size[0]
+ ):
+ return True
+ return False
+
+ def __str__(self):
+ return (
+ f"{self.name} object located at the position {self.rect.topleft}"
+ )
+
+
+class Bullet(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(BULLET_IMG_PATH, **kwargs)
+ self.name = "Bullet"
+
+
+class Target(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ kwargs["size"] = 40, 40
+ super().__init__(TARGET_IMG_PATH, **kwargs)
+ self.name = "Target"
+
+
+class Tank(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(TANK_IMG_PATH, **kwargs)
+ self.direction = [0, 0]
+ self.SPEED = kwargs["speed"] if "speed" in kwargs else [2, 2]
+ self.speed["x"], self.speed["y"] = 0, 0
+
+ def set_speed(self) -> None:
+ # use math stuff to calculate the speed given that the
+ # max speed is self.SPEED
+ self.speed["x"] = (
+ self.direction[0]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[0]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[0] * self.SPEED[0]
+ )
+ self.speed["y"] = (
+ self.direction[1]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[1]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[1] * self.SPEED[1]
+ )
+
+ def set_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] -= 1
+ if direction == "down":
+ self.direction[1] += 1
+ if direction == "left":
+ self.direction[0] -= 1
+ if direction == "right":
+ self.direction[0] += 1
+
+ def unset_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] += 1
+ if direction == "down":
+ self.direction[1] -= 1
+ if direction == "left":
+ self.direction[0] += 1
+ if direction == "right":
+ self.direction[0] -= 1
+
+
+class App:
+ """
+ The abstract base class for the actual Tank_game class. It's
+ main purpose is to define a structure for the game.
+ It's structure is as follows:
+ Upon initialization, it runs the create_objects method
+ It's mainloop is comprised of the following methods:
+ check_events
+ check_collisions
+ move_objects
+ update_display
+ """
+
+ def __init__(
+ self, flags=RESIZABLE, width=960, height=540, title="My Game"
+ ):
+ pygame.init()
+ self.size = [width, height]
+ self.screen = pygame.display.set_mode(self.size, flags)
+ pygame.display.set_caption(title, title)
+
+ self.running = True
+
+ self.create_objects()
+
+ def create_objects(self):
+ """
+ This should create the initial objects on the screen.
+ """
+ pass
+
+ def check_events(self, event):
+ """
+ This should take user input and handle it appropriately.
+ """
+ pass
+
+ def update_display(self):
+ """
+ This should utilize clear the screen and then draw
+ all current objects onto the screen.
+ """
+ pass
+
+ def move_objects(self):
+ """
+ This should utilize the move method that the game objects have.
+ """
+ pass
+
+ def check_collisions(self):
+ """
+ This should utilize the check_collision method that the game objects
+ have.
+ """
+ pass
+
+ def mainloop(self):
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ break
+ else:
+ self.check_events(
+ event
+ ) # this will handle checking for user input
+ # such as KEYUP and MOUSEBUTTONDOWN events needed to run the game
+ self.check_collisions() # checks collisions between bullet/tank and targets
+ self.move_objects() # moves each object on the screen
+ self.update_display() # redraws updated objects onto the screen
+ pygame.display.update() # pygame’s method to show the updated screen
+ time.sleep(0.01) # not necessary; it's a frame cap
+ pygame.quit()
+
+
+class Tank_Game(App):
+ def __init__(self):
+ # this can be changed, it's the number of targets allowed at a time.
+ # we initialize this before super().__init__ because super().__init__ calls
+ # create_objects, which utilizes self.NUM_TARGETS
+ self.NUM_TARGETS = 3
+
+ super().__init__(title="Tanks")
+
+ self.playerscore = 0 # the player's score
+
+ # sets the display icon to the TankIcon.png provided
+ pygame.display.set_icon(pygame.image.load("./TankIcon.png"))
+
+ def create_objects(self):
+ """
+ This creates the initial objects seen when the game
+ first starts up.
+ """
+ # tank
+ self.tank = Tank(speed=TANKSPEED)
+ self.tank.moveto(
+ (
+ self.size[0] / 2 - self.tank.size[0], # move to middle x
+ self.size[1] - self.tank.size[1], # move to bottom y
+ )
+ )
+
+ # targets
+ self.targets = [Target(speed=[0, 0]) for i in range(self.NUM_TARGETS)]
+ for target in self.targets:
+ target.moveto(
+ (
+ random.randint(
+ 0, self.size[0] - target.size[0]
+ ), # random x
+ random.randint(
+ 0, self.size[1] - target.size[1]
+ ), # random y
+ )
+ )
+
+ # bullets
+ self.bullets = []
+
+ # Score text
+ self.font = pygame.font.SysFont(pygame.font.get_default_font(), 32)
+
+ def check_events(self, event):
+ """
+ We imported all from pygame.locals, so that means
+ that we can check KEYDOWN and KEYUP and individual
+ keys such as K_w (w key), K_a (a key), etc.
+ """
+ # change the path of the tank if w, a, s, or d was pressed
+ if event.type == KEYDOWN:
+ if event.key == K_w:
+ self.tank.set_path("up")
+ if event.key == K_s:
+ self.tank.set_path("down")
+ if event.key == K_a:
+ self.tank.set_path("left")
+ if event.key == K_d:
+ self.tank.set_path("right")
+ if event.type == KEYUP:
+ if event.key == K_w:
+ self.tank.unset_path("up")
+ if event.key == K_s:
+ self.tank.unset_path("down")
+ if event.key == K_a:
+ self.tank.unset_path("left")
+ if event.key == K_d:
+ self.tank.unset_path("right")
+ self.tank.set_speed()
+
+ # create bullets if mouse button was pressed
+ if event.type == MOUSEBUTTONDOWN:
+ bul = Bullet(speed=BULLETSPEED)
+ bul.moveto(
+ (self.tank.rect.centerx, (self.tank.rect.top - bul.size[1]))
+ ) # move the bullet to the front of the tank
+
+ # math stuff to calculate trajectory
+ mouse_pos = pygame.mouse.get_pos()
+ h = mouse_pos[1] - bul.rect.center[1]
+ w = mouse_pos[0] - bul.rect.center[0]
+ hyp = math.sqrt(h**2 + w**2)
+ vertical_speed = (
+ BULLETSPEED[1] * (h / hyp) if hyp != 0 else BULLETSPEED[1] * h
+ )
+ horizontal_speed = (
+ BULLETSPEED[0] * (w / hyp) if hyp != 0 else BULLETSPEED[0] * w
+ )
+
+ bul.set_speed((horizontal_speed, vertical_speed))
+ self.bullets.append(bul)
+
+ def move_objects(self):
+ """
+ This method moves the objects within the game.
+ If a bullet is outside of the screen, it is
+ not moved and is unreferenced.
+ """
+ self.tank.move()
+
+ self.bullets = [
+ bullet
+ for bullet in self.bullets
+ if bullet.check_out_of_screen(self.size) is False
+ ]
+
+ for bullet in self.bullets:
+ bullet.move()
+
+ def check_collisions(self):
+ """
+ This checks whether any of the objects within the game have collided
+ with each other. Specifically, we are looking for collisions between
+ bullets and targets or the tank and targets
+ """
+ deletions = 0 # number of targets deleted
+ num_bullets = len(self.bullets)
+
+ # check bullet-target collisions
+ for i in range(num_bullets):
+ for target in self.targets:
+ # if the bullet collided with the target
+ if self.bullets[i - deletions].check_collision(target) is True:
+ # pop both the bullet and target so that they will be
+ # effectively deleted
+ self.bullets.pop(i - deletions)
+ self.targets.pop(self.targets.index(target))
+
+ # give points for hitting the target
+ self.playerscore += 20
+ deletions += 1
+ break # stop the current iteration since the target and
+ # bullet are popped, so referencing them would error.
+
+ # check tank-target collisions
+ for target in self.targets:
+ if self.tank.check_collision(target) is True:
+ self.targets.pop(self.targets.index(target))
+ deletions += 1
+ self.playerscore += 10 # only 10 for running over targets lol
+
+ # create a new target for every deleted target
+ for i in range(deletions):
+ a = Target(speed=[0, 0])
+ a.moveto(
+ (
+ random.randint(0, self.size[0] - a.size[0]),
+ random.randint(0, self.size[1] - a.size[1]),
+ )
+ )
+ self.targets.append(a)
+
+ def update_display(self):
+ self.screen.fill(SANDBROWN)
+
+ # tank
+ self.tank.draw(self.screen, SANDBROWN)
+
+ # targets
+ for target in self.targets:
+ target.draw(self.screen, BLACK)
+
+ # bullets
+ for bullet in self.bullets:
+ bullet.draw(self.screen, BLACK)
+
+ # score text
+ font_img = self.font.render(
+ "Score: %s" % str(self.playerscore), True, BLACK
+ )
+ font_rect = font_img.get_rect()
+ pygame.draw.rect(self.screen, SANDBROWN, font_rect, 1)
+ self.screen.blit(font_img, font_rect)
+
+
+game = Tank_Game()
+game.mainloop()
diff --git a/games/chapter4/examples/TankIcon.png b/games/chapter4/examples/TankIcon.png
new file mode 100644
index 00000000..d9e00bbe
Binary files /dev/null and b/games/chapter4/examples/TankIcon.png differ
diff --git a/games/chapter4/examples/Target.png b/games/chapter4/examples/Target.png
new file mode 100644
index 00000000..d48bc78e
Binary files /dev/null and b/games/chapter4/examples/Target.png differ
diff --git a/games/chapter4/examples/bullet.png b/games/chapter4/examples/bullet.png
new file mode 100644
index 00000000..fce8c161
Binary files /dev/null and b/games/chapter4/examples/bullet.png differ
diff --git a/games/chapter4/examples/completetank.png b/games/chapter4/examples/completetank.png
new file mode 100644
index 00000000..036cd84b
Binary files /dev/null and b/games/chapter4/examples/completetank.png differ
diff --git a/games/chapter4/examples/tank_game.zip b/games/chapter4/examples/tank_game.zip
new file mode 100644
index 00000000..c6900645
Binary files /dev/null and b/games/chapter4/examples/tank_game.zip differ
diff --git a/games/chapter4/practice/flappy_bird/OOPflappybird.py b/games/chapter4/practice/flappy_bird/OOPflappybird.py
new file mode 100644
index 00000000..08ca2aa2
--- /dev/null
+++ b/games/chapter4/practice/flappy_bird/OOPflappybird.py
@@ -0,0 +1,494 @@
+# TODO
+# Create the FlappyBird game!!
+
+# You are provided with some starting code.
+# The starting code, however, doesn't run by itself.
+# What you need to do:
+# define GameObj's draw method
+# define GameObj's check_collision method.
+
+# Complete all the methods within Tubes class
+
+# Complete the draw_score and draw_buttons methods in the FlappyBird class
+
+import pygame
+import random
+
+pygame.init()
+
+# screen
+width = 800
+height = 600
+SIZE = (width, height)
+screen = pygame.display.set_mode(SIZE)
+
+# colors
+LGREEN = (62, 245, 59)
+DGREEN = (40, 143, 39)
+YELLOW = (250, 250, 37)
+WHITE = (255, 255, 255)
+BLACK = (0, 0, 0)
+RED = (255, 0, 0)
+LILAC = (175, 95, 237)
+LBLUE = (80, 221, 242)
+DBLUE = (80, 99, 242)
+PINK = (245, 144, 188)
+CYAN = (0, 150, 150)
+
+# images
+BACKGROUNDIMG = pygame.image.load("./background.png")
+BACKGROUNDIMG = pygame.transform.scale(BACKGROUNDIMG, (width, height))
+SPRITESHEET = pygame.image.load("./flyingbird.png")
+COINPIC = pygame.image.load("./coin.png")
+
+# ---------- States of the Game ----------
+MENUSTATE = 0 # Menu Screen
+GAMESTATE = 1 # Play Game
+LOSESTATE = 2 # u loose >:)
+QUITSTATE = 3
+NUMSTATES = 4
+
+
+class GameObj:
+ """
+ An abstract class used as the base class for all the
+ game's objects
+ """
+
+ def __init__(self):
+ """
+ This __init__ method provides no functionality.
+ It just enables the methods defined in this class.
+ Thus, calling super().__init__ is unnecessary.
+ """
+ self.rect = pygame.Rect
+
+ def draw(
+ self,
+ screen: pygame.Surface,
+ color: tuple,
+ specific_rect: pygame.Rect = None,
+ ):
+ """
+ Draws a rectangle onto the screen in the specified color.
+ If specific_rect is not None, draw specific_rect onto the screen.
+ If specific_rect is None, draw self.rect onto the screen.
+ """
+ pass
+
+ def move(self, speed: dict = None, specific_rect: pygame.Rect = None):
+ """
+ Moves a rectangle.
+ @param speed - The speed to move the rectangle at. It should be
+ a dictionary of form {'x': int, 'y': int}; for example,
+ {'x':33, 'y':-22}. If no speed is provided, uses self.speed
+ @param specific_rect - if specific_rect is None, then this method
+ will move self.rect. If specific_rect is not None, then this method will
+ move specific_rect
+ """
+ if not speed and hasattr(self, "speed"):
+ if specific_rect:
+ return specific_rect.move(self.speed["x"], self.speed["y"])
+ else:
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+ if speed:
+ if specific_rect:
+ return specific_rect.move(speed["x"], speed["y"])
+ else:
+ self.rect = self.rect.move(speed["x"], speed["y"])
+
+ def check_collision(self, other, specific_rect: pygame.Rect = None):
+ """
+ Checks if rectangles have collided. If specific_rect is not None,
+ checks if specific_rect collides with other.rect. If specific_rect is None,
+ checks if self.rect collides with other.rect.
+ """
+ pass
+
+
+class Tubes(GameObj):
+ """
+ Class to represent the two tubes.
+
+ Ex:
+ The tubes will look sort of like the below drawing
+ (one on the top, one on the bottom)
+ (let - be top or bottom of school)
+ ------------
+ | |
+ |_|
+
+ _
+ | |
+ | |
+ | |
+ ------------
+ """
+
+ TUBEGAP = 230 # smaller TUBEGAP -> smaller dist between tubes
+ TUBEWIDTH = 100
+
+ def __init__(self, bottom_tube_height: int):
+ """
+ Initializes two pygame.Rect objects: one for the
+ top tube (call it top_tube) and one for the bottom tube
+ (call it bottom_tube). Uses the TUBEGAP
+ and TUBEWIDTH variables as dimensions.
+ """
+ pass
+
+ def draw(self, screen: pygame.Surface):
+ """
+ Uses the draw() method from the inherited
+ GameObj class to draw the top and bottom tubes.
+ Hint: this will use the specific_rect argument
+ """
+ pass
+
+ def move(self, speed: dict):
+ """
+ Uses the move() method from the inherited
+ GameObj class to move the specific top and bottom tubes.
+ Hint: this will use the specific_rect argument
+ """
+ pass
+
+ def check_collision(self, other) -> bool:
+ """
+ Uses the check_collision() method from the inherited
+ GameObj class to check for any collisions
+ between the given object and the tubes.
+ Hint: this will use the specific_rect argument
+
+ Returns:
+ boolean - if either tube is collided with, return True
+ """
+ pass
+
+
+class Coin(GameObj):
+ """
+ The coin that the bird will get
+ in-between tubes.
+ Doesn't need to do anything, so pretty short class.
+ """
+
+ def __init__(self, center_y: int):
+ """
+ Makes a coin object.
+ The coin's x coordinate will be the width of the screen
+ The coin's y coordinate will be centered around `center_y`
+ @param center_y:int - the y coordinate to center the coin around
+ """
+ temprect = COINPIC.get_rect()
+ self.rect = pygame.Rect(
+ width,
+ center_y - temprect.height // 2,
+ temprect.width,
+ temprect.height,
+ )
+
+ def draw(self, screen: pygame.Surface):
+ super().draw(screen, BLACK)
+
+ def blit(self, screen: pygame.Surface):
+ screen.blit(COINPIC, self.rect)
+
+
+class Bird(GameObj):
+ """
+ The bird itself. It processes the sprites
+ and handles jumping.
+ """
+
+ start_center_pos = (width // 8, height // 2)
+
+ def __init__(self):
+ self.process_spritesheet(SPRITESHEET, 3, 3)
+ self.rect = pygame.Rect(
+ self.start_center_pos[0] - self.sprite_frame_width // 2,
+ self.start_center_pos[1] - self.sprite_frame_height // 2,
+ self.sprite_frame_width,
+ self.sprite_frame_height,
+ )
+ self.momentum = 0 # the bird's current vertical speed
+ self.jump_height = 15
+ self.min_speed = -10 # the maximum speed the bird flies down at
+ self.cur_sprite_idx = 0
+
+ def process_spritesheet(
+ self,
+ spritesheet: pygame.Surface,
+ num_pics_x: int,
+ num_pics_y: int,
+ offset_x: int = 0,
+ offset_y: int = 0,
+ ):
+ """
+ Creates sprites from the spritesheet.
+ @param spritesheet: pygame.Surface - the spritesheet.
+ @param num_pics_x: int - the number of sprites in each row on
+ the spritesheet
+ @param num_pics_y: int - the number of sprites in each column
+ on the spritesheet
+ @param offset_x: int - the x offset before the sprite rows start
+ @param offset_y: int - the y offset before the sprite columns start
+ """
+ self.sprites = []
+ self.sprite_frame_width = (
+ spritesheet.get_width() - offset_x
+ ) // num_pics_x
+ self.sprite_frame_height = (
+ spritesheet.get_height() - offset_y
+ ) // num_pics_y
+ for row in range(num_pics_x):
+ for column in range(num_pics_y):
+ temp = spritesheet.subsurface(
+ (
+ row * self.sprite_frame_width + offset_x,
+ column * self.sprite_frame_height + offset_y,
+ self.sprite_frame_width,
+ self.sprite_frame_height,
+ )
+ )
+ # get the bounding box for the actual colored pixels
+ # (so that we won't be blit-ing extra empty pixels)
+ # (makes collisions more accurate)
+ temprect = temp.get_bounding_rect()
+ # then, append the shortened image to the sprites list
+ self.sprites.append(temp.subsurface(temprect))
+
+ def draw(self, screen: pygame.Surface, framecount: int):
+ curr_sprite_idx = framecount // 5 % len(self.sprites)
+ if curr_sprite_idx != self.cur_sprite_idx:
+ # if it is now a different sprite, adjust self.rect
+ # so that it won't be bigger or smaller than the new sprite
+ self.cur_sprite_idx = curr_sprite_idx
+ temp = self.sprites[self.cur_sprite_idx]
+ self.rect = temp.get_rect().move(
+ self.rect.topleft[0], self.rect.topleft[1]
+ )
+ super().draw(screen, BLACK)
+
+ def blit(self, screen: pygame.Surface):
+ screen.blit(self.sprites[self.cur_sprite_idx], self.rect)
+
+ def process_movement(self, event):
+ if event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
+ self.momentum = self.jump_height
+
+ def move(self):
+ super().move({"x": 0, "y": -self.momentum})
+ self.momentum -= 1
+
+ # if the bird would fly down faster than self.min_speed,
+ # cap self.momentum at self.min_speed
+ if self.momentum < self.min_speed:
+ self.momentum = self.min_speed
+
+
+class Button(GameObj):
+ """
+ A button with text. Used for the
+ 'Quit Game' 'Start Game' and 'Retry' buttons
+ """
+
+ def __init__(
+ self,
+ center_x: int,
+ center_y: int,
+ bgcolor: tuple,
+ textcolor: tuple,
+ text: str = "",
+ textsize: int = 32,
+ ):
+ """
+ Creates a Button object.
+ @param center_x: int - the x coordinate of the button's center
+ @param center_y: int - the y coordinate of the button's center
+ @param bgcolor: tuple - the color for the button's background
+ @param textcolor: tuple - the color for the button's text
+ @param text: str - the text to put inside the button
+ @param textsize: int - the size of the button's text
+ """
+ self.font = pygame.font.SysFont("arial", textsize)
+ self.font_img = self.font.render(text, True, textcolor)
+ self.rect = self.font_img.get_rect()
+ self.rect.center = (center_x, center_y)
+ self.bgcolor = bgcolor
+ self.active = True
+
+ def draw(self, screen: pygame.Surface):
+ super().draw(screen, self.bgcolor)
+ screen.blit(self.font_img, self.rect)
+
+ def is_clicked(self, event: pygame.event.Event):
+ if event.type == pygame.MOUSEBUTTONDOWN:
+ return event.pos in self
+
+ def __contains__(self, coordinate):
+ return self.rect.contains((coordinate[0], coordinate[1], 0, 0))
+
+
+class FlappyBird:
+ """
+ This is the game class.
+ """
+
+ def __init__(self):
+ self.running = True
+ self.gamestate = MENUSTATE
+ self.create_buttons()
+ self.framecount = 0
+ self.clock = pygame.time.Clock()
+
+ def create_buttons(self, button1text="Start Game", button2bg=RED):
+ self.buttons = {
+ "start": Button(
+ width // 2, height // 4, LGREEN, LILAC, button1text
+ ),
+ "quit": Button(
+ width // 2, height // 4 * 3, button2bg, LILAC, "Quit Game"
+ ),
+ }
+
+ def mainloop(self):
+ while self.running:
+ events = pygame.event.get()
+ for event in events:
+ self.set_state(event)
+ if event.type == pygame.QUIT:
+ self.running = False
+ if self.gamestate == MENUSTATE:
+ screen.fill(LBLUE)
+ self.draw_buttons(screen)
+
+ elif self.gamestate == GAMESTATE:
+ self.draw_all()
+ self.check_collisions()
+
+ # update bird's speed
+ for event in events:
+ self.bird.process_movement(event)
+ self.move_objects()
+
+ self.create_tubes()
+
+ elif self.gamestate == LOSESTATE:
+ screen.fill(RED)
+ self.draw_buttons(screen)
+
+ elif self.gamestate == QUITSTATE:
+ self.running = False
+
+ self.framecount += 1
+ pygame.display.update()
+ self.clock.tick(60)
+ pygame.quit()
+
+ def set_state(self, event):
+ if event.type == pygame.MOUSEBUTTONDOWN:
+ if all([but.active for but in self.buttons.values()]):
+ if self.buttons["start"].is_clicked(event):
+ self.buttons["start"].active = False
+ self.buttons["quit"].active = False
+ self.start_game()
+ elif self.buttons["quit"].is_clicked(event):
+ self.gamestate = QUITSTATE
+ self.buttons["start"].active = False
+ self.buttons["quit"].active = False
+
+ def draw_background(self):
+ screen.blit(BACKGROUNDIMG, (self.background_x, 0))
+ screen.blit(BACKGROUNDIMG, (self.background_x + width, 0))
+ self.background_x -= 2
+ if self.background_x < -1 * width:
+ self.background_x = 0
+
+ def draw_score(self):
+ """
+ Writes the player's score onto the screen in the top
+ right corner.
+ Hint: this uses pygame fonts
+ """
+ pass
+
+ def draw_buttons(self, screen: pygame.Surface):
+ """
+ Draws the "start" and "quit" buttons onto the screen.
+ Hint: this uses `self.buttons` (which is already made)
+ """
+ pass
+
+ def create_tubes(self):
+ """
+ Creates tubes and puts a coin in the middle of each tube.
+ """
+ if (
+ len(self.tubes) == 0
+ or self.tubes[-1].bottom_tube.right < width - 200
+ ):
+ bottom_tube_height = random.randint(0, height - Tubes.TUBEGAP)
+ self.tubes.append(Tubes(bottom_tube_height))
+ self.coins.append(
+ Coin(height - bottom_tube_height - (Tubes.TUBEGAP // 2))
+ )
+
+ def draw_all(self):
+ # draw bird and coin rectangles before background so that they won't
+ # show
+ self.bird.draw(screen, self.framecount)
+ for coin in self.coins:
+ coin.draw(screen)
+
+ self.draw_background()
+
+ for tube in self.tubes:
+ tube.draw(screen)
+
+ # blit images/sprites onto the screen
+ self.bird.blit(screen)
+ for coin in self.coins:
+ coin.blit(screen)
+
+ self.draw_score()
+
+ def check_collisions(self):
+ for tube in self.tubes:
+ if tube.check_collision(self.bird):
+ self.gamestate = LOSESTATE
+ self.create_buttons("Retry?", LBLUE)
+ if tube.bottom_tube.right < 0:
+ self.tubes.remove(tube)
+
+ for coin in self.coins:
+ if self.bird.check_collision(coin):
+ self.score += 1
+ self.coins.remove(coin)
+ if coin.rect.right < 0:
+ self.coins.remove(coin)
+
+ if self.bird.rect.bottom > height: # fell out of screen
+ self.gamestate = LOSESTATE
+ self.create_buttons("Retry?", LBLUE)
+
+ def move_objects(self):
+ SPEED = 3 # x speed that objects move towards the bird at
+ for tube in self.tubes:
+ tube.move({"x": -SPEED, "y": 0})
+ for coin in self.coins:
+ coin.move({"x": -SPEED, "y": 0})
+ self.bird.move()
+
+ def start_game(self):
+ self.gamestate = GAMESTATE
+ self.background_x = 0
+ self.score = 0
+ self.bird = Bird()
+ self.tubes = []
+ self.coins = []
+ self.create_tubes()
+
+
+a = FlappyBird()
+a.mainloop()
diff --git a/games/chapter4/practice/flappy_bird/background.png b/games/chapter4/practice/flappy_bird/background.png
new file mode 100644
index 00000000..0be8c233
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/background.png differ
diff --git a/games/chapter4/practice/flappy_bird/bird.png b/games/chapter4/practice/flappy_bird/bird.png
new file mode 100644
index 00000000..2b8a5b32
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/bird.png differ
diff --git a/games/chapter4/practice/flappy_bird/coin.png b/games/chapter4/practice/flappy_bird/coin.png
new file mode 100644
index 00000000..dc457bff
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/coin.png differ
diff --git a/games/chapter4/practice/flappy_bird/explosion_transparent.png b/games/chapter4/practice/flappy_bird/explosion_transparent.png
new file mode 100644
index 00000000..a92d7099
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/explosion_transparent.png differ
diff --git a/games/chapter4/practice/flappy_bird/flappy_bird.zip b/games/chapter4/practice/flappy_bird/flappy_bird.zip
new file mode 100644
index 00000000..4a21a88a
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/flappy_bird.zip differ
diff --git a/games/chapter4/practice/flappy_bird/flyingbird.png b/games/chapter4/practice/flappy_bird/flyingbird.png
new file mode 100644
index 00000000..25bd824a
Binary files /dev/null and b/games/chapter4/practice/flappy_bird/flyingbird.png differ
diff --git a/games/chapter4/practice/hockey.py b/games/chapter4/practice/hockey.py
new file mode 100644
index 00000000..78db3c80
--- /dev/null
+++ b/games/chapter4/practice/hockey.py
@@ -0,0 +1,625 @@
+# Make a two-player hockey game! The application will consist
+# of two rectangular paddles, starting on each side of the screen,
+# and one circular ball that players must bounce around. Players can
+# move the paddles in any direction to hit the ball into the goal.
+# If the ball makes contact with safe parts of the screen, it will
+# bounce off at a random angle but in the same general direction
+# (left or right). It will do the same if it makes contact with one
+# of the paddles, but will head towards the opposite general direction
+# instead. If the ball touches the goals on either side of the screen,
+# the application will say “Game Over. Player _ Wins”. You must put
+# your code in classes and have separate keys for each player to
+# move their paddles.
+
+# please use the provided constants.
+
+# TODO -
+# Fill in the Game_obj class (init is done)
+# Fill in Player class's init, draw, and setpath methods
+# Fill in Ball class's collide_line and get_obj_path methods
+# Fill in BoundingLine's init method
+# Fill in Hockey class's update_display, move_objects, check_events,
+# and check_collisions methods
+
+
+import pygame
+from pygame.locals import (
+ K_w,
+ K_s,
+ K_a,
+ K_d,
+ K_UP,
+ K_DOWN,
+ K_LEFT,
+ K_RIGHT,
+ KEYDOWN,
+ KEYUP,
+ QUIT,
+ RESIZABLE,
+)
+from pygame.rect import Rect
+
+import math
+import time
+import random
+
+# define the necessary color constants using rgb values
+BLACK = (0, 0, 0)
+GREEN = (0, 120, 0)
+RED = (120, 0, 0)
+WHITE = (255, 255, 255)
+
+# define player controls
+PLAYER1CONTROLS = {"up": K_w, "down": K_s, "left": K_a, "right": K_d}
+PLAYER2CONTROLS = {
+ "up": K_UP,
+ "down": K_DOWN,
+ "left": K_LEFT,
+ "right": K_RIGHT,
+}
+
+# initial screensize
+SCREENSIZE = [900, 600]
+
+# how big the ball's radius will be
+BALL_RADIUS = 3
+
+
+class Game_obj:
+ def __init__(self):
+ """
+ This should just declare the variables
+ later used in the other methods
+ """
+ self.speed = {"x": 0, "y": 0}
+ self.rect = Rect
+ self.prev_rect = Rect
+
+ def move(self):
+ """
+ This should first set self.prev_rect equal to self.rect.
+ Then, it should move self.rect according to self.speed
+ by using self.rect's move method as demonstrated in OOP_game.py.
+ """
+ pass # your code here
+
+ def move_to(self, coordinate):
+ """
+ This should first set self.prev_rect equal to self.rect.
+ Then, it should move self.rect so that its top left lies at
+ the provided coordinate.
+ """
+ pass # your code here
+
+ def check_collision(self, other):
+ """
+ This should return either True or False based on whether
+ self.rect's collide_rect method returns 1 or 0 (respectively)
+ similar to how it is done in OOP_game.py
+ """
+ pass # your code here
+
+
+class Player(Game_obj):
+ PLAYERSPEED = (3, 3)
+ PADDLESIZE = (10, 50) # x width, y width
+
+ def __init__(self, control_keys):
+ """
+ Creates a player rectangle. It should have an attribute
+ self.control_keys from provided control keys. It should
+ create a rectangle at (0, 0) that has self.PADDLESIZE dimensions.
+ It should also initialize self.path to [0,0] (x_path, y_path)
+ Arguments:
+ control_keys - (dict) should be a dictionary of the following format:
+ {
+ "up": (KEY) (ex: K_w),
+ "down": (KEY) (ex: K_s),
+ "left": (KEY) (ex: K_a),
+ "right": (KEY) (ex: K_d)
+ }
+ """
+ pass
+
+ def draw(self, surface: pygame.Surface):
+ """
+ This should draw self.rect onto the provided surface in
+ the color GREEN
+ Note: the surface acts just like 'window' in previous
+ lessons
+ """
+ pass # your code here
+
+ def set_path(self, event):
+ """
+ This is the method that calls self.key_checker with
+ the provided event and 'up', 'down', 'left', and 'right'
+
+ note: a call to self.key_checker will look like:
+ self.key_checker(event, 'direction string here')
+ """
+ pass # your code here
+
+ def key_checker(self, event, direction):
+ """
+ Helper function to deal with event keys. Sets self.path
+ according to PATH_VALUES
+ Arguments:
+ event(pygame.event.Event) - the event
+ direction(str) - the direction to check KEYDOWN and KEYUP for.
+ """
+ PATH_VALUES = {"up": 1, "down": 1, "left": 0, "right": 0}
+ DIRECTION_VALUES = {"up": -1, "down": 1, "left": -1, "right": 1}
+
+ # if the event doesn't have a key attribute, just return
+ if not hasattr(event, "key"):
+ return
+
+ # if it does, then check if it the right key
+ if event.key == self.control_keys[direction]:
+ if event.type == KEYUP:
+ self.path[PATH_VALUES[direction]] += -DIRECTION_VALUES[
+ direction
+ ]
+ if event.type == KEYDOWN:
+ self.path[PATH_VALUES[direction]] += DIRECTION_VALUES[
+ direction
+ ]
+
+ def set_speed(self):
+ """
+ Sets the speed according to the Player object's path.
+ This should be called after self.path has been set.
+ """
+ # this is provided since it's math-intensive.
+ self.speed["x"] = (
+ self.path[0]
+ / math.sqrt(sum(abs(num) for num in self.path))
+ * self.PLAYERSPEED[0]
+ if (sum(abs(num) for num in self.path)) != 0
+ else self.path[0] * self.PLAYERSPEED[0]
+ )
+ self.speed["y"] = (
+ self.path[1]
+ / math.sqrt(sum(abs(num) for num in self.path))
+ * self.PLAYERSPEED[1]
+ if (sum(abs(num) for num in self.path)) != 0
+ else self.path[1] * self.PLAYERSPEED[1]
+ )
+
+
+class Ball(Game_obj):
+ BALLSPEED = (6, 6)
+
+ def __init__(self, radius):
+ super().__init__()
+ self.rect = Rect(0, 0, radius * 2, radius * 2)
+ self.radius = radius
+
+ # set up initial speed
+ initial_ang = random.randint(1, int(math.pi / 2 * 100)) / 100
+ self.speed["x"] = (
+ math.cos(initial_ang)
+ * self.BALLSPEED[0]
+ * (-1 if random.randint(0, 1) == 0 else 1)
+ )
+ self.speed["y"] = (
+ math.sin(initial_ang)
+ * self.BALLSPEED[1]
+ * (-1 if random.randint(0, 1) == 0 else 1)
+ )
+
+ def draw(self, screen: pygame.Surface):
+ pygame.draw.circle(
+ screen, WHITE, center=self.rect.center, radius=self.radius
+ )
+
+ def collide_line(self, other):
+ """
+ Checks if the ball has hit a line.
+ If it did, update the speed accordingly
+
+ IE:
+ if it collided with top or bottom, set
+ self.speed['y'] to negative self.speed['y']
+ if it collided with right or left,
+ set self.speed['x'] to negative self.speed['x']
+
+ Arguments:
+ other (BoundingLine or Goal) - the line to check for a collision with
+ Returns:
+ True - if the collision happened
+ False - if the collision didn't happen
+ """
+ pass # your code here
+
+ def get_obj_path(self, object: Game_obj) -> tuple:
+ """
+ if the object's speed is greater than 0, x_path = 1
+ elif the object's speed is less than 0, x_path = -1
+ else, the x_path = 0.
+ Do the same for y speed for a variable y_path.
+ """
+ # your code here
+
+ return # (x_path, y_path) (uncomment when you write your code here)
+
+ def get_paddle_collision_dir(self, paddle: Player) -> tuple:
+ """
+ Gets the direction in which the ball will be headed
+ after a collision with a paddle.
+ Does not actually check if the collision happened
+ Provided since it's somewhat complicated
+
+ Arguments:
+ paddle (Player) - the player that the ball 'collided with'
+ Returns:
+ tuple(int, int) - a tuple of length 2 with just +-1's
+ ex: (1, 1) or (1, -1) or (-1, 1), or (-1, -1)
+ It corresponds to the direction in which the ball
+ will be headed. The first item will be the x direction
+ and the second item will be the y direction.
+ """
+ paddle_x_dir, paddle_y_dir = self.get_obj_path(paddle)
+
+ ball_x_dir, ball_y_dir = self.get_obj_path(self)
+
+ resulting_x_dir = None
+ resulting_y_dir = None
+
+ if paddle.speed["x"] == 0 or paddle_x_dir == ball_x_dir:
+ if abs(paddle.speed["x"]) > abs(self.speed["x"]):
+ resulting_x_dir = paddle_x_dir
+ elif abs(paddle.speed["x"]) < abs(self.speed["x"]):
+ resulting_x_dir = -ball_x_dir
+ else:
+ resulting_x_dir = -ball_x_dir
+
+ if paddle.speed["y"] == 0 or paddle_y_dir == ball_y_dir:
+ if abs(paddle.speed["y"]) > abs(self.speed["y"]):
+ resulting_y_dir = paddle_y_dir
+ elif abs(paddle.speed["y"]) < abs(self.speed["y"]):
+ resulting_y_dir = -ball_y_dir
+ else:
+ resulting_y_dir = -ball_y_dir
+
+ return (resulting_x_dir, resulting_y_dir)
+
+ def collide_paddle(self, paddle: Player, executions: int) -> None:
+ """
+ Handles collisions with paddles.
+
+ Checks if the ball hit the provided player. If it did,
+ it will adjust the ball's direction.
+
+ Arguments:
+ paddle(Player) - the paddle to check for a collision with
+ executions(int) - the amount of executions of the game's mainloop
+ It's not important, but it prevents unwanted collisions during
+ the 0th execution when we first set up the game by moving
+ the objects to the right place
+ """
+ PROPORTION = 0.25 # used when "escaping" a collision
+ MINIMUM_ANGLE = (
+ 15 # this is in degrees; it's just a fine-tuning aspect
+ )
+ # that makes the game more realistic
+
+ resulting_x_dir = None
+ resulting_y_dir = None
+
+ a = self.trace_collisions(paddle)
+ if a[0] and executions != 0:
+ resulting_x_dir, resulting_y_dir = a[1]
+
+ if self.check_collision(paddle):
+ resulting_x_dir, resulting_y_dir = self.get_paddle_collision_dir(
+ paddle
+ )
+
+ # if resulting_x_dir and resulting_y_dir aren't None, then update ball speed
+ if resulting_x_dir and resulting_y_dir:
+ print(MINIMUM_ANGLE * math.pi / 180 * 100)
+ print(math.pi / 2 * 100)
+ angle = (
+ random.randint(
+ 0,
+ int(
+ math.pi / 2 * 100
+ - (MINIMUM_ANGLE * math.pi / 180 * 100)
+ ),
+ )
+ / 100
+ )
+
+ print("angle", angle)
+
+ self.speed["x"] = (
+ math.cos(angle) * self.BALLSPEED[0] * resulting_x_dir
+ )
+ self.speed["y"] = (
+ math.sin(angle) * self.BALLSPEED[1] * resulting_y_dir
+ )
+
+ # escape the collision so as to prevent the "same" collision from being
+ # handled when collide_paddle is called next time.
+ while self.check_collision(paddle):
+ self.move_to(
+ (
+ self.rect.topleft[0] + PROPORTION * self.speed["x"],
+ self.rect.topleft[1] + PROPORTION * self.speed["y"],
+ )
+ )
+
+ def trace_collisions(self, paddle):
+ COLLISIONS_TO_CHECK = 30 # the higher this is, the slower.
+
+ # find how much the ball moved during the past execution
+ delta_x = self.rect.topleft[0] - self.prev_rect.topleft[0]
+ delta_y = self.rect.topleft[1] - self.prev_rect.topleft[1]
+
+ # find how much the paddle moved during the past execution
+ paddle_delta_x = paddle.rect.topleft[0] - paddle.prev_rect.topleft[0]
+ paddle_delta_y = paddle.rect.topleft[1] - paddle.prev_rect.topleft[1]
+
+ # check COLLISIONS_TO_CHECK times for a collision that occurred during
+ # the "update game" phase (when we moved the objects)
+ for i in range(COLLISIONS_TO_CHECK):
+ # move both the ball and the paddle/player to where they would've been if we
+ # subdivided the move phase into COLLISIONS_TO_CHECK individual frames
+ ball_past = Ball(BALL_RADIUS)
+ ball_past.move_to(
+ (
+ self.prev_rect.topleft[0]
+ + (delta_x * i / COLLISIONS_TO_CHECK),
+ self.prev_rect.topleft[1]
+ + (delta_y * i / COLLISIONS_TO_CHECK),
+ )
+ )
+ paddle_past = Player({})
+ paddle_past.move_to(
+ (
+ paddle.prev_rect.topleft[0]
+ + (paddle_delta_x * i / COLLISIONS_TO_CHECK),
+ paddle.prev_rect.topleft[1]
+ + (paddle_delta_y * i / COLLISIONS_TO_CHECK),
+ )
+ )
+
+ # now that we have a ball and a paddle, check if they collided
+ if ball_past.check_collision(paddle_past):
+ return (True, ball_past.get_paddle_collision_dir(paddle_past))
+ # if the loop failed (didn't return), then
+ # return False and an empty tuple
+ return (False, tuple())
+
+
+class BoundingLine:
+ DEFAULT_SIZE = 3
+
+ def __init__(self, parameters):
+ """
+ Replace 'parameters' with real parameters.
+ This should take:
+ the starting coordinate of the line
+ the ending coordinate of the line
+ the line's name (which should either be
+ 'top', 'bottom', 'left', or 'right')
+ (optional) default_size - the size of the line. If not provided,
+ use DEFAULT_SIZE
+ This should create the rectangle that stretches from the start
+ coordinate to the end coordinate with a width or height
+ (depending on its orientation) of default_size
+ (or DEFAULT_SIZE if default_size isn't provided)
+
+ Make sure to:
+ if it is the bottom line: move it DEFAULT_SIZE units up
+ if it is the right line: move it DEFAULT_SIZE units left
+ *This is a workaround so that these will show on screen and not
+ be off-screen
+ """
+
+ def draw(self, screen: pygame.Surface, color):
+ pygame.draw.rect(screen, color, self.rect)
+
+
+class Goal(BoundingLine):
+ def draw(self, screen: pygame.Surface):
+ super().draw(
+ screen, WHITE
+ ) # the goal should be in white so you can see it
+
+
+class App:
+ def __init__(
+ self, flags=RESIZABLE, width=900, height=600, title="My game"
+ ):
+ pygame.init()
+ self.size = [width, height]
+ self.screen = pygame.display.set_mode(self.size, flags)
+ pygame.display.set_caption(title, title)
+ self.running = True
+
+ self.GAMESTATE = 0
+ self.WONSTATE = 1
+ self.QUITSTATE = 2
+
+ self.currstate = self.GAMESTATE
+ self.winning_player = 0 # will be 1 or 2 when a player won
+
+ self.executions = 0 # useful for debugging
+
+ def mainloop(self):
+ while self.running:
+ # main game loop (for the game itself)
+ # because this is a while loop, the game will keep going until someone won
+ # so we don't need to worry about the post-game text being displayed
+ if self.currstate == self.GAMESTATE:
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ # set the variables that are keeping the game running
+ # to values that won't keep the game running
+ self.running = False
+ self.currstate = self.QUITSTATE
+ else:
+ self.check_events(event)
+ self.check_collisions()
+ self.move_objects()
+ self.update_display()
+ pygame.display.update()
+ time.sleep(0.01)
+ self.executions += 1
+
+ if self.currstate == self.WONSTATE:
+ # 'post-game' game loop (just shows winning text)
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ self.currstate = self.QUITSTATE
+ self.display_winning_text()
+
+ if self.currstate == self.QUITSTATE:
+ pygame.quit()
+
+ def check_events(self, event) -> None:
+ pass
+
+ def check_collisions(self) -> None:
+ pass
+
+ def move_objects(self) -> None:
+ pass
+
+ def update_display(self) -> None:
+ pass
+
+ def display_winning_text(self) -> None:
+ pass
+
+
+class Hockey(App):
+ """
+ This is the functional class whose mainloop will be called
+ to play hockey.
+ This should inherit from App (where mainloop is defined)
+ Its methods should utilize the methods within the
+ game object classes.
+ """
+
+ def __init__(self):
+ super().__init__(title="Hockey!")
+
+ # initialize players
+ self.player_1 = Player(PLAYER1CONTROLS)
+ self.player_2 = Player(PLAYER2CONTROLS)
+
+ # move players to starting positions
+ self.player_1.move_to(
+ (
+ self.size[0] / 8 - self.player_1.rect.width,
+ self.size[1] / 2 - self.player_1.rect.height,
+ )
+ )
+ self.player_2.move_to(
+ (
+ self.size[0] / 8 * 7 - self.player_2.rect.width,
+ self.size[1] / 2 - self.player_2.rect.height,
+ )
+ )
+
+ # initialize ball and move it to starting position (center)
+ self.ball = Ball(BALL_RADIUS)
+ self.ball.move_to(
+ (
+ self.size[0] / 2 - self.ball.rect.width,
+ self.size[1] / 2 - self.ball.rect.height,
+ )
+ )
+
+ # initialize bounding lines - the edges of the screen off which the
+ # ball should bounce
+ self.top_line = BoundingLine((0, 0), (self.size[0], 0), "top")
+ self.bottom_line = BoundingLine(
+ (0, self.size[1]), (self.size[0], self.size[1]), "bottom"
+ )
+ self.left_line = BoundingLine((0, 0), (0, self.size[1]), "left")
+ self.right_line = BoundingLine(
+ (self.size[0], 0), (self.size[0], self.size[1]), "right"
+ )
+ self.bounding_lines = [
+ self.top_line,
+ self.bottom_line,
+ self.left_line,
+ self.right_line,
+ ]
+
+ # initialize Goals
+ self.goal_1 = Goal(
+ (0, (self.size[1] / 2) - (5 * self.size[1] / 16)),
+ (0, (self.size[1] / 2) + (self.size[1] / 16)),
+ "left",
+ 3,
+ )
+ self.goal_2 = Goal(
+ (self.size[0], (self.size[1] / 2) - (5 * self.size[1] / 16)),
+ (self.size[0], (self.size[1] / 2) + (self.size[1] / 16)),
+ "right",
+ 3,
+ )
+
+ def update_display(self):
+ """
+ This should fill the screen with BLACK and then
+ use the draw method of each of the objects to draw them onto
+ the screen.
+ """
+ pass # your code here
+
+ def move_objects(self):
+ """
+ This should use the move method for the players and the ball
+ """
+ pass # your code here
+
+ def check_events(self):
+ """
+ This should set the path for each of the players before setting
+ their speed.
+ """
+ pass # your code here
+
+ def check_collisions(self):
+ """
+ This should check whether the ball collided with the goal, the
+ bounding lines, or a player's paddle
+
+ If the ball collided with a goal, then set self.curstate to
+ self.WINSTATE, and set self.winner to player 1 if the ball
+ hit goal 2 or player 2 if the ball hit goal 1
+
+ Note: use the ball's methods (ie collide_paddle or collide_line)
+ """
+ pass # your code here
+
+ def display_winning_text(self):
+ self.screen.fill(BLACK)
+
+ self.font = pygame.font.SysFont(pygame.font.get_default_font(), 32)
+ if self.winning_player != 0:
+ font_img = self.font.render(
+ "Game Over. Player %s won" % str(self.winning_player),
+ True,
+ WHITE,
+ )
+ else:
+ # this won't actually be seen, but it prevents "Player 0 won" from showing
+ # up on the screen for a split-second if QUIT was pressed before someone won
+ font_img = self.font.render("Nobody won", True, WHITE)
+ font_rect = font_img.get_rect()
+ pygame.draw.rect(self.screen, BLACK, font_rect, 1)
+ self.screen.blit(font_img, font_rect)
+ pygame.display.update() # show the new text.
+
+
+our_game = Hockey()
+our_game.mainloop()
diff --git a/games/chapter4/solutions/flappy_bird/.DS_Store b/games/chapter4/solutions/flappy_bird/.DS_Store
new file mode 100644
index 00000000..5008ddfc
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/.DS_Store differ
diff --git a/games/chapter4/solutions/flappy_bird/OOPflappybird.py b/games/chapter4/solutions/flappy_bird/OOPflappybird.py
new file mode 100644
index 00000000..3df29777
--- /dev/null
+++ b/games/chapter4/solutions/flappy_bird/OOPflappybird.py
@@ -0,0 +1,500 @@
+import pygame
+import random
+
+pygame.init()
+
+# screen
+width = 800
+height = 600
+SIZE = (width, height)
+screen = pygame.display.set_mode(SIZE)
+
+# colors
+LGREEN = (62, 245, 59)
+DGREEN = (40, 143, 39)
+YELLOW = (250, 250, 37)
+WHITE = (255, 255, 255)
+BLACK = (0, 0, 0)
+RED = (255, 0, 0)
+LILAC = (175, 95, 237)
+LBLUE = (80, 221, 242)
+DBLUE = (80, 99, 242)
+PINK = (245, 144, 188)
+CYAN = (0, 150, 150)
+
+# images
+BACKGROUNDIMG = pygame.image.load("./background.png")
+BACKGROUNDIMG = pygame.transform.scale(BACKGROUNDIMG, (width, height))
+SPRITESHEET = pygame.image.load("./flyingbird.png")
+COINPIC = pygame.image.load("./coin.png")
+
+# ---------- States of the Game ----------
+MENUSTATE = 0 # Menu Screen
+GAMESTATE = 1 # Play Game
+LOSESTATE = 2 # u loose
+QUITSTATE = 3
+NUMSTATES = 4
+
+
+class GameObj:
+ """
+ An abstract class used as the base class for all the
+ game's objects
+ """
+
+ def __init__(self):
+ """
+ This __init__ method provides no functionality.
+ It merely enables the methods defined in this class.
+ Thus, calling super().__init__ is unnecessary.
+ """
+ self.rect = pygame.Rect
+
+ def draw(
+ self,
+ screen: pygame.Surface,
+ color: tuple,
+ specific_rect: pygame.Rect = None,
+ ):
+ """
+ Draws a rectangle onto the screen in the specified color.
+ If specific_rect is not None, draw specific_rect onto the screen.
+ If specific_rect is None, draw self.rect onto the screen.
+ """
+ if not specific_rect:
+ pygame.draw.rect(screen, color, self.rect)
+ else:
+ pygame.draw.rect(screen, color, specific_rect)
+
+ def move(self, speed: dict = None, specific_rect: pygame.Rect = None):
+ """
+ Moves a rectangle.
+ @param speed - The speed to move the rectangle at. It should be
+ a dictionary of form {'x': int, 'y': int}; for example,
+ {'x':33, 'y':-22}. If no speed is provided, uses self.speed
+ @param specific_rect - if specific_rect is None, then this method
+ will move self.rect. If specific_rect is not None, then this method will
+ move specific_rect
+ """
+ if not speed and hasattr(self, "speed"):
+ if specific_rect:
+ return specific_rect.move(self.speed["x"], self.speed["y"])
+ else:
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+ if speed:
+ if specific_rect:
+ return specific_rect.move(speed["x"], speed["y"])
+ else:
+ self.rect = self.rect.move(speed["x"], speed["y"])
+
+ def check_collision(self, other, specific_rect: pygame.Rect = None):
+ if not specific_rect:
+ return self.rect.colliderect(other.rect) == 1
+ else:
+ return specific_rect.colliderect(other.rect) == 1
+
+
+class Tubes(GameObj):
+ """
+ Class to represent the two tubes.
+
+ Ex:
+ The tubes will look sort of like the below drawing
+ (one on the top, one on the bottom)
+ (let - be top or bottom of school)
+ ------------
+ | |
+ |_|
+
+ _
+ | |
+ | |
+ | |
+ ------------
+ """
+
+ TUBEGAP = 230 # smaller TUBEGAP -> smaller dist between tubes
+ TUBEWIDTH = 100
+
+ def __init__(self, bottom_tube_height: int):
+ """
+ Initializes two pygame.Rect objects: one for the
+ top tube (call it top_tube) and one for the bottom tube
+ (call it bottom_tube). Uses the TUBEGAP
+ and TUBEWIDTH variables as dimensions.
+ """
+ self.bottom_tube = pygame.Rect(
+ width,
+ height - bottom_tube_height,
+ self.TUBEWIDTH,
+ bottom_tube_height,
+ )
+ self.top_tube = pygame.Rect(
+ width,
+ 0,
+ self.TUBEWIDTH,
+ height - bottom_tube_height - self.TUBEGAP,
+ )
+
+ def draw(self, screen: pygame.Surface):
+ """
+ Uses the draw() method from the inherited
+ GameObj class to draw the top and bottom tubes.
+ Hint: this will use the specific_rect argument
+ """
+ super().draw(screen, DGREEN, self.bottom_tube)
+ super().draw(screen, DGREEN, self.top_tube)
+
+ def move(self, speed: dict):
+ """
+ Uses the move() method from the inherited
+ GameObj class to move the specific top and bottom tubes.
+ Hint: this will use the specific_rect argument
+ """
+ self.bottom_tube = super().move(speed, self.bottom_tube)
+ self.top_tube = super().move(speed, self.top_tube)
+
+ def check_collision(self, other) -> bool:
+ """
+ Uses the check_collision() method from the inherited
+ GameObj class to check for any collisions
+ between the given object and the tubes.
+ Hint: this will use the specific_rect argument
+
+ Returns:
+ boolean - if either tube is collided with, return True
+ """
+ bottom = super().check_collision(other, self.bottom_tube)
+ top = super().check_collision(other, self.top_tube)
+ return bottom or top
+
+
+class Coin(GameObj):
+ """
+ The coin that the bird will get
+ in-between tubes.
+ Doesn't need to do anything, so pretty short class.
+ """
+
+ def __init__(self, center_y):
+ """
+ Makes a coin object.
+ The coin's x coordinate will be the width of the screen
+ The coin's y coordinate will be centered around `center_y`
+ @param center_y:int - the y coordinate to center the coin around
+ """
+ temprect = COINPIC.get_rect()
+ self.rect = pygame.Rect(
+ width,
+ center_y - temprect.height // 2,
+ temprect.width,
+ temprect.height,
+ )
+
+ def draw(self, screen: pygame.Surface):
+ super().draw(screen, BLACK)
+
+ def blit(self, screen: pygame.Surface):
+ screen.blit(COINPIC, self.rect)
+
+
+class Bird(GameObj):
+ """
+ The bird itself. It processes the sprites
+ and handles jumping.
+ """
+
+ start_center_pos = (width // 8, height // 2)
+
+ def __init__(self):
+ self.process_spritesheet(SPRITESHEET, 3, 3)
+ self.rect = pygame.Rect(
+ self.start_center_pos[0] - self.sprite_frame_width // 2,
+ self.start_center_pos[1] - self.sprite_frame_height // 2,
+ self.sprite_frame_width,
+ self.sprite_frame_height,
+ )
+ self.momentum = 0 # the bird's current vertical speed
+ self.jump_height = 15
+ self.min_speed = -10 # the maximum speed the bird flies down at
+ self.cur_sprite_idx = 0
+
+ def process_spritesheet(
+ self,
+ spritesheet: pygame.Surface,
+ num_pics_x: int,
+ num_pics_y: int,
+ offset_x: int = 0,
+ offset_y: int = 0,
+ ):
+ """
+ Creates sprites from the spritesheet.
+ @param spritesheet: pygame.Surface - the spritesheet.
+ @param num_pics_x: int - the number of sprites in each row on
+ the spritesheet
+ @param num_pics_y: int - the number of sprites in each column
+ on the spritesheet
+ @param offset_x: int - the x offset before the sprite rows start
+ @param offset_y: int - the y offset before the sprite columns start
+ """
+ self.sprites = []
+ self.sprite_frame_width = (
+ spritesheet.get_width() - offset_x
+ ) // num_pics_x
+ self.sprite_frame_height = (
+ spritesheet.get_height() - offset_y
+ ) // num_pics_y
+ for row in range(num_pics_x):
+ for column in range(num_pics_y):
+ temp = spritesheet.subsurface(
+ (
+ row * self.sprite_frame_width + offset_x,
+ column * self.sprite_frame_height + offset_y,
+ self.sprite_frame_width,
+ self.sprite_frame_height,
+ )
+ )
+ # get the bounding box for the actual colored pixels
+ # (so that we won't be blit-ing extra empty pixels)
+ # (makes collisions more accurate)
+ temprect = temp.get_bounding_rect()
+ # then, append the shortened image to the sprites list
+ self.sprites.append(temp.subsurface(temprect))
+
+ def draw(self, screen: pygame.Surface, framecount: int):
+ curr_sprite_idx = framecount // 5 % len(self.sprites)
+ if curr_sprite_idx != self.cur_sprite_idx:
+ # if it is now a different sprite, adjust self.rect
+ # so that it won't be bigger or smaller than the new sprite
+ self.cur_sprite_idx = curr_sprite_idx
+ temp = self.sprites[self.cur_sprite_idx]
+ self.rect = temp.get_rect().move(
+ self.rect.topleft[0], self.rect.topleft[1]
+ )
+ super().draw(screen, BLACK)
+
+ def blit(self, screen: pygame.Surface):
+ screen.blit(self.sprites[self.cur_sprite_idx], self.rect)
+
+ def process_movement(self, event):
+ if event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
+ self.momentum = self.jump_height
+
+ def move(self):
+ super().move({"x": 0, "y": -self.momentum})
+ self.momentum -= 1
+
+ # if the bird would fly down faster than self.min_speed,
+ # cap self.momentum at self.min_speed
+ if self.momentum < self.min_speed:
+ self.momentum = self.min_speed
+
+
+class Button(GameObj):
+ """
+ A Button with text. Used for the
+ 'Quit Game' 'Start Game' and 'Retry' buttons
+ """
+
+ def __init__(
+ self,
+ center_x: int,
+ center_y: int,
+ bgcolor: tuple,
+ textcolor: tuple,
+ text: str = "",
+ textsize: int = 32,
+ ):
+ """
+ Creates a Button object.
+ @param center_x: int - the x coordinate of the button's center
+ @param center_y: int - the y coordinate of the button's center
+ @param bgcolor: tuple - the color for the button's background
+ @param textcolor: tuple - the color for the button's text
+ @param text: str - the text to put inside the button
+ @param textsize: int - the size of the button's text
+ """
+ self.font = pygame.font.SysFont("arial", textsize)
+ self.font_img = self.font.render(text, True, textcolor)
+ self.rect = self.font_img.get_rect()
+ self.rect.center = (center_x, center_y)
+ self.bgcolor = bgcolor
+ self.active = True
+
+ def draw(self, screen: pygame.Surface):
+ super().draw(screen, self.bgcolor)
+ screen.blit(self.font_img, self.rect)
+
+ def is_clicked(self, event: pygame.event.Event):
+ if event.type == pygame.MOUSEBUTTONDOWN:
+ return event.pos in self
+
+ def __contains__(self, coordinate):
+ return self.rect.contains((coordinate[0], coordinate[1], 0, 0))
+
+
+class FlappyBird:
+ """
+ This is the game class.
+ """
+
+ def __init__(self):
+ self.running = True
+ self.gamestate = MENUSTATE
+ self.create_buttons()
+ self.framecount = 0
+ self.clock = pygame.time.Clock()
+
+ def create_buttons(self, button1text="Start Game", button2bg=RED):
+ self.buttons = {
+ "start": Button(
+ width // 2, height // 4, LGREEN, LILAC, button1text
+ ),
+ "quit": Button(
+ width // 2, height // 4 * 3, button2bg, LILAC, "Quit Game"
+ ),
+ }
+
+ def mainloop(self):
+ while self.running:
+ events = pygame.event.get()
+ for event in events:
+ self.set_state(event)
+ if event.type == pygame.QUIT:
+ self.running = False
+ if self.gamestate == MENUSTATE:
+ screen.fill(LBLUE)
+ self.draw_buttons(screen)
+
+ elif self.gamestate == GAMESTATE:
+ self.draw_all()
+ self.check_collisions()
+
+ # update bird's speed
+ for event in events:
+ self.bird.process_movement(event)
+ self.moveobjects()
+
+ self.create_tubes()
+
+ elif self.gamestate == LOSESTATE:
+ screen.fill(RED)
+ self.draw_buttons(screen)
+
+ elif self.gamestate == QUITSTATE:
+ self.running = False
+
+ self.framecount += 1
+ pygame.display.update()
+ self.clock.tick(60)
+ pygame.quit()
+
+ def set_state(self, event):
+ if event.type == pygame.MOUSEBUTTONDOWN:
+ if all([but.active for but in self.buttons.values()]):
+ if self.buttons["start"].is_clicked(event):
+ self.buttons["start"].active = False
+ self.buttons["quit"].active = False
+ self.startgame()
+ elif self.buttons["quit"].is_clicked(event):
+ self.gamestate = QUITSTATE
+ self.buttons["start"].active = False
+ self.buttons["quit"].active = False
+
+ def draw_background(self):
+ screen.blit(BACKGROUNDIMG, (self.background_x, 0))
+ screen.blit(BACKGROUNDIMG, (self.background_x + width, 0))
+ self.background_x -= 2
+ if self.background_x < -1 * width:
+ self.background_x = 0
+
+ def draw_score(self):
+ """
+ Writes the player's score onto the screen in the top
+ right corner.
+ Hint: this uses pygame fonts
+ """
+ font = pygame.font.SysFont("arial", 32)
+ font_img = font.render(f"Score : {self.score}", True, WHITE)
+ screen.blit(font_img, font_img.get_rect().move(width - 200, 0))
+
+ def draw_buttons(self, screen):
+ """
+ Draws the "start" and "quit" buttons onto the screen.
+ Hint: this uses `self.buttons` (which is already made)
+ """
+ self.buttons["start"].draw(screen)
+ self.buttons["quit"].draw(screen)
+
+ def create_tubes(self):
+ """
+ Creates tubes and puts a coin in the middle of each tube.
+ """
+ if (
+ len(self.tubes) == 0
+ or self.tubes[-1].bottom_tube.right < width - 200
+ ):
+ bottom_tube_height = random.randint(0, height - Tubes.TUBEGAP)
+ self.tubes.append(Tubes(bottom_tube_height))
+ self.coins.append(
+ Coin(height - bottom_tube_height - (Tubes.TUBEGAP // 2))
+ )
+
+ def draw_all(self):
+ # draw bird and coin rectangles before background so that they won't
+ # show
+ self.bird.draw(screen, self.framecount)
+ for coin in self.coins:
+ coin.draw(screen)
+
+ self.draw_background()
+
+ for tube in self.tubes:
+ tube.draw(screen)
+
+ # blit images/sprites onto the screen
+ self.bird.blit(screen)
+ for coin in self.coins:
+ coin.blit(screen)
+
+ self.draw_score()
+
+ def check_collisions(self):
+ for tube in self.tubes:
+ if tube.check_collision(self.bird):
+ self.gamestate = LOSESTATE
+ self.create_buttons("Retry?", LBLUE)
+ if tube.bottom_tube.right < 0:
+ self.tubes.remove(tube)
+
+ for coin in self.coins:
+ if self.bird.check_collision(coin):
+ self.score += 1
+ self.coins.remove(coin)
+ if coin.rect.right < 0:
+ self.coins.remove(coin)
+
+ if self.bird.rect.bottom > height: # fell out of screen
+ self.gamestate = LOSESTATE
+ self.create_buttons("Retry?", LBLUE)
+
+ def moveobjects(self):
+ SPEED = 3 # x speed that objects move towards the bird at
+ for tube in self.tubes:
+ tube.move({"x": -SPEED, "y": 0})
+ for coin in self.coins:
+ coin.move({"x": -SPEED, "y": 0})
+ self.bird.move()
+
+ def startgame(self):
+ self.gamestate = GAMESTATE
+ self.background_x = 0
+ self.score = 0
+ self.bird = Bird()
+ self.tubes = []
+ self.coins = []
+ self.create_tubes()
+
+
+a = FlappyBird()
+a.mainloop()
diff --git a/games/chapter4/solutions/flappy_bird/background.png b/games/chapter4/solutions/flappy_bird/background.png
new file mode 100644
index 00000000..0be8c233
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/background.png differ
diff --git a/games/chapter4/solutions/flappy_bird/bird.png b/games/chapter4/solutions/flappy_bird/bird.png
new file mode 100644
index 00000000..2b8a5b32
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/bird.png differ
diff --git a/games/chapter4/solutions/flappy_bird/coin.png b/games/chapter4/solutions/flappy_bird/coin.png
new file mode 100644
index 00000000..dc457bff
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/coin.png differ
diff --git a/games/chapter4/solutions/flappy_bird/explosion_transparent.png b/games/chapter4/solutions/flappy_bird/explosion_transparent.png
new file mode 100644
index 00000000..a92d7099
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/explosion_transparent.png differ
diff --git a/games/chapter4/solutions/flappy_bird/flyingbird.png b/games/chapter4/solutions/flappy_bird/flyingbird.png
new file mode 100644
index 00000000..25bd824a
Binary files /dev/null and b/games/chapter4/solutions/flappy_bird/flyingbird.png differ
diff --git a/games/chapter4/solutions/hockey.py b/games/chapter4/solutions/hockey.py
new file mode 100644
index 00000000..ab4dd66f
--- /dev/null
+++ b/games/chapter4/solutions/hockey.py
@@ -0,0 +1,606 @@
+# Make a two-player hockey game! The application will consist
+# of two rectangular paddles, starting on each side of the screen,
+# and one circular ball that players must bounce around. Players can
+# move the paddles in any direction to hit the ball into the goal.
+# If the ball makes contact with safe parts of the screen, it will
+# bounce off at a random angle but in the same general direction
+# (left or right). It will do the same if it makes contact with one
+# of the paddles, but will head towards the opposite general direction
+# instead. If the ball touches the goals on either side of the screen,
+# the application will say “Game Over. Player _ Wins”. You must put
+# your code in classes and have separate keys for each player to
+# move their paddles.
+
+
+import pygame
+from pygame.locals import (
+ K_w,
+ K_s,
+ K_a,
+ K_d,
+ K_UP,
+ K_DOWN,
+ K_LEFT,
+ K_RIGHT,
+ KEYDOWN,
+ KEYUP,
+ QUIT,
+ RESIZABLE,
+)
+from pygame.rect import Rect
+
+import math
+import time
+import random
+
+# define the necessary color constants using rgb values
+BLACK = (0, 0, 0)
+GREEN = (0, 120, 0)
+RED = (120, 0, 0)
+WHITE = (255, 255, 255)
+
+# define player controls
+PLAYER1CONTROLS = {"up": K_w, "down": K_s, "left": K_a, "right": K_d}
+PLAYER2CONTROLS = {
+ "up": K_UP,
+ "down": K_DOWN,
+ "left": K_LEFT,
+ "right": K_RIGHT,
+}
+
+# initial screensize
+SCREENSIZE = [900, 600]
+
+# how big the ball's radius will be
+BALL_RADIUS = 3
+
+
+class Game_obj:
+ def __init__(self):
+ # we don't want to pass actual values to the Rect class since
+ # we want this class to be abstract
+ self.rect = Rect
+ self.prev_rect = Rect
+ self.speed = {"x": 0, "y": 0}
+
+ def move(self):
+ self.prev_rect = self.rect
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+
+ def move_to(self, position: tuple):
+ self.prev_rect = self.rect
+ self.rect = self.rect.move(
+ position[0] - self.rect.topleft[0],
+ position[1] - self.rect.topleft[1],
+ )
+
+ def check_collision(self, other) -> bool:
+ return self.rect.colliderect(other.rect) == 1
+
+
+class Player(Game_obj):
+ PLAYERSPEED = (3, 3)
+ PADDLESIZE = (10, 50) # x width, y width
+
+ def __init__(self, control_keys: dict) -> None:
+ """
+ Creates a player rectangle with the provided control keys.
+ Arguments:
+ control_keys - (dict) should be a dictionary of the following format:
+ {
+ "up": (KEY) (ex: K_w),
+ "down": (KEY) (ex: K_s),
+ "left": (KEY) (ex: K_a),
+ "right": (KEY) (ex: K_d)
+ }
+ """
+ super().__init__()
+ self.control_keys = control_keys
+ self.rect = Rect(0, 0, self.PADDLESIZE[0], self.PADDLESIZE[1])
+ self.path = [0, 0]
+
+ def draw(self, screen: pygame.Surface):
+ pygame.draw.rect(screen, GREEN, self.rect)
+
+ def set_path(self, event):
+ self.key_checker(event, "up")
+ self.key_checker(event, "down")
+ self.key_checker(event, "left")
+ self.key_checker(event, "right")
+
+ def key_checker(self, event: pygame.event.Event, direction: str) -> None:
+ """
+ Helper function to deal with event keys. Sets self.path
+ according to PATH_VALUES
+ Arguments:
+ event(pygame.event.Event) - the event
+ direction(str) - the direction to check KEYDOWN and KEYUP for.
+ """
+ PATH_VALUES = {"up": 1, "down": 1, "left": 0, "right": 0}
+ DIRECTION_VALUES = {"up": -1, "down": 1, "left": -1, "right": 1}
+
+ # if the event doesn't have a key attribute, just return
+ if not hasattr(event, "key"):
+ return
+
+ # if it does, then check if it the right key
+ if event.key == self.control_keys[direction]:
+ if event.type == KEYUP:
+ self.path[PATH_VALUES[direction]] += -DIRECTION_VALUES[
+ direction
+ ]
+ if event.type == KEYDOWN:
+ self.path[PATH_VALUES[direction]] += DIRECTION_VALUES[
+ direction
+ ]
+
+ def set_speed(self) -> None:
+ """
+ Sets the speed according to the Player object's path.
+ This should be called after self.path has been set.
+ """
+ self.speed["x"] = (
+ self.path[0]
+ / math.sqrt(sum(abs(num) for num in self.path))
+ * self.PLAYERSPEED[0]
+ if (sum(abs(num) for num in self.path)) != 0
+ else self.path[0] * self.PLAYERSPEED[0]
+ )
+ self.speed["y"] = (
+ self.path[1]
+ / math.sqrt(sum(abs(num) for num in self.path))
+ * self.PLAYERSPEED[1]
+ if (sum(abs(num) for num in self.path)) != 0
+ else self.path[1] * self.PLAYERSPEED[1]
+ )
+
+
+class Ball(Game_obj):
+ BALLSPEED = (6, 6)
+
+ def __init__(self, radius: int) -> None:
+ super().__init__()
+ self.rect = Rect(0, 0, radius * 2, radius * 2)
+ self.radius = radius
+
+ # set up initial speed
+ initial_ang = random.randint(1, int(math.pi / 2 * 100)) / 100
+ self.speed["x"] = (
+ math.cos(initial_ang)
+ * self.BALLSPEED[0]
+ * (-1 if random.randint(0, 1) == 0 else 1)
+ )
+ self.speed["y"] = (
+ math.sin(initial_ang)
+ * self.BALLSPEED[1]
+ * (-1 if random.randint(0, 1) == 0 else 1)
+ )
+
+ def draw(self, screen: pygame.Surface):
+ pygame.draw.circle(
+ screen, WHITE, center=self.rect.center, radius=self.radius
+ )
+
+ def collide_line(self, other) -> bool:
+ """
+ Checks if the ball has hit a line.
+ If it did, update the speed accordingly
+
+ Arguments:
+ other (BoundingLine or Goal) - the line to check for a collision with
+ Returns:
+ True - if the collision happened
+ False - if the collision didn't happen
+ """
+ if self.check_collision(other):
+ if other.name == "top" or other.name == "bottom":
+ self.speed["y"] = -self.speed["y"]
+ return True
+ elif other.name == "left" or other.name == "right":
+ self.speed["x"] = -self.speed["x"]
+ return True
+ return False
+
+ def get_obj_path(self, object: Game_obj) -> tuple:
+ if object.speed["x"] > 0:
+ x_path = 1
+ elif object.speed["x"] < 0:
+ x_path = -1
+ else:
+ x_path = 0
+
+ if object.speed["y"] > 0:
+ y_path = 1
+ elif object.speed["y"] < 0:
+ y_path = -1
+ else:
+ y_path = 0
+
+ return (x_path, y_path)
+
+ def get_paddle_collision_dir(self, paddle: Player) -> tuple:
+ """
+ Gets the direction in which the ball will be headed
+ after a collision with a paddle.
+ Does not actually check if the collision happened
+
+ Arguments:
+ paddle (Player) - the player that the ball 'collided with'
+ Returns:
+ tuple(int, int) - a tuple of length 2 with just +-1's
+ ex: (1, 1) or (1, -1) or (-1, 1), or (-1, -1)
+ It corresponds to the direction in which the ball
+ will be headed. The first item will be the x direction
+ and the second item will be the y direction.
+ """
+ paddle_x_dir, paddle_y_dir = self.get_obj_path(paddle)
+
+ ball_x_dir, ball_y_dir = self.get_obj_path(self)
+
+ resulting_x_dir = None
+ resulting_y_dir = None
+
+ if paddle.speed["x"] == 0 or paddle_x_dir == ball_x_dir:
+ if abs(paddle.speed["x"]) > abs(self.speed["x"]):
+ resulting_x_dir = paddle_x_dir
+ elif abs(paddle.speed["x"]) < abs(self.speed["x"]):
+ resulting_x_dir = -ball_x_dir
+ else:
+ resulting_x_dir = -ball_x_dir
+
+ if paddle.speed["y"] == 0 or paddle_y_dir == ball_y_dir:
+ if abs(paddle.speed["y"]) > abs(self.speed["y"]):
+ resulting_y_dir = paddle_y_dir
+ elif abs(paddle.speed["y"]) < abs(self.speed["y"]):
+ resulting_y_dir = -ball_y_dir
+ else:
+ resulting_y_dir = -ball_y_dir
+
+ return (resulting_x_dir, resulting_y_dir)
+
+ def collide_paddle(self, paddle: Player, executions: int) -> None:
+ """
+ Handles collisions with paddles.
+ Arguments:
+ paddle(Player) - the paddle to check for a collision with
+ executions(int) - the amount of executions of the game's mainloop
+ It's not important, but it prevents unwanted collisions during
+ the 0th execution when we first set up the game by moving
+ the objects to the right place
+ """
+ PROPORTION = 0.25 # used when "escaping" a collision
+ MINIMUM_ANGLE = (
+ 30 # this is in degrees; it's just a fine-tuning aspect
+ )
+ # that makes the game more realistic
+
+ resulting_x_dir = None
+ resulting_y_dir = None
+
+ a = self.trace_collisions(paddle)
+ if a[0] and executions != 0:
+ resulting_x_dir, resulting_y_dir = a[1]
+
+ if self.check_collision(paddle):
+ resulting_x_dir, resulting_y_dir = self.get_paddle_collision_dir(
+ paddle
+ )
+
+ # if resulting_x_dir and resulting_y_dir aren't None, then update ball speed
+ if resulting_x_dir and resulting_y_dir:
+ print(MINIMUM_ANGLE * math.pi / 180 * 100)
+ print(math.pi / 2 * 100)
+ angle = (
+ random.randint(
+ 0,
+ int(
+ math.pi / 2 * 100
+ - (MINIMUM_ANGLE * math.pi / 180 * 100)
+ ),
+ )
+ / 100
+ )
+
+ print("angle", angle)
+
+ self.speed["x"] = (
+ math.cos(angle) * self.BALLSPEED[0] * resulting_x_dir
+ )
+ self.speed["y"] = (
+ math.sin(angle) * self.BALLSPEED[1] * resulting_y_dir
+ )
+
+ # escape the collision so as to prevent the "same" collision from being
+ # handled when collide_paddle is called next time.
+ while self.check_collision(paddle):
+ self.move_to(
+ (
+ self.rect.topleft[0] + PROPORTION * self.speed["x"],
+ self.rect.topleft[1] + PROPORTION * self.speed["y"],
+ )
+ )
+
+ def trace_collisions(self, paddle):
+ COLLISIONS_TO_CHECK = 30 # the higher this is, the slower.
+
+ # find how much the ball moved during the past execution
+ delta_x = self.rect.topleft[0] - self.prev_rect.topleft[0]
+ delta_y = self.rect.topleft[1] - self.prev_rect.topleft[1]
+
+ # find how much the paddle moved during the past execution
+ paddle_delta_x = paddle.rect.topleft[0] - paddle.prev_rect.topleft[0]
+ paddle_delta_y = paddle.rect.topleft[1] - paddle.prev_rect.topleft[1]
+
+ # check COLLISIONS_TO_CHECK times for a collision that occurred during
+ # the "update game" phase (when we moved the objects)
+ for i in range(COLLISIONS_TO_CHECK):
+ # move both the ball and the paddle/player to where they would've been if we
+ # subdivided the move phase into COLLISIONS_TO_CHECK individual frames
+ ball_past = Ball(BALL_RADIUS)
+ ball_past.move_to(
+ (
+ self.prev_rect.topleft[0]
+ + (delta_x * i / COLLISIONS_TO_CHECK),
+ self.prev_rect.topleft[1]
+ + (delta_y * i / COLLISIONS_TO_CHECK),
+ )
+ )
+ paddle_past = Player({})
+ paddle_past.move_to(
+ (
+ paddle.prev_rect.topleft[0]
+ + (paddle_delta_x * i / COLLISIONS_TO_CHECK),
+ paddle.prev_rect.topleft[1]
+ + (paddle_delta_y * i / COLLISIONS_TO_CHECK),
+ )
+ )
+
+ # now that we have a ball and a paddle, check if they collided
+ if ball_past.check_collision(paddle_past):
+ return (True, ball_past.get_paddle_collision_dir(paddle_past))
+ # if the loop failed (didn't return), then
+ # return False and an empty tuple
+ return (False, tuple())
+
+
+class BoundingLine:
+ DEFAULT_SIZE = 3
+
+ def __init__(
+ self,
+ start_coord: tuple,
+ end_coord: tuple,
+ name: str,
+ default_size=None,
+ ):
+ if default_size: # if a default size is provided
+ self.DEFAULT_SIZE = default_size
+
+ self.start_coord = start_coord # starting coordinate, normally topleft
+ self.end_coord = end_coord # ending coordinate, normally bottomright
+
+ self.rect = Rect(
+ start_coord[0],
+ start_coord[1],
+ end_coord[0]
+ if end_coord[0] - start_coord[0] != 0
+ else self.DEFAULT_SIZE,
+ end_coord[1]
+ if end_coord[1] - start_coord[1] != 0
+ else self.DEFAULT_SIZE,
+ )
+
+ self.name = name # this comes in handy in Ball's collide_line method
+
+ # small fix for bottom and right bounding lines getting drawn outside of
+ # the screen
+ if self.name == "bottom" and self.rect.bottom > SCREENSIZE[1]:
+ self.rect = self.rect.move(0, -3)
+ elif self.name == "right" and self.rect.right > SCREENSIZE[0]:
+ self.rect = self.rect.move(-3, 0)
+
+ def draw(self, screen: pygame.Surface, color=RED):
+ pygame.draw.rect(screen, color, self.rect)
+
+
+class Goal(BoundingLine):
+ def draw(self, screen: pygame.Surface):
+ super().draw(
+ screen, WHITE
+ ) # the goal should be in white so you can see it
+
+
+class App:
+ def __init__(
+ self, flags=RESIZABLE, width=900, height=600, title="My game"
+ ):
+ pygame.init()
+ self.size = [width, height]
+ self.screen = pygame.display.set_mode(self.size, flags)
+ pygame.display.set_caption(title, title)
+ self.running = True
+
+ self.GAMESTATE = 0
+ self.WONSTATE = 1
+ self.QUITSTATE = 2
+
+ self.currstate = self.GAMESTATE
+ self.winning_player = 0 # will be 1 or 2 when a player won
+
+ self.executions = 0 # useful for debugging
+
+ def mainloop(self):
+ while self.running:
+ # main game loop (for the game itself)
+ # because this is a while loop, the game will keep going until someone won
+ # so we don't need to worry about the post-game text being displayed
+ if self.currstate == self.GAMESTATE:
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ # set the variables that are keeping the game running
+ # to values that won't keep the game running
+ self.running = False
+ self.currstate = self.QUITSTATE
+ else:
+ self.check_events(event)
+ self.check_collisions()
+ self.move_objects()
+ self.update_display()
+ pygame.display.update()
+ time.sleep(0.01)
+ self.executions += 1
+
+ if self.currstate == self.WONSTATE:
+ # 'post-game' game loop (just shows winning text)
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ self.currstate = self.QUITSTATE
+ self.display_winning_text()
+
+ if self.currstate == self.QUITSTATE:
+ pygame.quit()
+
+ def check_events(self, event) -> None:
+ pass
+
+ def check_collisions(self) -> None:
+ pass
+
+ def move_objects(self) -> None:
+ pass
+
+ def update_display(self) -> None:
+ pass
+
+ def display_winning_text(self) -> None:
+ pass
+
+
+class Hockey(App):
+ def __init__(self):
+ super().__init__(title="Hockey!")
+
+ # initialize players
+ self.player_1 = Player(PLAYER1CONTROLS)
+ self.player_2 = Player(PLAYER2CONTROLS)
+
+ # move players to starting positions
+ self.player_1.move_to(
+ (
+ self.size[0] / 8 - self.player_1.rect.width,
+ self.size[1] / 2 - self.player_1.rect.height,
+ )
+ )
+ self.player_2.move_to(
+ (
+ self.size[0] / 8 * 7 - self.player_2.rect.width,
+ self.size[1] / 2 - self.player_2.rect.height,
+ )
+ )
+
+ # initialize ball and move it to starting position (center)
+ self.ball = Ball(BALL_RADIUS)
+ self.ball.move_to(
+ (
+ self.size[0] / 2 - self.ball.rect.width,
+ self.size[1] / 2 - self.ball.rect.height,
+ )
+ )
+
+ # initialize bounding lines - the edges of the screen off which the
+ # ball should bounce
+ self.top_line = BoundingLine((0, 0), (self.size[0], 0), "top")
+ self.bottom_line = BoundingLine(
+ (0, self.size[1]), (self.size[0], self.size[1]), "bottom"
+ )
+ self.left_line = BoundingLine((0, 0), (0, self.size[1]), "left")
+ self.right_line = BoundingLine(
+ (self.size[0], 0), (self.size[0], self.size[1]), "right"
+ )
+ self.bounding_lines = [
+ self.top_line,
+ self.bottom_line,
+ self.left_line,
+ self.right_line,
+ ]
+
+ # initialize Goals
+ self.goal_1 = Goal(
+ (0, (self.size[1] / 2) - (5 * self.size[1] / 16)),
+ (0, (self.size[1] / 2) + (self.size[1] / 16)),
+ "left",
+ 3,
+ )
+ self.goal_2 = Goal(
+ (self.size[0], (self.size[1] / 2) - (5 * self.size[1] / 16)),
+ (self.size[0], (self.size[1] / 2) + (self.size[1] / 16)),
+ "right",
+ 3,
+ )
+
+ def update_display(self) -> None:
+ self.screen.fill(BLACK) # fill the screen with black to clear it
+
+ # draw main objects
+ self.player_1.draw(self.screen)
+ self.player_2.draw(self.screen)
+ self.ball.draw(self.screen)
+
+ # draw bounding lines
+ self.top_line.draw(self.screen)
+ self.bottom_line.draw(self.screen)
+ self.left_line.draw(self.screen)
+ self.right_line.draw(self.screen)
+
+ # draw goals
+ self.goal_1.draw(self.screen)
+ self.goal_2.draw(self.screen)
+
+ def move_objects(self) -> None:
+ self.player_1.move()
+ self.player_2.move()
+ self.ball.move()
+
+ def check_events(self, event) -> None:
+ self.player_1.set_path(event)
+ self.player_2.set_path(event)
+
+ self.player_1.set_speed()
+ self.player_2.set_speed()
+
+ def check_collisions(self) -> None:
+ for line in self.bounding_lines:
+ self.ball.collide_line(line)
+ self.ball.collide_paddle(self.player_1, self.executions)
+ self.ball.collide_paddle(self.player_2, self.executions)
+
+ if self.ball.collide_line(self.goal_1):
+ self.currstate = self.WONSTATE
+ self.winning_player = 2 # player 2 (right player) scored
+ elif self.ball.collide_line(self.goal_2):
+ self.currstate = self.WONSTATE
+ self.winning_player = 1 # player 1 (left player) scored
+
+ def display_winning_text(self) -> None:
+ self.screen.fill(BLACK)
+
+ self.font = pygame.font.SysFont(pygame.font.get_default_font(), 32)
+ if self.winning_player != 0:
+ font_img = self.font.render(
+ "Game Over. Player %s won" % str(self.winning_player),
+ True,
+ WHITE,
+ )
+ else:
+ # this won't actually be seen, but it prevents "Player 0 won" from showing
+ # up on the screen for a split-second if QUIT was pressed before someone won
+ font_img = self.font.render("Nobody won", True, WHITE)
+ font_rect = font_img.get_rect()
+ pygame.draw.rect(self.screen, BLACK, font_rect, 1)
+ self.screen.blit(font_img, font_rect)
+ pygame.display.update() # show the new text.
+
+
+our_game = Hockey()
+our_game.mainloop()
diff --git a/games/chapter5/examples/bounce.wav b/games/chapter5/examples/bounce.wav
new file mode 100644
index 00000000..44f62bce
Binary files /dev/null and b/games/chapter5/examples/bounce.wav differ
diff --git a/games/chapter5/examples/bouncing_rect.py b/games/chapter5/examples/bouncing_rect.py
new file mode 100644
index 00000000..f838b0b5
--- /dev/null
+++ b/games/chapter5/examples/bouncing_rect.py
@@ -0,0 +1,47 @@
+import pygame
+import time # not necessary, but used for frame cap
+
+pygame.init() # initialize pygame module
+
+bounce_sound = pygame.mixer.Sound("./bounce.wav")
+
+SCREEN_SIZE = (600, 400)
+RECT_SIZE = (100, 100)
+RED = (255, 0, 0)
+BLACK = (0, 0, 0)
+momentum = [1, 1] # (down and right)
+
+window = pygame.display.set_mode(SCREEN_SIZE)
+running = True
+
+# start the rectangle in the middle of the screen
+x = SCREEN_SIZE[0] // 2
+y = SCREEN_SIZE[1] // 2
+
+while running:
+ # if the rectangle collided with the left or right side
+ # of the screen
+ if x + RECT_SIZE[0] >= SCREEN_SIZE[0] or x <= 0:
+ momentum[0] = -momentum[0]
+ bounce_sound.play() # add in the bounce sound for collisions
+ # if the rectangle collided with the top or bottom
+ # of the screen
+ if y + RECT_SIZE[1] >= SCREEN_SIZE[1] or y <= 0:
+ momentum[1] = -momentum[1]
+ bounce_sound.play() # add in the bounce sound for collisions
+
+ # add the speed to the current x and y to get the
+ # new x and y
+ x += momentum[0]
+ y += momentum[1]
+
+ window.fill(BLACK) # 'erase' the previous frame
+ pygame.draw.rect(window, RED, (x, y, RECT_SIZE[0], RECT_SIZE[1]))
+ pygame.display.update() # update the display
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ running = False
+ time.sleep(0.01) # frame cap to make the rectangle more visible
+
+pygame.quit() # deactivate the pygame module
diff --git a/games/chapter5/practice/TankIcon.png b/games/chapter5/practice/TankIcon.png
new file mode 100644
index 00000000..d9e00bbe
Binary files /dev/null and b/games/chapter5/practice/TankIcon.png differ
diff --git a/games/chapter5/practice/Target.png b/games/chapter5/practice/Target.png
new file mode 100644
index 00000000..d48bc78e
Binary files /dev/null and b/games/chapter5/practice/Target.png differ
diff --git a/games/chapter5/practice/bullet.png b/games/chapter5/practice/bullet.png
new file mode 100644
index 00000000..fce8c161
Binary files /dev/null and b/games/chapter5/practice/bullet.png differ
diff --git a/games/chapter5/practice/completetank.png b/games/chapter5/practice/completetank.png
new file mode 100644
index 00000000..036cd84b
Binary files /dev/null and b/games/chapter5/practice/completetank.png differ
diff --git a/games/chapter5/practice/explosion.wav b/games/chapter5/practice/explosion.wav
new file mode 100644
index 00000000..c33f0412
Binary files /dev/null and b/games/chapter5/practice/explosion.wav differ
diff --git a/games/chapter5/practice/fire.wav b/games/chapter5/practice/fire.wav
new file mode 100644
index 00000000..17a60459
Binary files /dev/null and b/games/chapter5/practice/fire.wav differ
diff --git a/games/chapter5/practice/simple_bg.wav b/games/chapter5/practice/simple_bg.wav
new file mode 100644
index 00000000..b09f4e25
Binary files /dev/null and b/games/chapter5/practice/simple_bg.wav differ
diff --git a/games/chapter5/practice/sound_tanks.py b/games/chapter5/practice/sound_tanks.py
new file mode 100644
index 00000000..e33e7e6a
--- /dev/null
+++ b/games/chapter5/practice/sound_tanks.py
@@ -0,0 +1,468 @@
+# Edit the code from the tank game so that it will:
+# play an explosion sound when a target is hit or run over
+# play a gunshot sound when a bullet is "fired" (created)
+# play background music during the whole game.
+
+# you can either use the provided .wav files or find
+# your own (responsibly, of course).
+
+# Note:
+# because the pygame.init is inside App's init method, we don't put
+# sounds here. instead, we make the sounds and the music inside Tankgame's
+# init method.
+
+
+import pygame
+
+from pygame.locals import (
+ K_w,
+ K_s,
+ K_a,
+ K_d,
+ KEYDOWN,
+ KEYUP,
+ QUIT,
+ RESIZABLE,
+ MOUSEBUTTONDOWN,
+)
+import time
+import math
+import random
+
+BULLET_IMG_PATH = "./bullet.png"
+TARGET_IMG_PATH = "./target.png"
+TANK_IMG_PATH = "./completetank.png"
+
+BLACK = (255, 255, 255)
+DIRTBROWN = (168, 95, 0)
+SANDBROWN = (237, 201, 175)
+
+TANKSPEED = [2, 2] # speed x and speed y
+BULLETSPEED = [8, 8]
+
+
+class Game_obj:
+ def __init__(self, picture: str, **kwargs) -> None:
+ """
+ A basic game object class. It handles collisions,
+ the basic drawing method, the move and moveto methods,
+ and the check_out_of_screen method.
+
+ Arguments:
+ picture:str - the location of the picture that will be displayed on
+ the screen for this object
+ Valid keyword arguments:
+ "size":tuple(x,y) - a specific size that you want to have the object be.
+ The picture will be scaled to that size and the hitbox
+ will be updated accordingly.
+ "position":tuple(x,y) - the tuple at which the top left of the object
+ should be positioned at
+ "speed":tuple(x,y) - the tuple that represents the object's speed.
+ """
+ self.name = ""
+
+ # self.image will be a pygame.Surface class
+ self.image = pygame.image.load(picture)
+ self.image = (
+ pygame.transform.scale(
+ self.image, (kwargs["size"][0], kwargs["size"][1])
+ )
+ if "size" in kwargs
+ else self.image
+ )
+
+ self.rect = (
+ self.image.get_rect()
+ ) # self.rect will be of pygame.Rect class
+ self.size = self.rect.size # will be a tuple of (sizex, sizey)
+
+ if "position" in kwargs:
+ self.moveto(kwargs["position"])
+
+ self.speed = (
+ {"x": kwargs["speed"][0], "y": kwargs["speed"][1]}
+ if "speed" in kwargs
+ else {"x": 0, "y": 0}
+ )
+
+ def check_collision(self, other: object) -> bool:
+ if not isinstance(other, Game_obj):
+ raise TypeError(
+ "Invalid type; need a game_obj or a child class of game_obj"
+ )
+ # the rect class's colliderect method returns 1 if there is
+ # a collision and 0 if there isn't a collision
+ return self.rect.colliderect(other.rect) == 1
+
+ def draw(self, screen: pygame.Surface, color: tuple) -> None:
+ pygame.draw.rect(screen, color, self.rect, 0)
+ screen.blit(self.image, self.rect)
+
+ def move(self) -> None:
+ """
+ Moves the object according to it's current speed.
+ """
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+ # self.draw(screen, color)
+
+ def set_speed(self, new_speed: tuple) -> None:
+ """
+ Sets the object's speed to the provided tuple
+ Arguments:
+ new_speed (tuple(x,y)) - a tuple containing the desired speed for
+ the object to have.
+ """
+ self.speed["x"], self.speed["y"] = new_speed[0], new_speed[1]
+
+ def moveto(self, position: tuple) -> None:
+ """
+ A helper function that moves the rectangle to the desired position.
+
+ Arguments:
+ position (tuple) - the x and y coordinates of where you want the rectangle's
+ top left to be moved to.
+ """
+ self.rect = self.rect.move(
+ position[0] - self.rect.topleft[0],
+ position[1] - self.rect.topleft[1],
+ )
+
+ def check_out_of_screen(self, screen_size: tuple) -> bool:
+ """
+ Checks whether or not the object is completely outside of the screen.
+ Returns True or False accordingly.
+ Arguments:
+ screen_size (tuple) - the size of the screen (x,y)
+ """
+ if (
+ self.rect.bottom > screen_size[1]
+ or self.rect.top < 0
+ or self.rect.left < 0
+ or self.rect.right > screen_size[0]
+ ):
+ return True
+ return False
+
+ def __str__(self):
+ return (
+ f"{self.name} object located at the position {self.rect.topleft}"
+ )
+
+
+class Bullet(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(BULLET_IMG_PATH, **kwargs)
+ self.name = "Bullet"
+
+
+class Target(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ kwargs["size"] = 40, 40
+ super().__init__(TARGET_IMG_PATH, **kwargs)
+ self.name = "Target"
+
+
+class Tank(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(TANK_IMG_PATH, **kwargs)
+ self.direction = [0, 0]
+ self.SPEED = kwargs["speed"] if "speed" in kwargs else [2, 2]
+ self.speed["x"], self.speed["y"] = 0, 0
+
+ def set_speed(self) -> None:
+ # use math stuff to calculate the speed given that the
+ # max speed is self.SPEED
+ self.speed["x"] = (
+ self.direction[0]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[0]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[0] * self.SPEED[0]
+ )
+ self.speed["y"] = (
+ self.direction[1]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[1]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[1] * self.SPEED[1]
+ )
+
+ def set_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] -= 1
+ if direction == "down":
+ self.direction[1] += 1
+ if direction == "left":
+ self.direction[0] -= 1
+ if direction == "right":
+ self.direction[0] += 1
+
+ def unset_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] += 1
+ if direction == "down":
+ self.direction[1] -= 1
+ if direction == "left":
+ self.direction[0] += 1
+ if direction == "right":
+ self.direction[0] -= 1
+
+
+class App:
+ """
+ The abstract base class for the actual Tank_game class. It's
+ main purpose is to define a structure for the game.
+ It's structure is as follows:
+ Upon initialization, it runs the create_objects method
+ It's mainloop is comprised of the following methods:
+ check_events
+ check_collisions
+ move_objects
+ update_display
+ """
+
+ def __init__(
+ self, flags=RESIZABLE, width=960, height=540, title="My Game"
+ ):
+ pygame.init()
+ self.size = [width, height]
+ self.screen = pygame.display.set_mode(self.size, flags)
+ pygame.display.set_caption(title, title)
+
+ self.running = True
+
+ self.create_objects()
+
+ def create_objects(self):
+ """
+ This should create the initial objects on the screen.
+ """
+ pass
+
+ def check_events(self, event):
+ """
+ This should take user input and handle it appropriately.
+ """
+ pass
+
+ def update_display(self):
+ """
+ This should utilize clear the screen and then draw
+ all current objects onto the screen.
+ """
+ pass
+
+ def move_objects(self):
+ """
+ This should utilize the move method that the game objects have.
+ """
+ pass
+
+ def check_collisions(self):
+ """
+ This should utilize the check_collision method that the game objects
+ have.
+ """
+ pass
+
+ def mainloop(self):
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ break
+ else:
+ self.check_events(
+ event
+ ) # this will handle checking for user input
+ # such as KEYUP and MOUSEBUTTONDOWN events needed to run the game
+ self.check_collisions() # checks collisions between bullet/tank and targets
+ self.move_objects() # moves each object on the screen
+ self.update_display() # redraws updated objects onto the screen
+ pygame.display.update() # pygame’s method to show the updated screen
+ time.sleep(0.01) # not necessary; it's a frame cap
+ pygame.quit()
+
+
+class Tank_Game(App):
+ def __init__(self):
+ # this can be changed, it's the number of targets allowed at a time.
+ # we initialize this before super().__init__ because super().__init__ calls
+ # create_objects, which utilizes self.NUM_TARGETS
+ self.NUM_TARGETS = 3
+
+ super().__init__(title="Tanks")
+
+ self.playerscore = 0 # the player's score
+
+ # sets the display icon to the TankIcon.png provided
+ pygame.display.set_icon(pygame.image.load("./TankIcon.png"))
+
+ def create_objects(self):
+ """
+ This creates the initial objects seen when the game
+ first starts up.
+ """
+ # tank
+ self.tank = Tank(speed=TANKSPEED)
+ self.tank.moveto(
+ (
+ self.size[0] / 2 - self.tank.size[0], # move to middle x
+ self.size[1] - self.tank.size[1], # move to bottom y
+ )
+ )
+
+ # targets
+ self.targets = [Target(speed=[0, 0]) for i in range(self.NUM_TARGETS)]
+ for target in self.targets:
+ target.moveto(
+ (
+ random.randint(
+ 0, self.size[0] - target.size[0]
+ ), # random x
+ random.randint(
+ 0, self.size[1] - target.size[1]
+ ), # random y
+ )
+ )
+
+ # bullets
+ self.bullets = []
+
+ # Score text
+ self.font = pygame.font.SysFont(pygame.font.get_default_font(), 32)
+
+ def check_events(self, event):
+ """
+ We imported all from pygame.locals, so that means
+ that we can check KEYDOWN and KEYUP and individual
+ keys such as K_w (w key), K_a (a key), etc.
+ """
+ # change the path of the tank if w, a, s, or d was pressed
+ if event.type == KEYDOWN:
+ if event.key == K_w:
+ self.tank.set_path("up")
+ if event.key == K_s:
+ self.tank.set_path("down")
+ if event.key == K_a:
+ self.tank.set_path("left")
+ if event.key == K_d:
+ self.tank.set_path("right")
+ if event.type == KEYUP:
+ if event.key == K_w:
+ self.tank.unset_path("up")
+ if event.key == K_s:
+ self.tank.unset_path("down")
+ if event.key == K_a:
+ self.tank.unset_path("left")
+ if event.key == K_d:
+ self.tank.unset_path("right")
+ self.tank.set_speed()
+
+ # create bullets if mouse button was pressed
+ if event.type == MOUSEBUTTONDOWN:
+ bul = Bullet(speed=BULLETSPEED)
+ bul.moveto(
+ (self.tank.rect.centerx, (self.tank.rect.top - bul.size[1]))
+ ) # move the bullet to the front of the tank
+
+ # math stuff to calculate trajectory
+ mouse_pos = pygame.mouse.get_pos()
+ h = mouse_pos[1] - bul.rect.center[1]
+ w = mouse_pos[0] - bul.rect.center[0]
+ hyp = math.sqrt(h**2 + w**2)
+ vertical_speed = (
+ BULLETSPEED[1] * (h / hyp) if hyp != 0 else BULLETSPEED[1] * h
+ )
+ horizontal_speed = (
+ BULLETSPEED[0] * (w / hyp) if hyp != 0 else BULLETSPEED[0] * w
+ )
+
+ bul.set_speed((horizontal_speed, vertical_speed))
+ self.bullets.append(bul)
+
+ def move_objects(self):
+ """
+ This method moves the objects within the game.
+ If a bullet is outside of the screen, it is
+ not moved and is unreferenced.
+ """
+ self.tank.move()
+
+ self.bullets = [
+ bullet
+ for bullet in self.bullets
+ if bullet.check_out_of_screen(self.size) is False
+ ]
+
+ for bullet in self.bullets:
+ bullet.move()
+
+ def check_collisions(self):
+ """
+ This checks whether any of the objects within the game have collided
+ with each other. Specifically, we are looking for collisions between
+ bullets and targets or the tank and targets
+ """
+ deletions = 0 # number of targets deleted
+ num_bullets = len(self.bullets)
+
+ # check bullet-target collisions
+ for i in range(num_bullets):
+ for target in self.targets:
+ # if the bullet collided with the target
+ if self.bullets[i - deletions].check_collision(target) is True:
+ # pop both the bullet and target so that they will be
+ # effectively deleted
+ self.bullets.pop(i - deletions)
+ self.targets.pop(self.targets.index(target))
+
+ # give points for hitting the target
+ self.playerscore += 20
+ deletions += 1
+ break # stop the current iteration since the target and
+ # bullet are popped, so referencing them would error.
+
+ # check tank-target collisions
+ for target in self.targets:
+ if self.tank.check_collision(target) is True:
+ self.targets.pop(self.targets.index(target))
+ deletions += 1
+ self.playerscore += 10 # only 10 for running over targets lol
+
+ # create a new target for every deleted target
+ for i in range(deletions):
+ a = Target(speed=[0, 0])
+ a.moveto(
+ (
+ random.randint(0, self.size[0] - a.size[0]),
+ random.randint(0, self.size[1] - a.size[1]),
+ )
+ )
+ self.targets.append(a)
+
+ def update_display(self):
+ self.screen.fill(SANDBROWN)
+
+ # tank
+ self.tank.draw(self.screen, SANDBROWN)
+
+ # targets
+ for target in self.targets:
+ target.draw(self.screen, BLACK)
+
+ # bullets
+ for bullet in self.bullets:
+ bullet.draw(self.screen, BLACK)
+
+ # score text
+ font_img = self.font.render(
+ "Score: %s" % str(self.playerscore), True, BLACK
+ )
+ font_rect = font_img.get_rect()
+ pygame.draw.rect(self.screen, SANDBROWN, font_rect, 1)
+ self.screen.blit(font_img, font_rect)
+
+
+game = Tank_Game()
+game.mainloop()
diff --git a/games/chapter5/solutions/TankIcon.png b/games/chapter5/solutions/TankIcon.png
new file mode 100644
index 00000000..d9e00bbe
Binary files /dev/null and b/games/chapter5/solutions/TankIcon.png differ
diff --git a/games/chapter5/solutions/Target.png b/games/chapter5/solutions/Target.png
new file mode 100644
index 00000000..d48bc78e
Binary files /dev/null and b/games/chapter5/solutions/Target.png differ
diff --git a/games/chapter5/solutions/bullet.png b/games/chapter5/solutions/bullet.png
new file mode 100644
index 00000000..fce8c161
Binary files /dev/null and b/games/chapter5/solutions/bullet.png differ
diff --git a/games/chapter5/solutions/completetank.png b/games/chapter5/solutions/completetank.png
new file mode 100644
index 00000000..036cd84b
Binary files /dev/null and b/games/chapter5/solutions/completetank.png differ
diff --git a/games/chapter5/solutions/explosion.wav b/games/chapter5/solutions/explosion.wav
new file mode 100644
index 00000000..c33f0412
Binary files /dev/null and b/games/chapter5/solutions/explosion.wav differ
diff --git a/games/chapter5/solutions/fire.wav b/games/chapter5/solutions/fire.wav
new file mode 100644
index 00000000..17a60459
Binary files /dev/null and b/games/chapter5/solutions/fire.wav differ
diff --git a/games/chapter5/solutions/simple_bg.wav b/games/chapter5/solutions/simple_bg.wav
new file mode 100644
index 00000000..b09f4e25
Binary files /dev/null and b/games/chapter5/solutions/simple_bg.wav differ
diff --git a/games/chapter5/solutions/sound_tanks.py b/games/chapter5/solutions/sound_tanks.py
new file mode 100644
index 00000000..051e181c
--- /dev/null
+++ b/games/chapter5/solutions/sound_tanks.py
@@ -0,0 +1,483 @@
+# Edit the code from the tank game so that it will:
+# play an explosion sound when a target is hit or run over
+# play a gunshot sound when a bullet is "fired" (created)
+# play background music during the whole game.
+
+# you can either use the provided .wav files or find
+# your own (responsibly, of course).
+
+# Note:
+# because the pygame.init is inside App's init method, we don't put
+# sounds here. instead, we make the sounds and the music inside Tankgame's
+# init method.
+
+
+import pygame
+
+from pygame.locals import (
+ K_w,
+ K_s,
+ K_a,
+ K_d,
+ KEYDOWN,
+ KEYUP,
+ QUIT,
+ RESIZABLE,
+ MOUSEBUTTONDOWN,
+)
+import time
+import math
+import random
+
+BULLET_IMG_PATH = "./bullet.png"
+TARGET_IMG_PATH = "./target.png"
+TANK_IMG_PATH = "./completetank.png"
+
+BLACK = (255, 255, 255)
+DIRTBROWN = (168, 95, 0)
+SANDBROWN = (237, 201, 175)
+
+TANKSPEED = [2, 2] # speed x and speed y
+BULLETSPEED = [8, 8]
+
+
+class Game_obj:
+ def __init__(self, picture: str, **kwargs) -> None:
+ """
+ A basic game object class. It handles collisions,
+ the basic drawing method, the move and moveto methods,
+ and the check_out_of_screen method.
+
+ Arguments:
+ picture:str - the location of the picture that will be displayed on
+ the screen for this object
+ Valid keyword arguments:
+ "size":tuple(x,y) - a specific size that you want to have the object be.
+ The picture will be scaled to that size and the hitbox
+ will be updated accordingly.
+ "position":tuple(x,y) - the tuple at which the top left of the object
+ should be positioned at
+ "speed":tuple(x,y) - the tuple that represents the object's speed.
+ """
+ self.name = ""
+
+ # self.image will be a pygame.Surface class
+ self.image = pygame.image.load(picture)
+ self.image = (
+ pygame.transform.scale(
+ self.image, (kwargs["size"][0], kwargs["size"][1])
+ )
+ if "size" in kwargs
+ else self.image
+ )
+
+ self.rect = (
+ self.image.get_rect()
+ ) # self.rect will be of pygame.Rect class
+ self.size = self.rect.size # will be a tuple of (sizex, sizey)
+
+ if "position" in kwargs:
+ self.moveto(kwargs["position"])
+
+ self.speed = (
+ {"x": kwargs["speed"][0], "y": kwargs["speed"][1]}
+ if "speed" in kwargs
+ else {"x": 0, "y": 0}
+ )
+
+ def check_collision(self, other: object) -> bool:
+ if not isinstance(other, Game_obj):
+ raise TypeError(
+ "Invalid type; need a game_obj or a child class of game_obj"
+ )
+ # the rect class's colliderect method returns 1 if there is
+ # a collision and 0 if there isn't a collision
+ return self.rect.colliderect(other.rect) == 1
+
+ def draw(self, screen: pygame.Surface, color: tuple) -> None:
+ pygame.draw.rect(screen, color, self.rect, 0)
+ screen.blit(self.image, self.rect)
+
+ def move(self) -> None:
+ """
+ Moves the object according to it's current speed.
+ """
+ self.rect = self.rect.move(self.speed["x"], self.speed["y"])
+ # self.draw(screen, color)
+
+ def set_speed(self, new_speed: tuple) -> None:
+ """
+ Sets the object's speed to the provided tuple
+ Arguments:
+ new_speed (tuple(x,y)) - a tuple containing the desired speed for
+ the object to have.
+ """
+ self.speed["x"], self.speed["y"] = new_speed[0], new_speed[1]
+
+ def moveto(self, position: tuple) -> None:
+ """
+ A helper function that moves the rectangle to the desired position.
+
+ Arguments:
+ position (tuple) - the x and y coordinates of where you want the rectangle's
+ top left to be moved to.
+ """
+ self.rect = self.rect.move(
+ position[0] - self.rect.topleft[0],
+ position[1] - self.rect.topleft[1],
+ )
+
+ def check_out_of_screen(self, screen_size: tuple) -> bool:
+ """
+ Checks whether or not the object is completely outside of the screen.
+ Returns True or False accordingly.
+ Arguments:
+ screen_size (tuple) - the size of the screen (x,y)
+ """
+ if (
+ self.rect.bottom > screen_size[1]
+ or self.rect.top < 0
+ or self.rect.left < 0
+ or self.rect.right > screen_size[0]
+ ):
+ return True
+ return False
+
+ def __str__(self):
+ return (
+ f"{self.name} object located at the position {self.rect.topleft}"
+ )
+
+
+class Bullet(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(BULLET_IMG_PATH, **kwargs)
+ self.name = "Bullet"
+
+
+class Target(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ kwargs["size"] = 40, 40
+ super().__init__(TARGET_IMG_PATH, **kwargs)
+ self.name = "Target"
+
+
+class Tank(Game_obj):
+ def __init__(self, **kwargs) -> None:
+ super().__init__(TANK_IMG_PATH, **kwargs)
+ self.direction = [0, 0]
+ self.SPEED = kwargs["speed"] if "speed" in kwargs else [2, 2]
+ self.speed["x"], self.speed["y"] = 0, 0
+
+ def set_speed(self) -> None:
+ # use math stuff to calculate the speed given that the
+ # max speed is self.SPEED
+ self.speed["x"] = (
+ self.direction[0]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[0]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[0] * self.SPEED[0]
+ )
+ self.speed["y"] = (
+ self.direction[1]
+ / math.sqrt(sum(abs(num) for num in self.direction))
+ * self.SPEED[1]
+ if (sum(abs(num) for num in self.direction)) != 0
+ else self.direction[1] * self.SPEED[1]
+ )
+
+ def set_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] -= 1
+ if direction == "down":
+ self.direction[1] += 1
+ if direction == "left":
+ self.direction[0] -= 1
+ if direction == "right":
+ self.direction[0] += 1
+
+ def unset_path(self, direction: str) -> None:
+ if direction == "up":
+ self.direction[1] += 1
+ if direction == "down":
+ self.direction[1] -= 1
+ if direction == "left":
+ self.direction[0] += 1
+ if direction == "right":
+ self.direction[0] -= 1
+
+
+class App:
+ """
+ The abstract base class for the actual Tank_game class. It's
+ main purpose is to define a structure for the game.
+ It's structure is as follows:
+ Upon initialization, it runs the create_objects method
+ It's mainloop is comprised of the following methods:
+ check_events
+ check_collisions
+ move_objects
+ update_display
+ """
+
+ def __init__(
+ self, flags=RESIZABLE, width=960, height=540, title="My Game"
+ ):
+ pygame.init()
+ self.size = [width, height]
+ self.screen = pygame.display.set_mode(self.size, flags)
+ pygame.display.set_caption(title, title)
+
+ self.running = True
+
+ self.create_objects()
+
+ def create_objects(self):
+ """
+ This should create the initial objects on the screen.
+ """
+ pass
+
+ def check_events(self, event):
+ """
+ This should take user input and handle it appropriately.
+ """
+ pass
+
+ def update_display(self):
+ """
+ This should utilize clear the screen and then draw
+ all current objects onto the screen.
+ """
+ pass
+
+ def move_objects(self):
+ """
+ This should utilize the move method that the game objects have.
+ """
+ pass
+
+ def check_collisions(self):
+ """
+ This should utilize the check_collision method that the game objects
+ have.
+ """
+ pass
+
+ def mainloop(self):
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ break
+ else:
+ self.check_events(
+ event
+ ) # this will handle checking for user input
+ # such as KEYUP and MOUSEBUTTONDOWN events needed to run the game
+ self.check_collisions() # checks collisions between bullet/tank and targets
+ self.move_objects() # moves each object on the screen
+ self.update_display() # redraws updated objects onto the screen
+ pygame.display.update() # pygame’s method to show the updated screen
+ time.sleep(0.01) # not necessary; it's a frame cap
+ pygame.quit()
+
+
+class Tank_Game(App):
+ def __init__(self):
+ # this can be changed, it's the number of targets allowed at a time.
+ # we initialize this before super().__init__ because super().__init__ calls
+ # create_objects, which utilizes self.NUM_TARGETS
+ self.NUM_TARGETS = 3
+
+ super().__init__(title="Tanks")
+
+ self.playerscore = 0 # the player's score
+
+ # sets the display icon to the TankIcon.png provided
+ pygame.display.set_icon(pygame.image.load("./TankIcon.png"))
+
+ # set up the sounds
+ self.fire_sound = pygame.mixer.Sound("./fire.wav")
+ self.explosion_sound = pygame.mixer.Sound("./explosion.wav")
+
+ # load the music and play it
+ pygame.mixer.music.load("./simple_bg.wav")
+ pygame.mixer.music.play(-1)
+
+ def create_objects(self):
+ """
+ This creates the initial objects seen when the game
+ first starts up.
+ """
+ # tank
+ self.tank = Tank(speed=TANKSPEED)
+ self.tank.moveto(
+ (
+ self.size[0] / 2 - self.tank.size[0], # move to middle x
+ self.size[1] - self.tank.size[1], # move to bottom y
+ )
+ )
+
+ # targets
+ self.targets = [Target(speed=[0, 0]) for i in range(self.NUM_TARGETS)]
+ for target in self.targets:
+ target.moveto(
+ (
+ random.randint(
+ 0, self.size[0] - target.size[0]
+ ), # random x
+ random.randint(
+ 0, self.size[1] - target.size[1]
+ ), # random y
+ )
+ )
+
+ # bullets
+ self.bullets = []
+
+ # Score text
+ self.font = pygame.font.SysFont(pygame.font.get_default_font(), 32)
+
+ def check_events(self, event):
+ """
+ We imported all from pygame.locals, so that means
+ that we can check KEYDOWN and KEYUP and individual
+ keys such as K_w (w key), K_a (a key), etc.
+ """
+ # change the path of the tank if w, a, s, or d was pressed
+ if event.type == KEYDOWN:
+ if event.key == K_w:
+ self.tank.set_path("up")
+ if event.key == K_s:
+ self.tank.set_path("down")
+ if event.key == K_a:
+ self.tank.set_path("left")
+ if event.key == K_d:
+ self.tank.set_path("right")
+ if event.type == KEYUP:
+ if event.key == K_w:
+ self.tank.unset_path("up")
+ if event.key == K_s:
+ self.tank.unset_path("down")
+ if event.key == K_a:
+ self.tank.unset_path("left")
+ if event.key == K_d:
+ self.tank.unset_path("right")
+ self.tank.set_speed()
+
+ # create bullets if mouse button was pressed
+ if event.type == MOUSEBUTTONDOWN:
+ bul = Bullet(speed=BULLETSPEED)
+ bul.moveto(
+ (self.tank.rect.centerx, (self.tank.rect.top - bul.size[1]))
+ ) # move the bullet to the front of the tank
+
+ # math stuff to calculate trajectory
+ mouse_pos = pygame.mouse.get_pos()
+ h = mouse_pos[1] - bul.rect.center[1]
+ w = mouse_pos[0] - bul.rect.center[0]
+ hyp = math.sqrt(h**2 + w**2)
+ vertical_speed = (
+ BULLETSPEED[1] * (h / hyp) if hyp != 0 else BULLETSPEED[1] * h
+ )
+ horizontal_speed = (
+ BULLETSPEED[0] * (w / hyp) if hyp != 0 else BULLETSPEED[0] * w
+ )
+
+ bul.set_speed((horizontal_speed, vertical_speed))
+ self.bullets.append(bul)
+
+ # play the bullet fired sound since a new bullet was fired
+ self.fire_sound.play()
+
+ def move_objects(self):
+ """
+ This method moves the objects within the game.
+ If a bullet is outside of the screen, it is
+ not moved and is unreferenced.
+ """
+ self.tank.move()
+
+ self.bullets = [
+ bullet
+ for bullet in self.bullets
+ if bullet.check_out_of_screen(self.size) is False
+ ]
+
+ for bullet in self.bullets:
+ bullet.move()
+
+ def check_collisions(self):
+ """
+ This checks whether any of the objects within the game have collided
+ with each other. Specifically, we are looking for collisions between
+ bullets and targets or the tank and targets
+ """
+ deletions = 0 # number of targets deleted
+ num_bullets = len(self.bullets)
+
+ # check bullet-target collisions
+ for i in range(num_bullets):
+ for target in self.targets:
+ # if the bullet collided with the target
+ if self.bullets[i - deletions].check_collision(target) is True:
+ # pop both the bullet and target so that they will be
+ # effectively deleted
+ self.bullets.pop(i - deletions)
+ self.targets.pop(self.targets.index(target))
+
+ # give points for hitting the target
+ self.playerscore += 20
+ deletions += 1
+
+ self.explosion_sound.play() # play the explosion sound
+
+ break # stop the current iteration since the target and
+ # bullet are popped, so referencing them would error.
+
+ # check tank-target collisions
+ for target in self.targets:
+ if self.tank.check_collision(target) is True:
+ self.targets.pop(self.targets.index(target))
+ deletions += 1
+ self.playerscore += 10 # only 10 for running over targets lol
+ self.explosion_sound.play()
+
+ # create a new target for every deleted target
+ for i in range(deletions):
+ a = Target(speed=[0, 0])
+ a.moveto(
+ (
+ random.randint(0, self.size[0] - a.size[0]),
+ random.randint(0, self.size[1] - a.size[1]),
+ )
+ )
+ self.targets.append(a)
+
+ def update_display(self):
+ self.screen.fill(SANDBROWN)
+
+ # tank
+ self.tank.draw(self.screen, SANDBROWN)
+
+ # targets
+ for target in self.targets:
+ target.draw(self.screen, BLACK)
+
+ # bullets
+ for bullet in self.bullets:
+ bullet.draw(self.screen, BLACK)
+
+ # score text
+ font_img = self.font.render(
+ "Score: %s" % str(self.playerscore), True, BLACK
+ )
+ font_rect = font_img.get_rect()
+ pygame.draw.rect(self.screen, SANDBROWN, font_rect, 1)
+ self.screen.blit(font_img, font_rect)
+
+
+game = Tank_Game()
+game.mainloop()