If you haven’t already, take a look at Chapter 17, before you read this one, as this set of exercises relies on what was built in the last chapter. Just like the last one, I’m going to batch the instructions and code, rather than going though each one individually, because each subsequent exercise relates to the entire set of problems. Now, without further ado, let’s dive in.
Instructions
[7] We can represent a rectangle by knowing three things: the location of its lower left corner, its width, and its height. Create a class definition for a Rectangle class using this idea. To create a Rectangle object at location (4,5) with width 6 and height 5, we would do the following:
[8] Add the following accessor methods to the Rectangle class:
getWidth
,getHeight
,__str__
.
[9] Add a method
area
to the Rectangle class that returns the area of any instance:
[10] Write a
perimeter
method in the Rectangle class so that we can find the perimeter of any rectangle instance:
[11] Write a
transpose
method in the Rectangle class that swaps the width and the height of any rectangle instance:
[12] Write a new method in the Rectangle class to test if a Point falls within the rectangle. For this exercise, assume that a rectangle at (0,0) with width 10 and height 5 has open upper bounds on the width and height, i.e. it stretches in the x direction from [0 to 10), where 0 is included but 10 is excluded, and from [0 to 5) in the y direction. So it does not contain the point (10, 2). These tests should pass:
[13] Write a new method called
diagonal
that will return the length of the diagonal that runs from the lower left corner to the opposite corner.
[14] In games, we often put a rectangular “bounding box” around our sprites in the game. We can then do collision detection between, say, bombs and spaceships, by comparing whether their rectangles overlap anywhere.
Write a function to determine whether two rectangles collide. Hint: this might be quite a tough exercise! Think carefully about all the cases before you code.
Starting Point
As mentioned at the start of this post, the exercises in Chapter 17 are very important for the exercises in this Chapter. As you can see from the questions, create a Rectangle
object, relies on the use of the Point
class from the previous chapter. So once again, if you haven’t read that one yet, do so before reading this one.
Discussion of the Solutions
Problems 7: Creating the Initial Class
The starting point for the previous chapter was a class that had already been created, so I didn’t get a chance to explain that process to you.
class Rectangle:
def __init__(self, pos, width, height):
self.pos = pos
if width < 0:
width = 0
if height < 0:
height = 0
self.width = width
self.height = height
As I understand it, the purpose of creating the __init__
method is to provide variables that can be referenced and manipulated by other internal methods without needing to call some other method to get to it. In the case of Rectangle
, pos
, width
and height
are core parameters for the other methods, so they get defined as variables inherent to self
.
Problem 8: Accessor Methods
The starting point for Chapter 17 also included accessor methods. The point of these is to give objects based on the class access to the variables defined in __init__. Here are those methods for Rectangle:
def getWidth(self):
return self.width
def getHeight(self):
return self.height
def __str__(self):
return "Lower Left Coordinates: {}; Width: {}; Height: {}".format(self.pos, self.width, self.height)
Problem 14: Collision!
Before I get into an explanation, let’s take a look at the code:
def collission(r1, r2):
conditionW = False
conditionH = False
r2Arr = r2.getCoords()
r1Arr = r1.getCoords()
# Compare widths and heights
if r2.width > r1.width:
if r1Arr[0] in range(r2Arr[0], r2Arr[2] + 1) or r1Arr[2] in range(r2Arr[0], r2Arr[2] + 1):
conditionW = True
elif r2Arr[0] in range(r1Arr[0], r1Arr[2] + 1) or r2Arr[2] in range(r1Arr[0], r1Arr[2] + 1):
conditionW = True
if r2.height > r1.height:
if r1Arr[1] in range(r2Arr[1], r2Arr[3] + 1) or r1Arr[3] in range(r2Arr[1], r2Arr[3] + 1):
conditionH = True
elif r2Arr[1] in range(r1Arr[1], r1Arr[3] + 1) or r2Arr[3] in range(r1Arr[1], r1Arr[3] + 1):
conditionH = True
if conditionW and conditionH:
return "Collission!"
return "All safe for now."
When you look at this within the context of the overall code below, you’ll notice that this function is defined outside of class Rectangle
, rather than as a method within it. The reason for this was that while it relies on the Rectangle
class, it’s not exactly relevant to it. When looking at the properties of a single rectangle, I don’t need to know if it’s going to crash into another one. Crashing isn’t exactly part of a rectangle’s inherent characteristics. As such, it makes more sense as a standalone function rather than a method of the class it relies on.
With that out of the way, let’s discuss the meat of the function. Just like the Missionaries and Cannibals exercise in Chapter 16, a lot of my time was spent using a virtual canvas to diagram and write out my thoughts. Here are some of the issues I considered:
- How do I know when one rectangle is within the boundaries of another?
- Assuming one is bigger than the other, how do I account for situations where the corners of one rectangle are completely outside of the other even though there is overlap?
- Actually, it’s possible that the above could happen even for a smaller rectangle, assuming that the area is smaller, but the either height or width is stretched enough to fall outside of the larger rectangle’s perimeter limits. What then?
- If I use the width and height and the corresponding x and y end points of those line segments to figure out if points overlap, I’m going to get some false alarms when one of these dimensions happens to be within the bounds of the other, even though those rectangles aren’t actually overlapping. How do I avoid this?
Answering these questions required me to spend a lot of time moving, stretching, and squishing pairs of rectangles, but after a while, I finally got it. The answer to all of these questions was to ignore the area of both rectangles and simply focus on the widths and heights.
First, I would need to know where in space these to rectangles lived. This meant that I needed their bottom left coordinates. Then, I needed to compare their respective widths and heights to see which one was bigger. Once I had that information, I could use the larger height and width, respectively as the point of reference. If both the smaller width and height happened to be within the bounds of the larger one, ladies and gents, we had ourselves a collision.
This is going to be a project for future and better skilled me to do, but eventually, I would like to simulate this so you can have a more tangible visual of what I’ve just described. For now, though, the text and code will have to do.
Final Code
# Runestone.Academy thinkcspy course
# Chapter 18
# Problem 7 - 14
from points import Point
# Solution to Problem 7
class Rectangle:
def __init__(self, pos, width, height):
self.pos = pos
if width < 0:
width = 0
if height < 0:
height = 0
self.width = width
self.height = height
# the following variables were added to help with problem getCoords
self.lowerX = self.pos.getX()
self.upperX = self.lowerX + self.width
self.lowerY = self.pos.getY()
self.upperY = self.lowerY + self.height
# Solution to Problem 8
def getWidth(self):
return self.width
def getHeight(self):
return self.height
# getCoords was added to help with problem 14
def getCoords(self):
return [self.lowerX, self.lowerY, self.upperX, self.upperY]
def __str__(self):
return "Lower Left Coordinates: {}; Width: {}; Height: {}".format(self.pos, self.width, self.height)
# Solution to Problem 9
def area(self):
return self.width * self.height
# Solution to Problem 10
def perimeter(self):
return self.width * 2 + self.height * 2
# Solution to Problem 11
def transpose(self):
width = self.width
self.width = self.height
self.height = width
return self
# Solution to Problem 12
def contains(self, pnt):
pntX = pnt.getX()
pntY = pnt.getY()
if pntX > self.lowerX and pntX < self.upperX and pntY > self.lowerY and pntY < self.upperY:
return True
return False
# Solution to Problem 13
def diagonal(self):
pnt = Point(self.width, self.height)
return pnt.distanceFromOrigin()
# Solution to Problem 14
def collission(r1, r2):
conditionW = False
conditionH = False
r2Arr = r2.getCoords()
r1Arr = r1.getCoords()
# Compare widths and heights
if r2.width > r1.width:
if r1Arr[0] in range(r2Arr[0], r2Arr[2] + 1) or r1Arr[2] in range(r2Arr[0], r2Arr[2] + 1):
conditionW = True
elif r2Arr[0] in range(r1Arr[0], r1Arr[2] + 1) or r2Arr[2] in range(r1Arr[0], r1Arr[2] + 1):
conditionW = True
if r2.height > r1.height:
if r1Arr[1] in range(r2Arr[1], r2Arr[3] + 1) or r1Arr[3] in range(r2Arr[1], r2Arr[3] + 1):
conditionH = True
elif r2Arr[1] in range(r1Arr[1], r1Arr[3] + 1) or r2Arr[3] in range(r1Arr[1], r1Arr[3] + 1):
conditionH = True
if conditionW and conditionH:
return "Collission!"
return "All safe for now."