z3 0.0.20180629 → 0.0.20220203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -4
  3. data/Rakefile +15 -8
  4. data/examples/abc_path +187 -0
  5. data/examples/abc_path-1.txt +7 -0
  6. data/examples/algebra_problems +12 -12
  7. data/examples/aquarium +133 -0
  8. data/examples/aquarium-1.txt +11 -0
  9. data/examples/bridges +2 -2
  10. data/examples/cats_organized_neatly +133 -0
  11. data/examples/cats_organized_neatly-10.txt +15 -0
  12. data/examples/cats_organized_neatly-3.txt +8 -0
  13. data/examples/cats_organized_neatly-48.txt +32 -0
  14. data/examples/circuit_problems +4 -4
  15. data/examples/clogic_puzzle +2 -2
  16. data/examples/color_nonogram +150 -0
  17. data/examples/color_nonogram-1.txt +23 -0
  18. data/examples/crossflip +2 -4
  19. data/examples/dominion +153 -0
  20. data/examples/dominion-1.txt +8 -0
  21. data/examples/dominosa +133 -0
  22. data/examples/dominosa-1.txt +8 -0
  23. data/examples/eulero +99 -0
  24. data/examples/eulero-1.txt +5 -0
  25. data/examples/four_hackers_puzzle +2 -2
  26. data/examples/futoshiki +128 -0
  27. data/examples/futoshiki-1.txt +17 -0
  28. data/examples/kakurasu +73 -0
  29. data/examples/kakurasu-1.txt +2 -0
  30. data/examples/kakuro +2 -2
  31. data/examples/killer_sudoku +88 -0
  32. data/examples/killer_sudoku-1.txt +17 -0
  33. data/examples/killer_sudoku-2.txt +53 -0
  34. data/examples/kinematics_problems +20 -20
  35. data/examples/knights_puzzle +2 -2
  36. data/examples/kropki +100 -0
  37. data/examples/kropki-1.txt +13 -0
  38. data/examples/letter_connections +2 -2
  39. data/examples/light_up +2 -2
  40. data/examples/minisudoku +2 -2
  41. data/examples/miracle_sudoku +135 -0
  42. data/examples/miracle_sudoku-1.txt +9 -0
  43. data/examples/mortal_coil_puzzle +2 -2
  44. data/examples/nanro +245 -0
  45. data/examples/nanro-1.txt +8 -0
  46. data/examples/nine_clocks +106 -0
  47. data/examples/nonogram +2 -2
  48. data/examples/pyramid_nonogram +2 -2
  49. data/examples/regexp_crossword_solver +2 -2
  50. data/examples/regexp_solver +2 -2
  51. data/examples/renzoku +124 -0
  52. data/examples/renzoku-1.txt +17 -0
  53. data/examples/sandwich_sudoku +101 -0
  54. data/examples/sandwich_sudoku-1.txt +10 -0
  55. data/examples/selfref +2 -2
  56. data/examples/simple_regexp_parser.rb +58 -56
  57. data/examples/skyscrapers +118 -0
  58. data/examples/skyscrapers-1.txt +6 -0
  59. data/examples/skyscrapers-2.txt +11 -0
  60. data/examples/star_battle +134 -0
  61. data/examples/star_battle-1.txt +10 -0
  62. data/examples/stitches +180 -0
  63. data/examples/stitches-1.txt +11 -0
  64. data/examples/sudoku +2 -2
  65. data/examples/suguru +199 -0
  66. data/examples/suguru-1.txt +17 -0
  67. data/examples/verbal_arithmetic +2 -2
  68. data/examples/yajilin +268 -0
  69. data/examples/yajilin-1.txt +10 -0
  70. data/lib/z3/ast.rb +8 -0
  71. data/lib/z3/expr/expr.rb +16 -15
  72. data/lib/z3/low_level.rb +6 -2
  73. data/lib/z3/low_level_auto.rb +180 -36
  74. data/lib/z3/optimize.rb +4 -4
  75. data/lib/z3/very_low_level.rb +8 -5
  76. data/lib/z3/very_low_level_auto.rb +45 -9
  77. data/spec/expr_spec.rb +62 -0
  78. data/spec/integration/abc_path_spec.rb +21 -0
  79. data/spec/integration/aquarium_spec.rb +27 -0
  80. data/spec/integration/cats_organized_neatly_spec.rb +14 -0
  81. data/spec/integration/color_nonogram_spec.rb +28 -0
  82. data/spec/integration/dominion_spec.rb +14 -0
  83. data/spec/integration/dominosa_spec.rb +21 -0
  84. data/spec/integration/eulero_spec.rb +11 -0
  85. data/spec/integration/futoshiki_spec.rb +23 -0
  86. data/spec/integration/kakurasu_spec.rb +18 -0
  87. data/spec/integration/killer_sudoku_spec.rb +10 -0
  88. data/spec/integration/knights_puzzle_spec.rb +11 -11
  89. data/spec/integration/kropki_spec.rb +19 -0
  90. data/spec/integration/miracle_sudoku_spec.rb +15 -0
  91. data/spec/integration/mortal_coil_puzzle_spec.rb +8 -6
  92. data/spec/integration/nanro_spec.rb +39 -0
  93. data/spec/integration/nine_clocks_spec.rb +30 -0
  94. data/spec/integration/oneofus_spec.rb +7 -15
  95. data/spec/integration/regexp_crossword_solver_spec.rb +1 -1
  96. data/spec/integration/renzoku_spec.rb +23 -0
  97. data/spec/integration/sandwich_sudoku_spec.rb +15 -0
  98. data/spec/integration/skyscraper_spec.rb +10 -0
  99. data/spec/integration/star_battle_spec.rb +27 -0
  100. data/spec/integration/stitches_spec.rb +25 -0
  101. data/spec/integration/suguru_spec.rb +23 -0
  102. data/spec/integration/yajilin_spec.rb +25 -0
  103. data/spec/optimize_spec.rb +6 -4
  104. data/spec/set_expr_spec.rb +14 -8
  105. data/spec/solver_spec.rb +4 -5
  106. data/spec/spec_helper.rb +15 -0
  107. metadata +105 -25
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+ require "paint"
6
+
7
+ class SandwichSudokuSolver
8
+ def initialize(path)
9
+ data = Pathname(path).read
10
+ data = data.strip.split("\n").map do |line|
11
+ line.split.map{|c| c =~ /\A\d+\z/ ? c.to_i : nil}
12
+ end
13
+ @col_counts = data.shift
14
+ @row_counts = data.map(&:shift)
15
+ @xsize = @col_counts.size
16
+ @ysize = @row_counts.size
17
+ @data = data
18
+ raise unless data.size == @ysize
19
+ raise unless data.all?{|r| r.size == @ysize}
20
+ @solver = Z3::Solver.new
21
+ end
22
+
23
+ def call
24
+ @cells = (0..8).map do |y|
25
+ (0..8).map do |x|
26
+ cell_var(@data[y][x], x, y)
27
+ end
28
+ end
29
+
30
+ @cells.each do |row|
31
+ @solver.assert Z3.Distinct(*row)
32
+ end
33
+ @cells.transpose.each do |column|
34
+ @solver.assert Z3.Distinct(*column)
35
+ end
36
+ @cells.each_slice(3) do |rows|
37
+ rows.transpose.each_slice(3) do |square|
38
+ @solver.assert Z3.Distinct(*square.flatten)
39
+ end
40
+ end
41
+
42
+ 9.times do |x|
43
+ assert_sandwich "c#{x+1}", @col_counts[x], col_vars(x)
44
+ end
45
+
46
+ 9.times do |y|
47
+ assert_sandwich "s#{y+1}", @row_counts[y], row_vars(y)
48
+ end
49
+
50
+ # TODO: SANDWICHES
51
+
52
+ if @solver.satisfiable?
53
+ @model = @solver.model
54
+ print_answer!
55
+ else
56
+ puts "failed to solve"
57
+ end
58
+ end
59
+
60
+ def assert_sandwich(name, count, vars)
61
+ ss = Z3.Int("#{name}-ss")
62
+ se = Z3.Int("#{name}-se")
63
+ @solver.assert ss >= 0
64
+ @solver.assert ss <= 8
65
+ @solver.assert se >= 0
66
+ @solver.assert se <= 8
67
+ @solver.assert ss < se
68
+ 9.times do |i|
69
+ @solver.assert ((vars[i] == 1) | (vars[i] == 9)) == ((ss == i) | (se == i))
70
+ end
71
+ e = 9.times.map{|i|
72
+ Z3.And(i > ss, i < se).ite(vars[i], 0)
73
+ }
74
+ @solver.assert Z3.Add(*e) == count
75
+ end
76
+
77
+ def row_vars(y)
78
+ @cells[y]
79
+ end
80
+
81
+ def col_vars(x)
82
+ @cells.map{|line| line[x]}
83
+ end
84
+
85
+ def cell_var(cell, x, y)
86
+ v = Z3.Int("cell[#{x+1},#{y+1}]")
87
+ @solver.assert v >= 1
88
+ @solver.assert v <= 9
89
+ @solver.assert v == cell if cell != nil
90
+ v
91
+ end
92
+
93
+ def print_answer!
94
+ @cells.each do |row|
95
+ puts row.map{|v| @model[v]}.join(" ")
96
+ end
97
+ end
98
+ end
99
+
100
+ path = ARGV[0] || Pathname(__dir__) + "sandwich_sudoku-1.txt"
101
+ SandwichSudokuSolver.new(path).call
@@ -0,0 +1,10 @@
1
+ 19 7 15 19 4 0 6 9 35
2
+ 5 . . . . . . . . 1
3
+ 13 . . . . . . . . .
4
+ 20 . . . . . . . . .
5
+ 9 . . . . . . . . .
6
+ 12 . . . . 1 . . . .
7
+ 0 . . . . . . . . .
8
+ 4 . . . . . . . . .
9
+ 14 . . . . . . . . .
10
+ 5 . . . . . . . . .
data/examples/selfref CHANGED
@@ -30,7 +30,7 @@ class SelfRefPuzzleSolver
30
30
  Z3.Or(*ary.map{|i| cons_answers == i})
