mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
309 lines
12 KiB
Lua
309 lines
12 KiB
Lua
#!/usr/bin/env luajit
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
local function eq(a, b, msg)
|
|
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
|
end
|
|
|
|
local romBundle = require("tests.game3_cache").bundle()
|
|
package.loaded["src.core.game3.scripting.space"] = { ensureBundle = function() return romBundle end }
|
|
if not romBundle then
|
|
package.loaded["src.core.game3.rom_text"] = {
|
|
plain = function(key, ctx)
|
|
local d = ctx and ctx.dynamic
|
|
if d and d[0] then return key .. ":" .. d[0] end
|
|
return key
|
|
end,
|
|
ascii = function(key) return key end, has = function() return true end,
|
|
ir = function(key) return { { t = "text", s = key } } end,
|
|
key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
|
|
lazy = function(t) return t end,
|
|
}
|
|
end
|
|
local RomText = require("src.core.game3.rom_text")
|
|
|
|
local session = { name = "RED", gender = 0, trainerId = 0x1234 }
|
|
local saves = 0
|
|
local game = { session = session, saveGame = function() saves = saves + 1 return true end }
|
|
package.loaded["src.core.game3.runtime"] = {
|
|
getSession = function() return session end,
|
|
isActive = function() return true end,
|
|
_game = game,
|
|
}
|
|
|
|
local Chat = require("src.core.game3.link.chat")
|
|
if not romBundle then
|
|
Chat._rows = {}
|
|
local ascii = { "ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY", "Z ", "01234", "56789", ".,!? ", "-/& " }
|
|
for i, key in ipairs(Chat.KEYBOARD[0]) do Chat._rows[key] = Chat.tokens(ascii[i]) end
|
|
for i, key in ipairs(Chat.KEYBOARD[1]) do Chat._rows[key] = Chat.tokens(ascii[i]:lower()) end
|
|
for _, key in ipairs(Chat.KEYBOARD[2]) do Chat._rows[key] = Chat.tokens("{EMOJI_HAPPY}{EMOJI_ANGRY}:;.") end
|
|
end
|
|
|
|
local function channel(seat, players)
|
|
local ch = { relay = true, sent = {}, inbox = {}, isClosed = false, left = false }
|
|
ch.playerList = players or { { seat = 0, name = "RED" }, { seat = 1, name = "BLUE" } }
|
|
function ch:send(msg) self.sent[#self.sent + 1] = msg end
|
|
function ch:take(t)
|
|
for i, m in ipairs(self.inbox) do
|
|
if m.type == t then return table.remove(self.inbox, i) end
|
|
end
|
|
return nil
|
|
end
|
|
function ch:seat() return seat end
|
|
function ch:players() return self.playerList end
|
|
function ch:closed() return self.isClosed end
|
|
function ch:leave() self.left = true end
|
|
function ch:push(fromSeat, msg)
|
|
msg.seat = fromSeat
|
|
self.inbox[#self.inbox + 1] = msg
|
|
end
|
|
function ch:last(t)
|
|
for i = #self.sent, 1, -1 do
|
|
if self.sent[i].type == t then return self.sent[i] end
|
|
end
|
|
return nil
|
|
end
|
|
return ch
|
|
end
|
|
|
|
local input = { pressed = {}, down = {} }
|
|
function input:wasPressed(k) return self.pressed[k] == true end
|
|
function input:isDown(k) return self.down[k] == true end
|
|
local function press(k)
|
|
input.pressed = { [k] = true }
|
|
input.down = { [k] = true }
|
|
Chat.handleInput(input)
|
|
input.pressed, input.down = {}, {}
|
|
Chat.update(1 / 60)
|
|
end
|
|
local function frames(n)
|
|
for _ = 1, n do Chat.update(1 / 60) end
|
|
end
|
|
|
|
print("[test] 1. the wire form keeps emoji as one character each")
|
|
local toks = { "H", "I", "{EMOJI_HAPPY}", "é", "…" }
|
|
local wire = Chat.encode(toks)
|
|
eq(#Chat.tokens(wire), 5, "five code points on the wire")
|
|
local back = Chat.decode(wire)
|
|
eq(table.concat(back, ","), table.concat(toks, ","), "and back")
|
|
local hostile = Chat.decode("{COLOR RED}HI\252\19\42" .. string.rep("X", 20))
|
|
eq(hostile[1], "H", "tags other than emoji are dropped")
|
|
check(#hostile <= Chat.MAX_LENGTH, "and a line never exceeds 15 characters")
|
|
eq(Chat.cleanName("{PLAYER}BLUE12345"), "BLUE123", "names are 7 plain characters")
|
|
eq(Chat.caseOf("a"), "A", "a <-> A")
|
|
eq(Chat.caseOf("é"), "É", "é <-> É")
|
|
eq(Chat.caseOf("1"), nil, "digits do not toggle")
|
|
|
|
print("[test] 2. starting announces the player and loads the registered phrases")
|
|
local ch = channel(1)
|
|
local done
|
|
check(Chat.start({ channel = ch, screen = false, onDone = function(r) done = r end }), "chat starts")
|
|
eq(Chat.seat, 1, "seat 1")
|
|
local hello = ch:last("game3_union_hello")
|
|
eq(hello and hello.name, "RED", "CHAT_MESSAGE_JOIN carries the name")
|
|
eq(#Chat.registered, 10, "ten registered phrases")
|
|
if romBundle then
|
|
eq(Chat.registered[1], RomText.plain("gText_Hello"), "the cart's defaults (UnionRoomChat_InitializeRegisteredTexts)")
|
|
end
|
|
|
|
print("[test] 3. typing from the keyboard, deleting, toggling case, sending")
|
|
press("a")
|
|
eq(table.concat(Chat.buffer), "A", "A types the key under the cursor")
|
|
press("right")
|
|
press("a")
|
|
eq(table.concat(Chat.buffer), "AB", "right moves one column")
|
|
press("r")
|
|
eq(table.concat(Chat.buffer), "Ab", "R toggles the last letter's case")
|
|
check(Chat.canToggleCase(), "the A<->a icon shows")
|
|
press("b")
|
|
eq(table.concat(Chat.buffer), "A", "B deletes")
|
|
for _ = 1, 20 do press("a") end
|
|
eq(#Chat.buffer, Chat.MAX_LENGTH, "the buffer stops at 15 characters")
|
|
press("start")
|
|
local line = ch:last("game3_chat_line")
|
|
eq(line and Chat.decode(line.text)[1], "A", "START sends the line")
|
|
eq(line and #Chat.tokens(line.text), 15, "all 15 characters")
|
|
eq(#Chat.buffer, 0, "and clears the buffer")
|
|
eq(#Chat.lines, 1, "the player's own line is in the log")
|
|
eq(Chat.lines[1].seat, 1, "in its seat colour")
|
|
|
|
print("[test] 4. joins, lines and leaves from other seats")
|
|
ch:push(0, { type = "game3_union_hello", name = "BLUE", gender = 0, trainerId = 2, activity = 0x45 })
|
|
ch:push(0, { type = "game3_chat_line", name = "BLUE", text = "HELLO" })
|
|
Chat.update(1 / 60)
|
|
eq(#Chat.lines, 3, "a join line and a chat line")
|
|
eq(Chat.lines[2].text, Chat.joinedLine("BLUE"), "gText_F700JoinedChat")
|
|
check(Chat.lines[3].text:sub(-6) == ":HELLO", "BLUE: HELLO")
|
|
eq(Chat.lines[3].seat, 0, "leader colour")
|
|
ch.playerList[3] = { seat = 2, name = "GOLD" }
|
|
ch:push(2, { type = "game3_union_hello", name = "GOLD", gender = 0, trainerId = 3, activity = 0x45 })
|
|
Chat.update(1 / 60)
|
|
eq(Chat.members[2], "GOLD", "a third trainer joins the running chat")
|
|
table.remove(ch.playerList, 3)
|
|
Chat.update(1 / 60)
|
|
eq(Chat.lines[#Chat.lines].text, Chat.leftLine("GOLD"), "a seat gone from the room left the chat")
|
|
check(Chat.exitType == nil, "the chat keeps going")
|
|
for i = 1, 12 do ch:push(0, { type = "game3_chat_line", name = "BLUE", text = "L" .. i }) end
|
|
Chat.update(1 / 60)
|
|
check(#Chat.lines < Chat.MAX_LINES, "the log keeps the last rows")
|
|
check(Chat.scroll ~= nil, "and scrolls")
|
|
frames(3)
|
|
eq(Chat.scroll, nil, "three 5 px steps")
|
|
|
|
print("[test] 5. SELECT swaps pages; the REGISTER page registers the last 10 characters")
|
|
press("select")
|
|
eq(Chat.routine, "switch", "the swap menu")
|
|
eq(Chat.swap and Chat.swap.cursor, Chat.PAGE.UPPER, "on the current page")
|
|
press("select")
|
|
eq(Chat.swap and Chat.swap.cursor, Chat.PAGE.LOWER, "SELECT moves the cursor")
|
|
press("a")
|
|
check(Chat.slide ~= nil, "the keyboard slides out")
|
|
frames(12)
|
|
eq(Chat.page, Chat.PAGE.LOWER, "and in on LOWER")
|
|
eq(Chat.slide, nil, "slide finished")
|
|
press("a")
|
|
eq(Chat.buffer[1], "a", "lower-case keys")
|
|
Chat.buffer = Chat.tokens("0123456789ABCDE")
|
|
press("select")
|
|
press("down")
|
|
press("a")
|
|
frames(12)
|
|
eq(Chat.page, Chat.PAGE.EMOJI, "EMOJI page")
|
|
press("select")
|
|
press("down")
|
|
press("a")
|
|
frames(12)
|
|
eq(Chat.page, Chat.PAGE.REGISTER, "the registered phrases page")
|
|
press("r")
|
|
eq(Chat.routine, "register", "R on the phrase page registers")
|
|
eq(Chat.msg and Chat.msg.id, "REGISTER_WHERE", "gText_RegisterTextWhere")
|
|
eq(Chat.registerStart(), 5, "only the last 10 characters register")
|
|
press("down")
|
|
press("down")
|
|
press("a")
|
|
eq(Chat.registered[3], "56789ABCDE", "row 3 now holds the text")
|
|
check(Chat.changed, "registered texts changed")
|
|
Chat.buffer = {}
|
|
press("r")
|
|
eq(Chat.routine, "input_text", "nothing to register: gText_InputText")
|
|
press("a")
|
|
eq(Chat.routine, "input", "back to typing")
|
|
Chat.row = 0
|
|
press("a")
|
|
eq(table.concat(Chat.buffer), Chat.registered[1] .. " ", "A on a phrase types it and a space")
|
|
|
|
print("[test] 6. leaving asks to save the changed phrases, then saves")
|
|
Chat.buffer = {}
|
|
press("b")
|
|
eq(Chat.routine, "quit", "B on an empty line asks to quit")
|
|
eq(Chat.msg and Chat.msg.id, "QUIT", "gText_QuitChatting")
|
|
eq(Chat.yesNo and Chat.yesNo.cursor, 1, "NO is the default")
|
|
press("up")
|
|
press("a")
|
|
eq(ch:last("game3_chat_bye") ~= nil, true, "a member leaving says CHAT_MESSAGE_LEAVE")
|
|
frames(2)
|
|
eq(Chat.routine, "ask_save", "registered texts changed: OK to save?")
|
|
press("up")
|
|
press("a")
|
|
frames(1)
|
|
eq(Chat.routine, "ask_overwrite", "already a saved file")
|
|
press("up")
|
|
press("a")
|
|
frames(3)
|
|
eq(saves, 1, "the game was saved")
|
|
eq(session.registeredTexts and session.registeredTexts[3], "56789ABCDE", "with the new phrase")
|
|
eq(Chat.msg and Chat.msg.id, "SAVED", "gText_RegisteredTextChanged_SavedTheGame")
|
|
frames(Chat.SAVED_FRAMES + 3)
|
|
eq(Chat.state, "off", "the chat closed")
|
|
eq(done, "left", "onDone reports the player left")
|
|
|
|
print("[test] 7. the leader leaving disbands the chat for everyone")
|
|
ch = channel(1)
|
|
done = nil
|
|
Chat.start({ channel = ch, screen = false, onDone = function(r) done = r end })
|
|
Chat.present[0] = true
|
|
Chat.members[0] = "BLUE"
|
|
ch:push(0, { type = "game3_chat_bye" })
|
|
Chat.update(1 / 60)
|
|
eq(Chat.exitType, "disbanded", "CHAT_MESSAGE_DISBAND")
|
|
frames(2)
|
|
eq(Chat.routine, "disbanded", "ChatEntryRoutine_Disbanded")
|
|
eq(Chat.msg and Chat.msg.id, "LEADER_LEFT", "gText_LeaderHasLeftEndingChat")
|
|
eq(Chat.hostName, "BLUE", "naming the leader")
|
|
frames(Chat.EXIT_DELAY + 3)
|
|
eq(done, "disbanded", "and the chat ends")
|
|
eq(ch:last("game3_chat_bye"), nil, "without a bye of our own")
|
|
|
|
print("[test] 8. the leader alone again exits (CHATEXIT_LEADER_LAST)")
|
|
ch = channel(0)
|
|
done = nil
|
|
Chat.start({ channel = ch, screen = false, onDone = function(r) done = r end })
|
|
Chat.update(1 / 60)
|
|
ch:push(1, { type = "game3_chat_bye" })
|
|
Chat.update(1 / 60)
|
|
eq(Chat.exitType, "leader_last", "the only member left")
|
|
frames(2)
|
|
eq(Chat.msg and Chat.msg.id, "EXITING", "gText_ExitingTheChat")
|
|
frames(Chat.EXIT_DELAY + 3)
|
|
eq(done, "leader_last", "the leader leaves too")
|
|
|
|
print("[test] 9. the leader quitting warns, then disbands")
|
|
ch = channel(0)
|
|
done = nil
|
|
Chat.start({ channel = ch, screen = false, onDone = function(r) done = r end })
|
|
Chat.update(1 / 60)
|
|
press("b")
|
|
press("up")
|
|
press("a")
|
|
frames(1)
|
|
eq(Chat.routine, "quit_leader", "If the LEADER leaves, the chat will end")
|
|
press("up")
|
|
press("a")
|
|
check(ch:last("game3_chat_bye") ~= nil, "the leader's bye disbands the room")
|
|
frames(3)
|
|
eq(done, "left", "and the leader leaves")
|
|
|
|
print("[test] 10. a dropped link ends the chat")
|
|
ch = channel(1)
|
|
done = nil
|
|
Chat.start({ channel = ch, screen = false, onDone = function(r) done = r end })
|
|
ch.isClosed = true
|
|
Chat.update(1 / 60)
|
|
check(Chat.exitType == "disbanded" or Chat.exitType == "dropped", "the room vanished")
|
|
frames(Chat.EXIT_DELAY + 3)
|
|
check(done ~= nil, "the chat closed")
|
|
|
|
print("[test] 11. a hand-edited save is cut to the cart's registered text and record limits")
|
|
local Schema = require("src.core.game3.save_schema_firered")
|
|
local longRecords = {}
|
|
for i = 1, 25 do longRecords[i] = { name = "TRAINERNAME" .. i, trainerId = 70000 + i } end
|
|
local restored = Schema.fromSaveTable({
|
|
registeredTexts = { "ABCDEFGHIJKLMNOP", "HI{HAPPY}" },
|
|
trainerNameRecords = longRecords,
|
|
})
|
|
eq(restored.registeredTexts[1], "ABCDEFGHIJ", "registered texts keep REGISTER_CHARS tokens")
|
|
eq(restored.registeredTexts[2], "HI{HAPPY}", "an emoji tag counts as one token")
|
|
eq(restored.registeredTexts[10], "", "all ten rows present")
|
|
eq(#restored.trainerNameRecords, 20, "trainerNameRecords[20]")
|
|
eq(restored.trainerNameRecords[1].name, "TRAINER", "names cut to PLAYER_NAME_LENGTH")
|
|
eq(restored.trainerNameRecords[1].trainerId, (70001) % 65536, "ids are u16")
|
|
|
|
Chat.reset()
|
|
if failed == 0 then
|
|
print("[pass] union room chat")
|
|
os.exit(0)
|
|
end
|
|
print("[fail] union room chat: " .. failed)
|
|
os.exit(1)
|