mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 04:06:10 +02:00
CLOSES #1496
This commit is contained in:
@@ -66,13 +66,39 @@ table.sort(MOVES)
|
||||
-- in the peer's inbox, so one side is mid-queue when the other's action
|
||||
-- arrives (Net.loopbackPair on its own delivers instantly, which is the one
|
||||
-- thing the real relay never does)
|
||||
local function laggyPair(delayA, delayB)
|
||||
-- mutation mode: a peer whose messages arrive with a random field (or the
|
||||
-- type itself) at the wrong Lua type. What is delivered is what Session
|
||||
-- would deliver -- Wire.sanitize's output, or nothing at all -- so a run
|
||||
-- exercises the real receive path rather than a hand-written stand-in.
|
||||
local Wire = require("src.link.Wire")
|
||||
local HOSTILE = { {}, { 1, 2, 3 }, 0, -1, 999, "s", "", true, false, math.huge }
|
||||
|
||||
local function mutate(rnd, msg)
|
||||
local keys = {}
|
||||
for k in pairs(msg) do
|
||||
if k ~= "type" then keys[#keys + 1] = k end
|
||||
end
|
||||
table.sort(keys)
|
||||
if #keys == 0 or rnd(1, 100) <= 20 then
|
||||
msg.type = HOSTILE[rnd(1, #HOSTILE)]
|
||||
else
|
||||
msg[keys[rnd(1, #keys)]] = HOSTILE[rnd(1, #HOSTILE)]
|
||||
end
|
||||
return msg
|
||||
end
|
||||
|
||||
local function laggyPair(delayA, delayB, rnd, mutateRate)
|
||||
local a, b = Net.loopbackPair()
|
||||
a.wire, b.wire = {}, {}
|
||||
a.delay, b.delay = delayA or 0, delayB or 0
|
||||
a.mutateRate, b.mutateRate = mutateRate or 0, mutateRate or 0
|
||||
local function send(self, msg)
|
||||
if self.closed then return end
|
||||
local decoded = Json.decode(Json.encode(msg)) -- same round trip as the wire
|
||||
if decoded and self.mutateRate > 0 then
|
||||
if rnd(1, 100) <= self.mutateRate then decoded = mutate(rnd, decoded) end
|
||||
decoded = Wire.sanitize(decoded)
|
||||
end
|
||||
if decoded then table.insert(self.wire, { msg = decoded, at = self.delay }) end
|
||||
end
|
||||
local function update(self)
|
||||
@@ -143,7 +169,7 @@ local function firstMismatch(a, b)
|
||||
end
|
||||
|
||||
-- Returns nil when the run agreed, or a description of how it split.
|
||||
local function runOne(seed)
|
||||
local function runOne(seed, mutateRate)
|
||||
local rnd = makeRandom(seed)
|
||||
-- the two sides are deliberately different clients
|
||||
local optsA = { animations = false, textSpeed = 1, battleStyle = "SET" }
|
||||
@@ -155,7 +181,7 @@ local function runOne(seed)
|
||||
gameA.save.party = randomParty(rnd, rnd(1, 4))
|
||||
gameB.save.party = randomParty(rnd, rnd(1, 4))
|
||||
|
||||
local netA, netB = laggyPair(lagA, lagB)
|
||||
local netA, netB = laggyPair(lagA, lagB, rnd, mutateRate)
|
||||
local battleSeed = rnd(1, 2 ^ 30)
|
||||
local battleA = LinkBattle.newHost(gameA, netA, {
|
||||
myParty = Protocol.packParty(gameA.save.party),
|
||||
@@ -230,6 +256,12 @@ local function runOne(seed)
|
||||
end
|
||||
end
|
||||
local turn, part = firstMismatch(battleA, battleB)
|
||||
if turn and mutateRate then
|
||||
-- a corrupted action IS a divergence; what matters here is that the
|
||||
-- match ends by its own rules (desync draw, forfeit, disconnect)
|
||||
-- rather than throwing
|
||||
return nil, battleA.turnCount or 0
|
||||
end
|
||||
if turn then
|
||||
return ("seed %d: turn %d %s split (lag %d/%d, steps %d/%d)"):format(
|
||||
seed, turn, part, lagA, lagB, stepsA, stepsB), battleA.turnCount or 0
|
||||
@@ -237,7 +269,7 @@ local function runOne(seed)
|
||||
end
|
||||
-- a battle still running at the guard is a stalemate (two mons that cannot
|
||||
-- KO each other), not a split; only a finished one can be checked mirrored
|
||||
if guard < 60000
|
||||
if not mutateRate and guard < 60000
|
||||
and (battleA.player.mon.hp ~= battleB.enemy.mon.hp
|
||||
or battleA.enemy.mon.hp ~= battleB.player.mon.hp) then
|
||||
return ("seed %d: final HP not mirrored (%d/%d vs %d/%d)"):format(
|
||||
@@ -250,6 +282,8 @@ end
|
||||
local RUNS = tonumber(arg and arg[1]) or 40
|
||||
local FIRST = tonumber(arg and arg[2]) or 1
|
||||
|
||||
local MUTATION_RUNS = tonumber(arg and arg[3]) or math.max(4, math.floor(RUNS / 4))
|
||||
|
||||
local failures, turns = 0, 0
|
||||
for seed = FIRST, FIRST + RUNS - 1 do
|
||||
local ok, why, t = pcall(runOne, seed)
|
||||
@@ -263,5 +297,21 @@ for seed = FIRST, FIRST + RUNS - 1 do
|
||||
end
|
||||
end
|
||||
print(("link desync fuzz: %d runs, %d turns, %d failures"):format(RUNS, turns, failures))
|
||||
|
||||
local mutationFailures = 0
|
||||
for seed = FIRST, FIRST + MUTATION_RUNS - 1 do
|
||||
local ok, why = pcall(runOne, seed, 15)
|
||||
if not ok then
|
||||
mutationFailures = mutationFailures + 1
|
||||
print("FAIL link mutation fuzz seed " .. seed .. ": " .. tostring(why))
|
||||
elseif why then
|
||||
mutationFailures = mutationFailures + 1
|
||||
print("FAIL link mutation fuzz " .. why)
|
||||
end
|
||||
end
|
||||
print(("link mutation fuzz: %d runs, %d failures"):format(
|
||||
MUTATION_RUNS, mutationFailures))
|
||||
|
||||
assert(failures == 0, failures .. " lockstep run(s) diverged")
|
||||
assert(mutationFailures == 0, mutationFailures .. " mutated run(s) threw")
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user