31
31
  end
32
32
 
33
- def solve!
33
+ def call
34
34
  @a_answers = Z3.Add(*(1..20).map{|i| @a[i][1]})
35
35
  @b_answers = Z3.Add(*(1..20).map{|i| @a[i][2]})
36
36
  @c_answers = Z3.Add(*(1..20).map{|i| @a[i][3]})
@@ -205,7 +205,7 @@ class SelfRefPuzzleSolver
205
205
  end
206
206
 
207
207
 
208
- SelfRefPuzzleSolver.new.solve!
208
+ SelfRefPuzzleSolver.new.call
209
209
 
210
210
  __END__
211
211
  http://faculty.uml.edu/jpropp/srat-Q.txt
@@ -15,7 +15,7 @@ class SimpleRegexpParser
15
15
  end
16
16
 
17
17
  def sequence(*parts)
18
- parts = parts.select{|x| x[0] != :empty}
18
+ parts = parts.select { |x| x[0] != :empty }
19
19
  case parts.size
20
20
  when 0
21
21
  [:empty]
@@ -46,9 +46,9 @@ class SimpleRegexpParser
46
46
  # Saves us time to reuse ruby regexp engine for 1 character case
47
47
  def character_type(char_rx)
48
48
  char_rx = Regexp.new(char_rx)
