kelan.io

Regions - My Protocol Extension Approach

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

protocol Region
{
    func contains(position: Position) -> Bool
}


// Back to being a struct
struct Circle: Region
{
    let radius: Distance
    func contains(position: Position) -> Bool
    {
        return hypot(position.x, position.y) <= radius
    }
}

struct Rectangle: Region
{
    let width: Distance
    let height: Distance
    func contains(position: Position) -> Bool {
        return (fabs(position.x) <= width/2
            && fabs(position.y) <= height/2)
    }
}

struct ComposedRegion: Region
{
    let containsRule: Position -> Bool  ///< holds the complex logic as a function

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


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Transformations as protocol extensions

extension Region
{
    func shift(by offset: Position) -> Region
    {
        // TODO: Is it clearer to write them with a "local function", like this?
        func hitTest(point: Position) -> Bool {
            let shiftedPoint = Position(x: point.x - offset.x, y: point.y - offset.y)
            return self.contains(shiftedPoint)
        }
        return ComposedRegion(containsRule: hitTest)
    }

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

    func intersection(with other: Region) -> Region
    {
        return ComposedRegion(containsRule: { point in
            self.contains(point) && other.contains(point)
        })
    }

    func union(with other: Region) -> Region
    {
        return ComposedRegion(containsRule: { point in
            self.contains(point) || other.contains(point)
        })
    }

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

}



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

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

// 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