diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b8a2aad
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Advent of Code
+
+This repo contains my attempt at this year's Advent of Code (2025)
+
+I will solve this AoC using Lua in the context of the ComputerCraft Minecraft mod.\
+This project can also be run using the amazing [CraftOS-PC emulator](https://github.com/MCJack123/craftos2)
+
+## Progress
+
+
+#### Stars: 0/24
+
+|Mon|Tue|Wed|Thu|Fri|Sat|Sun|
+|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
+|1
|2
|3
|4
|5
|6
|7
|
+|8
|9
|10
|11
|12
|||
+
diff --git a/src/calendar.lua b/src/calendar.lua
new file mode 100644
index 0000000..ab5f7f5
--- /dev/null
+++ b/src/calendar.lua
@@ -0,0 +1,125 @@
+package.path = "/aoc/src/lib/?.lua;" .. package.path
+require("aoc")
+local json = require("json")
+local utils = require("utils")
+local buffer = require("buffer")
+local stats = json.loads(utils.readFile(RES_PATH .. "/stats.json")) or {}
+local readmePath = ROOT_PATH .. "/README.md"
+local outBuf = buffer.Buffer.new()
+
+local function insertInReadme()
+ local body = utils.readFile(readmePath) or ""
+ local tagStart = ""
+ local tagEnd = ""
+ body = body:gsub(
+ tagStart:gsub("%-", "%%-") .. ".-" .. tagEnd:gsub("%-", "%%-"),
+ tagStart .. "\n" .. outBuf:tostring() .. tagEnd
+ )
+ utils.writeFile(readmePath, body, true)
+end
+
+local totalStars = 0
+for _, s in pairs(stats) do
+ if s.puzzle1 then totalStars = totalStars + 1 end
+ if s.puzzle2 then totalStars = totalStars + 1 end
+end
+
+local startTS = 1764543600
+
+local startDate = os.date("*t", startTS)
+local firstWeekday = (startDate.wday + 5) % 7
+local padding = {x=3, y=1}
+
+local dayNames = {
+ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
+}
+
+local day = -firstWeekday + 1
+
+local rowSep = "+"
+local padRow = "|"
+for _ = 1, 7 do
+ rowSep = rowSep .. string.rep("-", padding.x * 2 + 3) .. "+"
+ padRow = padRow .. string.rep(" ", padding.x * 2 + 3) .. "|"
+end
+
+local function printRow(cells, cellWidth, padding)
+ local xPad = string.rep(" ", padding.x)
+ local height = 0
+ for _, cell in ipairs(cells) do
+ height = math.max(height, type(cell) == "string" and 1 or #cell)
+ end
+
+ for i = -padding.y + 1, height + padding.y do
+ write("|")
+ for _, cell in ipairs(cells) do
+ local text = cell[i] or ""
+ if type(cell) == "string" and i == 1 then
+ text = cell
+ end
+ local pad = cellWidth - #text
+ local lPad = math.ceil(pad / 2)
+ local rPad = pad - lPad
+ write(xPad .. string.rep(" ", lPad))
+ if i > 1 and i <= height then
+ term.setTextColor(colors.orange)
+ end
+ write(text)
+ term.setTextColor(colors.white)
+ write(string.rep(" ", rPad) .. xPad .. "|")
+ end
+ print()
+ end
+ print(rowSep)
+end
+
+term.clear()
+term.setCursorPos(1, 1)
+term.setTextColor(colors.white)
+
+print(("Stars: %d/24"):format(totalStars, 24))
+outBuf:print(("#### Stars: %d/24\n"):format(totalStars, 24))
+
+print(rowSep)
+printRow(dayNames, 3, padding)
+
+outBuf:print("|" .. table.concat(dayNames, "|") .. "|")
+for _ = 1, 7 do
+ outBuf:write("|:-:")
+end
+outBuf:print("|")
+
+while day < 12 do
+ local row = {}
+ local outRow = {}
+ for _ = 1, 7 do
+ if day < 1 or day > 12 then
+ table.insert(row, "")
+ table.insert(outRow, "")
+ else
+ local dayStats = stats[("day%02d"):format(day)]
+ local stars = 0
+ if dayStats.puzzle1 then stars = stars + 1 end
+ if dayStats.puzzle2 then stars = stars + 1 end
+ local cell = {
+ tostring(day),
+ string.rep("\x04", stars, " ")
+ }
+ table.insert(row, cell)
+ cell = {
+ tostring(day),
+ string.rep(":star:", stars, "")
+ }
+ table.insert(outRow, table.concat(cell, "
"))
+ end
+ day = day + 1
+ end
+ printRow(row, 3, padding)
+ outBuf:print("|" .. table.concat(outRow, "|") .. "|")
+end
+
+insertInReadme()
+
+---@diagnostic disable-next-line: undefined-field
+term.screenshot()
+os.sleep(2)
\ No newline at end of file
diff --git a/src/lib/aoc.lua b/src/lib/aoc.lua
index 713029e..5c1b7d5 100644
--- a/src/lib/aoc.lua
+++ b/src/lib/aoc.lua
@@ -1,5 +1,6 @@
-SRC_PATH = "/aoc/src"
-RES_PATH = "/aoc/res"
+ROOT_PATH = "/aoc"
+SRC_PATH = ROOT_PATH .. "/src"
+RES_PATH = ROOT_PATH .. "/res"
CACHE_PATH = "/.cache/aoc"
package.path = SRC_PATH .. "/lib/?.lua;" .. package.path
@@ -12,8 +13,9 @@ local dates = require("dates")
local days = require("days")
local progress = require "progress"
local today = os.date("*t")
+local aoc = {}
-local function loadStats(path)
+function aoc.loadStats(path)
path = shell.resolve(path)
if not fs.exists(path) then
printError("Stats file not found (" .. path .. ")")
@@ -29,7 +31,7 @@ local function loadStats(path)
return data
end
-local function printDateInfo()
+function aoc.printDateInfo()
if dates.isBefore(START_DATE, today) then
print("AoC 2025 has not started yet")
return
@@ -42,7 +44,7 @@ local function printDateInfo()
print("Day " .. day .. "/" .. END_DATE.day)
end
-local function printStats(stats, selected)
+function aoc.printStats(stats, selected)
local keys = {}
for k in pairs(stats) do
table.insert(keys, k)
@@ -100,7 +102,7 @@ local function printStats(stats, selected)
print("Press END to quit")
end
-local function printBanner()
+function aoc.printBanner()
term.setTextColor(colors.green)
print("+--------------------------------------+")
print("| Welcome to the Advent of Code 2025 |")
@@ -108,8 +110,8 @@ local function printBanner()
term.setTextColor(colors.white)
end
-local function main()
- local stats = loadStats("aoc/res/stats.json") or {}
+function aoc.main()
+ local stats = aoc.loadStats("aoc/res/stats.json") or {}
local selectedDay = math.max(1, math.min(END_DATE.day, today.day))
if not dates.isInDateRange(START_DATE, today, END_DATE) then
selectedDay = 1
@@ -118,9 +120,9 @@ local function main()
while true do
term.clear()
term.setCursorPos(1, 1)
- printBanner()
- printDateInfo()
- printStats(stats, selectedDay)
+ aoc.printBanner()
+ aoc.printDateInfo()
+ aoc.printStats(stats, selectedDay)
local event, key, is_held = os.pullEvent("key")
if key == keys.up then
@@ -137,9 +139,9 @@ local function main()
dayStats.puzzle2
)
day:show()
- stats = loadStats("aoc/res/stats.json") or {}
+ stats = aoc.loadStats("aoc/res/stats.json") or {}
end
end
end
-main()
\ No newline at end of file
+return aoc
diff --git a/src/lib/buffer.lua b/src/lib/buffer.lua
new file mode 100644
index 0000000..b0409ed
--- /dev/null
+++ b/src/lib/buffer.lua
@@ -0,0 +1,35 @@
+local buffer = {}
+
+---@class Buffer
+local Buffer = {_buf=""}
+Buffer.__index = Buffer
+
+---Creates a new buffer
+---@param str string?
+---@return Buffer
+function Buffer.new(str)
+ local buf = {}
+ buf._buf = str or ""
+ setmetatable(buf, Buffer)
+ return buf
+end
+
+function Buffer:tostring()
+ return self._buf
+end
+
+function Buffer:clear()
+ self._buf = ""
+end
+
+function Buffer:write(text)
+ self._buf = self._buf .. (text or "")
+end
+
+function Buffer:print(text)
+ self:write((text or "") .. "\n")
+end
+
+buffer.Buffer = Buffer
+
+return buffer
\ No newline at end of file