Files
gen1recomp/tests/engine/g3u_lockstep_test.lua
T
2026-10-07 14:31:38 -04:00

128 lines
4.6 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local F = require("tests.engine._g3u_fixture")
local Table = require("src.battle.g3u.Table")
local Match = require("src.battle.g3u.Match")
local Scope = require("src.battle.g3u.Scope")
local function deep(a, b)
if type(a) ~= type(b) then return false end
if type(a) ~= "table" then return a == b end
for k, v in pairs(a) do if not deep(v, b[k]) then return false end end
for k in pairs(b) do if a[k] == nil then return false end end
return true
end
local function legalSame(a, b)
for seat = 0, 1 do
if not deep(a:legalActions(seat), b:legalActions(seat)) then return false end
end
return true
end
local function battle(t, seed)
local rnd = F.lcg(seed)
local p0, p1 = F.randomParty(t, rnd, rnd(4)), F.randomParty(t, rnd, rnd(4))
local opts = { table = t, parties = { [0] = p0, [1] = p1 }, seed = seed * 7919 }
local a, b = Match.new(opts), Match.new(opts)
local ea, eb = a:start(), b:start()
if not deep(ea, eb) then return false, "start events differ" end
local steps, hashes = 0, 0
while a.phase ~= "over" do
steps = steps + 1
if steps > 600 then return false, "battle did not end" end
if a.phase ~= b.phase then return false, "phase differs" end
if not legalSame(a, b) then return false, "legal actions differ at turn " .. a.turn end
if a.phase == "choose" then
local acts = { [0] = F.pick(a, 0, rnd), [1] = F.pick(a, 1, rnd) }
ea, eb = a:submit(acts), b:submit(acts)
else
local picks = {}
for seat = 0, 1 do
if a:needs(seat) then
local l = a:legalActions(seat)
picks[seat] = l[rnd(#l)].index
end
end
ea, eb = a:replace(picks), b:replace(picks)
end
if not deep(ea, eb) then return false, "events differ at turn " .. a.turn end
if a:hash() ~= b:hash() then return false, "hash differs at turn " .. a.turn end
hashes = hashes + 1
end
if b.phase ~= "over" or not deep(a.result, b.result) then return false, "result differs" end
for turn = 0, a.turn do
if a:hash(turn) == nil or a:hash(turn) ~= b:hash(turn) then return false, "turn hash " .. turn end
end
return true, a.turn
end
local function run(label, t, count, base)
Scope.resetTrips()
local ok, turns, failure = 0, 0, nil
for i = 1, count do
local good, info = battle(t, base + i)
if good then
ok = ok + 1
turns = turns + info
elseif not failure then
failure = ("seed %d: %s"):format(base + i, tostring(info))
end
end
T.eq(ok, count, label .. ": every battle stays in lockstep " .. tostring(failure or ""))
T.check(turns > count * 3, label .. ": battles run real turns (" .. turns .. ")")
T.eq(#Scope.trips, 0, label .. ": no cache, text or math.random read during a match " .. tostring(Scope.trips[1]))
end
local t1 = assert(Table.build(F.gen1()))
local t2 = assert(Table.build(F.gen2()))
run("gen1 fixture", t1, 120, 1000)
run("gen2 fixture", t2, 80, 5000)
local function solo(t, seed, script)
local rnd = F.lcg(seed)
local p0, p1 = F.randomParty(t, rnd, 3), F.randomParty(t, rnd, 3)
local m = Match.new({ table = t, parties = { [0] = p0, [1] = p1 }, seed = script and script.seed or seed })
m:start()
local log, i = { steps = {}, seed = seed }, 0
while m.phase ~= "over" and i < 400 do
i = i + 1
local step = script and script.steps[i]
if m.phase == "choose" then
step = step or { [0] = F.pick(m, 0, rnd), [1] = F.pick(m, 1, rnd) }
if script and not (m:isLegal(0, step[0]) and m:isLegal(1, step[1])) then break end
m:submit(step)
else
if not step then
step = {}
for s = 0, 1 do if m:needs(s) then step[s] = m:legalActions(s)[1].index end end
end
local okR = pcall(m.replace, m, step)
if not okR then break end
end
log.steps[i] = step
end
local hashes = {}
for turn = 0, m.turn do hashes[turn] = m:hash(turn) end
return log, hashes
end
local log, first = solo(t1, 77)
local _, again = solo(t1, 77, log)
T.same(again, first, "a replay run after the first match finished reproduces every turn hash")
local _, other = solo(t1, 77, { seed = 78, steps = log.steps })
T.check(other[1] ~= first[1] or other[2] ~= first[2] or other[3] ~= first[3],
"a different seed changes the turn hashes")
for _, pair in ipairs({ { "red", "g1r-red" }, { "gold", "g1r-gold" } }) do
local data = F.real(pair[1], pair[2])
if data then
run(pair[1] .. " cache", assert(Table.build(data)), 60, 9000)
else
print("[skip] " .. pair[1] .. " cache not imported")
end
end
T.finish("g3u lockstep")