Files
gen1recomp/tests/game3_link_union_chat_test.lua
2026-09-24 05:17:32 -04:00

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)