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.
- checksums.yaml +4 -4
- data/README.md +2 -4
- data/Rakefile +15 -8
- data/examples/abc_path +187 -0
- data/examples/abc_path-1.txt +7 -0
- data/examples/algebra_problems +12 -12
- data/examples/aquarium +133 -0
- data/examples/aquarium-1.txt +11 -0
- data/examples/bridges +2 -2
- data/examples/cats_organized_neatly +133 -0
- data/examples/cats_organized_neatly-10.txt +15 -0
- data/examples/cats_organized_neatly-3.txt +8 -0
- data/examples/cats_organized_neatly-48.txt +32 -0
- data/examples/circuit_problems +4 -4
- data/examples/clogic_puzzle +2 -2
- data/examples/color_nonogram +150 -0
- data/examples/color_nonogram-1.txt +23 -0
- data/examples/crossflip +2 -4
- data/examples/dominion +153 -0
- data/examples/dominion-1.txt +8 -0
- data/examples/dominosa +133 -0
- data/examples/dominosa-1.txt +8 -0
- data/examples/eulero +99 -0
- data/examples/eulero-1.txt +5 -0
- data/examples/four_hackers_puzzle +2 -2
- data/examples/futoshiki +128 -0
- data/examples/futoshiki-1.txt +17 -0
- data/examples/kakurasu +73 -0
- data/examples/kakurasu-1.txt +2 -0
- data/examples/kakuro +2 -2
- data/examples/killer_sudoku +88 -0
- data/examples/killer_sudoku-1.txt +17 -0
- data/examples/killer_sudoku-2.txt +53 -0
- data/examples/kinematics_problems +20 -20
- data/examples/knights_puzzle +2 -2
- data/examples/kropki +100 -0
- data/examples/kropki-1.txt +13 -0
- data/examples/letter_connections +2 -2
- data/examples/light_up +2 -2
- data/examples/minisudoku +2 -2
- data/examples/miracle_sudoku +135 -0
- data/examples/miracle_sudoku-1.txt +9 -0
- data/examples/mortal_coil_puzzle +2 -2
- data/examples/nanro +245 -0
- data/examples/nanro-1.txt +8 -0
- data/examples/nine_clocks +106 -0
- data/examples/nonogram +2 -2
- data/examples/pyramid_nonogram +2 -2
- data/examples/regexp_crossword_solver +2 -2
- data/examples/regexp_solver +2 -2
- data/examples/renzoku +124 -0
- data/examples/renzoku-1.txt +17 -0
- data/examples/sandwich_sudoku +101 -0
- data/examples/sandwich_sudoku-1.txt +10 -0
- data/examples/selfref +2 -2
- data/examples/simple_regexp_parser.rb +58 -56
- data/examples/skyscrapers +118 -0
- data/examples/skyscrapers-1.txt +6 -0
- data/examples/skyscrapers-2.txt +11 -0
- data/examples/star_battle +134 -0
- data/examples/star_battle-1.txt +10 -0
- data/examples/stitches +180 -0
- data/examples/stitches-1.txt +11 -0
- data/examples/sudoku +2 -2
- data/examples/suguru +199 -0
- data/examples/suguru-1.txt +17 -0
- data/examples/verbal_arithmetic +2 -2
- data/examples/yajilin +268 -0
- data/examples/yajilin-1.txt +10 -0
- data/lib/z3/ast.rb +8 -0
- data/lib/z3/expr/expr.rb +16 -15
- data/lib/z3/low_level.rb +6 -2
- data/lib/z3/low_level_auto.rb +180 -36
- data/lib/z3/optimize.rb +4 -4
- data/lib/z3/very_low_level.rb +8 -5
- data/lib/z3/very_low_level_auto.rb +45 -9
- data/spec/expr_spec.rb +62 -0
- data/spec/integration/abc_path_spec.rb +21 -0
- data/spec/integration/aquarium_spec.rb +27 -0
- data/spec/integration/cats_organized_neatly_spec.rb +14 -0
- data/spec/integration/color_nonogram_spec.rb +28 -0
- data/spec/integration/dominion_spec.rb +14 -0
- data/spec/integration/dominosa_spec.rb +21 -0
- data/spec/integration/eulero_spec.rb +11 -0
- data/spec/integration/futoshiki_spec.rb +23 -0
- data/spec/integration/kakurasu_spec.rb +18 -0
- data/spec/integration/killer_sudoku_spec.rb +10 -0
- data/spec/integration/knights_puzzle_spec.rb +11 -11
- data/spec/integration/kropki_spec.rb +19 -0
- data/spec/integration/miracle_sudoku_spec.rb +15 -0
- data/spec/integration/mortal_coil_puzzle_spec.rb +8 -6
- data/spec/integration/nanro_spec.rb +39 -0
- data/spec/integration/nine_clocks_spec.rb +30 -0
- data/spec/integration/oneofus_spec.rb +7 -15
- data/spec/integration/regexp_crossword_solver_spec.rb +1 -1
- data/spec/integration/renzoku_spec.rb +23 -0
- data/spec/integration/sandwich_sudoku_spec.rb +15 -0
- data/spec/integration/skyscraper_spec.rb +10 -0
- data/spec/integration/star_battle_spec.rb +27 -0
- data/spec/integration/stitches_spec.rb +25 -0
- data/spec/integration/suguru_spec.rb +23 -0
- data/spec/integration/yajilin_spec.rb +25 -0
- data/spec/optimize_spec.rb +6 -4
- data/spec/set_expr_spec.rb +14 -8
- data/spec/solver_spec.rb +4 -5
- data/spec/spec_helper.rb +15 -0
- 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
|
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
|
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.
|
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
|
134
|
+
def parse(node = @tree)
|
135
135
|
result = case node
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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,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
|