local Board = {} function Board.new() 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 } end