lib/examples/hakank/minesweeper.ex

#
# Minesweeper in Elixir.
#
# From gecode/examples/minesweeper.cc:
# """
# A specification is a square matrix of characters. Alphanumeric
# characters represent the number of mines adjacent to that field.
# Dots represent fields with an unknown number of mines adjacent to
# it (or an actual mine).
# """
#
# E.g.
#      "..2.3."
#      "2....."
#      "..24.3"
#      "1.34.."
#      ".....3"
#      ".3.3.."
# """
#
# Also see:
# * http://www.janko.at/Raetsel/Minesweeper/index.htm
#
# * http://en.wikipedia.org/wiki/Minesweeper_(computer_game)
#
# * Ian Stewart on Minesweeper:
#   http://www.claymath.org/Popular_Lectures/Minesweeper/
#
# * Richard Kaye's Minesweeper Pages
#   http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.htm
#
# * Some Minesweeper Configurations
#   http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.pdf
#
#
# This program was created by Hakan Kjellerstrand, hakank@gmail.com
# See also my Elixir page: http://www.hakank.org/elxir/
#
## Boris Okner: modified to sync with the latest API,
## change naming and result handling.
##

defmodule CPSolver.Examples.Hakank.Minesweeper do
  import Hakank.CPUtils

  alias CPSolver.IntVariable
  alias CPSolver.Constraint.Sum
  # alias CPSolver.Constraint.Equal
  # alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent
  alias CPSolver.Model
  # alias CPSolver.Objective

  # import CPSolver.Constraint.Factory
  # import CPSolver.Variable.View.Factory

  # Problem from Gecode/examples/minesweeper.cc  problem 0
  #
  # Solution:
  #  1 0 0 0 0 1
  #  0 1 0 1 1 0
  #  0 0 0 0 1 0
  #  0 0 0 0 1 0
  #  0 1 1 1 0 0
  #  1 0 0 0 1 1
  @instance_dir "data/minesweeper"

  def problem(0) do
    # unknown value
    u = -1

    [
      [u, u, 2, u, 3, u],
      [2, u, u, u, u, u],
      [u, u, 2, 4, u, 3],
      [1, u, 3, 4, u, u],
      [u, u, u, u, u, 3],
      [u, 3, u, 3, u, u]
    ]
  end

  #
  # print_instance(p,rows,cols)
  #
  # Pretty print the problem instance `p`.
  #
  defp print_instance(p, rows, cols) do
    IO.puts("Problem instance:")

    for i <- 0..(rows - 1) do
      for j <- 0..(cols - 1) do
        v = mat_at(p, i, j)

        if v >= 0 do
          :io.format("~1w", [v])
        else
          :io.format("~1w", [~c"_"])
        end
      end

      IO.puts("")
    end

    IO.puts("")
  end

  def read_instance(file) do
    {:ok, contents} = File.read(file)

    [_, _ | p] =
      contents
      |> String.split("\n")
      |> Enum.filter(fn line -> !String.starts_with?(line, "#") end)
      |> Enum.map(fn s ->
        String.split(s, "", trim: true)
        |> Enum.map(fn c ->
          if c != "." do
            String.to_integer(c)
          else
            -1
          end
        end)
      end)

    p
  end

  def run(file \\ "") do
    p =
      if file == "" do
        problem(0)
      else
        read_instance(file)
      end

    minesweeper(p)
  end

  # Test all instances in data/minesweeper/minesweeper*.txt
  def run2() do
    # skipping = %{Path.join(@instance_dir,"minesweeper_kaye_splitter.txt") => true}

    for file <- Path.wildcard(Path.join(@instance_dir, "minesweeper*.txt")) do
      IO.puts("Solving file #{file}")
      # if Map.has_key?(skipping, file) do
      #  IO.puts("Skipping this instance since it has too many solutions.\n")
      # else
      minesweeper(read_instance(file))
      # end
    end
  end

  # Running minesweeper_kaye_splitter.txt
  # which has a huge number of solutions.
  def run3() do
    minesweeper(read_instance(Path.join(@instance_dir, "minesweeper_kaye_splitter.txt")))
  end

  @doc """
  minesweeper(p)

  Solves the Minesweeper instance `p`.

  """
  def minesweeper(p) do
    rows = length(p)
    cols = length(Enum.at(p, 0))
    print_instance(p, rows, cols)

    x =
      for i <- 0..(rows - 1) do
        for j <- 0..(cols - 1) do
          v = mat_at(p, i, j)

          if v >= 0 do
            IntVariable.new(0, name: "x[#{i},#{j}]")
          else
            IntVariable.new(0..1, name: "x[#{i},#{j}]")
          end
        end
      end

    constraints =
      for i <- 0..(rows - 1), j <- 0..(cols - 1), v = mat_at(p, i, j), v >= 0 do
        Sum.new(
          v,
          for a <- -1..1, b <- -1..1, (i + a) in 0..(rows - 1), (j + b) in 0..(cols - 1) do
            mat_at(x, i + a, j + b)
          end
        )
      end

    model =
      Model.new(
        x |> List.flatten(),
        constraints
      )

    Logger.configure(level: :info)

    opts = [
      search: {:first_fail, :indomain_min},
      timeout: :timer.hours(1),
      stop_on: {:max_solutions, 10}
    ]

    {:ok, res} =
      CPSolver.solve(
        model,
        opts
      )

    IO.inspect(res.statistics)

    res.solutions
    |> Enum.map(fn s -> Enum.take(s, rows * cols) |> print_matrix(rows, cols) end)

    nil
  end
end