Module:User:Cscott/Advent Of Code 2023/Day 18
Appearance
return (function()
local builders = {}
local function register(name, f)
builders[name] = f
end
register('llpeg', function() return require [[Module:User:Cscott/llpeg]] end)
register('day18', function(myrequire)
--[[ DAY 13 ]]--
local l = myrequire('llpeg')
--[[ PARSING ]]--
local nl = l.P"\n"
local SKIP = l.S" \t"^0
local patt = l.P{
"DigPlan",
DigPlan = l.Ct( l.V"Step" * (nl * l.V"Step")^0 * nl^0) * -1,
Step = l.Ct( l.Cg(l.V"Dir", "dir") * SKIP *
l.Cg(l.V"Number", "dist") * SKIP *
l.Cg(l.V"Color", "color") * SKIP ),
Dir = l.S"UDLR",
Number = ((l.P"-" ^ -1) * (l.R"09"^1)) / tonumber,
Color = l.P"(" * SKIP * l.P"#" * SKIP * l.C(l.R("09","af")^1) * SKIP * l.P")",
}
local function parse(source)
--print(inspect(source))
local ast, errlabel, pos = patt:match(source)
if not ast then
error(string.format("Error at pos %s label '%s'", pos, errlabel))
end
--print('Parsed with success!')
--print(inspect(ast))
--return Graph:new(ast)
return ast
end
--[[ Part 1 ]]--
local Ground = {}
Ground.__index = Ground
function Ground:new()
local p = 1
local data = {}
data[p] = {}
data[p][p] = { color="unknown", ['in'] = nil }
return setmetatable({x=p,y=p,data=data,minx=p,maxx=p,miny=p,maxy=p}, self)
end
function Ground:at(x,y)
if self.data[x] == nil then return nil end
return self.data[x][y]
end
function Ground:dig(dir, x, y, color)
if self.data[x] == nil then self.data[x] = {} end
self.data[self.x][self.y].out = dir
self.data[x][y] = { color=color, ['in']=dir }
self.x = x
self.y = y
if x < self.minx then self.minx = x end
if y < self.miny then self.miny = y end
if x > self.maxx then self.maxx = x end
if y > self.maxy then self.maxy = y end
end
local xoff = { U=0, D=0, R=1, L=-1 }
local yoff = { U=-1, D=1, R=0, L=0 }
function Ground:move(dir, color, n)
if n > 1 then
for i=1,n do self:move(dir, color, 1) end
else
self:dig(dir, self.x + xoff[dir], self.y + yoff[dir], color)
end
end
function Ground:fill()
local sum = 0
for y=self.miny,self.maxy do
local inside = false
for x=self.minx,self.maxx do
local sp = self:at(x,y)
if sp ~= nil then
sum = sum + 1
-- as with day 10, we shoot infintesimally "down" such that
-- we count as crossing anything with 'in' or 'out' = down.
if sp['in']=='U' or sp['out']=='D' then
inside = not inside
end
elseif inside then
self.data[x][y] = { color="unknown" }
sum = sum + 1
end
end
end
return sum
end
local shape = {
UU = "|",
UD = "i",
UR = "F",
UL = "7",
DU = "'",
DD = "|",
DR = "L",
DL = "J",
RU = "J",
RD = "7",
RR = "-",
RL = ".",
LU = "L",
LD = "F",
LR = ".",
LL = "-",
}
function Ground:print()
local result = {}
for y=self.miny,self.maxy do
for x=self.minx,self.maxx do
local sp = self:at(x,y)
if sp ~= nil then
if sp['in'] ~= nil and sp.out ~= nil then
local key = sp['in'] .. sp['out']
table.insert(result, shape[key])
else
table.insert(result, "#")
end
else
table.insert(result, " ")
end
end
table.insert(result, "\n")
end
return table.concat(result)
end
local function part1(source)
local instrs = parse(source)
local g = Ground:new()
for _,i in ipairs(instrs) do
g:move(i.dir, i.color, i.dist)
end
--print(g:print())
local sum = g:fill()
--print(g:print())
return sum
end
--[[ Part 2 ]]--
local Node = {}
Node.__index = Node
function Node:new(p)
return setmetatable(p or {}, self)
end
local NGround = {}
NGround.__index = NGround
function NGround:new()
local p = 1
local n = Node:new{x=p,y=p}
return setmetatable({ x=p, y=p, nodes={[p]={[p]=n}}, lastNode=n}, self)
end
function NGround:dig(dir, dist)
local x, y = self.x + xoff[dir]*dist, self.y + yoff[dir]*dist
self.x,self.y = x,y
self.lastNode.out = dir
self.lastNode = Node:new{x=x,y=y,['in']=dir}
if self.nodes[y] == nil then self.nodes[y] = {} end
if self.nodes[y][x] ~= nil then
self.lastNode = self.nodes[y][x]
self.lastNode['in'] = dir -- should be last node in the path
else
self.nodes[y][x] = self.lastNode
end
end
local dir_decode = { ["0"] = "R", ["1"] = "D", ["2"] = "L", ["3"] = "U" }
local function decode(color)
local dist = tonumber(color:sub(1,5), 16)
local dir = dir_decode[color:sub(-1)]
return dir, dist
end
local function sortedKeys(tbl)
local sorted = {}
for k,_ in pairs(tbl) do
table.insert(sorted, k)
end
table.sort(sorted)
return sorted
end
local function part2(source)
local testing = false
local instrs = parse(source)
local g = NGround:new()
for _,i in ipairs(instrs) do
if testing then
-- use the original directions, to test if we get the original result
g:dig(i.dir, i.dist)
else
-- use the new "part 2" decoded directions
local dir,dist = decode(i.color)
g:dig(dir, dist)
end
end
-- now go top to bottom as before, but skipping ahead to the
-- "next interesting y coordinate" each time.
local row = {}
local lastRow,lastWidth = nil, nil
local sum = 0
for _,y in ipairs(sortedKeys(g.nodes)) do
-- sum the width up to this point
if lastRow ~= nil then
-- add to the overall sum the number of blocks that have been dug
-- in all the rows since `lastRow` -- each of these contained
-- `lastWidth` blocks. Don't add the number of blocks in *this*
-- row, we'll handle that below.
sum = sum + lastWidth * (y - lastRow)
end
-- replace any existing x positions in row with the new ones
for _,n in pairs(g.nodes[y]) do
row[n.x] = n
end
-- now go through the row left to right. We're going to shoot
-- "just north of center" (north_inside) and "just south of center"
-- (south_inside) and keep track of the total cubes filled in
-- "on this row" (row_sum) using "either north inside or south inside"
-- as the criterion, and also separately track the cubes that will
-- be filled in on the rows south of here (south_sum). Also track
-- in "nrow" how many vertical lines are leaving this row, so that
-- we can account for them in future rows.
local north_inside = false
local nrow = {}
local last_south_inside = nil
local last_row_inside = nil
local row_sum = 0
local south_sum = 0
for _,x in ipairs(sortedKeys(row)) do
local sp = row[x]
local was_row_inside = (last_south_inside ~= nil) or north_inside
if sp['in']=='D' or sp['out']=='U' then
-- only these nodes cross a ray sent "just north of center"
north_inside = not north_inside
end
if sp['in']=='U' or sp['out']=='D' then
-- only these nodes cross a ray sent "just south of center"
nrow[x] = {['in']="D",['out']="D",x=x,y=y} -- vertical will continue
if last_south_inside == nil then
last_south_inside = x
else
south_sum = south_sum + (1 + x - last_south_inside)
last_south_inside = nil
end
end
local is_row_inside = (last_south_inside ~= nil) or north_inside
if is_row_inside and not was_row_inside then
last_row_inside = x
elseif was_row_inside and not is_row_inside then
row_sum = row_sum + (1 + x - last_row_inside)
end
end
row = nrow
lastRow = y + 1
lastWidth = south_sum
-- add to the overall sum the number of blocks dug *on this row*
sum = sum + row_sum
end
return sum
end
--[[ CLI ] ]--
local source = io.input("day18.input"):read("*a")
print('Contains:', part1(source))
print('Ultra Contains:', part2(source))
--[ [ END CLI ]]--
return {
part1 = function(frame)
local s = mw.title.new(frame.args[1]):getContent()
return part1(s)
end,
part2 = function(frame)
local s = mw.title.new(frame.args[1]):getContent()
return part2(s, tonumber(frame.args[2]))
end,
}
end)
local modules = {}
modules['table'] = require('table')
modules['string'] = require('string')
modules['strict'] = {}
local function myrequire(name)
if modules[name] == nil then
modules[name] = true
modules[name] = (builders[name])(myrequire)
end
return modules[name]
end
return myrequire('day18')
end)()