DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

2

Open Source Adventures: Episode 72: Crystal Z3 Solver for Dominosa Puzzle

Dominosa is a puzzle with simple rules:

  • there's a grid filled with numbers
  • you need to pair those numbers into dominos
  • each domino needs to be different

As usual, I recommend playing the puzzle a few times to get a good feel for it. It will make it easier to follow along.

I'll be using updated Crystal Z3 shard, as explained in the previous episode.

Plaintext input

Here's an example input:

77525629349
49071866039
82294832830
98723812246
50186000647
61556354899
56113420383
04114737281
06552340497
79171576958
Enter fullscreen mode Exit fullscreen mode

Solver

#!/usr/bin/env crystal

require "z3"

class DominosaSolver
  @board : Array(Array(Int32))
  @xsize : Int32

  def initialize(path)
    @board = File.read_lines(path).map(&.chars.map(&.to_i))
    @ysize = @board.size
    @xsize = @board[0].size
    @solver = Z3::Solver.new
    @horizontal = {} of Tuple(Int32, Int32) => Z3::BoolExpr
    @vertical = {} of Tuple(Int32, Int32) => Z3::BoolExpr
    @dominos = {} of Tuple(Int32, Int32) => Array(Z3::BoolExpr)
  end

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

  def connections(x,y)
    [
      @horizontal[{x,y}]?,
      @horizontal[{x-1,y}]?,
      @vertical[{x,y}]?,
      @vertical[{x,y-1}]?,
    ].compact
  end

  def add_domino(x1, y1, x2, y2, var)
    v1 = @board[y1][x1]
    v2 = @board[y2][x2]
    v1, v2 = [v1, v2].sort
    @dominos[{v1,v2}] ||= [] of Z3::BoolExpr
    @dominos[{v1,v2}].push var
  end

  def call
    # Setup horizontal variables
    each_coord do |x,y|
      next if x == @xsize - 1
      @horizontal[{x,y}] = var = Z3.bool("h #{x},#{y}")
      add_domino x, y, x+1, y, var
    end

    # Setup vertical variables
    each_coord do |x,y|
      next if y == @ysize - 1
      @vertical[{x,y}] = var = Z3.bool("v #{x},#{y}")
      add_domino x, y, x, y+1, var
    end

    # Each number belongs to exactly one domino
    each_coord do |x,y|
      @solver.assert 1 == Z3.add(connections(x,y).map{|v| v.ite(1,0)})
    end

    # Every type of domino has exactly one instance
    @dominos.each do |type, vars|
      @solver.assert 1 == Z3.add(vars.map{|v| v.ite(1,0)})
    end

    if @solver.satisfiable?
      model = @solver.model
      @ysize.times do |y|
        @xsize.times do |x|
          print @board[y][x]
          next if x == @xsize - 1
          print model[@horizontal[{x,y}]].to_b ? "*" : " "
        end
        print "\n"

        next if y == @ysize - 1

        @xsize.times do |x|
          print model[@vertical[{x,y}]].to_b ? "*" : " "
          next if x == @xsize - 1
          print " "
        end
        print "\n"
      end
    else
      puts "No solution"
    end
  end
end

DominosaSolver.new(ARGV[0]).call
Enter fullscreen mode Exit fullscreen mode

We're definitely taking advantage of Z3.add and of properly typed Model#[] here.

The nontrivial parts of the solver are add_domino method, and printing. With Z3 solvers it's fairly common that input and output are more complex than solver logic.

Arguably all the next if x == @xsize - 1 blocks could be replaced by some properly named helpers, hopefully it's not too complicated.

Result

$ ./dominosa 1.txt
7*7 5*2 5 6*2 9*3 4*9
        *
4 9 0 7 1 8 6*6 0 3 9
* * * *   *     * * *
8 2 2 9 4 8 3*2 8 3 0
        *
9 8*7 2 3 8*1 2*2 4 6
*     *           * *
5 0*1 8 6*0 0*0 6 4 7
                *
6*1 5*5 6*3 5*4 8 9*9

5 6 1 1*3 4*2 0*3 8*3
* * *
0 4 1 1*4 7*3 7*2 8 1
                  * *
0 6*5 5 2 3 4 0*4 9 7
*     * * * *
7 9*1 7 1 5 7 6*9 5*8
Enter fullscreen mode Exit fullscreen mode

Story so far

All the code is on GitHub.

Coming next

In the next episode I'll do one more puzzle solver, then it will be time to move on to different projects.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more