lib/examples/reindeers.ex

defmodule CPSolver.Examples.Reindeers do
  @doc """
  Santa always leaves plans for his elves to determine the order in which the
  reindeer will pull his sleigh. This year, for the European leg of his
  journey, his elves are working to the following schedule, which will form a
  single line of nine reindeer.

  Here are the rules:

    Comet behind Rudolph, Prancer and Cupid
    Blitzen behind Cupid
    Blitzen in front of Donder, Vixen and Dancer
    Cupid in front of Comet, Blitzen and Vixen
    Donder behind Vixen, Dasher and Prancer
    Rudolph behind Prancer
    Rudolph in front of Donder, Dancer and Dasher
    Vixen in front of Dancer and Comet
    Dancer behind Donder, Rudolph and Blitzen
    Prancer in front of Cupid, Donder and Blitzen
    Dasher behind Prancer
    Dasher in front of Vixen, Dancer and Blitzen
    Donder behind Comet and Cupid
    Cupid in front of Rudolph and Dancer
    Vixen behind Rudolph, Prancer and Dasher.

  """
  alias CPSolver.IntVariable, as: Variable
  alias CPSolver.Constraint.Less
  alias CPSolver.Constraint.AllDifferent

  def solve(opts \\ []) do
    {:ok, res} = CPSolver.solve_sync(model(), opts)

    res.solutions
    |> hd
    |> then(fn solution -> Enum.zip(res.variables, solution) end)
    |> print()

    {:ok, res}
  end

  def model() do
    reindeers = [
      Blitzen,
      Comet,
      Cupid,
      Dancer,
      Dasher,
      Donder,
      Prancer,
      Rudolph,
      Vixen
    ]

    domain = 1..length(reindeers)

    positions =
      [blitzen, comet, cupid, dancer, dasher, donder, prancer, rudolph, vixen] =
      Enum.map(reindeers, fn name -> Variable.new(domain, name: name) end)

    rules =
      behind(comet, [rudolph, prancer, cupid]) ++
        behind(blitzen, [cupid]) ++
        in_front_of(blitzen, [donder, vixen, dancer]) ++
        in_front_of(cupid, [comet, blitzen, vixen]) ++
        behind(donder, [vixen, dasher, prancer]) ++
        behind(rudolph, [prancer]) ++
        in_front_of(rudolph, [donder, dancer, dasher]) ++
        in_front_of(vixen, [dancer, comet]) ++
        behind(dancer, [donder, rudolph, blitzen]) ++
        in_front_of(prancer, [cupid, donder, blitzen]) ++
        behind(dasher, [prancer]) ++
        in_front_of(dasher, [vixen, dancer, blitzen]) ++
        behind(donder, [comet, cupid]) ++
        in_front_of(cupid, [rudolph, dancer]) ++
        behind(vixen, [rudolph, prancer, dasher])

    %{
      variables: positions,
      ## AllDifferent is optional
      constraints: [AllDifferent.new(positions) | rules]
    }
  end

  defp behind(reindeer, list) do
    Enum.map(list, fn r -> Less.new(reindeer, r) end)
  end

  defp in_front_of(reindeer, list) do
    Enum.map(list, fn r -> Less.new(r, reindeer) end)
  end

  def order(solution) do
    solution
    |> Enum.sort_by(fn {_r, place} -> place end)
    |> Enum.map(fn {r, _place} -> r end)
  end

  def print(solution) do
    solution
    |> order
    |> Enum.map_join(" ", fn name -> inspect(name) end)
    |> then(fn str -> IO.ANSI.green() <> " -> #{str} ->" <> IO.ANSI.reset() end)
    |> IO.puts()
  end
end