Module:User:Cscott/Advent Of Code 2023/Day 7

return (function()
local builders = {}
local function register(name, f)
  builders[name] = f
end
register('llpeg.lpegrex', function() return require [[Module:User:Cscott/lpegrex]] end)

register('llpeg', function() return require [[Module:User:Cscott/llpeg]] end)

register('day7', function(myrequire)
--[[ DAY 7 ]]--
local lpegrex = myrequire('llpeg.lpegrex')
local l = myrequire('llpeg')

--[[ PARSING ]]--
local patt = lpegrex.compile([[
Lines <-- {| nl* HandBid (nl+ HandBid)* nl* |}
HandBid <-- {| {:hand: Hand :} [ ]+ {:bid: Number :} |}
Hand <-- Card Card Card Card Card
Card <-- [AKQJT98765432j]
Number <-- %d+ -> tonumber
nl <-- %nl
SKIP <-- [ ]*
NAME_SUFFIX <-- [_%w]+
]])

function parse(source)
   --print(inspect(source))
   local ast, errlabel, pos = patt:match(source)
   if not ast then
      local lineno, colno, line = lpegrex.calcline(source, pos)
      local colhelp = string.rep(' ', colno-1)..'^'
      error('syntax error: '..lineno..':'..colno..': '..errlabel..
            '\n'..line..'\n'..colhelp)
   end
   --print('Parsed with success!')
   --print(inspect(ast))
   return ast
end

--[[ PART 1 ]]--

local JOKER = 1 -- must be lower than the "2"

local card_value = {
   j=JOKER, -- joker!
   ["2"]=2, ["3"]=3, ["4"]=4, ["5"]=5, ["6"]=6, ["7"]=7,
   ["8"]=8, ["9"]=9, T=10, J=11, Q=12, K=13, A=14,
}
local Kind = { -- chosen so they sort in order
   HighCard = "0high",
   OnePair = "1pair",
   TwoPair = "2pair",
   ThreeOfAKind = "3ofakind",
   FullHouse = "3zfullhouse",
   FourOfAKind = "4ofakind",
   FiveOfAKind = "5ofakind",
}

local split_patt = l.Ct((l.P(1) / card_value)^5)
function split(hand)
   -- split hand into cards, map each to value
   return split_patt:match(hand)
end

function kind1(hand)
   -- count duplicates
   local count = {}
   local max = 0
   for _,v in ipairs(hand) do
      count[v] = (count[v] or 0) + 1
      max = math.max(max, count[v])
   end
   if max == 5 then return Kind.FiveOfAKind end
   if max == 4 then return Kind.FourOfAKind end
   -- count pairs
   local pairCount = 0
   for _,cnt in pairs(count) do
      if cnt == 2 then pairCount = pairCount + 1 end
   end
   if max == 3 then
      if pairCount == 1 then return Kind.FullHouse end
      return Kind.ThreeOfAKind
   end
   if pairCount == 2 then return Kind.TwoPair end
   if pairCount == 1 then return Kind.OnePair end
   return Kind.HighCard
end

function compare_hands(a, b)
   if a.kind ~= b.kind then return a.kind < b.kind end
   -- sort by first card, then second, etc.
   for i=1,5 do
      if a.split[i] ~= b.split[i] then return a.split[i] < b.split[i] end
   end
   --print(inspect(a), inspect(b))
   --error("duplicate hands")
   return false
end

function winnings(source, compute_kind_func)
   local hands = parse(source)
   --print(inspect(hands))
   -- compute the kind for every hand
   for _,h in ipairs(hands) do
      h.split = split(h.hand)
      h.kind = compute_kind_func(h.split)
   end
   -- sort the hands
   table.sort(hands, compare_hands)
   -- sum the winnings!
   local sum = 0
   for rank,h in ipairs(hands) do
      -- print(rank,h.hand,h.bid,h.kind)
      sum = sum + rank * h.bid
   end
   return sum
end

--[[ Part 2 ]]--
function kind2(hand)
   -- count duplicates
   local count = {}
   local max = 0
   for _,v in ipairs(hand) do
      count[v] = (count[v] or 0) + 1
      max = math.max(max, count[v])
   end
   local jokers = count[JOKER] or 0
   if max == 5 then return Kind.FiveOfAKind end
   if max == 4 then
      if jokers > 0 then return Kind.FiveOfAKind end
      return Kind.FourOfAKind
   end
   -- count pairs
   local pairCount = 0
   for _,cnt in pairs(count) do
      if cnt == 2 then pairCount = pairCount + 1 end
   end
   if max == 3 then
      if jokers == 3 then
         -- option 1: three jokers, two matching cards
         if pairCount == 1 then return Kind.FiveOfAKind end
         -- option 2: three jokers, two non matching cards
         return Kind.FourOfAKind
      elseif jokers == 2 then
         -- option 3: three cards, two jokers
         return Kind.FiveOfAKind
      elseif jokers == 1 then
         -- option 4: three cards, 1 card, 1 joker
         return Kind.FourOfAKind
      elseif pairCount == 1 then
         -- option 5: no jokers, full house
         return Kind.FullHouse
      else
         -- option 6: no jokers
         return Kind.ThreeOfAKind
      end
   end
   -- max <= 2
   if pairCount == 2 then
      if jokers == 2 then
         -- two jokers, two matching cards, 1 non matching
         return Kind.FourOfAKind
      elseif jokers == 1 then
         -- one joker, two pair
         return Kind.FullHouse
      else
         return Kind.TwoPair
      end
   elseif pairCount == 1 then
      if jokers > 0 then
         -- one pair of jokers + one card, or one pair + 1 joker
         return Kind.ThreeOfAKind
      else
         return Kind.OnePair
      end
   elseif jokers > 0 then
      return Kind.OnePair
   else
      return Kind.HighCard
   end
end

function part1(source)
   return winnings(source, kind1) -- part 1
end

function part2(source)
   source = source:gsub("J","j") -- jokers!
   return winnings(source, kind2)
end

--[[ CLI start ] ]--
local source = io.input("day7.input"):read("a")
print(part1(source))
print(part2(source))
--[ [ CLI end ]]--

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)
   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('day7')
end)()