49
- codes = (0..127).select{|c| c.chr =~ char_rx}
49
+ codes = (0..127).select { |c| c.chr =~ char_rx }
50
50
  # This is mostly here to make debugging easier
51
- if codes.size > 127-codes.size
51
+ if codes.size > 127 - codes.size
52
52
  [:neg_set, (0..127).to_a - codes]
53
53
  else
54
54
  [:set, codes]
@@ -64,7 +64,7 @@ class SimpleRegexpParser
64
64
  end
65
65
 
66
66
  def literal(chars)
67
- sequence(*chars.map{|c| character_type(c)})
67
+ sequence(*chars.map { |c| character_type(c) })
68
68
  end
69
69
 
70
70
  def star(part)
@@ -85,10 +85,10 @@ class SimpleRegexpParser
85
85
 
86
86
  def repeat(part, min, max)
87
87
  if max == -1
88
- sequence(star(part), *([part]*min))
88
+ sequence(star(part), *([part] * min))
89
89
  else
90
90
  maybe_part = alternative([:empty], part)
91
- sequence(*([part]*min), *([maybe_part] * (max-min)))
91
+ sequence(*([part] * min), *([maybe_part] * (max - min)))
92
92
  end
93
93
  end
94
94
 
@@ -105,7 +105,7 @@ class SimpleRegexpParser
105
105
  sequence(repeat(base, min, max), part)
