z3 0.0.20180624 → 0.0.20211213

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) 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/bitvec_expr.rb +10 -0
  72. data/lib/z3/expr/expr.rb +16 -15
  73. data/lib/z3/low_level.rb +6 -2
  74. data/lib/z3/low_level_auto.rb +180 -36
  75. data/lib/z3/optimize.rb +5 -4
  76. data/lib/z3/printer.rb +29 -1
  77. data/lib/z3/sort/bitvec_sort.rb +1 -0
  78. data/lib/z3/sort/sort.rb +9 -5
  79. data/lib/z3/very_low_level.rb +8 -5
  80. data/lib/z3/very_low_level_auto.rb +45 -9
  81. data/spec/bitvec_expr_spec.rb +35 -21
  82. data/spec/bitvec_sort_spec.rb +4 -0
  83. data/spec/expr_spec.rb +62 -0
  84. data/spec/integration/abc_path_spec.rb +21 -0
  85. data/spec/integration/aquarium_spec.rb +27 -0
  86. data/spec/integration/cats_organized_neatly_spec.rb +14 -0
  87. data/spec/integration/color_nonogram_spec.rb +28 -0
  88. data/spec/integration/dominion_spec.rb +14 -0
  89. data/spec/integration/dominosa_spec.rb +21 -0
  90. data/spec/integration/eulero_spec.rb +11 -0
  91. data/spec/integration/futoshiki_spec.rb +23 -0
  92. data/spec/integration/kakurasu_spec.rb +18 -0
  93. data/spec/integration/killer_sudoku_spec.rb +10 -0
  94. data/spec/integration/knights_puzzle_spec.rb +11 -11
  95. data/spec/integration/kropki_spec.rb +19 -0
  96. data/spec/integration/miracle_sudoku_spec.rb +15 -0
  97. data/spec/integration/mortal_coil_puzzle_spec.rb +8 -6
  98. data/spec/integration/nanro_spec.rb +39 -0
  99. data/spec/integration/nine_clocks_spec.rb +30 -0
  100. data/spec/integration/oneofus_spec.rb +7 -15
  101. data/spec/integration/regexp_crossword_solver_spec.rb +1 -1
  102. data/spec/integration/renzoku_spec.rb +23 -0
  103. data/spec/integration/sandwich_sudoku_spec.rb +15 -0
  104. data/spec/integration/skyscraper_spec.rb +10 -0
  105. data/spec/integration/star_battle_spec.rb +27 -0
  106. data/spec/integration/stitches_spec.rb +25 -0
  107. data/spec/integration/suguru_spec.rb +23 -0
  108. data/spec/integration/yajilin_spec.rb +25 -0
  109. data/spec/interface_spec.rb +18 -0
  110. data/spec/optimize_spec.rb +6 -4
  111. data/spec/printer_spec.rb +30 -0
  112. data/spec/set_expr_spec.rb +14 -8
  113. data/spec/solver_spec.rb +4 -5
  114. data/spec/spec_helper.rb +15 -0
  115. metadata +104 -24
@@ -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