Last active
September 7, 2020 02:43
-
-
Save petertseng/6c7a89947101dda83190df5f10b532e7 to your computer and use it in GitHub Desktop.
Towers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/towers.html | |
# Should be able to do Hard, | |
# which is listed in latin.c as diff_set_0, | |
# handled by checking all permutations for all rows + columns, | |
# and can do Extreme, | |
# which is listed in latin.c as both diff_set_1 and diff_forcing, | |
# which are implemented as xwing and forcing_chain | |
# | |
# https://www.janko.at/Raetsel/Wolkenkratzer/index.htm | |
# Have not yet determined whether any puzzle on janko.at actually has diagonal labels, | |
# but some have a diagonal restriction where diagonals can't contain repeated numbers | |
class Grid | |
attr_reader :side_length | |
def self.from_link_or_file(s) | |
s.include?('http') ? from_link(s) : from_janko(File.readlines(s)) | |
end | |
def self.from_link(link) | |
link.include?('janko.at') ? from_janko(`curl #{link}`.lines) : from_sgtatham(link) | |
end | |
def self.from_sgtatham(link) | |
# Example string: | |
# (url)#6:2/2/3/2/1/3/4/2/2/1/3/3/4/1/2/2/2/3/2/3/3/1/2/3,a4h3a3l3b2f1 | |
l, str = link.split(?#, 2) | |
str ||= l | |
side_length_str, heights_str = str.split(?:, 2) | |
side_length = Integer(side_length_str) | |
heights_str, known_str = heights_str.split(?,, 2) | |
# sadly but understandably, split removes all trailing elements, | |
# so in the case of something like 3//, I need to add one element and remove it after. | |
heights = (heights_str + '/0').split(?/).map { |x| x.empty? ? nil : Integer(x) } | |
raise "popped off something weird from #{heights_str}" if heights.pop != 0 | |
raise "bad heights #{heights} #{heights.size}, need #{side_length * 4}" if heights.size != side_length * 4 | |
heights = heights.each_slice(side_length).to_a | |
knowns = Array.new(side_length) { Array.new(side_length, nil) } | |
nonzero_digit = (?1..?9) | |
i = 0 | |
known_str&.each_char { |c| | |
if nonzero_digit.cover?(c) | |
y, x = i.divmod(side_length) | |
knowns[y][x] = Integer(c) | |
i += 1 | |
elsif c == ?_ | |
# Seems to be used to separate two consecutive digits, | |
# which isn't currently useful since only up to 9x9 is supported, | |
# but maybe useful if one day two-digit numbers are allowed. | |
next | |
else | |
i += 1 + c.ord - ?a.ord | |
end | |
} | |
new(knowns, *heights) | |
end | |
def self.from_janko(lines) | |
labels = false | |
problem = false | |
diagonals = false | |
side_length = nil | |
max_height = nil | |
heights = [] | |
knowns = [] | |
conv = ->l { l.split.map { _1 == ?- ? nil : Integer(_1) } } | |
lines.each { |l| | |
if l.start_with?('size') | |
raise "already know side_length #{side_length}" if side_length | |
side_length = Integer(l.split.last) | |
next | |
elsif l.start_with?('depth') | |
raise "already know max_height #{max_height}" if max_height | |
max_height = Integer(l.split.last) | |
elsif l.chomp.match?(/puzzle\s+skyscrapers,\s+diagonals/) | |
diagonals = true | |
elsif l.chomp == 'clabels' | |
labels = true | |
next | |
elsif l.chomp == 'rlabels' | |
next | |
elsif l.chomp == 'problem' | |
problem = true | |
labels = false | |
next | |
elsif l.chomp == 'solution' | |
break | |
end | |
heights << conv[l] if labels | |
knowns << conv[l] if problem | |
} | |
knowns = Array.new(side_length) { Array.new(side_length, nil) } if knowns.empty? | |
new(knowns, *heights, max_height: max_height, diagonals: diagonals) | |
end | |
# top and bottom are left-to-right | |
# left and right are top-to-bottom | |
# knowns is a 2d array that must be square | |
# int for a known height, nil for unknown | |
def initialize(knowns, top, bottom, left, right, max_height: nil, diagonals: false) | |
@side_length = knowns.size | |
@diagonals = diagonals | |
sizes = knowns.map(&:size) | |
raise "bad sizes #{sizes}" if sizes.any? { _1 != @side_length } | |
@max_height = max_height || @side_length | |
heights = (1..@max_height).to_a | |
raise "too many heights #{heights}" if heights.size > @side_length | |
heights.unshift(0) until heights.size == @side_length | |
@heights = heights.freeze | |
@bit_position_of = heights.uniq.each_with_index.to_h.freeze | |
@num_unique_heights = @bit_position_of.size | |
@num_zeroes = @heights.index(1) | |
@top = top.freeze | |
@bottom = bottom.freeze | |
@left = left.freeze | |
@right = right.freeze | |
# Helps not do unnecessary work for the larger sizes such as 8 and 9. | |
# Try commenting out the unless @row_dirty / unless @col_dirty for those sizes to compare. | |
@row_dirty = Array.new(@side_length) { |i| left[i] || right[i] } | |
@col_dirty = Array.new(@side_length) { |i| top[i] || bottom[i] } | |
@diag_dirty = { | |
falling: false, | |
rising: false, | |
} | |
reset_changed | |
# Pencil marks: | |
# | |
# Each element of the array is a row of the grid. | |
# | |
# Within each row: | |
# Least-significant side_length bits are the leftmost cell of that row | |
# Most-significant side_length bits are the rightmost cell of that row | |
# | |
# Within each chunk of side_length bits: | |
# Least-significant bit is whether 1 is possible | |
# Most-significant bit is whether side_length is possible | |
# 1 if possible, else 0 | |
@pencil_marks = Array.new(@side_length, (1 << @num_unique_heights * @side_length) - 1) | |
# this might make some rows/cols dirty that have no number | |
# but I guess that's fine? if it reveals any hidden/naked groups | |
knowns.each_with_index { |known_row, y| | |
# e.g 111111 | |
full_cell = (1 << @num_unique_heights) - 1 | |
known_row.each_with_index { |known, x| | |
next unless known | |
# eg 111011 | |
except_known = full_cell & ~(1 << @bit_position_of[known]) | |
clear_cell(y, x, except_known) | |
} | |
} | |
# Cache possible permutations for each row/col, | |
# so that we don't repeatedly generate ones we've previously eliminated. | |
@row_perms = @pencil_marks.map { |row| | |
self.class.permutations_for_group(row, @heights, @bit_position_of).to_a | |
}.freeze | |
@col_perms = @side_length.times.map { |x| | |
self.class.permutations_for_group(col_mask(x), @heights, @bit_position_of).to_a | |
}.freeze | |
if diagonals | |
@diag_perms = { | |
falling: self.class.permutations_for_group(diag_mask(:falling), @heights, @bit_position_of).to_a, | |
rising: self.class.permutations_for_group(diag_mask(:rising), @heights, @bit_position_of).to_a, | |
}.freeze | |
end | |
end | |
def col_mask(x) | |
# 0 (top row) least significant bit | |
@pencil_marks.each_with_index.sum { |row, y| | |
row[x * @num_unique_heights, @num_unique_heights] << (@num_unique_heights * y) | |
} | |
end | |
def diag_mask(dir) | |
# 0 (top row) least significant bit | |
x0, dx = self.class.diag_xs(dir, side_length) | |
@pencil_marks.each_with_index.sum { |row, y| | |
x = x0 + dx * y | |
row[x * @num_unique_heights, @num_unique_heights] << (@num_unique_heights * y) | |
} | |
end | |
def self.diag_xs(dir, side_length) | |
# 0 (top row) least significant bit | |
case dir | |
when :falling | |
[0, 1] | |
when :rising | |
[side_length - 1, -1] | |
else | |
raise "unknown diag dir #{dir}" | |
end | |
end | |
def solved? | |
@pencil_marks.all? { |row| | |
row.digits(1 << @num_unique_heights).all? { self.class.power_of_two?(_1) } | |
} | |
end | |
def possible?(y, x, n) | |
@pencil_marks[y][x * @num_unique_heights + @bit_position_of[n]] != 0 | |
end | |
# If change, returns the resulting union of masks. | |
# If no change, returns nil. | |
def self.step_for_group(perms, original_mask, bit_position_of, forward, reverse, type) | |
union_of_masks = 0 | |
#puts ?- * 40 if type == :col | |
# Possible but a bit slow for larger grids. | |
#(1..side_length).to_a.permutation { |perm| | |
#permutations_for_group(original_mask, side_length).each { |perm| | |
num_unique_heights = bit_position_of.size | |
perms.select! { |perm| | |
# Note that when making a bitfield this way, | |
# element 0 will be the least significant bit. | |
bits = perm.each_with_index.sum { |x, i| | |
1 << (bit_position_of[x] + num_unique_heights * i) | |
} | |
next unless original_mask.allbits?(bits) | |
#puts 'Yes: %s %036b is allowed by %036b' % [perm, bits, original_mask] if type == :col | |
# TODO: heights_ok? only ever needs to happen once per group, since height clues don't change. | |
# only the bit masks change. | |
next unless heights_ok?(perm, forward, reverse) | |
#puts 'Yes: %s %036b is allowed by %036b and height' % [perm, bits, original_mask] if type == :col | |
union_of_masks |= bits | |
} | |
union_of_masks if union_of_masks != original_mask | |
end | |
def self.permutations_for_group(mask, heights, bit_position_of) | |
unique_heights = bit_position_of.keys | |
num_unique_heights = bit_position_of.size | |
unknown_num = (1 << num_unique_heights) - 1 | |
pos = -1 | |
# known_or_not becomes an array of: | |
# negative number N: at that position is -N | |
# non-negative number N: at that position is permutation's Nth element. | |
# | |
# For example, Grid.permutations_for_group(0b1111111101001111, a = [1, 2, 3, 4], a.each_with_index.to_h) | |
# unknown_num = [1, 2, 4] | |
# known_or_not = [0, -3, 1, 2] | |
# so we'll generate permutations of [1, 2, 4], and insert 3 in the correct position. | |
known_or_not = heights.size.times.map { |i| | |
cell = mask[i * num_unique_heights, num_unique_heights] | |
if power_of_two?(cell) | |
unknown_num &= ~cell | |
# which bit is set? | |
# cell = 100 -> bit = 2 | |
bit = 0 | |
until cell == 1 | |
bit += 1 | |
cell >>= 1 | |
end | |
# I guess I could handle it by shifting it so that: | |
# non-positive N: at that position is -N | |
# positive N: at that position is perm's N-1th element. | |
# but no need to do that yet. | |
raise "sorry, can't handle known 0 yet since negative numbers represent known numbers..." if bit == 0 && unique_heights.first == 0 | |
-unique_heights[bit] | |
else | |
pos += 1 | |
end | |
} | |
unknown_num = bit_position_of.select { |_digit, pos| unknown_num[pos] != 0 }.keys | |
unknown_num.unshift(0) until unknown_num.count(0) == heights.count(0) | |
Enumerator.new { |y| | |
unknown_num.permutation { |perm| | |
y << known_or_not.map { _1 < 0 ? -_1 : perm[_1] } | |
} | |
} | |
end | |
def self.xwing(pencil_marks, num_unique_heights, num_zeroes) | |
side_length = pencil_marks.size | |
# cols_in_row[i][y] is bitfield telling in which cols digit i can occur in row y. | |
cols_in_row = Array.new(side_length) { Array.new(side_length, 0) } | |
rows_in_col = Array.new(side_length) { Array.new(side_length, 0) } | |
pencil_marks.each_with_index { |row, y| | |
i = 0 | |
until row == 0 | |
if row & 1 != 0 | |
x, digit = i.divmod(num_unique_heights) | |
cols_in_row[digit][y] |= 1 << x | |
rows_in_col[digit][x] |= 1 << y | |
end | |
row >>= 1 | |
i += 1 | |
end | |
} | |
cols_in_row.each_with_index { |cir, digit| | |
next if num_zeroes > 1 && digit == 0 | |
next unless (xwing_rows, xwing_cols = xwing_find_internal(cir)) | |
return [digit, :cols_in_row, xwing_rows, xwing_cols] | |
} | |
rows_in_col.each_with_index { |ric, digit| | |
next if num_zeroes > 1 && digit == 0 | |
next unless (xwing_cols, xwing_rows = xwing_find_internal(ric)) | |
return [digit, :rows_in_col, xwing_rows, xwing_cols] | |
} | |
nil | |
end | |
def self.xwing_find_internal(group) | |
side_length = group.size | |
(1 << side_length).times { |selected_bitfield| | |
popcount = selected_bitfield.to_s(2).count(?1) | |
# 1 would just be a hidden single in this dimension, covered by permutations. | |
next if popcount <= 1 | |
# n - 1 would just be a hidden single in the other dimension, covered by permutations. | |
next if popcount >= side_length - 1 | |
in_xwing = 0 | |
not_in_xwing = 0 | |
has_known = false | |
group.each_with_index { |g, group_idx| | |
if selected_bitfield[group_idx] != 0 | |
if power_of_two?(g) | |
# no point because a known will already have eliminated in its row and column. | |
has_known = true | |
break | |
end | |
in_xwing |= g | |
else | |
not_in_xwing |= g | |
end | |
} | |
next if has_known | |
affected = in_xwing & not_in_xwing | |
next if affected == 0 | |
next if in_xwing.to_s(2).count(?1) > popcount | |
return [selected_bitfield, in_xwing] | |
} | |
nil | |
end | |
# TODO: A few more cases to take into account: | |
# | |
# On a diagonal for an odd-sized grid, centre square and two corners see the other two corners, | |
# so max size can go up to three if those three specific squares are involved. | |
# | |
# Rows and columns can use this logic as well if one of the cells is on a diagonal. | |
# | |
# Further, if it's an edge row or column, | |
# Centre square of that row/column and two corners will see centre of the entire grid, | |
# so another case where max size would be three. | |
def self.two_in_diag(pencil_marks, num_unique_heights, num_zeroes) | |
side_length = pencil_marks.size | |
%i(falling rising).each { |dir| | |
possible_pos = Array.new(side_length) { [] } | |
x0, dx = diag_xs(dir, side_length) | |
pencil_marks.each_with_index { |row, y| | |
x = x0 + dx * y | |
cell = row[x * num_unique_heights, num_unique_heights] | |
num_unique_heights.times { |digit| | |
next if num_zeroes > 1 && digit == 0 | |
possible_pos[digit] << [y, x] if cell[digit] != 0 | |
} | |
} | |
possible_pos.each_with_index { |poses, digit| | |
next if poses.size != 2 | |
good_targets = visible_to_all_with_digit(pencil_marks, num_unique_heights, poses, digit, diagonals: true) | |
return { | |
targets: good_targets, | |
digit: digit, | |
cells: poses, | |
} unless good_targets.empty? | |
} | |
} | |
nil | |
end | |
def self.visible_to_all_with_digit(pencil_marks, num_unique_heights, poses, digit, diagonals:) | |
raise "duplicate cells in #{poses}" if poses.uniq.size != poses.size | |
side_length = pencil_marks.size | |
targets = poses.map { visible_to(side_length, *_1, diagonals: diagonals) }.reduce(:&) | |
targets.select { |ty, tx| | |
# TODO: Oops, repeated code w/ inst method possible? | |
# except no lookup of bit_position_of here | |
pencil_marks[ty][tx * num_unique_heights + digit] != 0 | |
} | |
end | |
def self.visible_to(side_length, y, x, diagonals:) | |
row = ((0...side_length).to_a - [x]).map { |vx| [y, vx] } | |
col = ((0...side_length).to_a - [y]).map { |vy| [vy, x] } | |
diag1 = diagonals && y == x ? ((0...side_length).to_a - [y]).map { |vy| [vy, vy] } : [] | |
diag2 = diagonals && y + x == side_length - 1 ? ((0...side_length).to_a - [y]).map { |vy| [vy, side_length - 1 - vy] } : [] | |
row + col + diag1 + diag2 | |
end | |
def self.forcing_chain(pencil_marks, num_unique_heights, num_zeroes, diagonals: false) | |
side_length = pencil_marks.size | |
cells = pencil_marks.map.with_index { |row, y| | |
side_length.times.map { |x| | |
cell = row[x * num_unique_heights, num_unique_heights] | |
next if cell.to_s(2).count(?1) != 2 | |
# zeroes can't participate in forcing chains if there are too many of them. | |
next if num_zeroes > 1 && cell & 1 != 0 | |
{ | |
y: y, | |
x: x, | |
bits: bits = num_unique_heights.times.select { cell[_1] != 0 }.freeze, | |
opp: {bits[0] => bits[1], bits[1] => bits[0]}, | |
neigh: Hash.new { |h, k| h[k] = [] } | |
}.freeze | |
} | |
} | |
connect_pairs = ->candidates { | |
candidates.compact.combination(2) { |a, b| | |
(a[:bits] & b[:bits]).each { |samebit| | |
a[:neigh][samebit] << b | |
b[:neigh][samebit] << a | |
} | |
} | |
} | |
cells.each { |row| connect_pairs[row] } | |
side_length.times { |x| | |
col = cells.map { _1[x] } | |
connect_pairs[col] | |
} | |
%i(falling rising).each { |dir| | |
x0, dx = diag_xs(dir, side_length) | |
diag = cells.map.with_index { |row, y| row[x0 + dx * y] } | |
connect_pairs[diag] | |
} if diagonals | |
side_length.times { |x| | |
cells.map { _1[x] }.compact.combination(2) { |a, b| | |
(a[:bits] & b[:bits]).each { |samebit| | |
a[:neigh][samebit] << b | |
b[:neigh][samebit] << a | |
} | |
} | |
} | |
cells = cells.flatten.compact | |
cells.each { | |
_1[:neigh].default_proc = nil | |
_1[:neigh].freeze | |
} | |
cells.flatten.compact.each { |cell| | |
if (c = forcing_chain_find_internal(pencil_marks, num_unique_heights, cells, cell, *cell[:bits], diagonals: diagonals)) | |
return c | |
end | |
if (c = forcing_chain_find_internal(pencil_marks, num_unique_heights, cells, cell, *cell[:bits].reverse, diagonals: diagonals)) | |
return c | |
end | |
} | |
nil | |
end | |
def self.forcing_chain_find_internal(pencil_marks, num_unique_heights, cells, cell, left, right, diagonals:) | |
gen = -1 | |
q = [[cell, left]] | |
prev = {cell => nil} | |
until q.empty? | |
gen += 1 | |
nq = [] | |
while (cand, cands_left = q.shift) | |
cands_right = cand[:opp][cands_left] | |
if cands_right == left | |
poses = [cell, cand].map { _1.values_at(:y, :x) } | |
good_targets = visible_to_all_with_digit(pencil_marks, num_unique_heights, poses, left, diagonals: diagonals) | |
return { | |
targets: good_targets, | |
digit: left, | |
cells: reconstruct_path(prev, cand), | |
} unless good_targets.empty? | |
end | |
cand[:neigh][cands_right]&.each { |neigh| | |
next if prev.has_key?(neigh) | |
prev[neigh] = cand | |
# candidate's right is the new left | |
nq << [neigh, cands_right] | |
} | |
end | |
q = nq | |
end | |
nil | |
end | |
def self.reconstruct_path(prevs, n) | |
path = [n] | |
current = n | |
while (current = prevs[current]) | |
path.unshift(current) | |
end | |
path | |
end | |
def step | |
reset_changed | |
@side_length.times { |y| | |
next unless @row_dirty[y] | |
# Recall that @pencil_marks rows are stored such that element 0 is on the left. | |
# | |
# For generated row permutations: element 0 is also on the left. | |
# so forward is left, reverse is right. | |
next unless (changed_row = self.class.step_for_group(@row_perms[y], @pencil_marks[y], @bit_position_of, @left[y], @right[y], :row)) | |
changed, new_known = self.class.compare(@pencil_marks[y], changed_row, @side_length, @num_unique_heights) | |
@pencil_marks[y] = changed_row | |
changed.each_key { |x| | |
mark_cell_changed(y, x, dirty_row: false) | |
} | |
@row_dirty[y] = false | |
return { | |
type: :row, | |
y: y, | |
changed: changed.size, | |
new_known: new_known.size, | |
} | |
} | |
@side_length.times { |x| | |
next unless @col_dirty[x] | |
# 0 (top row) least significant bit | |
col_mask = col_mask(x) | |
# For generated col permutations: element 0 is on the top, | |
# which matches how col_mask is generated above. | |
# so forward is top, reverse is bottom. | |
next unless (changed_col = self.class.step_for_group(@col_perms[x], col_mask, @bit_position_of, @top[x], @bottom[x], :col)) | |
changed, new_known = self.class.compare(col_mask, changed_col, @side_length, @num_unique_heights) | |
changed.each { |y, new_cell| | |
set_cell(y, x, new_cell, dirty_col: false) | |
} | |
@col_dirty[x] = false | |
return { | |
type: :col, | |
x: x, | |
changed: changed.size, | |
new_known: new_known.size, | |
} | |
} | |
if @diagonals | |
%i(falling rising).each { |dir| | |
next unless @diag_dirty[dir] | |
diag_mask = diag_mask(dir) | |
next unless (changed_diag = self.class.step_for_group(@diag_perms[dir], diag_mask, @bit_position_of, nil, nil, :diag)) | |
changed, new_known = self.class.compare(diag_mask, changed_diag, @side_length, @num_unique_heights) | |
x0, dx = self.class.diag_xs(dir, side_length) | |
changed.each { |y, new_cell| | |
x = x0 + y * dx | |
set_cell(y, x, new_cell, dirty_diag: false) | |
} | |
@diag_dirty[dir] = false | |
return { | |
type: :diag, | |
dir: dir, | |
changed: changed.size, | |
new_known: new_known.size, | |
} | |
} | |
if (forced_diag = self.class.two_in_diag(@pencil_marks.dup, @num_unique_heights, @num_zeroes)) | |
mask = 1 << forced_diag[:digit] | |
forced_diag[:targets].each { |targ| | |
clear_cell(*targ, mask) | |
} | |
return { | |
type: :two_in_diag, | |
targets: forced_diag[:targets], | |
digit: forced_diag[:digit] + 1, | |
cells: forced_diag[:cells], | |
} | |
end | |
end | |
if (digit, dir, xwing_rows, xwing_cols = self.class.xwing(@pencil_marks.dup, @num_unique_heights, @num_zeroes)) | |
mask = 1 << digit | |
case dir | |
when :cols_in_row | |
# Look for the designated columns in other rows and clear that digit. | |
@side_length.times { |y| | |
next if xwing_rows[y] != 0 | |
@side_length.times { |x| | |
next if xwing_cols[x] == 0 | |
clear_cell(y, x, mask) | |
} | |
} | |
when :rows_in_col | |
# Look for the designated rows in other columns and clear that digit. | |
@side_length.times { |y| | |
next if xwing_rows[y] == 0 | |
@side_length.times { |x| | |
next if xwing_cols[x] != 0 | |
clear_cell(y, x, mask) | |
} | |
} | |
else raise "unknown dir #{dir}" | |
end | |
bits_in_num = ->n { (0...@side_length).select { n[_1] != 0 } } | |
return { | |
type: :xwing, | |
xwing_dir: dir, | |
digit: digit + 1, | |
rows: bits_in_num[xwing_rows], | |
cols: bits_in_num[xwing_cols], | |
} | |
end | |
if (chain = self.class.forcing_chain(@pencil_marks.dup, @num_unique_heights, @num_zeroes, diagonals: @diagonals)) | |
mask = 1 << chain[:digit] | |
chain[:targets].each { |targ| | |
clear_cell(*targ, mask) | |
} | |
return { | |
type: :forcing_chain, | |
targets: chain[:targets], | |
digit: chain[:digit] + 1, | |
len: chain[:cells].size, | |
cells: chain[:cells].map { |cell| | |
{ | |
pos: [cell[:y], cell[:x]], | |
digits: cell[:bits].map(&:succ), | |
} | |
}, | |
} | |
end | |
false | |
end | |
def self.power_of_two?(x) | |
x != 0 && x & (x - 1) == 0 | |
end | |
def self.compare(old, new, side_length, num_unique_heights) | |
changed = {} | |
new_known = [] | |
side_length.times { |x| | |
old_cell = old[0, num_unique_heights] | |
new_cell = new[0, num_unique_heights] | |
if old_cell != new_cell | |
changed[x] = new_cell | |
if power_of_two?(old_cell) | |
raise "why changing a known cell? #{old_cell} to #{new_cell}" | |
elsif power_of_two?(new_cell) | |
new_known << x | |
end | |
end | |
old >>= num_unique_heights | |
new >>= num_unique_heights | |
} | |
[changed, new_known] | |
end | |
def self.heights_ok?(heights, forward, reverse) | |
if forward | |
min = 0 | |
forward_sum = heights.count { |h| min = h if h > min } | |
return false if forward_sum != forward | |
end | |
if reverse | |
min = 0 | |
reverse_sum = heights.reverse.count { |h| min = h if h > min } | |
return false if reverse_sum != reverse | |
end | |
true | |
end | |
def reset_changed | |
@changed = Array.new(@side_length) { Array.new(@side_length, false) } | |
end | |
def clear_neigh_of_known(y, x) | |
known_bit = @pencil_marks[y][x * @num_unique_heights, @num_unique_heights] | |
raise "bad clear! #{y} #{x} is #{known_bit}" unless self.class.power_of_two?(known_bit) | |
# If there are multiple zeroes, don't clear | |
# Actually we could clear if the number of zeroes in the group has met the quota, | |
# but I think I'll just let permutation checking check that. | |
return if @num_zeroes > 1 && known_bit == 1 | |
# Don't dirty the same dimension as that being cleared. | |
# It's just been checked, since presumably that's what resulted in this known. | |
# Clear the row (same y, different x) | |
@side_length.times { |xx| | |
next if xx == x | |
clear_cell(y, xx, known_bit, dirty_row: false) | |
} | |
# Clear the column (same x, different y) | |
@side_length.times { |yy| | |
next if yy == y | |
clear_cell(yy, x, known_bit, dirty_col: false) | |
} | |
return unless @diagonals | |
if y == x | |
@side_length.times { |yy| | |
next if yy == y | |
clear_cell(yy, yy, known_bit, dirty_diag: false) | |
} | |
end | |
if y + x == @side_length - 1 | |
@side_length.times { |yy| | |
next if yy == y | |
clear_cell(yy, @side_length - 1 - yy, known_bit, dirty_diag: false) | |
} | |
end | |
end | |
def clear_cell(y, x, mask, **dirty_args) | |
shift = @num_unique_heights * x | |
current = @pencil_marks[y] >> shift | |
# Cell already does not have this digit | |
return if current & mask == 0 | |
raise "why setting #{y} #{x} from #{current} to 0? mask #{mask}" if current & ~mask == 0 | |
@pencil_marks[y] &= ~(mask << shift) | |
mark_cell_changed(y, x, **dirty_args) | |
end | |
def set_cell(y, x, new_cell, **dirty_args) | |
full_cell = (1 << @num_unique_heights) - 1 | |
shift = @num_unique_heights * x | |
@pencil_marks[y] &= ~(full_cell << shift) | |
@pencil_marks[y] |= new_cell << shift | |
mark_cell_changed(y, x, **dirty_args) | |
end | |
def mark_cell_changed(y, x, dirty_col: true, dirty_row: true, dirty_diag: true) | |
@changed[y][x] = true | |
@col_dirty[x] = true if dirty_col | |
@row_dirty[y] = true if dirty_row | |
if dirty_diag | |
@diag_dirty[:falling] = true if x == y | |
@diag_dirty[:rising] = true if x + y == @side_length - 1 | |
end | |
clear_neigh_of_known(y, x) if self.class.power_of_two?(@pencil_marks[y][x * @num_unique_heights, @num_unique_heights]) | |
end | |
def to_sgtatham | |
page = 'https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/towers.html' | |
heights = (@top + @bottom + @left + @right).join(?/) | |
knowns = '' | |
gap_char = ->gap { | |
(gap == 0 ? (knowns.empty? ? '' : ?_) : ?a.ord - 1 + gap) | |
} | |
gap = 0 | |
@pencil_marks.each { |row| | |
@side_length.times { |x| | |
cell = row[x * @num_unique_heights, @num_unique_heights] | |
if self.class.power_of_two?(cell) | |
knowns << gap_char[gap] | |
knowns << @bit_position_of.find { |_digit, pos| cell[pos] != 0 }.first.to_s | |
gap = 0 | |
else | |
gap += 1 | |
end | |
} | |
} | |
knowns << gap_char[gap] if gap != 0 && !knowns.empty? | |
"#{page}##{@side_length}:#{heights},#{knowns}" | |
end | |
def to_s | |
rows = @pencil_marks.zip(@left, @right, @changed).map { |row, left, right, changed| | |
stuff = changed.map.with_index { |cell_changed, cell_id| | |
cell = row[cell_id * @num_unique_heights, @num_unique_heights] | |
digits = @bit_position_of.select { |_digit, pos| cell[pos] != 0 }.keys.join | |
"\e[#{cell_changed ? 1 : 0};#{digits.size == 1 ? 32 : 37}m%#{@num_unique_heights}s\e[0m" % digits | |
}.join(' | ') | |
"#{left || ' '} | #{stuff} | #{right}" | |
} | |
rows.unshift(head_foot_to_s(@top)) | |
rows << head_foot_to_s(@bottom) | |
rows.join("\n #{?- * (@side_length * (@num_unique_heights + 3) + 1)}\n") | |
end | |
def head_foot_to_s(row) | |
' | ' + row.map { _1 ? "%#{@num_unique_heights}d" % _1 : ' ' * @side_length }.join(' | ') + ' |' | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require_relative 'grid' | |
def up_left(size) | |
size.times { | |
`xdotool key Up` | |
`xdotool key Left` | |
} | |
end | |
def fill(digits, size, grid) | |
puts "about to fill cells with #{digits.join} in 2 seconds (make sure cursor in hint mode)" | |
s = digits.join | |
sleep 2 | |
up_left(size) | |
size.times { |y| | |
size.times { |xi| | |
x = y.even? ? xi : size - 1 - xi | |
s = digits.select { |d| grid.possible?(y, x, d) }.join if grid | |
# if empty, xdotool will say not enough args | |
`xdotool type #{s}` unless s.empty? | |
`xdotool key #{y.even? ? :Right : :Left}` | |
} | |
`xdotool key Down` | |
} | |
end | |
if (size = Integer(ARGV[0], exception: false)) | |
grid = nil | |
else | |
grid = Grid.from_link_or_file(ARGV[0]) | |
size = grid.side_length | |
unless ARGV[0].include?('http') && ARGV[0].include?('sgtatham') | |
puts grid.to_sgtatham | |
puts "press enter when ready for #{size}" | |
_ = STDIN.gets | |
end | |
end | |
fill([size], size, grid) | |
s = (1...size).to_a | |
puts "press enter when ready for #{s}" | |
_ = STDIN.gets | |
fill(s, size, grid) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require_relative 'grid' | |
verbose = ARGV.delete('-v') | |
interactive = ARGV.delete('-i') | |
grid = Grid.from_link_or_file(ARGV[0]) | |
puts grid | |
stat = Hash.new(0) | |
while (r = grid.step) | |
stat[r[:type]] += 1 | |
if verbose | |
p r | |
puts grid | |
end | |
end | |
# If verbose, we've already printed out the final state. | |
puts grid if !verbose && !interactive | |
puts grid.solved? ? 'solvable' : 'unsolvable' | |
puts stat | |
if interactive | |
grid = Grid.from_link_or_file(ARGV[0]) | |
while (r = grid.step) | |
p r | |
puts grid | |
_ = STDIN.gets | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment