import Darwin
struct Position { let x, y: Float }
typealias Distance = Float
// This is the key insight, modelling the region as a function. (But, I think it's also hard to think this way when you're used to OO).
typealias Region = Position -> Bool
// Define a circle (centered at origin)
func circle(radius: Distance) -> Region
{
return { point in
hypot(point.x, point.y) < radius
}
}
// might as well make a rect too
func rect(width: Distance, height: Distance) -> Region
{
return { point in
fabs(point.x) <= width/2 && fabs(point.y) <= height/2
}
}
let unitCircle = circle(1.0)
unitCircle(Position(x: 0.5, y: 0.5))
unitCircle(Position(x: 2, y: 3))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// now, make a func that can transform a region
func shift(region: Region, by offset: Position) -> Region
{
return { point in
region(Position(x: point.x - offset.x, y: point.y - offset.y))
}
}
let shiftedCircle = shift(unitCircle, by: Position(x: 1.5, y: 3.5))
shiftedCircle(Position(x: 0.5, y: 0.5))
shiftedCircle(Position(x: 2, y: 3))
// We can define other tranformations too
func invert(region: Region) -> Region
{
return { point in !region(point) }
}
func intersection(of a: Region, with b: Region) -> Region
{
return { point in a(point) && b(point) }
}
func union(of a: Region, with b: Region) -> Region
{
return { point in a(point) || b(point) }
}
func difference(of region: Region, minusRegion: Region) -> Region
{
return intersection(of: region, with: invert(minusRegion))
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Now, just go straight to the most complex example from before
let ownPosition = Position(x: 10, y: 12)
let weaponRange = circle(5.0)
let safeDistance = circle(1.0)
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(Position(x: 0, y: 0)) // too far away
shouldFireAtTarget(Position(x: 9, y: 15)) // hit!
shouldFireAtTarget(Position(x: 10.5, y: 12)) // too close to self
shouldFireAtTarget(Position(x: 12.25, y: 9.25)) // too close to friendly
// But, that fully-nested function composition is hard to parse (because you
// have to read it "inside-out").
// So, do the composition in separate steps, so it's easier to read.
let safeRange = difference(of: weaponRange, minusRegion: safeDistance)
let shiftedSafeRange = shift(safeRange, by: ownPosition)
let shouldFireAtTarget2 = difference(of: shiftedSafeRange, minusRegion: friendlyRegion)
// Test this version
shouldFireAtTarget2(Position(x: 0, y: 0)) // too far away
shouldFireAtTarget2(Position(x: 9, y: 15)) // hit!
shouldFireAtTarget2(Position(x: 10.5, y: 12)) // too close to self
shouldFireAtTarget2(Position(x: 12.25, y: 9.25)) // too close to friendly