z3 0.0.20180624 → 0.0.20211213

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 (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
data/examples/stitches ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+ require "paint"
6
+
7
+ class StitchesSolver
8
+ def initialize(path)
9
+ data = Pathname(path).read
10
+ data = data.strip.split("\n").map do |line|
11
+ line.split
12
+ end
13
+ @col_counts = data.shift.map(&:to_i)
14
+ @row_counts = data.map(&:shift).map(&:to_i)
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
+ raise unless @col_counts.sum == @row_counts.sum
21
+ @solver = Z3::Solver.new
22
+ @groups = @data.flatten.uniq
23
+ end
24
+
25
+ def call
26
+ assign_colors!
27
+ setup_cell_vars!
28
+ setup_stitch_vars!
29
+
30
+ setup_cell_assertions!
31
+ setup_stitch_assertions!
32
+ setup_cell_stitch_assertions!
33
+
34
+ if @solver.satisfiable?
35
+ @model = @solver.model
36
+ print_answer!
37
+ else
38
+ puts "failed to solve"
39
+ end
40
+ end
41
+
42
+ def assign_colors!
43
+ @fg = @groups.to_h{|g| [g, [rand(256),rand(256),rand(256)]] }
44
+ @bg = @groups.to_h{|g| [g, [rand(256),rand(256),rand(256)]] }
45
+ end
46
+
47
+ def setup_cell_vars!
48
+ @cell_vars = {}
49
+ @xsize.times do |x|
50
+ @ysize.times do |y|
51
+ v = Z3.Bool("c#{x},#{y}")
52
+ @cell_vars[[x,y]] = v
53
+ end
54
+ end
55
+ end
56
+
57
+ def row_of_cell_vars(y)
58
+ @xsize.times.map{|x| @cell_vars[[x,y]].ite(1,0) }
59
+ end
60
+
61
+ def col_of_cell_vars(x)
62
+ @ysize.times.map{|y| @cell_vars[[x,y]].ite(1,0) }
63
+ end
64
+
65
+ def setup_cell_assertions!
66
+ @xsize.times do |x|
67
+ @solver.assert Z3.Add(*col_of_cell_vars(x)) == @col_counts[x]
68
+ end
69
+
70
+ @ysize.times do |y|
71
+ @solver.assert Z3.Add(*row_of_cell_vars(y)) == @row_counts[y]
72
+ end
73
+ end
74
+
75
+ def setup_stitch_vars!
76
+ @hstitch = {}
77
+ @vstitch = {}
78
+ @stitch_groups = {}
79
+
80
+ (@xsize-1).times do |x|
81
+ @ysize.times do |y|
82
+ v = Z3.Bool("h#{x},#{y}")
83
+ g1 = @data[y][x]
84
+ g2 = @data[y][x+1]
85
+ pair = [g1,g2].sort.uniq
86
+ next if pair.size == 1
87
+ @stitch_groups[pair] ||= []
88
+ @stitch_groups[pair] << v
89
+ @hstitch[[x,y]] = v
90
+ end
91
+ end
92
+
93
+ @xsize.times do |x|
94
+ (@ysize-1).times do |y|
95
+ v = Z3.Bool("v#{x},#{y}")
96
+ g1 = @data[y][x]
97
+ g2 = @data[y+1][x]
98
+ pair = [g1,g2].sort.uniq
99
+ next if pair.size == 1
100
+ @stitch_groups[pair] ||= []
101
+ @stitch_groups[pair] << v
102
+ @vstitch[[x,y]] = v
103
+ end
104
+ end
105
+
106
+ group_count = (2 * @stitch_groups.keys.size)
107
+ raise unless @col_counts.sum % group_count == 0
108
+ @stitch_count = @col_counts.sum / group_count
109
+ end
110
+
111
+ def setup_stitch_assertions!
112
+ @stitch_groups.each do |g, vars|
113
+ @solver.assert Z3.Add(*vars.map{|v| v.ite(1,0)}) == @stitch_count
114
+ end
115
+ end
116
+
117
+ def setup_cell_stitch_assertions!
118
+ @xsize.times do |x|
119
+ @ysize.times do |y|
120
+ vars = [
121
+ @hstitch[[x,y]],
122
+ @hstitch[[x-1,y]],
123
+ @vstitch[[x,y]],
124
+ @vstitch[[x,y-1]],
125
+ ].compact.map{|v| v.ite(1,0)}
126
+ if vars.empty?
127
+ @solver.assert !@cell_vars[[x,y]]
128
+ else
129
+ @solver.assert @cell_vars[[x,y]].ite(1,0) == Z3.Add(*vars)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def group_at(x,y)
136
+ return nil if x < 0 or y < 0
137
+ return nil if x >= @xsize or y >= @ysize
138
+ @data[y][x]
139
+ end
140
+
141
+ def paint_for(s,x1,y1,x2=x1,y2=y1)
142
+ g1 = group_at(x1,y1)
143
+ g2 = group_at(x2,y2)
144
+ if g1 == g2
145
+ Paint[s, @fg[g1], @bg[g1]]
146
+ else
147
+ s
148
+ end
149
+ end
150
+
151
+ def print_answer!
152
+ @ysize.times do |y|
153
+ @xsize.times do |x|
154
+ s = @model[@cell_vars[[x,y]]].to_b ? "*" : " "
155
+ print paint_for(s, x, y)
156
+ if @hstitch[[x,y]]
157
+ hs = @model[@hstitch[[x,y]]].to_b ? "-" : " "
158
+ else
159
+ hs = " "
160
+ end
161
+ print paint_for(hs, x, y, x+1, y)
162
+ end
163
+ print "\n"
164
+ next if y == @ysize - 1
165
+ @xsize.times do |x|
166
+ if @vstitch[[x,y]]
167
+ vs = @model[@vstitch[[x,y]]].to_b ? "|" : " "
168
+ else
169
+ vs = " "
170
+ end
171
+ print paint_for(vs, x, y, x, y+1)
172
+ print paint_for(" ", x, y, x+1, y+1)
173
+ end
174
+ print "\n"
175
+ end
176
+ end
177
+ end
178
+
179
+ path = ARGV[0] || Pathname(__dir__) + "stitches-1.txt"
180
+ StitchesSolver.new(path).call
@@ -0,0 +1,11 @@
1
+ 6 8 6 9 2 5 4 8 2 2
2
+ 3 a a a a a a a b b b
3
+ 6 a a c c c a a b c b
4
+ 4 a f f c c b b b c b
5
+ 6 f f f f c c c c c b
6
+ 8 h h h f c h c b b b
7
+ 4 g h h h h h c c b b
8
+ 6 g g e e e h h h e b
9
+ 8 g g g g e h h e e e
10
+ 5 d d e e e e e e e e
11
+ 2 d d d d e e e e e e
data/examples/sudoku CHANGED
@@ -13,7 +13,7 @@ class SudokuSolver
13
13
  @solver = Z3::Solver.new
14
14
  end
15
15
 
16
- def solve!
16
+ def call
17
17
  @cells = (0..8).map do |j|
18
18
  (0..8).map do |i|
19
19
  cell_var(@data[j][i], i, j)
@@ -58,4 +58,4 @@ class SudokuSolver
58
58
  end
59
59
 
60
60
  path = ARGV[0] || Pathname(__dir__) + "sudoku-1.txt"
61
- SudokuSolver.new(path).solve!
61
+ SudokuSolver.new(path).call
data/examples/suguru ADDED
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+
6
+ class Suguru
7
+ def initialize(path)
8
+ @data = Pathname(path).readlines.map(&:chomp)
9
+ @ysize = (@data.size - 1) / 2
10
+ @xsize = (@data[0].size - 1) / 2
11
+ raise unless @data.size == 2 * @ysize + 1
12
+ raise unless @data.all?{ |row| row.size == 2 * @xsize + 1 }
13
+ @solver = Z3::Solver.new
14
+ end
15
+
16
+ def call
17
+ @connections = calculate_connections
18
+ @containers = calculate_containers
19
+
20
+ @ysize.times do |y|
21
+ @ysize.times do |x|
22
+ v = cell_data(x, y)
23
+ if v
24
+ @solver.assert cell(x, y) == v
25
+ end
26
+ end
27
+ end
28
+
29
+ @containers.each do |name, cells|
30
+ @solver.assert Z3.Distinct(*cells)
31
+ cells.each do |c|
32
+ @solver.assert c >= 1
33
+ @solver.assert c <= cells.size
34
+ end
35
+ end
36
+
37
+ @ysize.times do |y|
38
+ @xsize.times do |x|
39
+ @solver.assert cell(x, y) != cell(x+1, y)
40
+ @solver.assert cell(x, y) != cell(x-1, y+1)
41
+ @solver.assert cell(x, y) != cell(x, y+1)
42
+ @solver.assert cell(x, y) != cell(x+1, y+1)
43
+ end
44
+ end
45
+
46
+ if @solver.satisfiable?
47
+ @model = @solver.model
48
+ print_answer
49
+ else
50
+ puts "failed to solve"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def calculate_containers
57
+ cells = @xsize.times.flat_map{|x| @ysize.times.map{|y| [x,y]}}
58
+
59
+ containers = {}
60
+ cell_assignments = {}
61
+ cells.each do |c|
62
+ ct = c.join(",")
63
+ containers[ct] = [c]
64
+ cell_assignments[c] = ct
65
+ end
66
+
67
+ @connections.each do |c1, c2|
68
+ # Already in same container
69
+ ct1 = cell_assignments[c1]
70
+ ct2 = cell_assignments[c2]
71
+ next if ct1 == ct2
72
+ containers.delete(ct2).each do |c3|
73
+ cell_assignments[c3] = ct1
74
+ containers[ct1] << c3
75
+ end
76
+ end
77
+
78
+ containers.transform_values{|cells| cells.map{|x,y| cell(x,y)}}
79
+ end
80
+
81
+ def calculate_connections
82
+ result = []
83
+
84
+ @ysize.times do |y|
85
+ (0..@xsize-2).each do |x|
86
+ if cell_open_right?(x, y)
87
+ result << [[x,y], [x+1,y]]
88
+ end
89
+ end
90
+ end
91
+
92
+ @xsize.times do |x|
93
+ (0..@ysize-2).each do |y|
94
+ if cell_open_down?(x, y)
95
+ result << [[x,y], [x,y+1]]
96
+ end
97
+ end
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ def cell_open_right?(x, y)
104
+ @data[2*y+1][2*x+2] == " "
105
+ end
106
+
107
+ def cell_open_down?(x, y)
108
+ @data[2*y+2][2*x+1] == " "
109
+ end
110
+
111
+ def cell_data(x, y)
112
+ v = @data[2*y+1][2*x+1]
113
+ return nil if v == " "
114
+ v.to_i
115
+ end
116
+
117
+ def cell(x, y)
118
+ Z3.Int("c#{x},#{y}")
119
+ end
120
+
121
+ def print_answer
122
+ output = @data.map(&:dup)
123
+ @ysize.times do |y|
124
+ @xsize.times do |x|
125
+ # This only works for 1-9, could use hex or something for more
126
+ value = @model[cell(x,y)].to_s
127
+ output[2*y+1][2*x+1, 1] = value
128
+ end
129
+ end
130
+
131
+ puts output
132
+ end
133
+ end
134
+
135
+ path = ARGV[0] || Pathname(__dir__) + "suguru-1.txt"
136
+ Suguru.new(path).call
137
+
138
+
139
+ __END__
140
+
141
+ # Based on https://play.google.com/store/apps/details?id=com.alexuvarov.android.kropki.puzzle&hl=en_GB
142
+ #
143
+ # NxN grid
144
+ # Each cell contains numbers 1 to N
145
+ # Each row and each column contains distinct numbers
146
+ # If there's black dot (o) between cells, one is twice the other
147
+ # If there's white dot (*) between cells, one is the other plus one
148
+ # If there's no dot, neither of these is true
149
+ # (for 1/2, dot can be of either color)
150
+
151
+ class Kropki
152
+ def call
153
+ # Cells contain numbers 1 - N
154
+ @size.times do |y|
155
+ @size.times do |x|
156
+ @solver.assert cell(x, y) >= 1
157
+ @solver.assert cell(x, y) <= @size
158
+ end
159
+ end
160
+
161
+ # Each row and column distinct
162
+ @size.times do |y|
163
+ @solver.assert Z3.Distinct(*@size.times.map{ |x| cell(x,y) })
164
+ end
165
+ @size.times do |x|
166
+ @solver.assert Z3.Distinct(*@size.times.map{ |y| cell(x,y) })
167
+ end
168
+
169
+ # Horizontal dots
170
+ @size.times do |y|
171
+ (0...@size).each do |x|
172
+ dot_constraints cell(x, y), cell(x+1, y), @data[2*y+1][2*x+2]
173
+ end
174
+ end
175
+
176
+ # Vertical dots
177
+ @size.times do |x|
178
+ (0...@size).each do |y|
179
+ dot_constraints cell(x, y), cell(x, y+1), @data[2*y+2][2*x+1]
180
+ end
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def dot_constraints(c1, c2, dot)
187
+ if dot == "o"
188
+ @solver.assert Z3.Or(c1 == c2 + 1, c2 == c1 + 1)
189
+ elsif dot == "*"
190
+ @solver.assert Z3.Or(c1 == c2 * 2, c2 == c1 * 2)
191
+ else
192
+ @solver.assert c1 != c2 + 1
193
+ @solver.assert c2 != c1 + 1
194
+ @solver.assert c1 != c2 * 2
195
+ @solver.assert c2 != c1 * 2
196
+ end
197
+ end
198
+
199
+ end
@@ -0,0 +1,17 @@
1
+ +---+-----+
2
+ | |1 4 |
3
+ | | +---+
4
+ |2 6| | 2|
5
+ | | | |
6
+ | | |4 3|
7
+ +---+-+ |
8
+ | 3 | |
9
+ | +-+---+
10
+ |2 | |
11
+ +-+ +-+ |
12
+ | | |6 4|
13
+ +-+---+ |
14
+ | |3 | 3|
15
+ | | +---+
16
+ | | 1 2 |
17
+ +-+-------+
@@ -16,7 +16,7 @@ class VerbalArithmetic
16
16
  @c = c.chars.map{|v| @vars[v]}
17
17
  end
18
18
 
19
- def solve!
19
+ def call
20
20
  @solver.assert @a[0] != 0
21
21
  @solver.assert @b[0] != 0
22
22
  @solver.assert @c[0] != 0
@@ -44,4 +44,4 @@ class VerbalArithmetic
44
44
  end
45
45
  end
46
46
 
47
- VerbalArithmetic.new("SEND", "MORE", "MONEY").solve!
47
+ VerbalArithmetic.new("SEND", "MORE", "MONEY").call
data/examples/yajilin ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require_relative "../lib/z3"
5
+
6
+ class Yajilin
7
+ def initialize(path)
8
+ @data = Pathname(path)
9
+ .readlines
10
+ .map{|line|
11
+ line
12
+ .chomp
13
+ .split
14
+ .map{|x| x == "." ? nil : x}
15
+ }
16
+ @xsize = @data.size
17
+ @ysize = @data[0].size
18
+ raise unless @data.all?{|row| row.size == @xsize}
19
+ @solver = Z3::Solver.new
20
+ end
21
+
22
+ def on_board?(x,y)
23
+ x >= 0 and y >= 0 and x < @xsize and y < @ysize
24
+ end
25
+
26
+ # true = white
27
+ # false = black
28
+ def cvar(x,y)
29
+ return unless on_board?(x,y)
30
+ Z3.Bool("c[#{x},#{y}]")
31
+ end
32
+
33
+ # true - connected
34
+ # right from (x,y)
35
+ def hvar(x,y)
36
+ return unless on_board?(x,y) and x != @xsize - 1
37
+ Z3.Bool("h[#{x},#{y}]")
38
+ end
39
+
40
+ # true - connected
41
+ # down from (x,y)
42
+ def vvar(x,y)
43
+ return unless on_board?(x,y) and y != @ysize - 1
44
+ Z3.Bool("v[#{x},#{y}]")
45
+ end
46
+
47
+ # Every cell has a number that shows its position within the loop
48
+ def nvar(x,y)
49
+ Z3.Int("n[#{x},#{y}]")
50
+ end
51
+
52
+ def hint_cell?(x,y)
53
+ !!@data[y][x]
54
+ end
55
+
56
+ def neighbours(x,y)
57
+ [
58
+ [x+1, y ],
59
+ [x-1, y ],
60
+ [x, y+1],
61
+ [x, y-1],
62
+ ].select{|nx,ny|
63
+ on_board?(nx, ny) and !hint_cell?(nx,ny)
64
+ }
65
+ end
66
+
67
+ def neighbour_cvars(x,y)
68
+ neighbours(x,y)
69
+ .map{|nx,ny| cvar(nx, ny)}
70
+ end
71
+
72
+ def connections_at(x,y)
73
+ [
74
+ hvar(x,y),
75
+ hvar(x-1,y),
76
+ vvar(x,y),
77
+ vvar(x,y-1),
78
+ ].compact
79
+ end
80
+
81
+ def count_loops_at(x,y)
82
+ Z3.Add(*connections_at(x,y).map{|n| n.ite(1,0)})
83
+ end
84
+
85
+ def assert_connections
86
+ each_xy do |x,y|
87
+ if hint_cell?(x, y)
88
+ @solver.assert count_loops_at(x,y) == 0
89
+ else
90
+ @solver.assert cvar(x,y).implies(count_loops_at(x,y) == 2)
91
+ @solver.assert (~cvar(x,y)).implies(count_loops_at(x,y) == 0)
92
+ end
93
+ end
94
+ end
95
+
96
+ def assert_no_black_multiples
97
+ each_xy do |x,y|
98
+ next if hint_cell?(x, y)
99
+ @solver.assert (~cvar(x,y)).implies(Z3.And(*neighbour_cvars(x,y)))
100
+ end
101
+ end
102
+
103
+ def stripe_left(x0,y)
104
+ (0...x0).map{|x| cvar(x,y) unless hint_cell?(x,y) }.compact
105
+ end
106
+
107
+ def stripe_right(x0,y)
108
+ (x0+1...@xsize).map{|x| cvar(x,y) unless hint_cell?(x,y) }.compact
109
+ end
110
+
111
+ def stripe_up(x,y0)
112
+ (0...y0).map{|y| cvar(x,y) unless hint_cell?(x,y) }.compact
113
+ end
114
+
115
+ def stripe_down(x,y0)
116
+ (y0+1...@ysize).map{|y| cvar(x,y) unless hint_cell?(x,y) }.compact
117
+ end
118
+
119
+ def assert_arrows
120
+ each_xy do |x,y|
121
+ if hint_cell?(x, y)
122
+ count = @data[y][x][0].to_i
123
+ dir = @data[y][x][1]
124
+ case dir
125
+ when "<", "←"
126
+ stripe = stripe_left(x,y)
127
+ when ">", "→"
128
+ stripe = stripe_right(x,y)
129
+ when "^", "↑"
130
+ stripe = stripe_up(x,y)
131
+ when "_", "↓"
132
+ stripe = stripe_down(x,y)
133
+ else
134
+ raise
135
+ end
136
+
137
+ @solver.assert Z3.Add(*stripe.map{|c| c.ite(0,1)}) == count
138
+ end
139
+ end
140
+ end
141
+
142
+ def each_xy
143
+ @ysize.times do |y|
144
+ @xsize.times do |x|
145
+ yield(x,y)
146
+ end
147
+ end
148
+ end
149
+
150
+ def connections_and_nvars(x,y)
151
+ [
152
+ [x+1, y, hvar(x, y)],
153
+ [x-1, y, hvar(x-1, y)],
154
+ [x, y+1, vvar(x, y)],
155
+ [x, y-1, vvar(x, y-1)],
156
+ ].select{|nx,ny,convar|
157
+ on_board?(nx, ny) and !hint_cell?(nx,ny)
158
+ }.map{|nx,ny,convar| [nvar(nx,ny), convar] }
159
+ end
160
+
161
+ # Loop will have Ns of 0+, all distinct and consecutive
162
+ # Non-loop will have Ns of MAX + cell_number
163
+ def assert_single_loop
164
+ # max_value = @xsize * @ysize + 10
165
+ max_value = 1000
166
+
167
+ each_xy do |x,y|
168
+ cell_idx = x + y * @xsize
169
+ @solver.assert nvar(x,y) >= 0
170
+ if hint_cell?(x,y)
171
+ @solver.assert nvar(x,y) == max_value + cell_idx
172
+ else
173
+ @solver.assert (~cvar(x,y)).implies( nvar(x,y) == max_value + cell_idx )
174
+
175
+ nval = connections_and_nvars(x,y)
176
+ .map{|n,c| c.ite(n+1, max_value) }
177
+ .reduce{|a,b| (a <= b).ite(a, b) }
178
+
179
+ @solver.assert cvar(x,y).implies((nvar(x,y) == 0) | (nvar(x,y) == nval) )
180
+ @solver.assert cvar(x,y).implies(nvar(x,y) < max_value)
181
+ end
182
+ end
183
+
184
+ # only one loop start
185
+ @solver.assert Z3.Add(
186
+ *enum_for(:each_xy).map{|x,y|
187
+ (nvar(x,y) == 0).ite(1,0)
188
+ }) == 1
189
+ end
190
+
191
+ def call
192
+ assert_connections
193
+ assert_no_black_multiples
194
+ assert_arrows
195
+ assert_single_loop
196
+
197
+ if @solver.satisfiable?
198
+ @model = @solver.model
199
+ print_answer
200
+ else
201
+ puts "failed to solve"
202
+ end
203
+ end
204
+
205
+ def hint_data(x,y)
206
+ u = @data[y][x]
207
+ n = u[0]
208
+ a = {
209
+ "<" => "←",
210
+ ">" => "→",
211
+ "_" => "↓",
212
+ "^" => "↑",
213
+ }[u[1]] || u[1]
214
+ n + a
215
+ end
216
+
217
+ # TODO: make it nicer
218
+ def print_answer
219
+ @ysize.times do |y|
220
+ @xsize.times do |x|
221
+ if hint_cell?(x,y)
222
+ print hint_data(x,y)
223
+ # print "O "
224
+ else
225
+ c = @model[cvar(x,y)].to_b
226
+ if c
227
+ print "* "
228
+ else
229
+ print "# "
230
+ end
231
+ end
232
+
233
+ next if x == @xsize - 1
234
+ if @model[hvar(x,y)].to_b
235
+ print "-"
236
+ else
237
+ print " "
238
+ end
239
+ end
240
+ print "\n"
241
+
242
+ next if y == @ysize-1
243
+ @xsize.times do |x|
244
+ if @model[vvar(x,y)].to_b
245
+ print "| "
246
+ else
247
+ print " "
248
+ end
249
+ end
250
+
251
+ print "\n"
252
+ end
253
+ end
254
+
255
+ def print_debug_loop_info
256
+ print "\n"
257
+ @ysize.times do |y|
258
+ @xsize.times do |x|
259
+ n = @model[nvar(x,y)].to_i
260
+ print "%8d" % n
261
+ end
262
+ print "\n"
263
+ end
264
+ end
265
+ end
266
+
267
+ path = ARGV[0] || Pathname(__dir__) + "yajilin-1.txt"
268
+ Yajilin.new(path).call
@@ -0,0 +1,10 @@
1
+ . . . . . . . . . .
2
+ . . . . 2↓ . . . . .
3
+ . . . . . . . . . .
4
+ . . 2↑ . . . . 2↑ . .
5
+ . . . 1↓ . . . . . .
6
+ . . . . . . 1→ . . .
7
+ . . . . . . . . 0→ .
8
+ 0↑ . . 1← . 1↓ . . . .
9
+ . . . . . . . . 2← .
10
+ . . . . . . . . . .