feat: add day menu
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
SRC_PATH = "/aoc/src"
|
SRC_PATH = "/aoc/src"
|
||||||
|
RES_PATH = "/aoc/res"
|
||||||
|
|
||||||
package.path = SRC_PATH .. "/lib/?.lua;" .. package.path
|
package.path = SRC_PATH .. "/lib/?.lua;" .. package.path
|
||||||
|
|
||||||
@@ -6,6 +7,8 @@ START_DATE = {day=1, month=12, year=2025}
|
|||||||
END_DATE = {day=12, month=12, year=2025}
|
END_DATE = {day=12, month=12, year=2025}
|
||||||
|
|
||||||
local json = require("json")
|
local json = require("json")
|
||||||
|
local dates = require("dates")
|
||||||
|
local days = require("days")
|
||||||
local today = os.date("*t")
|
local today = os.date("*t")
|
||||||
|
|
||||||
local function loadStats(path)
|
local function loadStats(path)
|
||||||
@@ -24,52 +27,12 @@ local function loadStats(path)
|
|||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
|
|
||||||
---Checks whether date2 is after date1
|
|
||||||
---@param date1 any
|
|
||||||
---@param date2 any
|
|
||||||
---@return boolean
|
|
||||||
local function isAfter(date1, date2)
|
|
||||||
if date2.year < date1.year then
|
|
||||||
return false
|
|
||||||
elseif date2.year > date1.year then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if date2.month < date1.month then
|
|
||||||
return false
|
|
||||||
elseif date2.month > date1.month then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return date2.day > date1.day
|
|
||||||
end
|
|
||||||
|
|
||||||
---Checks whether date2 is before date1
|
|
||||||
---@param date1 any
|
|
||||||
---@param date2 any
|
|
||||||
---@return boolean
|
|
||||||
local function isBefore(date1, date2)
|
|
||||||
if date2.year > date1.year then
|
|
||||||
return false
|
|
||||||
elseif date2.year < date1.year then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if date2.month > date1.month then
|
|
||||||
return false
|
|
||||||
elseif date2.month < date1.month then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return date2.day < date1.day
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isInDateRange(startDate, targetDate, endDate)
|
|
||||||
return not (isBefore(startDate, targetDate) or isAfter(endDate, targetDate))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function printDateInfo()
|
local function printDateInfo()
|
||||||
if isBefore(START_DATE, today) then
|
if dates.isBefore(START_DATE, today) then
|
||||||
print("AoC 2025 has not started yet")
|
print("AoC 2025 has not started yet")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if isAfter(END_DATE, today) then
|
if dates.isAfter(END_DATE, today) then
|
||||||
print("AoC 2025 has ended")
|
print("AoC 2025 has ended")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -77,8 +40,7 @@ local function printDateInfo()
|
|||||||
print("Day " .. day .. "/" .. END_DATE.day)
|
print("Day " .. day .. "/" .. END_DATE.day)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function printStats()
|
local function printStats(stats, selected)
|
||||||
local stats = loadStats("aoc/res/stats.json")
|
|
||||||
local keys = {}
|
local keys = {}
|
||||||
for k in pairs(stats) do
|
for k in pairs(stats) do
|
||||||
table.insert(keys, k)
|
table.insert(keys, k)
|
||||||
@@ -91,7 +53,13 @@ local function printStats()
|
|||||||
local value = stats[key]
|
local value = stats[key]
|
||||||
local day = tonumber(key:sub(4))
|
local day = tonumber(key:sub(4))
|
||||||
local date = {day=day, month=START_DATE.month, year=START_DATE.year}
|
local date = {day=day, month=START_DATE.month, year=START_DATE.year}
|
||||||
if not isBefore(date, today) then
|
term.setTextColor(colors.lightBlue)
|
||||||
|
if selected == day then
|
||||||
|
write("- ")
|
||||||
|
else
|
||||||
|
write(" ")
|
||||||
|
end
|
||||||
|
if not dates.isBefore(date, today) then
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
else
|
else
|
||||||
term.setTextColor(colors.gray)
|
term.setTextColor(colors.gray)
|
||||||
@@ -123,15 +91,42 @@ local function printStats()
|
|||||||
print()
|
print()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function main()
|
local function printBanner()
|
||||||
term.setTextColor(colors.green)
|
term.setTextColor(colors.green)
|
||||||
textutils.slowPrint("+--------------------------------------+", 80)
|
print("+--------------------------------------+")
|
||||||
textutils.slowPrint("| Welcome to the Advent of Code 2025 |", 80)
|
print("| Welcome to the Advent of Code 2025 |")
|
||||||
textutils.slowPrint("+--------------------------------------+", 80)
|
print("+--------------------------------------+")
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
|
end
|
||||||
|
|
||||||
printDateInfo()
|
local function main()
|
||||||
printStats()
|
local stats = loadStats("aoc/res/stats.json")
|
||||||
|
local selectedDay = math.max(1, math.min(END_DATE.day, today.day))
|
||||||
|
|
||||||
|
while true do
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
printBanner()
|
||||||
|
printDateInfo()
|
||||||
|
printStats(stats, selectedDay)
|
||||||
|
|
||||||
|
local event, key, is_held = os.pullEvent("key")
|
||||||
|
if key == keys.up then
|
||||||
|
selectedDay = math.max(1, selectedDay - 1)
|
||||||
|
elseif key == keys.down then
|
||||||
|
selectedDay = math.min(END_DATE.day, selectedDay + 1)
|
||||||
|
elseif key == keys["end"] then
|
||||||
|
break
|
||||||
|
elseif key == keys.enter then
|
||||||
|
local dayStats = stats[("day%02d"):format(selectedDay)]
|
||||||
|
local day = days.Day.new(
|
||||||
|
selectedDay,
|
||||||
|
dayStats.puzzle1,
|
||||||
|
dayStats.puzzle2
|
||||||
|
)
|
||||||
|
day:show()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
main()
|
||||||
46
src/lib/dates.lua
Normal file
46
src/lib/dates.lua
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
local dates = {}
|
||||||
|
|
||||||
|
---Checks whether date2 is after date1
|
||||||
|
---@param date1 any
|
||||||
|
---@param date2 any
|
||||||
|
---@return boolean
|
||||||
|
function dates.isAfter(date1, date2)
|
||||||
|
if date2.year < date1.year then
|
||||||
|
return false
|
||||||
|
elseif date2.year > date1.year then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if date2.month < date1.month then
|
||||||
|
return false
|
||||||
|
elseif date2.month > date1.month then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return date2.day > date1.day
|
||||||
|
end
|
||||||
|
|
||||||
|
---Checks whether date2 is before date1
|
||||||
|
---@param date1 any
|
||||||
|
---@param date2 any
|
||||||
|
---@return boolean
|
||||||
|
function dates.isBefore(date1, date2)
|
||||||
|
if date2.year > date1.year then
|
||||||
|
return false
|
||||||
|
elseif date2.year < date1.year then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if date2.month > date1.month then
|
||||||
|
return false
|
||||||
|
elseif date2.month < date1.month then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return date2.day < date1.day
|
||||||
|
end
|
||||||
|
|
||||||
|
function dates.isInDateRange(startDate, targetDate, endDate)
|
||||||
|
return not (
|
||||||
|
dates.isBefore(startDate, targetDate) or
|
||||||
|
dates.isAfter(endDate, targetDate)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return dates
|
||||||
143
src/lib/days.lua
Normal file
143
src/lib/days.lua
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
local utils = require("utils")
|
||||||
|
local days = {}
|
||||||
|
|
||||||
|
local DAY_CACHE_PATH = "/.cache/days"
|
||||||
|
|
||||||
|
local CHOICES = {
|
||||||
|
create = "Create files",
|
||||||
|
example = "Run examples",
|
||||||
|
real = "Run with real input",
|
||||||
|
main = "Back to main menu"
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class Day
|
||||||
|
---@field day integer
|
||||||
|
---@field title string?
|
||||||
|
---@field puzzle1 boolean
|
||||||
|
---@field puzzle2 boolean
|
||||||
|
local Day = {day = 0, title = nil, puzzle1 = false, puzzle2 = false}
|
||||||
|
Day.__index = Day
|
||||||
|
|
||||||
|
---Creates a new Day object
|
||||||
|
---@param dayI integer
|
||||||
|
---@param puzzle1 boolean
|
||||||
|
---@param puzzle2 boolean
|
||||||
|
---@return Day
|
||||||
|
function Day.new(dayI, puzzle1, puzzle2)
|
||||||
|
local day = {}
|
||||||
|
setmetatable(day, Day)
|
||||||
|
day.day = dayI
|
||||||
|
day.puzzle1 = puzzle1
|
||||||
|
day.puzzle2 = puzzle2
|
||||||
|
return day
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns the title of this day.
|
||||||
|
---
|
||||||
|
---This function looks in the following places, in order:
|
||||||
|
---1. self.title
|
||||||
|
---2. Cache directory (DAY_CACHE_PATH)
|
||||||
|
---3. HTTP request to adventofcode.com
|
||||||
|
---@return string
|
||||||
|
function Day:getTitle()
|
||||||
|
if self.title then
|
||||||
|
return self.title
|
||||||
|
end
|
||||||
|
local cachePath = DAY_CACHE_PATH .. ("/%02d.txt"):format(self.day)
|
||||||
|
if fs.exists(cachePath) then
|
||||||
|
local cache = fs.open(cachePath, "r")
|
||||||
|
if cache then
|
||||||
|
local title = cache.readLine()
|
||||||
|
cache.close()
|
||||||
|
if title then
|
||||||
|
self.title = title
|
||||||
|
return title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fs.makeDir(DAY_CACHE_PATH)
|
||||||
|
local res = http.get("https://adventofcode.com/2024/day/" .. self.day)
|
||||||
|
local title = "Day " .. self.day
|
||||||
|
if res then
|
||||||
|
local body = res.readAll() or ""
|
||||||
|
local htmlTitle = body:match("%-%-%- (Day %d+: .-) %-%-%-")
|
||||||
|
if htmlTitle then
|
||||||
|
title = htmlTitle
|
||||||
|
self.title = title
|
||||||
|
local cache = fs.open(cachePath, "w")
|
||||||
|
if cache then
|
||||||
|
cache.writeLine(title)
|
||||||
|
cache.close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return title
|
||||||
|
end
|
||||||
|
|
||||||
|
function Day:srcDir()
|
||||||
|
return SRC_PATH .. ("/day%02d"):format(self.day)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Day:examplePath(suffix)
|
||||||
|
local filename = ("day%02d"):format(self.day)
|
||||||
|
if suffix then
|
||||||
|
filename = filename .. "_" .. suffix
|
||||||
|
end
|
||||||
|
return RES_PATH .. "/examples/" .. filename .. ".txt"
|
||||||
|
end
|
||||||
|
|
||||||
|
function Day:inputPath()
|
||||||
|
local filename = ("day%02d"):format(self.day)
|
||||||
|
return RES_PATH .. "/inputs/" .. filename .. ".txt"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---Creates the files for this day.
|
||||||
|
---
|
||||||
|
---This method creates the following files:
|
||||||
|
---- Script for puzzle 1
|
||||||
|
---- Script for puzzle 2
|
||||||
|
---- Example input file
|
||||||
|
---- Real input file
|
||||||
|
function Day:createFiles()
|
||||||
|
local srcDir = self:srcDir()
|
||||||
|
fs.makeDir(srcDir)
|
||||||
|
local files = {
|
||||||
|
srcDir .. "/puzzle1.lua",
|
||||||
|
srcDir .. "/puzzle2.lua",
|
||||||
|
self:examplePath(),
|
||||||
|
self:inputPath()
|
||||||
|
}
|
||||||
|
for _, path in ipairs(files) do
|
||||||
|
local f = fs.open(path, "a")
|
||||||
|
if f then
|
||||||
|
f.close()
|
||||||
|
else
|
||||||
|
printError("Could not create file " .. path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Displays this day and prompts the user with possible actions
|
||||||
|
function Day:show()
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
term.setTextColor(colors.green)
|
||||||
|
print(" -*- [ " .. self:getTitle() .. " ] -*-")
|
||||||
|
|
||||||
|
if fs.exists(self:srcDir()) then
|
||||||
|
local c = utils.promptChoices({CHOICES.example, CHOICES.real, CHOICES.main})
|
||||||
|
if c == CHOICES.example then
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local c = utils.promptChoices({CHOICES.create, CHOICES.main})
|
||||||
|
if c == CHOICES.create then
|
||||||
|
self:createFiles()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print("Puzzle 1")
|
||||||
|
end
|
||||||
|
|
||||||
|
days.Day = Day
|
||||||
|
|
||||||
|
return days
|
||||||
28
src/lib/utils.lua
Normal file
28
src/lib/utils.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
local utils = {}
|
||||||
|
|
||||||
|
function utils.promptChoices(choices)
|
||||||
|
local c = 1
|
||||||
|
local ox, oy = term.getCursorPos()
|
||||||
|
while true do
|
||||||
|
term.setCursorPos(ox, oy)
|
||||||
|
for i, choice in ipairs(choices) do
|
||||||
|
if i == c then
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
else
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
end
|
||||||
|
print(choice)
|
||||||
|
end
|
||||||
|
local event, key, is_held = os.pullEvent("key")
|
||||||
|
if key == keys.up then
|
||||||
|
c = math.max(1, c - 1)
|
||||||
|
elseif key == keys.down then
|
||||||
|
c = math.min(#choices, c + 1)
|
||||||
|
elseif key == keys.enter then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return choices[c]
|
||||||
|
end
|
||||||
|
|
||||||
|
return utils
|
||||||
Reference in New Issue
Block a user