export ALL_EDGE_TYPES, EdgeType
export BallOrSocket, Ball, Socket, Straight
export opposite, Edge, isperimeter, edges_mate

We define some number of "edge types". If the puzzle pieces are all square, then there is only one edge type. If they are rectangular then there are two edge types: one for each edge length. Each different shape of interlocking edge represents another edge type.

MORE PRACTICALLY THOUGH: since we don't want to match two puzzle pieces on a flat perimeter edge, each edge on the perimeter of the puzzle should have a different edge type. For a 3 by 5 puzzle, that gives 16 edge types before you start defining edge types for the internal edges.

We define EdgeType and create an instance for each distinct edge type. All an EdgeType needs is uniqueness. It might be handy to note if the EdgeType represents a perimeter edge though, so we track that.

We accumulate a catalog of every EdgeType in ALL_EDGE_TYPES.

"""
    ALL_EDGE_TYPES

`ALL_EDGE_TYPES` is a vector of all of the `EdgeType`s that have been
created.
"""
ALL_EDGE_TYPES = []

let
    NEXT_EDGE_UID = 1

    struct EdgeType
        isperimeter::Bool
        uid::Int

        # For testing:
        EdgeType(isperimeter, uid) = new(isperimeter, uid)

        function EdgeType(isperimeter)
            et = new(isperimeter,
                     let
                         uid = NEXT_EDGE_UID
                         NEXT_EDGE_UID += 1
                         uid
                     end)
            push!(ALL_EDGE_TYPES, et)
            return et
        end
    end
end

@doc """
    EdgeType(isperimeter::Bool)

Creates a unique `EdgeType`.

`isperimeter` indicates if the EdgeType is for an edge on the
perimeter of the puzzle.
""" EdgeType


isperimeter(e::EdgeType) = e.isperimeter

At the edge where two puzzle pieces interlock, the edges of those pieces are mirror images of each other. At that meeting edge, one piece has a ball and the other has a socket. If the edge is at the border of the puzzle then it is straight.

The edge of a puzzle piece is thus characterized by its EdgeType and whether it is a ball, socket, or straight.

Ball and Socket are opposites.

Straight is its own opposite.

"""
    BallOrSocket
    Ball
    Socket
    Straight
"""
abstract type BallOrSocket end
struct Ball <: BallOrSocket end
struct Socket <: BallOrSocket end
struct Straight <: BallOrSocket end

opposite(::Ball) = Socket()
opposite(::Socket) = Ball()
opposite(::Straight) = Straight()


"""
    Edge(::EdgeType, ::BallOrSocket)

`Edge` represents one edge of a puzzle piece.  It has an `edge_type`.
The `bs` field indicates whether the edge is a *ball* or *socket*.

"""
struct Edge
    edge_type::EdgeType
    bs::BallOrSocket
end

isperimeter(e::Edge) = isperimeter(e.edge_type)
isperimeter(::Missing) = false

opposite(edge::Edge) = Edge(edge.edge_type, opposite(edge.bs))

It is easier to index things if there is a total ordering defined for them.

EdgeTypes can be ordered by their uid.

We arbitrarily decide that Ball comes before Socket.

We can then define a total ordering on Edges.

function Base.isless(a::EdgeType, b::EdgeType)::Bool
    a.uid < b.uid
end

Base.isless(::BallOrSocket, ::BallOrSocket) = false
Base.isless(::Ball, ::Socket) = true
Base.isless(::Ball, ::Straight) = true
Base.isless(::Socket, ::Straight) = true

function Base.isless(a::Edge, b::Edge)
    (isless(a.edge_type, b.edge_type) ||
        (a.edge_type == b.edge_type &&
        isless(a.bs, b.bs)))
end

Two Edges match if they have the same EdgeType and their bss are opposites.

"""
    edges_mate(::Edge, ::Edge)::Bool

Two `Edge`s mate if they have the same `EdgeType` and their `bs`s are
opposites.
"""
function edges_mate(e1::Edge, e2::Edge)::Bool
    # For perimeter edges the EdgeType doesn't matter:
    (e1.bs == e2.bs == Straight()) ||
        ((e1.edge_type == e2.edge_type) &&
        (opposite(e1.bs) == e2.bs))
end

This page was generated using Literate.jl.