Files
gen1recomp/tests/game3_encounters_areas_test.lua

291 lines
10 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/wild_encounter.c:117, :269, :309, :446, :509, :519
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 function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local Rng = require("src.core.game3.rng")
local Encounters = require("src.core.game3.encounters")
local Runtime = require("src.core.game3.runtime")
print("[test] 1. the Rock Smash and fishing entry points exist")
check(type(Encounters.rollRocks) == "function", "Encounters.rollRocks exists")
check(type(Encounters.rollFishing) == "function", "Encounters.rollFishing exists")
check(type(Encounters.hasFishingMons) == "function", "Encounters.hasFishingMons exists")
if type(Encounters.rollRocks) ~= "function" or type(Encounters.rollFishing) ~= "function" then
print("[test] no Rock Smash / fishing area path at all; the rest cannot run")
finish()
end
local ROCK_SLOTS = {
{ species = 74, minLevel = 5, maxLevel = 20 },
{ species = 74, minLevel = 10, maxLevel = 20 },
{ species = 74, minLevel = 15, maxLevel = 30 },
{ species = 75, minLevel = 25, maxLevel = 40 },
{ species = 75, minLevel = 30, maxLevel = 40 },
}
local FISH_SLOTS = {}
for i = 1, 10 do
FISH_SLOTS[i] = { species = 1000 + i, minLevel = 10, maxLevel = 10 }
end
local FLAT_ROCKS = {}
for i = 1, 5 do
FLAT_ROCKS[i] = { species = 2000 + i, minLevel = 12, maxLevel = 12 }
end
Encounters._tables = {
SMASHABLE = { land = { rate = 7, slots = ROCK_SLOTS }, rocks = { rate = 50, slots = ROCK_SLOTS } },
ALWAYS = { rocks = { rate = 100, slots = FLAT_ROCKS } },
NEVER = { rocks = { rate = 0, slots = ROCK_SLOTS } },
DRY = { land = { rate = 21, slots = ROCK_SLOTS } },
POND = { fishing = { rate = 0, slots = FISH_SLOTS } },
}
Encounters._loaded = true
local prevSession = Runtime.session
local function setLead(ability)
Runtime.session = {
map = "SMASHABLE",
party = { { species = 74, level = 50, hp = 20, abilityId = ability } },
}
end
setLead(0)
local function reseed(n)
Rng.SeedRng(n)
Rng.SeedWildEncounterRng(n)
Encounters.resetRateModifiers()
end
print("[test] 2. a map with no rockSmashMonsInfo denies without touching the RNG")
reseed(0x51A5)
local before = Rng.getState()
eq(Encounters.rollRocks("DRY"), nil, "a land-only map has no Rock Smash encounter")
eq(Encounters.rollRocks("NO_SUCH_MAP"), nil, "an unknown map has no Rock Smash encounter")
eq(Rng.getState().value, before.value, "the denial consumes no Random()")
eq(Rng.getState().wild, before.wild, "the denial consumes no WildEncounterRandom()")
print("[test] 3. rate 100 always rolls, rate 0 never does")
reseed(0x0B0B)
local hits, species = 0, {}
for _ = 1, 200 do
local enc = Encounters.rollRocks("ALWAYS")
if enc then
hits = hits + 1
species[enc.species] = (species[enc.species] or 0) + 1
if enc.level ~= 12 then check(false, "flat slot returned level " .. tostring(enc.level)) end
end
end
eq(hits, 200, "rate 100 clears MAX_ENCOUNTER_RATE every time")
reseed(0x0B0B)
local zero = 0
for _ = 1, 200 do
if Encounters.rollRocks("NEVER") then zero = zero + 1 end
end
eq(zero, 0, "rate 0 never rolls")
print("[test] 4. Rock Smash picks slots on the water/rock weights (60/30/5/4/1)")
-- pokefirered/src/wild_encounter.c:101
reseed(0x7777)
local counts = {}
local N = 20000
for _ = 1, N do
local enc = Encounters.rollRocks("ALWAYS")
if enc then counts[enc.species] = (counts[enc.species] or 0) + 1 end
end
local WANT = { [2001] = 60, [2002] = 30, [2003] = 5, [2004] = 4, [2005] = 1 }
for sp, pct in pairs(WANT) do
local got = (counts[sp] or 0) * 100 / N
check(math.abs(got - pct) < 1.5,
string.format("slot for species %d is %.2f%% of rolls, pret says %d%%", sp, got, pct))
end
print("[test] 5. Rock Smash passes ignoreAbility, so Stench and Illuminate do nothing")
-- pokefirered/src/wild_encounter.c:453
local ABILITY_STENCH, ABILITY_ILLUMINATE = 1, 35
setLead(0)
eq(Encounters.encounterRate(21), 336, "neutral lead: rate 21 is 336/1600")
setLead(ABILITY_STENCH)
eq(Encounters.encounterRate(21), 168, "Stench halves the ordinary threshold")
eq(Encounters.encounterRate(21, { ignoreAbility = true }), 336, "ignoreAbility skips Stench")
setLead(ABILITY_ILLUMINATE)
eq(Encounters.encounterRate(21), 672, "Illuminate doubles the ordinary threshold")
eq(Encounters.encounterRate(21, { ignoreAbility = true }), 336, "ignoreAbility skips Illuminate")
local function smashRun(ability)
setLead(ability)
reseed(0x2468)
local out = {}
for i = 1, 400 do
local enc = Encounters.rollRocks("SMASHABLE")
out[i] = enc and (enc.species * 1000 + enc.level) or 0
end
return table.concat(out, ",")
end
local function landRun(ability)
setLead(ability)
reseed(0x2468)
local out = {}
for i = 1, 400 do
local enc = Encounters.rollLand("DRY", nil, false)
out[i] = enc and (enc.species * 1000 + enc.level) or 0
end
return table.concat(out, ",")
end
local neutralSmash = smashRun(0)
check(neutralSmash == smashRun(ABILITY_STENCH), "a Stench lead changes no Rock Smash roll")
check(neutralSmash == smashRun(ABILITY_ILLUMINATE), "an Illuminate lead changes no Rock Smash roll")
local neutralLand = landRun(0)
check(neutralLand ~= landRun(ABILITY_STENCH),
"the same Stench lead does move the ordinary land roll (the test can see the difference)")
setLead(0)
print("[test] 6. fishing area presence")
check(Encounters.hasFishingMons("POND"), "POND has fishingMonsInfo")
check(not Encounters.hasFishingMons("DRY"), "DRY has none")
check(not Encounters.hasFishingMons("NO_SUCH_MAP"), "an unknown map has none")
reseed(0x9999)
local fb = Rng.getState()
eq(Encounters.rollFishing("DRY", "old"), nil, "no fishing table means no encounter")
eq(Rng.getState().value, fb.value, "the fishing denial consumes no Random()")
print("[test] 7. every rod's slot window, at the exact pret boundaries")
-- pokefirered/src/wild_encounter.c:117
local function seedForDraw(target)
for s = 1, 500000 do
Rng.SeedRng(s)
if Rng.Random() % 100 == target then return s end
end
return nil
end
local BOUNDARIES = {
{ "old", 0, 1 }, { "old", 69, 1 }, { "old", 70, 2 }, { "old", 99, 2 },
{ "good", 0, 3 }, { "good", 59, 3 }, { "good", 60, 4 }, { "good", 79, 4 },
{ "good", 80, 5 }, { "good", 99, 5 },
{ "super", 0, 6 }, { "super", 39, 6 }, { "super", 40, 7 }, { "super", 79, 7 },
{ "super", 80, 8 }, { "super", 94, 8 }, { "super", 95, 9 }, { "super", 98, 9 },
{ "super", 99, 10 },
}
for _, row in ipairs(BOUNDARIES) do
local rod, draw, slot = row[1], row[2], row[3]
local s = seedForDraw(draw)
if not s then
check(false, "no seed produces Random() % 100 == " .. draw)
else
Rng.SeedRng(s)
local enc = Encounters.rollFishing("POND", rod)
eq(enc and enc.species, 1000 + slot,
string.format("%s rod, draw %d -> slot %d", rod, draw, slot))
end
end
print("[test] 8. rod aliases resolve the same way")
local ALIASES = { old = { 0, 262, "OLD_ROD", "ITEM_OLD_ROD" },
good = { 1, 263, "GOOD_ROD", "ITEM_GOOD_ROD" },
super = { 2, 264, "SUPER_ROD", "ITEM_SUPER_ROD" } }
local WINDOW = { old = { 1, 2 }, good = { 3, 5 }, super = { 6, 10 } }
for rod, list in pairs(ALIASES) do
for _, alias in ipairs(list) do
reseed(0x1357)
local enc = Encounters.rollFishing("POND", alias)
local slot = enc and (enc.species - 1000) or -1
check(slot >= WINDOW[rod][1] and slot <= WINDOW[rod][2],
string.format("%s as %s lands in slots %d-%d (got %s)",
rod, tostring(alias), WINDOW[rod][1], WINDOW[rod][2], tostring(slot)))
end
end
print("[test] 9. fishing takes no rate test, no behaviour gate and no Repel check")
Runtime.session = {
map = "POND",
repelSteps = 250,
party = { { species = 74, level = 100, hp = 20 } },
}
reseed(0x4242)
local bites = 0
for _ = 1, 300 do
if Encounters.rollFishing("POND", "super") then bites = bites + 1 end
end
eq(bites, 300, "a level-100 lead under Repel still lands every cast (rate 0 table)")
setLead(0)
print("[test] 10. ChooseWildMonLevel draws even when min == max")
-- pokefirered/src/wild_encounter.c:155
reseed(0x8642)
local enc = Encounters.rollFishing("POND", "old")
check(enc ~= nil, "flat-level cast produced an encounter")
eq(enc and enc.level, 10, "flat slot still reports its level")
local after = Rng.getState().value
Rng.SeedRng(0x8642)
Rng.Random()
Rng.Random()
eq(Rng.getState().value, after, "one draw for the slot and one for the flat level")
print("[test] 11. an inverted slot range swaps rather than collapsing")
Encounters._tables.INVERTED = {
fishing = { rate = 0, slots = { { species = 129, minLevel = 20, maxLevel = 10 } } },
}
reseed(0xAAAA)
local lo, hi = 99, 0
for _ = 1, 400 do
local e = Encounters.rollFishing("INVERTED", "old")
if e then
if e.level < lo then lo = e.level end
if e.level > hi then hi = e.level end
end
end
eq(lo, 10, "inverted range floors at maxLevel")
eq(hi, 20, "inverted range ceils at minLevel")
print("[test] 12. a landed Rock Smash or cast re-arms the immunity steps")
setLead(0)
reseed(0x1111)
Encounters._stepsSinceLastEncounter = 4
Encounters._encounterRateBuff = 900
check(Encounters.rollRocks("ALWAYS") ~= nil, "rate 100 Rock Smash lands")
eq(Encounters._stepsSinceLastEncounter, 0, "Rock Smash resets stepsSinceLastEncounter")
eq(Encounters._encounterRateBuff, 0, "Rock Smash clears the rate bank")
Encounters._stepsSinceLastEncounter = 4
Encounters._encounterRateBuff = 900
check(Encounters.rollFishing("POND", "good") ~= nil, "a cast lands")
eq(Encounters._stepsSinceLastEncounter, 0, "fishing resets stepsSinceLastEncounter")
eq(Encounters._encounterRateBuff, 0, "fishing clears the rate bank")
print("[test] 13. a missed Rock Smash banks nothing")
setLead(0)
reseed(0x3333)
Encounters._encounterRateBuff = 0
local misses = 0
for _ = 1, 200 do
if not Encounters.rollRocks("SMASHABLE") then misses = misses + 1 end
end
check(misses > 0, "rate 50 misses sometimes (" .. misses .. " of 200)")
eq(Encounters._encounterRateBuff, 0, "RockSmashWildEncounter never calls AddToWildEncounterRateBuff")
Runtime.session = prevSession
finish()