-- Gen 2 clock setup: the InitClock screens and the RTC base they write. -- -- The cart does not store "the time": InitClock stores the RTC reading at the -- moment the player answered (wStartHour / wStartMinute / wStartDay) and every -- later read goes through it. src/core/gen2/Clock.lua is that arithmetic and -- src/ui/gen2/InitClock.lua is both screens that write it. -- luajit tests/gen2_clock_test.lua package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 clock") local check, eq = S.check, S.eq love = require("tests.love_stub") -- No font is loaded here (these are layout and arithmetic assertions), so -- Font.encode would warn once per unknown glyph and bury a real failure. require("src.core.Logger").warn = function() end local Clock = require("src.core.gen2.Clock") local InitClock = require("src.ui.gen2.InitClock") -- A stub input the screen drives off, the same shape Input:wasPressed has. local function fakeInput() local pressed = {} return { press = function(self, button) pressed[button] = true end, wasPressed = function(_self, button) if pressed[button] then pressed[button] = nil return true end return false end, } end -- --------------------------------------------------------------- the offsets do local save = {} check(not Clock.isSet(save), "a fresh save has no base") eq(Clock.hour(save), math.floor(Clock.hostMinutes() / 60), "so it reads the host clock straight through") Clock.setTime(save, 10, 0) check(Clock.isSet(save), "answering Oak writes the base") eq(Clock.hour(save), 10, "and the clock reads back what was set") eq(Clock.minute(save), 0, "minutes too") Clock.setTime(save, 23, 45) eq(Clock.hour(save), 23, "a second answer re-anchors it") eq(Clock.minute(save), 45, "minutes and all") -- The base is an OFFSET, not a frozen reading: the clock keeps running. local base = save.rtc.startMinute local moved = (Clock.hostMinutes() + 90 + base) % Clock.MINUTES_PER_DAY eq(math.floor(moved / 60), 1, "an hour and a half later it is 1:15, not still 23:45") eq(moved % 60, 15, "minutes and all") Clock.setTime(save, 0, 0) eq(Clock.hour(save), 0, "midnight is hour 0") Clock.setTime(save, 12, 30) eq(Clock.hour(save), 12, "noon is hour 12") end do local save = {} eq(Clock.weekday(save), Clock.hostWeekday(), "with no base the weekday is the host's") for day = 0, 6 do Clock.setWeekday(save, day) eq(Clock.weekday(save), day, ("SetDayOfWeek pins day %d"):format(day)) end eq(save.rtc.dayOfWeek, 6, "and leaves wCurDay where the daily rollovers read it") end -- ------------------------------------------------------------ the hour screen do local save = {} local input = fakeInput() local done = {} local screen = InitClock.new({ input = input }, { save = save, onDone = function(hour, minute) done = { hour = hour, minute = minute } end, }) eq(screen.phase, "intro", "the screen opens on Oak waking up") eq(screen.hour, 10, "with InitClock's own default of 10 AM") eq(screen.minute, 0, "and no minutes") -- OakTimeWokeUpText is two `para` pages, so A turns the page before it takes -- the box down. eq(#screen:pages(), 2, "Oak's opening is two pages") input:press("a") screen:update(0) eq(screen.phase, "intro", "the first A turns the page") check(screen:pageText():find("clock", 1, true) ~= nil, "onto the one that asks about the clock") input:press("a") screen:update(0) eq(screen.phase, "hour", "and the second opens the hour picker") -- DisplayHourOClock is PrintHour + String_oclock, so the word rides along eq(screen:display(), "DAY 10 o'clock", "which shows the hour DisplayHourOClock does") input:press("up") screen:update(0) eq(screen.hour, 11, "up walks the hour forward") input:press("down") screen:update(0) input:press("down") screen:update(0) eq(screen.hour, 9, "down walks it back") -- .DecreaseThroughMidnight / .AdvanceThroughMidnight: both ends wrap. screen.hour = 0 input:press("down") screen:update(0) eq(screen.hour, 23, "and midnight wraps to 11 PM") input:press("up") screen:update(0) eq(screen.hour, 0, "and back again") screen.hour = 7 input:press("a") screen:update(0) eq(screen.phase, "confirm-hour", "A confirms the hour") check(screen:question():find("o'clock", 1, true) ~= nil, "and the question reads it back") -- NO drops back to the picker (`jr c, .loop`). screen.yesNo = 2 input:press("a") screen:update(0) eq(screen.phase, "hour", "NO goes back to the picker") input:press("a") screen:update(0) input:press("a") screen:update(0) eq(screen.phase, "minute", "YES moves on to the minutes") input:press("up") screen:update(0) eq(screen.minute, 1, "up walks the minutes") screen.minute = 0 input:press("down") screen:update(0) eq(screen.minute, 59, "and they wrap at the hour") screen.minute = 30 eq(screen:display(), "30 min.", "DisplayMinutesWithMinString's own string") input:press("a") screen:update(0) eq(screen.phase, "confirm-minute", "A confirms them") input:press("a") screen:update(0) eq(screen.phase, "response", "and Oak answers with the time") -- "MORN 7:30", not "7 AM:30": OakText_ResponseToSetTime is PrintHour (the -- time-of-day word then the 1-12 hour) then ':' then two-digit minutes, so -- the meridiem never appears and cannot land between the hour and them. check(screen:question():find("MORN 7:30", 1, true) ~= nil, "which is the pair the player just set") input:press("a") screen:update(0) eq(done.hour, 7, "the screen hands the hour back") eq(done.minute, 30, "and the minutes") eq(Clock.hour(save), 7, "and the save now reads that hour") eq(Clock.minute(save), 30, "and those minutes") end -- OakText_ResponseToSetTime's own ladder. eq(InitClock.responseKey(2), "soDark", "before MORN_HOUR it is still dark") eq(InitClock.responseKey(4), "overslept", "MORN_HOUR is 'I overslept'") -- DAY_HOUR is 10 (constants/misc_constants.asm:38), and -- OakText_ResponseToSetTime's `cp DAY_HOUR + 1 / jr c, .morn` keeps the hour -- ITSELF in the plain "I overslept!" arm; "Yikes!" starts at 11. This read 9 -- when InitClock did, so both were an hour early together. eq(InitClock.responseKey(10), "overslept", "and so is DAY_HOUR itself") eq(InitClock.responseKey(11), "yikes", "past it Oak yikes") eq(InitClock.responseKey(18), "soDark", "and NITE_HOUR is dark again") -- PrintHour (engine/rtc/timeset.asm:672) is GetTimeOfDayString + PlaceString -- and THEN the 1-12 hour, so the cart prints the time-of-day word ahead of the -- number and no meridiem at all. These read "12 AM" / "1 PM" while InitClock -- built the string that way, which is what let InitClock.timeString append -- ":mm" to a meridiem and have Oak say "5 AM:30". eq(InitClock.hourString(0), "NITE 12", "PrintHour shows midnight as NITE 12") eq(InitClock.hourString(12), "DAY 12", "and noon as DAY 12") eq(InitClock.hourString(13), "DAY 1", "and the afternoon on a 12-hour clock") eq(InitClock.timeString(5, 30), "MORN 5:30", "and OakText_ResponseToSetTime's line is PrintHour, ':', two-digit minutes") -- --------------------------------------------------------- the weekday wheel do local save = {} local input = fakeInput() local picked local screen = InitClock.new({ input = input }, { mode = "day", save = save, onDone = function(day) picked = day end, }) eq(screen.phase, "day", "the wheel opens on its picker, with no preamble") eq(screen.day, 0, "`xor a / ld [wTempDayOfWeek], a`: SUNDAY") eq(screen:display(), "SUNDAY", "which is what the box shows") for _ = 1, 2 do input:press("up") screen:update(0) end eq(screen:display(), "TUESDAY", "up walks the wheel forward") input:press("down") screen:update(0) input:press("down") screen:update(0) input:press("down") screen:update(0) eq(screen:display(), "SATURDAY", "and it wraps past SUNDAY") input:press("a") screen:update(0) eq(screen.phase, "confirm-day", "A confirms it") input:press("b") screen:update(0) eq(screen.phase, "day", "B is NO and drops back to the wheel") input:press("a") screen:update(0) input:press("a") screen:update(0) eq(picked, 6, "YES hands the day back") eq(Clock.weekday(save), 6, "and the save reads it") end -- The driven path: a screen this new must not stall a scripted run, so it -- walks itself to the end on its defaults. do local save = {} local finished = false local screen = InitClock.new({}, { save = save, autoConfirm = true, onDone = function() finished = true end, }) for _ = 1, 20 do if finished then break end screen:update(0) end check(finished, "autoConfirm reaches the end on its own") eq(Clock.hour(save), Clock.DEFAULT_HOUR, "taking InitClock's 10 AM default") eq(Clock.minute(save), Clock.DEFAULT_MINUTE, "and no minutes") end -- Drawing must not throw with the stub canvas: the layout is transcribed from -- hlcoord calls, so a bad coordinate is a crash rather than a wrong pixel. do local screen = InitClock.new({}, { save = {} }) for _, phase in ipairs({ "intro", "hour", "confirm-hour", "minute", "confirm-minute", "response", "day", "confirm-day" }) do screen.phase = phase local ok, err = pcall(function() screen:drawPanel() end) check(ok, ("phase %s draws (%s)"):format(phase, tostring(err))) end end S.finish()