Regions - Daniel's Inheritance Refinement

import Darwin
struct Position { let x, y: Float }
typealias Distance = Float

// Now, try to remove the free functions

// In the video, he does it (before protocol extensions) by using class inheritance
// 1) Make a base class (Region), that has the contains() method (with a stub implementation)
// 2) Make Cirlce, Rectangle, AND ComplexRegion all be classes that inherit from Region
//       * We have to add the init method, and mark contains() as an override
// 3) Make the functions take/return Regions (instead of Containsable -> ComplexRegion)
// 4) Make new struct RegionTransformer
//      * This will take a Region and return a Region
//      * move the shift(), invert(), etc. methods inside of there
// 5) Add a `transformer = RegionTransformer()` property to the Region class
// 6) Use it like
//      let shiftedCircle = unitCircle.transformer.shift(unitCircle, offset: …)
//      * But it's unfortunate to have to repeate `unitCircle` in there.
// 7) So, add convenience methods, like Region.shift(), which calls it's own `transformer.shift(self,offset)`
// 8) Same for all methods

// same protocol here
protocol Containsable
{
func contains(position: Position) -> Bool
}

// but add a base class

class Region: Containsable
{
func contains(position: Position) -> Bool
{
assert(false, "subclass MUST override")
}

// come back to this later
let transformer = RegionTransformer()
}

// This is a class now, not a struct
class Circle: Region
{

// have to manually write inits, because it's a class (boo!)
{
}

// have to mark as override (nice!)
override func contains(position: Position) -> Bool
{
return hypot(position.x, position.y) <= radius
}
}

// This is also a class now
class ComposedRegion: Region
{
let containsRule: Position -> Bool  ///< holds the complex logic as a function

init(containsRule: Position -> Bool)
{
self.containsRule = containsRule
}

override func contains(position: Position) -> Bool
{
return containsRule(position)
}
}

// Now, instead of making free functions for the operations, put them in a struct
// Also, update them to take/return Regions
struct RegionTransformer
{
func shift(region: Region, by offset: Position) -> Region
{
return ComposedRegion(containsRule: {
point in
let shiftedPoint = Position(x: point.x - offset.x, y: point.y - offset.y)
return region.contains(shiftedPoint)
})
}

func invert(region: Region) -> Region
{
return ComposedRegion(containsRule: { point in !region.contains(point) })
}

func intersection(of a: Region, with b: Region) -> Region
{
return ComposedRegion(containsRule: { point in
a.contains(point) && b.contains(point)
})
}

func union(of a: Region, with b: Region) -> Region
{
return ComposedRegion(containsRule: { point in
a.contains(point) || b.contains(point)
})
}

func difference(of region: Region, minusRegion: Region) -> Region
{
return self.intersection(of: region, with: invert(minusRegion))
}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Simple examples

let circle = Circle(radius: 5)
let shiftedCircle = circle.transformer.shift(circle, by: Position(x: 10, y: 12))
shiftedCircle.contains(Position(x: 11, y: 13))

// but, that's ugly to have to pass `circle` twice there, so make convenience methods on Region

extension Region
{
func shift(by offset: Position) -> Region
{
return transformer.shift(self, by: offset)
}

func invert() -> Region
{
return transformer.invert(self)
}

func intersection(with other: Region) -> Region
{
return transformer.intersection(of: self, with: other)
}

func union(with other: Region) -> Region
{
return transformer.union(of: self, with: other)
}

func difference(minus region: Region) -> Region
{
return transformer.difference(of: self, minusRegion: region)
}
}

let shiftedCircle2 = circle.shift(by: Position(x: 10, y: 12))
shiftedCircle2.contains(Position(x: 11, y: 13))

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Now, the complex example

let ownPosition = Position(x: 10, y: 12)
let weaponRange = Circle(radius: 5.0)
let safeDistance = Circle(radius: 1.0)
let friendlyPosition = Position(x: 12, y: 9)
let friendlyRegion = safeDistance.shift(by: friendlyPosition)

// This is easy enough to follow like this, no need to break into separate steps.
let shouldFireAtTarget = weaponRange
.difference(minus: safeDistance)
.shift(by: ownPosition)
.difference(minus: friendlyRegion)

// Test it
shouldFireAtTarget.contains(Position(x: 0, y: 0))  // too far away
shouldFireAtTarget.contains(Position(x: 9, y: 15))  // hit!
shouldFireAtTarget.contains(Position(x: 10.5, y: 12))  // too close to self
shouldFireAtTarget.contains(Position(x: 12.25, y: 9.25))  // too close to friendly
```