106
106
  )
107
107
  else # (a){2,} -> a{1,}(a)
108
- sequence(repeat(base, min-1, max), part)
108
+ sequence(repeat(base, min - 1, max), part)
109
109
  end
110
110
  elsif max == 0 # a{0} -> empty, not really a thing
111
111
  :empty
@@ -115,10 +115,10 @@ class SimpleRegexpParser
115
115
  # with same group id for both ()s
116
116
  alternative(
117
117
  [:group, group, empty],
118
- sequence(repeat(base, min, max-1), part)
118
+ sequence(repeat(base, min, max - 1), part)
119
119
  )
120
120
  else # (a){2,3} -> a{1,2}(a)
121
- sequence(repeat(base, min-1, max-1), part)
121
+ sequence(repeat(base, min - 1, max - 1), part)
122
122
  end
123
123
  end
124
124
  end
@@ -131,58 +131,60 @@ class SimpleRegexpParser
131
131
  # * empty
132
132
  # * backref - \1
133
133
  # * group - (a)
134
- def parse(node=@tree)
134
+ def parse(node = @tree)
135
135
  result = case node
136
- when Regexp::Expression::Group::Capture
137
- # Assumes it's going to be parsed in right order
138
- group(new_group, sequence(*node.expressions.map{|n| parse(n)}))
139
- when Regexp::Expression::Alternation
140
- alternative(*node.expressions.map{|n| parse(n)})
141
- when Regexp::Expression::Assertion::Lookahead
142
- [:anchor, :lookahead, sequence(*node.expressions.map{|n| parse(n)})]
143
- when Regexp::Expression::Assertion::NegativeLookahead
144
- [:anchor, :negative_lookahead, sequence(*node.expressions.map{|n| parse(n)})]
145
- when Regexp::Expression::Assertion::Lookbehind
146
- [:anchor, :lookbehind, sequence(*node.expressions.map{|n| parse(n)})]
147
- when Regexp::Expression::Assertion::NegativeLookbehind
148
- [:anchor, :negative_lookbehind, sequence(*node.expressions.map{|n| parse(n)})]
149
- when Regexp::Expression::Subexpression
150
- # It's annoyingly subtypes a lot
151
- raise unless node.class == Regexp::Expression::Subexpression or
152
- node.class == Regexp::Expression::Group::Passive or
153
- node.class == Regexp::Expression::Root or
154
- node.class == Regexp::Expression::Alternative
155
- sequence(*node.expressions.map{|n| parse(n)})
156
- when Regexp::Expression::CharacterSet
157
- character_set(node.negative?, node.members)
158
- when Regexp::Expression::Literal
159
- literal(node.text.chars)
160
- when Regexp::Expression::CharacterType::Base
161
- character_type(node.text)
162
- when Regexp::Expression::EscapeSequence::Base
163
- character_type(node.text)
164
- when Regexp::Expression::Backreference::Number
165
- num = node.text[%r[\A\\(\d+)\z], 1] or raise "Parse error"
166
- backref(num.to_i)
167
- when Regexp::Expression::Anchor::BeginningOfString
168
- [:anchor, :bos]
169
- when Regexp::Expression::Anchor::EndOfString
170
- [:anchor, :eos]
171
- when Regexp::Expression::Anchor::BeginningOfLine
172
- [:anchor, :bol]
173
- when Regexp::Expression::Anchor::EndOfLine
174
- [:anchor, :eol]
175
- else
176
- raise "Unknown expression"
177
- end
136
+ when Regexp::Expression::Group::Capture
137
+ # Assumes it's going to be parsed in right order
138
+ group(new_group, sequence(*node.expressions.map { |n| parse(n) }))
139
+ when Regexp::Expression::Alternation
140
+ alternative(*node.expressions.map { |n| parse(n) })
141
+ when Regexp::Expression::Assertion::Lookahead
142
+ [:anchor, :lookahead, sequence(*node.expressions.map { |n| parse(n) })]
143
+ when Regexp::Expression::Assertion::NegativeLookahead
144
+ [:anchor, :negative_lookahead, sequence(*node.expressions.map { |n| parse(n) })]
145
+ when Regexp::Expression::Assertion::Lookbehind
146
+ [:anchor, :lookbehind, sequence(*node.expressions.map { |n| parse(n) })]
147
+ when Regexp::Expression::Assertion::NegativeLookbehind
148
+ [:anchor, :negative_lookbehind, sequence(*node.expressions.map { |n| parse(n) })]
149
+ when Regexp::Expression::CharacterSet
150
+ character_set(node.negative?, node.expressions)
151
+ when Regexp::Expression::Subexpression
152
+ # It's annoyingly subtypes a lot
153
+ unless (node.class == Regexp::Expression::Subexpression or
154
+ node.class == Regexp::Expression::Group::Passive or
155
+ node.class == Regexp::Expression::Root or
156
+ node.class == Regexp::Expression::Alternative)
157
+ raise "Don't know how to deal with #{node.class}"
158
+ end
159
+ sequence(*node.expressions.map { |n| parse(n) })
160
+ when Regexp::Expression::Literal
161
+ literal(node.text.chars)
162
+ when Regexp::Expression::CharacterType::Base
163
+ character_type(node.text)
164
+ when Regexp::Expression::EscapeSequence::Base
165
+ character_type(node.text)
166
+ when Regexp::Expression::Backreference::Number
167
+ num = node.text[%r[\A\\(\d+)\z], 1] or raise "Parse error"
168
+ backref(num.to_i)
169
+ when Regexp::Expression::Anchor::BeginningOfString
170
+ [:anchor, :bos]
171
+ when Regexp::Expression::Anchor::EndOfString
172
+ [:anchor, :eos]
173
+ when Regexp::Expression::Anchor::BeginningOfLine
174
+ [:anchor, :bol]
175
+ when Regexp::Expression::Anchor::EndOfLine
176
+ [:anchor, :eol]
177
+ else
178
+ raise "Unknown expression"
179
+ end
178
180
  if node.quantified?
