z3 0.0.20180629 → 0.0.20220203

Sign up to get free protection for your applications and to get access to all the features.
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
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
+ . . . . . . . . . .