2022-05-29 18:38:07 +02:00
|
|
|
local Board = {}
|
|
|
|
function Board.new()
|
2023-10-07 19:12:53 +02:00
|
|
|
local debug = false
|
|
|
|
|
|
|
|
local width = board_size
|
|
|
|
-- tiles
|
|
|
|
local tile_width = 8
|
|
|
|
-- pixels
|
|
|
|
local padding = 1
|
|
|
|
local tiles = {}
|
|
|
|
local locked = {}
|
|
|
|
-- list of indexes
|
|
|
|
|
|
|
|
local reset = function()
|
|
|
|
tiles = {}
|
|
|
|
for i = 1, width * width do
|
|
|
|
tiles[i] = 0
|
|
|
|
end
|
|
|
|
locked = {}
|
|
|
|
end
|
|
|
|
|
|
|
|
reset()
|
|
|
|
|
|
|
|
return {
|
|
|
|
reset = reset,
|
|
|
|
|
|
|
|
get_tile = function(self, idx)
|
|
|
|
return tiles[idx]
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- returns a COPY of the tiles array
|
|
|
|
get_tiles_copy = function(self)
|
|
|
|
return copy(tiles)
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- overwrites the whole tiles array
|
|
|
|
set_tiles = function(self, newtiles)
|
|
|
|
assert(#newtiles == #tiles, "New tiles array must have a length of " .. #tiles)
|
|
|
|
tiles = copy(newtiles)
|
|
|
|
end,
|
|
|
|
|
|
|
|
idx_xy = function(self, idx)
|
|
|
|
return idx_xy(idx, width)
|
|
|
|
end,
|
|
|
|
|
|
|
|
xy_idx = function(self, x, y)
|
|
|
|
return xy_idx(x, y, width)
|
|
|
|
end,
|
|
|
|
|
|
|
|
draw_coords = function(self, x, y)
|
|
|
|
local margin = (128 - (tile_width + padding) * width) / 2 - padding
|
|
|
|
if not y then x, y = self:idx_xy(x) end
|
|
|
|
return margin + (x - 1) * tile_width + (x - 1) * padding, margin + (y - 1) * tile_width + (y - 1) * padding
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_size = function(self)
|
|
|
|
return width
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_tile_width = function(self)
|
|
|
|
return tile_width + padding
|
|
|
|
end,
|
|
|
|
|
|
|
|
fill = function(self, idx, color, invert)
|
|
|
|
if idx > width * width then return end
|
|
|
|
if invert then
|
|
|
|
color = color == YELLOW and BLUE or YELLOW
|
|
|
|
end
|
|
|
|
tiles[idx] = color
|
|
|
|
end,
|
|
|
|
|
|
|
|
try_flip_tile = function(self, id)
|
|
|
|
local to_color = nil
|
|
|
|
if locked[id] then return end
|
|
|
|
local from_color = tiles[id]
|
|
|
|
if tiles[id] == 0 then
|
|
|
|
to_color = YELLOW
|
|
|
|
elseif tiles[id] == YELLOW then
|
|
|
|
to_color = BLUE
|
|
|
|
else
|
|
|
|
tiles[id] = 0
|
|
|
|
-- empty tile
|
|
|
|
end
|
|
|
|
if to_color then
|
|
|
|
tiles[id] = to_color
|
|
|
|
end
|
|
|
|
local x, y = self:draw_coords(id)
|
|
|
|
spawn_tile_transition(x, y, tile_width - 1, tile_width - 1, from_color, to_color)
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_rows = function(self)
|
|
|
|
local ret = {}
|
|
|
|
for i = 1, width do
|
|
|
|
add(ret, slice(tiles, (i - 1) * width + 1, i * width))
|
|
|
|
end
|
|
|
|
return ret
|
|
|
|
end,
|
|
|
|
|
|
|
|
get_cols = function(self)
|
|
|
|
local ret = {}
|
|
|
|
local rows = self.get_rows(self)
|
|
|
|
for i = 1, width do
|
|
|
|
add(ret, map(rows, function(v) return v[i] end))
|
|
|
|
end
|
|
|
|
return ret
|
|
|
|
end,
|
|
|
|
|
|
|
|
--- Returns true if all tiles are filled
|
|
|
|
is_complete = function(self)
|
|
|
|
return count(tiles, 0) == 0
|
|
|
|
end,
|
|
|
|
|
|
|
|
--- Returns true if the board is valid (respects the rules)
|
|
|
|
is_valid = function(self)
|
|
|
|
return #self:get_issues() == 0
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- returns a list of issues of the board's current state
|
|
|
|
get_issues = function(self, details)
|
|
|
|
local rows = self:get_rows()
|
|
|
|
local issues = {}
|
|
|
|
for y, row in ipairs(rows) do
|
|
|
|
local filled = count(row, 0) == 0
|
|
|
|
-- check count
|
|
|
|
if filled and count(row, BLUE) ~= count(row, YELLOW) then
|
|
|
|
add(issues, { "row", "count", row, y })
|
|
|
|
if (debug) printh("uneven count on row " .. y) if (not details) return issues
|
|
|
|
end
|
|
|
|
-- check identical lines
|
|
|
|
for k, other in ipairs(rows) do
|
|
|
|
if filled and equal(other, row) and other ~= row then
|
|
|
|
add(issues, { "row", "identical", row, y, k })
|
|
|
|
if (debug) printh("equal rows " .. k) if (not details) return issues
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- check triples
|
|
|
|
if self:count_consecutives(row) > 2 then
|
|
|
|
add(issues, { "row", "triples", row, y })
|
|
|
|
if (debug) printh("triples") if (not details) return issues
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local cols = self:get_cols()
|
|
|
|
for x, col in ipairs(cols) do
|
|
|
|
local filled = count(col, 0) == 0
|
|
|
|
|
|
|
|
-- check count
|
|
|
|
if filled and count(col, BLUE) ~= count(col, YELLOW) then
|
|
|
|
add(issues, { "col", "count", col, x })
|
|
|
|
if (debug) printh("uneven count") if (not details) return issues
|
|
|
|
end
|
|
|
|
-- check identical lines
|
|
|
|
for k, other in ipairs(cols) do
|
|
|
|
if filled and equal(other, col) and other ~= col then
|
|
|
|
add(issues, { "col", "identical", col, x, k })
|
|
|
|
if (debug) printh("equal cols") if (not details) return issues
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- check triples
|
|
|
|
if self:count_consecutives(col) > 2 then
|
|
|
|
add(issues, { "col", "triples", col, x })
|
|
|
|
if (debug) printh("triples") if (not details) return issues
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return issues
|
|
|
|
end,
|
|
|
|
|
|
|
|
count_consecutives = function(self, line)
|
|
|
|
local top = 0
|
|
|
|
local current = 0
|
|
|
|
local last = 0
|
|
|
|
for v in all(line) do
|
|
|
|
if v ~= last then
|
|
|
|
top = max(current, top)
|
|
|
|
current = 1
|
|
|
|
last = v
|
|
|
|
elseif v ~= 0 then
|
|
|
|
current += 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return max(current, top)
|
|
|
|
end,
|
|
|
|
|
|
|
|
--
|
|
|
|
-- Returns the index of a random zero tile
|
|
|
|
--
|
|
|
|
get_random_zero = function(self)
|
|
|
|
assert(count(tiles, 0) > 0, "No zero left")
|
|
|
|
local zeroes = filter(tiles, function(v) return v == 0 end, true)
|
|
|
|
local z = {}
|
|
|
|
for k, v in pairs(zeroes) do
|
|
|
|
add(z, k)
|
|
|
|
end
|
|
|
|
return rnd(z)
|
|
|
|
end,
|
|
|
|
|
|
|
|
--
|
|
|
|
-- Returns the index of a random non-zero tile
|
|
|
|
--
|
|
|
|
get_random_non_zero = function(self)
|
|
|
|
assert(count(tiles, 0) < #tiles, "All zeroes")
|
|
|
|
local numbers = filter(tiles, function(v) return v ~= 0 end, true)
|
|
|
|
local z = {}
|
|
|
|
for k, v in pairs(numbers) do
|
|
|
|
add(z, k)
|
|
|
|
end
|
|
|
|
return rnd(z)
|
|
|
|
end,
|
|
|
|
|
|
|
|
tostring = function(self)
|
|
|
|
local str = ''
|
|
|
|
for v in all(tiles) do
|
|
|
|
str ..= ", " .. v
|
|
|
|
end
|
|
|
|
return str
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- Solves 1 step of the board
|
|
|
|
-- Returns "valid" if it solved it without guessing
|
|
|
|
-- Returns "invalid" if the board cannot be solved
|
|
|
|
solve_step = function(self, random)
|
|
|
|
local zeroes = count(tiles, 0)
|
|
|
|
self:surround_doubles()
|
|
|
|
self:split_triples()
|
|
|
|
self:fill_lines()
|
|
|
|
self:no_identical_lines()
|
|
|
|
local changed = zeroes ~= count(tiles, 0)
|
|
|
|
|
|
|
|
if not changed and random and not self:is_complete() then
|
|
|
|
-- Set a random color
|
|
|
|
local z = self:get_random_zero()
|
|
|
|
self:fill(z, rnd({ BLUE, YELLOW }))
|
|
|
|
if (debug) printh("!!!!!!!!!!!!!!!!! RANDOM FILL AT " .. z) return "invalid"
|
|
|
|
end
|
|
|
|
return (changed or self:is_complete()) and "valid" or "invalid"
|
|
|
|
end,
|
|
|
|
|
|
|
|
surround_doubles = function(self)
|
|
|
|
for idx, v in ipairs(tiles) do
|
|
|
|
local x, y = self:idx_xy(idx)
|
|
|
|
if v == 0 then
|
|
|
|
local neighbors = {}
|
|
|
|
-- 2 tiles on the left
|
|
|
|
if x >= 3 then
|
|
|
|
add(neighbors, { idx, idx - 1, idx - 2 })
|
|
|
|
end
|
|
|
|
-- 2 tiles on the right
|
|
|
|
if x <= width - 2 then
|
|
|
|
add(neighbors, { idx, idx + 1, idx + 2 })
|
|
|
|
end
|
|
|
|
-- 2 tiles on top
|
|
|
|
if y >= 3 then
|
|
|
|
add(neighbors, { idx, idx - width, idx - width * 2 })
|
|
|
|
end
|
|
|
|
-- 2 tiles under
|
|
|
|
if y <= width - 2 then
|
|
|
|
add(neighbors, { idx, idx + width, idx + width * 2 })
|
|
|
|
end
|
|
|
|
|
|
|
|
-- only keep pairs that are identical (and not 0)
|
|
|
|
neighbors = filter(neighbors, function(o) return tiles[o[2]] == tiles[o[3]] and tiles[o[2]] ~= 0 end)
|
|
|
|
|
|
|
|
-- do the surrounding
|
|
|
|
for item in all(neighbors) do
|
|
|
|
if item[1] then
|
|
|
|
if (debug) printh("Surrounding at " .. item[1]) self:fill(item[1], tiles[item[2]], true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
split_triples = function(self)
|
|
|
|
for idx, col in ipairs(tiles) do
|
|
|
|
local x, y = self:idx_xy(idx)
|
|
|
|
if col == 0 then
|
|
|
|
if x > 1 and x < width then
|
|
|
|
-- check horizontal
|
|
|
|
local prev = tiles[idx - 1]
|
|
|
|
local next = tiles[idx + 1]
|
|
|
|
if prev ~= 0 and prev == next then
|
|
|
|
if (debug) printh("Splitting at " .. idx) self:fill(idx, prev, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if y > 1 and y < width then
|
|
|
|
-- check vertical
|
|
|
|
local prev = tiles[idx - width]
|
|
|
|
local next = tiles[idx + width]
|
|
|
|
if prev ~= 0 and prev == next then
|
|
|
|
if (debug) printh("Splitting at " .. idx) self:fill(idx, prev, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
fill_lines = function(self)
|
|
|
|
local rows = self:get_rows()
|
|
|
|
local cols = self:get_cols()
|
|
|
|
|
|
|
|
-- rows
|
|
|
|
for y, row in ipairs(rows) do
|
|
|
|
local a = count(row, BLUE)
|
|
|
|
local b = count(row, YELLOW)
|
|
|
|
if a ~= b then
|
|
|
|
if a == width / 2 then self:fill_row(y, YELLOW) end
|
|
|
|
if b == width / 2 then self:fill_row(y, BLUE) end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- columns
|
|
|
|
for x, col in ipairs(cols) do
|
|
|
|
local a = count(col, BLUE)
|
|
|
|
local b = count(col, YELLOW)
|
|
|
|
if a ~= b then
|
|
|
|
if a == width / 2 then self:fill_col(x, YELLOW) end
|
|
|
|
if b == width / 2 then self:fill_col(x, BLUE) end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
fill_row = function(self, y, color)
|
|
|
|
if (debug) printh("Filling line " .. y .. " in " .. (color == BLUE and "blue" or "yellow"))
|
|
|
|
local idx = self:xy_idx(1, y)
|
|
|
|
for i = idx, idx + width - 1 do
|
|
|
|
if self:get_tile(i) == 0 then
|
|
|
|
self:fill(i, color)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
fill_col = function(self, x, color)
|
|
|
|
if (debug) printh("Filling column " .. x .. " in " .. (color == BLUE and "blue" or "yellow"))
|
|
|
|
local idx = self:xy_idx(x, 1)
|
|
|
|
for i = idx, #tiles, width do
|
|
|
|
if self:get_tile(i) == 0 then
|
|
|
|
self:fill(i, color)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
-- Finds "identical" lines, and fill the 2 remaining tiles with inverted colors
|
|
|
|
no_identical_lines = function(self)
|
|
|
|
-- columns
|
|
|
|
local cols = self:get_cols()
|
|
|
|
for x, col in ipairs(cols) do
|
|
|
|
-- if the line has the corrent number of colors,
|
|
|
|
-- but missing 2 tiles
|
|
|
|
if count(col, 0) == 2 and count(col, BLUE) == count(col, YELLOW) then
|
|
|
|
local y1, y2 = unpack(find(col, 0)) -- get the position of the 2 missing tiles
|
|
|
|
-- create both both solutions
|
|
|
|
local ab = copy(col) ab[y1] = BLUE ab[y2] = YELLOW
|
|
|
|
local ba = copy(col) ba[y1] = YELLOW ba[y2] = BLUE
|
|
|
|
-- Check if a dupe exists
|
|
|
|
for x2, col in ipairs(cols) do
|
|
|
|
if equal(col, ab) then
|
|
|
|
self:fill(self:xy_idx(x, y1), YELLOW)
|
|
|
|
self:fill(self:xy_idx(x, y2), BLUE)
|
|
|
|
goto continue
|
|
|
|
elseif equal(col, ba) then
|
|
|
|
self:fill(self:xy_idx(x, y1), BLUE)
|
|
|
|
self:fill(self:xy_idx(x, y2), YELLOW)
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
|
|
|
|
-- rows
|
|
|
|
local rows = self:get_rows()
|
|
|
|
for y, row in ipairs(rows) do
|
|
|
|
if count(row, 0) == 2 and count(row, BLUE) == count(row, YELLOW) then
|
|
|
|
local x1, x2 = unpack(find(row, 0))
|
|
|
|
local ab = copy(row) ab[x1] = BLUE ab[x2] = YELLOW
|
|
|
|
local ba = copy(row) ba[x1] = YELLOW ba[x2] = BLUE
|
|
|
|
-- Check if a dupe exists
|
|
|
|
for y2, row in ipairs(rows) do
|
|
|
|
if equal(row, ab) then
|
|
|
|
self:fill(self:xy_idx(x1, y), YELLOW)
|
|
|
|
self:fill(self:xy_idx(x2, y), BLUE)
|
|
|
|
goto continue
|
|
|
|
elseif equal(row, ba) then
|
|
|
|
self:fill(self:xy_idx(x1, y), BLUE)
|
|
|
|
self:fill(self:xy_idx(x2, y), YELLOW)
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
lock_tiles = function(self)
|
|
|
|
locked = {}
|
|
|
|
for k, v in ipairs(tiles) do
|
|
|
|
if v > 0 then locked[k] = true end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
is_locked = function(self, idx)
|
|
|
|
return locked[idx]
|
|
|
|
end,
|
|
|
|
|
|
|
|
draw_bg_tile = function(self, k)
|
|
|
|
local w = tile_width
|
|
|
|
local x, y = self:draw_coords(k)
|
|
|
|
rectfill2(x + 1, y, w - 2, w, 1)
|
|
|
|
rectfill2(x, y + 1, w, w - 2, 1)
|
|
|
|
end,
|
|
|
|
|
|
|
|
draw = function(self)
|
|
|
|
local w = tile_width
|
|
|
|
for k, v in ipairs(tiles) do
|
|
|
|
self:draw_tile(k)
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
draw_tile = function(self, idx)
|
|
|
|
local w = tile_width
|
|
|
|
local v = tiles[idx]
|
|
|
|
if v > 0 then
|
|
|
|
local x, y = self:draw_coords(idx)
|
|
|
|
local color = get_main_color(v)
|
|
|
|
local shade = get_shade_color(v)
|
|
|
|
if color == 1 then fillp(▒) else fillp(█) end
|
|
|
|
if self:is_locked(idx) then
|
|
|
|
rectfill2(x, y, w, w, color)
|
|
|
|
else
|
|
|
|
roundedrect(x, y, w, w, color)
|
|
|
|
line(x + 1, y + w - 1, x + w - 2, y + w - 1, shade)
|
|
|
|
line(x + w - 1, y + 1, x + w - 1, y + w - 2, shade)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
fillp(█)
|
|
|
|
self:draw_bg_tile(idx)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
}
|
2022-05-29 18:38:07 +02:00
|
|
|
end
|