179
181
  min = node.quantifier.min
180
182
  max = node.quantifier.max
181
183
  result = if result[0] == :group
182
- repeat_group(result, min, max)
183
- else
184
- repeat(result, min, max)
185
- end
184
+ repeat_group(result, min, max)
185
+ else
186
+ repeat(result, min, max)
187
+ end
186
188
  end
187
189
 
188
190
  result
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+
6
+ class SkyscrapersSolver
7
+ def initialize(path)
8
+ parse_data(Pathname(path).read)
9
+ @solver = Z3::Solver.new
10
+ end
11
+
12
+ def call
13
+ setup_grid_vars
14
+ setup_constraints
15
+
16
+ if @solver.satisfiable?
17
+ @model = @solver.model
18
+ print_answer
19
+ else
20
+ puts "No solution"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def setup_grid_vars
27
+ @gridvars = @size.times.map do |y|
28
+ @size.times.map do |x|
29
+ v = Z3.Int("g#{x},#{y}")
30
+ if @grid[y][x]
31
+ @solver.assert v == @grid[y][x]
32
+ else
33
+ @solver.assert v >= 1
34
+ @solver.assert v <= @size
35
+ end
36
+ v
37
+ end
38
+ end
39
+ [*@gridvars, *@gridvars.transpose].each do |row|
40
+ @solver.assert Z3.Distinct(*row)
41
+ end
42
+ end
43
+
44
+ def setup_constraints
45
+ @size.times do |i|
46
+ setup_visibility "L#{i}", @left[i], @gridvars[i]
47
+ setup_visibility "R#{i}", @right[i], @gridvars[i].reverse
48
+ setup_visibility "T#{i}", @top[i], @gridvars.map{|row| row[i] }
49
+ setup_visibility "B#{i}", @bottom[i], @gridvars.map{|row| row[i] }.reverse
50
+ end
51
+ end
52
+
53
+ def setup_visibility(label, expected, vars)
54
+ return unless expected
55
+ # Variables between cells:
56
+ # - count seen
57
+ # - max seen
58
+
59
+ count_vars = (0..@size).map{|i| Z3.Int("#{label}-c#{i}") }
60
+ max_vars = (0..@size).map{|i| Z3.Int("#{label}-m#{i}") }
61
+
62
+ @solver.assert count_vars.first == expected
63
+ @solver.assert max_vars.first == 0
64
+
65
+ @size.times do |i|
66
+ count_near = count_vars[i]
67
+ count_far = count_vars[i+1]
68
+ max_near = max_vars[i]
69
+ max_far = max_vars[i+1]
70
+ current = vars[i]
71
+ visible = Z3.Bool("#{label}-v#{i}")
72
+ @solver.assert visible == (current > max_near)
73
+ @solver.assert count_near == Z3.IfThenElse(visible, count_far+1, count_far)
74
+ @solver.assert max_far == Z3.IfThenElse(visible, current, max_near)
75
+ end
76
+
77
+ @solver.assert count_vars.last == 0
78
+ # This is redundant:
79
+ @solver.assert max_vars.last == @size
80
+ end
81
+
82
+ def print_answer
83
+ @size.times do |y|
84
+ @size.times do |x|
85
+ v = @model[@gridvars[y][x]]
86
+ print "#{v} "
87
+ end
88
+ print "\n"
89
+ end
90
+ end
91
+
92
+ def parse_data(data)
93
+ data = data.lines.map do |line|
94
+ line.split.map do |x|
95
+ if x =~ /\d+/
96
+ x.to_i
97
+ elsif x == "." or x == "-"
98
+ nil
99
+ else
100
+ raise "Unrecognized symbol #{x.inspect} in input"
101
+ end
102
+ end
103
+ end
104
+
105
+ @top = data.shift
106
+ @bottom = data.pop
107
+ @left = data.map(&:shift)
108
+ @right = data.map(&:pop)
109
+ @size = @top.size
110
+ raise "Grid must be square" unless [@top.size, @bottom.size, @left.size, @right.size, *data.map(&:size)].uniq.size == 1
111
+ @grid = data
112
+ end
113
+
114
+ end
115
+
116
+ path = ARGV[0] || Pathname(__dir__) + "skyscrapers-1.txt"
117
+ SkyscrapersSolver.new(path).call
118
+
@@ -0,0 +1,6 @@
1
+ 2 - - -
2
+ - . . 1 . -
3
+ - . . . . 3
4
+ - . . . 2 -
5
+ - . . . . -
6
+ 3 - - 1
@@ -0,0 +1,11 @@
1
+ 3 - 4 - - 3 3 3 3
2
+ - . . . . . . . . 6 -
3
+ - . 1 . . . . . . . -
4
+ - 1 6 . . . . . . . -
5
+ - . . . 3 . . . . . 3
6
+ 4 . . . 7 . . . . . 3
7
+ 3 . . . . . 5 . . . 1
8
+ 4 . . . 4 . . . 6 5 3
9
+ - . . 4 . . . . . . 3
10
+ - . . . . . 2 6 . . -
11
+ - - - 3 4 2 4 4 -
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+
6
+ class StarBattle
7
+ def initialize(path)
8
+ @data = Pathname(path).readlines.map(&:chomp).map(&:split)
9
+ @size = @data.size
10
+ raise unless @data.all?{|row| row.size == @size}
11
+ raise unless containers.size == @size
12
+ @solver = Z3::Solver.new
13
+ end
14
+
15
+ def call
16
+ # Each row contains 2 stars
17
+ @size.times do |y|
18
+ sum = Z3.Add(*@size.times.map{|x| cell_var(x, y).ite(1, 0) })
19
+ @solver.assert sum == 2
20
+ end
21
+
22
+ # Each column contains 2 stars
23
+ @size.times do |x|
24
+ sum = Z3.Add(*@size.times.map{|y| cell_var(x, y).ite(1, 0) })
25
+ @solver.assert sum == 2
26
+ end
27
+
28
+ # Each container contains 2 stars
29
+ coords.group_by{|x,y| container_at(x,y) }.each do |name, cells|
30
+ sum = Z3.Add(*cells.map{|x,y| cell_var(x, y).ite(1, 0) })
31
+ @solver.assert sum == 2
32
+ end
33
+
34
+ # Can't be adjacent
35
+ coords.each do |x, y|
36
+ @solver.assert cell_var(x, y).implies !cell_var(x+1, y)
37
+ @solver.assert cell_var(x, y).implies !cell_var(x-1, y+1)
38
+ @solver.assert cell_var(x, y).implies !cell_var(x, y+1)
39
+ @solver.assert cell_var(x, y).implies !cell_var(x+1, y+1)
40
+ end
41
+
42
+ if @solver.satisfiable?
43
+ @model = @solver.model
44
+ print_answer
45
+ else
46
+ puts "failed to solve"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def coords
53
+ @size.times.flat_map do |y|
54
+ @size.times.map do |x|
55
+ [x,y]
56
+ end
57
+ end
58
+ end
59
+
60
+ def containers
61
+ @containers ||= @data.flatten.uniq.sort
62
+ end
63
+
64
+ def cell_var(x, y)
65
+ return nil unless (0...@size).include?(x)
66
+ return nil unless (0...@size).include?(y)
67
+ Z3.Bool("c#{x},#{y}")
68
+ end
69
+
70
+ def container_at(x, y)
71
+ return nil unless (0...@size).include?(x)
72
+ return nil unless (0...@size).include?(y)
73
+ @data[y][x]
74
+ end
75
+
76
+ def print_corner(x, y)
77
+ if [
78
+ container_at(x, y),
79
+ container_at(x, y-1),
80
+ container_at(x-1, y),
81
+ container_at(x-1, y-1),
82
+ ].uniq.size == 1
83
+ print " "
84
+ else
85
+ print "+"
86
+ end
87
+ end
88
+
89
+ def print_vertical(x, y)
90
+ if x == 0 or container_at(x, y) != container_at(x-1, y)
91
+ print "|"
92
+ else
93
+ print " "
94
+ end
95
+ end
96
+
97
+ def print_horizontal(x, y)
98
+ if y == 0 or container_at(x, y) != container_at(x, y-1)
99
+ print "-"
100
+ else
101
+ print " "
102
+ end
103
+ end
104
+
105
+ def print_cell(x, y)
106
+ if @model[cell_var(x, y)].to_b
107
+ print "*"
108
+ else
109
+ print " "
110
+ end
111
+ end
112
+
113
+ def print_answer
114
+ (0..@size).each do |y|
115
+ (0..@size).each do |x|
116
+ print_corner x, y
117
+ next if x == @size
118
+ print_horizontal x, y
119
+ end
120
+ print "\n"
121
+
122
+ next if y == @size
123
+ (0..@size).each do |x|
124
+ print_vertical x, y
125
+ next if x == @size
126
+ print_cell x, y
127
+ end
128
+ print "\n"
129
+ end
130
+ end
131
+ end
132
+
133
+ path = ARGV[0] || Pathname(__dir__) + "star_battle-1.txt"
134
+ StarBattle.new(path).call
@@ -0,0 +1,10 @@
1
+ a a a a a a a a a b
2
+ a c a d a a a a a b
3
+ a c a d d d d d b b
4
+ c c c d j j d b b b
5
+ c j j d d j d h e e
6
+ j j j j j j d h e e
7
+ j j j i i i i h h e
8
+ j g g g i i i h e e
9
+ f g g f f h i h e e
10
+ f f f f f h h h h h