DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Open Source Adventures: Episode 66: Crystal Z3 Solver for Mosaic Puzzle

Mosaic Puzzle is really close to Minesweeper Puzzle we did in the previous episode.

Logically the only difference is that hint cells can also contain mines. And for that reason, instead of "mines" it talks about black and white cells.

The difficulty we'll have will be with the output, not with the solver.

shard.yml

First we need to load Z3:

name: mosaic-solver
version: 0.0.1

dependencies:
  z3:
    github: taw/crystal-z3
Enter fullscreen mode Exit fullscreen mode

Solver

The solver is almost identical to the one for Minesweeper we did in the previous episode. The only difference in the solver part is that @vars[{x, y}]? is included in neighbourhood(x, y), and we no longer assert that hint cells must be empty.

#!/usr/bin/env crystal

require "z3"

class MosaicSolver
  @data : Array(Array(Nil | Int32))
  @ysize : Int32
  @xsize : Int32

  def initialize(path)
    @data = File.read_lines(path).map{|l| l.chars.map{|c| c.ascii_number? ? c.to_i : nil}}
    @ysize = @data.size
    @xsize = @data[0].size
    @solver = Z3::Solver.new
    @vars = {} of Tuple(Int32, Int32) => Z3::BoolExpr
  end

  def each_coord
    @ysize.times do |y|
      @xsize.times do |x|
        yield(x, y)
      end
    end
  end

  def neighbourhood(x, y)
    [
      @vars[{x, y+1}]?,
      @vars[{x, y}]?,
      @vars[{x, y-1}]?,
      @vars[{x+1, y+1}]?,
      @vars[{x+1, y}]?,
      @vars[{x+1, y-1}]?,
      @vars[{x-1, y+1}]?,
      @vars[{x-1, y}]?,
      @vars[{x-1, y-1}]?,
    ].compact
  end

  def neighbourhood_count(x, y)
    neighbourhood(x, y).map{|v| v.ite(1, 0)}.reduce{|a,b| a+b}
  end

  def call
    each_coord do |x, y|
      @vars[{x, y}] = Z3.bool("#{x},#{y}")
    end
    each_coord do |x, y|
      c = @data[y][x]
      if c
        @solver.assert neighbourhood_count(x, y) == c
      end
    end
    ...
  end
end

solver = MosaicSolver.new(ARGV[0])
solver.call
Enter fullscreen mode Exit fullscreen mode

Printing solution

OK, let's get to the complicated part, printing the solution. It's complicated because we need to use different colors and still print the number hints.

We could use a fancy library for it, but terminal codes aren't all that complicated, so I just hardcoded them.

    if @solver.check
      model = @solver.model
      @ysize.times do |y|
        @xsize.times do |x|
          v = (model[@vars[{x, y}]].to_s == "true")
          c = @data[y][x] || " "
          if v
            print "\e[30;104m"
          else
            print "\e[30;102m"
          end
          print c
        end
        print "\e[0m"
        print "\n"
      end
    else
      puts "No solution"
    end
Enter fullscreen mode Exit fullscreen mode

Result

Here's what it looks like:

Episode 66 screenshot

Story so far

All the code is on GitHub.

Coming next

Over the next few episodes I'll code a few more puzzle game solvers in Crystal and Z3.

Top comments (0)