kelan.io

Regions - Daniel's Intermediate Approach

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

// use the "key trick" from the FP approach, but in an OO manner
protocol Containsable
{
    func contains(position: Position) -> Bool
}

// Make structs that adopt the protocol
struct Circle: Containsable
{
    let radius: Distance
    func contains(position: Position) -> Bool
    {
        return hypot(position.x, position.y) <= radius
    }
}

// example
let unitCircle = Circle(radius: 1.0)
unitCircle.contains(Position(x: 0.5, y: 0.5))
unitCircle.contains(Position(x: 2, y: 3))


// We can easily do rect also
struct Rectangle: Containsable
{
    let width: Distance
    let height: Distance
    func contains(position: Position) -> Bool {
        return (fabs(position.x) <= width/2
            && fabs(position.y) <= height/2)
    }
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Transformations

// First, define a composition (a new struct that adopts the protocol)
struct ComposedRegion: Containsable
{
    let containsRule: Position -> Bool  ///< holds the complex logic as a function

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

// Now, do the transformations (that return the new type)

func shift(region: Containsable, by offset: Position) -> ComposedRegion
{
    return ComposedRegion(containsRule: {
        point in
        let shiftedPoint = Position(x: point.x - offset.x, y: point.y - offset.y)
        return region.contains(shiftedPoint)
    })
}

// example
let shiftedCirlce = shift(unitCircle, by: Position(x: 1.5, y: 3.5))
shiftedCirlce.contains(Position(x: 2, y: 3))


// Other Operations
func invert(region: Containsable) -> ComposedRegion
{
    return ComposedRegion(containsRule: { point in !region.contains(point) })
}

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

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

func difference(of region: Containsable, minusRegion: Containsable) -> ComposedRegion
{
    return intersection(of: region, with: invert(minusRegion))
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Now, the complex example
// Note: It should look a lot like the FP solution of this

let ownPosition = Position(x: 10, y: 12)
let weaponRange = Circle(radius: 5.0)   // <-- just make this a struct
let safeDistance = Circle(radius: 1.0)   // <-- and this
let friendlyPosition = Position(x: 12, y: 9)
let friendlyRegion = shift(safeDistance, by: friendlyPosition)

let shouldFireAtTarget = difference(
    of: shift(
        difference(
            of: weaponRange,
            minusRegion: safeDistance),
        by: ownPosition),
    minusRegion: 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