mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
886 lines
36 KiB
Lua
886 lines
36 KiB
Lua
#!/usr/bin/env luajit
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
local root = Cache.mountOrSkip("dodrio berry picking (ROM tables, art, text)", "dodrio_berry_picking/tables.lua")
|
|
|
|
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 Json = require("src.link.Json")
|
|
local Wire = require("src.link.Wire")
|
|
local MG = require("src.core.game3.minigames.common")
|
|
local G = require("src.core.game3.minigames.dodrio_berry_picking")
|
|
local R = G.Rules
|
|
local Sim = G.Sim
|
|
local Art = require("src.ui.game3.minigames.common_art")
|
|
local Countdown = require("src.ui.game3.minigames.common_countdown")
|
|
local FakeRelay = require("tests.support.fake_relay")
|
|
|
|
local T = assert(Art.tables("dodrio_berry_picking"))
|
|
|
|
local sounds = {}
|
|
local function resetSounds() sounds = {} end
|
|
G.audio = {
|
|
playSe = function(id) sounds[#sounds + 1] = "se" .. id end,
|
|
stopSe = function() end,
|
|
isSePlaying = function() return false end,
|
|
playSong = function(id) sounds[#sounds + 1] = "song" .. id end,
|
|
playFanfare = function(id) sounds[#sounds + 1] = "fanfare" .. id end,
|
|
fadeOutAndPlay = function(id) sounds[#sounds + 1] = "fade" .. id end,
|
|
}
|
|
|
|
local function canon(v)
|
|
if type(v) ~= "table" then return tostring(v) end
|
|
local keys = {}
|
|
for k in pairs(v) do keys[#keys + 1] = k end
|
|
table.sort(keys, function(x, y) return tostring(x) < tostring(y) end)
|
|
local out = {}
|
|
for _, k in ipairs(keys) do out[#out + 1] = tostring(k) .. "=" .. canon(v[k]) end
|
|
return "{" .. table.concat(out, ",") .. "}"
|
|
end
|
|
|
|
local function count(list, v)
|
|
local n = 0
|
|
for _, x in ipairs(list) do if x == v then n = n + 1 end end
|
|
return n
|
|
end
|
|
|
|
local function stepN(st, n, presses)
|
|
for _ = 1, n do R.step(st, presses or {}) end
|
|
end
|
|
|
|
print("[test] 1. ROM tables and cart geometry")
|
|
do
|
|
eq(select(1, R.activeColumns(3)), 2, "3 players: first active column 2")
|
|
eq(select(2, R.activeColumns(5)), 11, "5 players: all 11 columns (10 + wrap)")
|
|
eq(R.dodrioX(0, 3), 120, "own Dodrio centred with 3 players")
|
|
eq(R.dodrioX(3, 5), 24, "5 players: position 3 at x 24")
|
|
eq(R.headColumn(T, 3, 1, 0), 6, "sDodrioHeadToColumnMap[2][1] left head")
|
|
eq(R.activeColumn(T, 3, 2, 8), 6, "sActiveColumnMap[2][2][8] wraps to column 6")
|
|
eq(T.tree_border_x[5], 15, "sTreeBorderXPos 5 players")
|
|
eq(#T.prize_berry_ids, 3, "three prize sets")
|
|
end
|
|
|
|
print("[test] 2. first wave, prize and falling")
|
|
do
|
|
local st = R.new(T, 3, MG.rng(7))
|
|
eq(st.fall[2], 1, "even column starts at 1")
|
|
eq(st.fall[3], 0, "odd column starts hidden")
|
|
eq(st.ids[4], R.BERRY_BLUE, "first wave is blue")
|
|
local r = MG.rng(7)
|
|
eq(st.prize, T.prize_berry_ids[1][r:next() % 10 + 1], "prize from the first Random() and the 3-player set")
|
|
local st5 = R.new(T, 5, MG.rng(7))
|
|
eq(st5.prize, T.prize_berry_ids[3][MG.rng(7):next() % 10 + 1], "5 players use the third set")
|
|
stepN(st, T.berry_fall_delays[1][1] - 1)
|
|
eq(st.fall[2], 1, "blue waits sBerryFallDelays[0][BLUE] frames")
|
|
R.step(st, {})
|
|
eq(st.fall[2], 2, "then drops one step")
|
|
eq(st.fall[3], 1, "odd column entered on the same frame")
|
|
check(st.falling, "berries falling")
|
|
end
|
|
|
|
local function fresh(n, seed)
|
|
local st = R.new(T, n, MG.rng(seed or 99))
|
|
for c = 0, 10 do
|
|
st.fall[c] = 0
|
|
st.state[c] = R.BS_SQUISHED
|
|
st.newTimer[c] = 0
|
|
end
|
|
st.gray = 0
|
|
return st
|
|
end
|
|
|
|
print("[test] 3. picking, eating, difficulty")
|
|
do
|
|
local st = fresh(3)
|
|
local col = R.headColumn(T, 3, 0, 1)
|
|
eq(col, 5, "player 0 middle head over column 5")
|
|
st.state[col] = R.BS_NONE
|
|
st.fall[col] = 6
|
|
st.ids[col] = R.BERRY_GREEN
|
|
st.echo[col] = R.BERRY_GREEN
|
|
R.step(st, { [0] = { n = 1, d = R.PICK_MIDDLE } })
|
|
eq(st.inputState[0], R.IN_PICKED, "press in range picks")
|
|
eq(st.state[col], R.BS_PICKED, "berry picked")
|
|
local frames = 0
|
|
while st.state[col] == R.BS_PICKED and frames < 20 do
|
|
R.step(st, { [0] = { n = 1, d = R.PICK_MIDDLE } })
|
|
frames = frames + 1
|
|
end
|
|
eq(st.state[col], R.BS_EATEN, "eaten after the eat timer")
|
|
eq(st.res[0][R.BERRY_GREEN], 1, "green counted for player 0")
|
|
eq(st.eaten[0], 1, "berriesEaten")
|
|
eq(st.ate[0], 1, "ateBerry set")
|
|
eq(st.ids[col], R.BERRY_MISSED, "id cleared to BERRY_MISSED")
|
|
eq(st.fall[col], R.EAT_FALL_DIST, "eaten berry parks at EAT_FALL_DIST")
|
|
stepN(st, 25, { [0] = { n = 1, d = R.PICK_MIDDLE } })
|
|
eq(st.state[col], R.BS_NONE, "new berry after 20 frames")
|
|
eq(st.fall[col], 1, "new berry at the top")
|
|
eq(st.ate[0], 0, "ateBerry cleared")
|
|
eq(st.inputState[0], R.IN_NONE, "input released after 6 frames")
|
|
|
|
local d = fresh(3)
|
|
d.eaten[0] = 4
|
|
d.difficulty[0] = 0
|
|
local c2 = R.headColumn(T, 3, 0, 0)
|
|
d.state[c2] = R.BS_NONE
|
|
d.fall[c2] = 7
|
|
d.ids[c2] = R.BERRY_BLUE
|
|
d.echo[c2] = R.BERRY_BLUE
|
|
for i = 1, 10 do R.step(d, { [0] = { n = 1, d = R.PICK_LEFT } }) if d.state[c2] == R.BS_EATEN then break end end
|
|
eq(d.difficulty[0], 1, "5 berries eaten -> difficulty 1")
|
|
d.difficulty[0] = 21
|
|
d.eaten[0] = 49
|
|
d.state[c2] = R.BS_NONE
|
|
d.fall[c2] = 7
|
|
d.ids[c2], d.echo[c2] = 0, 0
|
|
d.inputState[0] = R.IN_NONE
|
|
for i = 1, 10 do R.step(d, { [0] = { n = 2, d = R.PICK_LEFT } }) if d.state[c2] == R.BS_EATEN then break end end
|
|
eq(d.difficulty[0], 22, "u8 threshold wraps at difficulty 21 (5 + 300 -> 49)")
|
|
end
|
|
|
|
print("[test] 4. misses")
|
|
do
|
|
local st = fresh(3)
|
|
local col = R.headColumn(T, 3, 1, 1)
|
|
st.state[col] = R.BS_NONE
|
|
st.fall[col] = 3
|
|
R.step(st, { [1] = { n = 1, d = R.PICK_MIDDLE } })
|
|
eq(st.inputState[1], R.IN_BAD_MISS, "out of range press is a bad miss")
|
|
eq(st.missed[1], 1, "missedBerry set")
|
|
stepN(st, 38, { [1] = { n = 2, d = R.PICK_MIDDLE } })
|
|
eq(st.inputState[1], R.IN_BAD_MISS, "still locked inside 40 frames")
|
|
eq(st.ack[1], 2, "the press during the lockout is consumed")
|
|
stepN(st, 1, { [1] = { n = 2, d = R.PICK_MIDDLE } })
|
|
eq(st.inputState[1], R.IN_NONE, "released after 40 frames")
|
|
|
|
local sq = fresh(3)
|
|
sq.state[4] = R.BS_NONE
|
|
sq.fall[4] = R.MAX_FALL_DIST
|
|
R.step(sq, {})
|
|
eq(sq.state[4], R.BS_SQUISHED, "berry squished at MAX_FALL_DIST")
|
|
eq(sq.gray, 1, "one gray square")
|
|
eq(sq.res[2][R.BERRY_MISSED], 1, "column 4 miss charged to player 2")
|
|
eq(sq.res[0][R.BERRY_MISSED], 1, "and player 0")
|
|
eq(sq.res[1][R.BERRY_MISSED], 0, "not player 1")
|
|
eq(sq.maxInRow, 0, "berries in a row only counts with 5 players")
|
|
|
|
local five = fresh(5)
|
|
five.inRow = 12
|
|
five.state[0] = R.BS_NONE
|
|
five.fall[0] = R.MAX_FALL_DIST
|
|
R.step(five, {})
|
|
eq(five.maxInRow, 12, "5 players: a miss banks the streak")
|
|
eq(five.inRow, 0, "and resets it")
|
|
|
|
local two = fresh(3, 5)
|
|
local c = 4
|
|
eq(R.headColumn(T, 3, 0, 0), c, "player 0 left head on 4")
|
|
eq(R.headColumn(T, 3, 2, 2), c, "player 2 right head on 4")
|
|
two.state[c] = R.BS_NONE
|
|
two.fall[c] = 6
|
|
two.ids[c], two.echo[c] = 0, 0
|
|
R.step(two, { [0] = { n = 1, d = R.PICK_LEFT }, [2] = { n = 1, d = R.PICK_RIGHT } })
|
|
eq(two.att0[c], 0, "first picker recorded")
|
|
eq(two.att1[c], R.PLAYER_NONE, "the berry is already PICKED for the second one")
|
|
eq(two.missed[2], 1, "so player 2 missed it")
|
|
for i = 1, 10 do
|
|
R.step(two, { [0] = { n = 1, d = R.PICK_LEFT }, [2] = { n = 1, d = R.PICK_RIGHT } })
|
|
if two.state[c] == R.BS_EATEN then break end
|
|
end
|
|
eq(two.state[c], R.BS_EATEN, "shared berry eaten")
|
|
eq(two.ate[0], 1, "player 0 ate it")
|
|
eq(two.ate[2], 0, "player 2 did not")
|
|
end
|
|
|
|
print("[test] 5. berry types by difficulty")
|
|
do
|
|
local st = fresh(3)
|
|
eq(R.berryIdByDifficulty(st, 0, 4), R.BERRY_BLUE, "d0 blue")
|
|
eq(R.berryIdByDifficulty(st, 1, 4), R.BERRY_GREEN, "d1 green")
|
|
eq(R.berryIdByDifficulty(st, 2, 4), R.BERRY_GOLD, "d2 gold")
|
|
st.prev[4] = R.BERRY_BLUE
|
|
eq(R.berryIdByDifficulty(st, 3, 4), R.BERRY_GREEN, "d3 alternates blue/green")
|
|
eq(R.berryIdByDifficulty(st, 4, 4), R.BERRY_GOLD, "d4 alternates blue/gold")
|
|
st.prev[4] = R.BERRY_GOLD
|
|
eq(R.berryIdByDifficulty(st, 5, 4), R.BERRY_GREEN, "d5 alternates gold/green")
|
|
st.prev[4] = R.BERRY_GREEN
|
|
eq(R.berryIdByDifficulty(st, 6, 4), R.BERRY_GOLD, "d6 cycles blue/green/gold")
|
|
eq(R.berryIdByDifficulty(st, 7, 4), R.BERRY_BLUE, "d7 wraps to blue")
|
|
st.difficulty[1] = 2
|
|
eq(R.newBerryId(st, 0, 5), R.BERRY_BLUE, "unshared column 5 uses player 0 only")
|
|
eq(R.newBerryId(st, 0, 6), R.BERRY_GOLD, "shared column 6 takes the hardest neighbour")
|
|
end
|
|
|
|
print("[test] 6. scores and rankings")
|
|
do
|
|
local res = R.newResults()
|
|
res[0][0], res[0][1], res[0][2], res[0][3] = 10, 5, 2, 1
|
|
eq(R.score(T, res[0]), 100 + 150 + 100 - 50, "10/30/50 points, -50 per miss")
|
|
res[1][3] = 5
|
|
eq(R.score(T, res[1]), 0, "score never goes negative")
|
|
res[2][0] = 30
|
|
eq(R.highestScore(T, res, 3), 300, "highest")
|
|
local sr = R.scoreResults(T, res, 3)
|
|
eq(sr[0].ranking, 0, "tie at the top: first")
|
|
eq(sr[2].ranking, 0, "tie at the top: first too")
|
|
eq(sr[1].ranking, 2, "then third")
|
|
local rows = R.rankedOrder(T, res, 3)
|
|
eq(rows[1].player, 0, "ranked list keeps player order inside a tie")
|
|
eq(rows[3].player, 1, "zero score last")
|
|
eq(rows[3].ranking, 2, "zero score shown at the last rank")
|
|
local none = R.rankedOrder(T, R.newResults(), 3)
|
|
eq(none[1].ranking, 2, "nobody scored: everybody last")
|
|
eq(R.berriesPicked({ [0] = 9000, 900, 200, 0 }), 9999, "berries picked capped at 9999")
|
|
end
|
|
|
|
print("[test] 7. snapshot encoding and leader handover")
|
|
do
|
|
local st = R.new(T, 5, MG.rng(31337))
|
|
local presses = {}
|
|
local rng = MG.rng(5)
|
|
for f = 1, 900 do
|
|
for p = 0, 4 do
|
|
if rng:next() % 11 == 0 then presses[p] = { n = f % 256, d = rng:next() % 3 + 1 } end
|
|
end
|
|
R.step(st, presses)
|
|
end
|
|
for p = 0, 4 do
|
|
st.res[p][0], st.res[p][1], st.res[p][2], st.res[p][3] = 19999, 19999, 19999, 65535
|
|
st.eaten[p] = 65535
|
|
end
|
|
local s = R.encode(st)
|
|
s.go, s.rd, s.m, s.e = 1, 99, 1048575, 1
|
|
local msg = { type = "game3_mg_state", f = 2147483, e = 99, s = s }
|
|
local bytes = #Json.encode(msg)
|
|
check(bytes <= 512, "5-player state at maximum counts fits the 512 byte cap (" .. bytes .. ")")
|
|
local wired = Wire.sanitize(Json.decode(Json.encode(msg)))
|
|
check(wired and type(wired.s) == "table", "state survives Wire.sanitize")
|
|
local copyRng = MG.rng(0)
|
|
local back = R.decode(T, 5, wired.s, copyRng)
|
|
eq(canon(R.encode(back)), canon(R.encode(st)), "decode(encode(state)) round trips")
|
|
eq(copyRng.state, st.rng.state, "RNG state rides the snapshot")
|
|
|
|
local a = R.new(T, 4, MG.rng(4242))
|
|
local script = {}
|
|
local ir = MG.rng(77)
|
|
local cur = {}
|
|
for f = 1, 2600 do
|
|
for p = 0, 3 do
|
|
if (f + p * 3) % 9 == 0 then cur[p] = { n = (f + p) % 256, d = ir:next() % 3 + 1 } end
|
|
end
|
|
local frame = {}
|
|
for p = 0, 3 do frame[p] = cur[p] end
|
|
script[f] = frame
|
|
end
|
|
for f = 1, 600 do R.step(a, script[f]) end
|
|
local handover = Json.decode(Json.encode(R.encode(a)))
|
|
local b = R.decode(T, 4, handover, MG.rng(0))
|
|
local same = true
|
|
local eatenAfter = 0
|
|
for f = 601, 2600 do
|
|
R.step(a, script[f])
|
|
R.step(b, script[f])
|
|
if Json.encode(R.encode(a)) ~= Json.encode(R.encode(b)) then same = false break end
|
|
end
|
|
for p = 0, 3 do eatenAfter = eatenAfter + a.eaten[p] end
|
|
check(eatenAfter > 0, "the handover run had picks in it (" .. eatenAfter .. ")")
|
|
check(same, "a new leader continues bit-for-bit from the last state")
|
|
end
|
|
|
|
print("[test] 8. game over")
|
|
do
|
|
local st = R.new(T, 5, MG.rng(3))
|
|
st.inRow, st.maxInRow = 4, 7
|
|
local f = 0
|
|
while not R.ended(st) and f < 5000 do R.step(st, {}) f = f + 1 end
|
|
check(R.ended(st), "no picks: the game ends (" .. f .. " frames)")
|
|
eq(st.gray, R.NUM_STATUS_SQUARES, "ten gray squares")
|
|
check(not st.falling, "nothing left falling")
|
|
eq(st.res[3][R.BERRY_IN_ROW], 7, "max berries in a row copied to every player")
|
|
end
|
|
|
|
local function players(n)
|
|
local out = {}
|
|
local names = { "RED", "BLUE", "LEAF", "GOLD", "KRIS" }
|
|
for s = 0, n - 1 do
|
|
out[#out + 1] = { seat = s, name = names[s + 1], trainerId = 0x100 + s, gender = s % 2, species = 85 }
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function newSim(n, seat, leader, seed)
|
|
return G.new({ game = "pick", seat = seat, seats = n, players = players(n), leader = leader,
|
|
seed = seed or 11, rng = MG.rng(seed or 11), partyMon = { species = 85, personality = 1, otId = 2 } })
|
|
end
|
|
|
|
local function press(dir)
|
|
return { wasPressed = function(_, b) return b == dir end, isDown = function() return false end }
|
|
end
|
|
local IDLE = { wasPressed = function() return false end, isDown = function() return false end }
|
|
|
|
print("[test] 9. sim: intro, own countdown, input gating, prediction")
|
|
do
|
|
resetSounds()
|
|
local sim = newSim(3, 1, false)
|
|
eq(sim.me, 1, "seat 1 is player 1")
|
|
local function frame(input)
|
|
sim:localInput(input or IDLE)
|
|
sim:update()
|
|
end
|
|
check(not sim:introDone(), "intro running")
|
|
local slide = 0
|
|
while sim.intro.stage == "slide" do frame() slide = slide + 1 end
|
|
eq(slide, 4 * (T.tree_border_x[3] + 1), "tree borders slide for 4 * (XPos + 1) frames")
|
|
eq(sim.intro.hofs, (T.tree_border_x[3] - 1) * 8, "borders stop at (XPos - 1) * 8")
|
|
local f = 0
|
|
while not sim:introDone() and f < 1000 do frame() f = f + 1 end
|
|
check(sim:introDone(), "intro finishes (" .. f .. " more frames)")
|
|
eq(count(sounds, "se" .. Sim.SE_CLICK), 10, "SE_CLICK per status square")
|
|
eq(count(sounds, "se" .. Sim.SE_M_CHARM), 6, "intro head stretches: SE_M_CHARM x6")
|
|
eq(sim.status.y[9], 8, "status bar settled at y 8")
|
|
check(count(sounds, "song" .. Sim.MUS_BERRY_PICK) == 1, "MUS_BERRY_PICK")
|
|
check(not sim.hostCountdown, "Match skipped its countdown (G.OWN_COUNTDOWN): the sim plays it")
|
|
check(sim.countdown ~= nil, "StartMinigameCountdown after the names")
|
|
eq(sim:localInput(press("up")).n, 0, "no picking during the countdown")
|
|
local cd = 0
|
|
while not sim.started and cd < 400 do frame() cd = cd + 1 end
|
|
check(sim.started, "countdown over, game on (" .. cd .. " frames)")
|
|
eq(count(sounds, "se50"), 9, "SE_BALL_BOUNCE_2 x9 from the countdown")
|
|
check(G.OWN_COUNTDOWN, "the module owns its countdown")
|
|
|
|
local host = newSim(3, 2, false)
|
|
host:update()
|
|
check(host.hostCountdown, "an update before any step means the Match is counting down")
|
|
for _ = 1, 600 do host:update() end
|
|
check(host.countdown == nil, "then the sim does not play a second countdown")
|
|
check(host:localInput(press("left")) ~= nil, "input opens when the Match starts play")
|
|
|
|
local i0 = sim:localInput(IDLE)
|
|
eq(i0.n, 0, "no press counted before the first press")
|
|
eq(i0.sh, 1, "input carries the mon's shininess (tid 2 ^ pid 1 < 8: shiny)")
|
|
local i = sim:localInput(press("up"))
|
|
eq(i and i.d, R.PICK_MIDDLE, "UP reaches up (middle head)")
|
|
sim:predict(i)
|
|
eq(sim:dodrioPose(1), R.PICK_MIDDLE, "own Dodrio predicted at once")
|
|
local again = sim:localInput(press("left"))
|
|
eq(again.n, 1, "a press while the head is out is ignored")
|
|
for _ = 1, 6 do sim:update() end
|
|
eq(sim:dodrioPose(1), R.PICK_NONE, "prediction ends after 6 frames")
|
|
local r = sim:localInput(press("right"))
|
|
eq(r.n, 2, "next press counted")
|
|
eq(r.d, R.PICK_RIGHT, "RIGHT")
|
|
end
|
|
|
|
print("[test] 10. sim: sounds and missed shake")
|
|
do
|
|
resetSounds()
|
|
local sim = newSim(3, 0, true)
|
|
sim.intro.done = true
|
|
sim.intro.stage = "done"
|
|
sim.started = true
|
|
local st = sim.st
|
|
local col = R.headColumn(T, 3, 0, 1)
|
|
for c = 0, 10 do st.fall[c], st.state[c], st.fallTimer[c] = 2, R.BS_NONE, 0 end
|
|
st.state[col], st.fall[col] = R.BS_NONE, 6
|
|
sim:leaderStep({ [1] = { n = 1, d = R.PICK_MIDDLE } })
|
|
for _ = 1, 8 do
|
|
sim:leaderStep({ [1] = { n = 1, d = R.PICK_MIDDLE } })
|
|
sim:update()
|
|
end
|
|
eq(count(sounds, "se" .. Sim.SE_SUCCESS), 1, "SE_SUCCESS once for an eaten berry")
|
|
resetSounds()
|
|
st.state[col], st.fall[col], st.ids[col] = R.BS_NONE, 2, 0
|
|
for _ = 1, 3 do sim:leaderStep({ [1] = { n = 2, d = R.PICK_MIDDLE } }) sim:update() end
|
|
eq(count(sounds, "se" .. Sim.SE_BOO), 1, "SE_BOO for a bad miss")
|
|
eq(sim.own.state, 1, "own Dodrio shakes")
|
|
local moved = false
|
|
for _ = 1, 25 do sim:update() if sim.own.dx ~= 0 then moved = true end end
|
|
check(moved, "shake moves the sprite")
|
|
eq(sim.own.state, 0, "shake ends")
|
|
eq(sim.own.dx, 0, "back at its position")
|
|
resetSounds()
|
|
st.state[col], st.fall[col], st.ids[col] = R.BS_NONE, R.MAX_FALL_DIST, R.BERRY_GOLD
|
|
st.gray = 9
|
|
for _ = 1, 3 do sim:leaderStep({}) sim:update() end
|
|
eq(count(sounds, "se" .. (Sim.SE_BALLOON_RED + R.BERRY_GOLD)), 1, "squish sound by berry colour")
|
|
eq(count(sounds, "song0"), 1, "map music stops at the tenth miss")
|
|
eq(count(sounds, "fanfare" .. Sim.MUS_TOO_BAD), 1, "FANFARE_TOO_BAD")
|
|
eq(sim:dodrioPose(0), R.PICK_DISABLED, "every Dodrio down")
|
|
end
|
|
|
|
local function recorderHooks(bag)
|
|
local log = { records = {}, saves = 0, added = {} }
|
|
local Bag = require("src.core.game3.bag")
|
|
log.hooks = {
|
|
updateRecords = function(score, picked, inRow) log.records[#log.records + 1] = { score, picked, inRow } end,
|
|
canAdd = function(item, qty) return Bag.canAdd(bag, item, qty) end,
|
|
addItem = function(item, qty) log.added[#log.added + 1] = item return (Bag.add(bag, item, qty)) end,
|
|
save = function() log.saves = log.saves + 1 end,
|
|
}
|
|
return log
|
|
end
|
|
|
|
local function endRound(sim, res)
|
|
local st = sim.st
|
|
for c = 0, 10 do st.fall[c], st.state[c] = R.MAX_FALL_DIST, R.BS_SQUISHED end
|
|
for p = 0, 2 do
|
|
local r = res[p + 1] or { 0, 0, 0, 0 }
|
|
st.res[p][0], st.res[p][1], st.res[p][2], st.res[p][3] = r[1], r[2], r[3], r[4]
|
|
end
|
|
st.gray = R.NUM_STATUS_SQUARES
|
|
st.phase = R.PHASE_WAIT
|
|
end
|
|
|
|
print("[test] 11. results, records, prize, save, play again, end of link")
|
|
do
|
|
local Bag = require("src.core.game3.bag")
|
|
local bag = Bag.new()
|
|
local log = recorderHooks(bag)
|
|
resetSounds()
|
|
local sim = G.new({ game = "pick", seat = 0, seats = 3, players = players(3), leader = true, seed = 5,
|
|
rng = MG.rng(5), partyMon = { species = 85 }, hooks = log.hooks })
|
|
sim.intro.done, sim.intro.stage, sim.started = true, "done", true
|
|
local others = { [2] = { n = 0 }, [3] = { n = 0 } }
|
|
local function step(btn)
|
|
local own = sim:localInput(btn and press(btn) or IDLE)
|
|
sim:leaderStep({ [1] = own, [2] = others[2], [3] = others[3] }, { true, true, true })
|
|
sim:update()
|
|
end
|
|
local function run(n, btn) for _ = 1, n do step(btn) end end
|
|
local function until_(cond, limit, btn)
|
|
for _ = 1, limit or 600 do
|
|
if cond() then return true end
|
|
step(btn)
|
|
end
|
|
return cond()
|
|
end
|
|
endRound(sim, { { 10, 20, 50, 2 }, { 5, 0, 0, 9 }, { 1, 1, 1, 0 } })
|
|
run(1)
|
|
eq(sim.view.ph, R.PHASE_END, "round over once nothing falls")
|
|
check(until_(function() return sim.flow ~= "game" end, 5), "results start")
|
|
check(until_(function() return sim.rs.show == "results" end, 400), "berry results window")
|
|
check(sim.rs.icons, "berry icons over the window")
|
|
eq(#log.records, 1, "TryUpdateRecords once")
|
|
eq(log.records[1][1], 100 + 600 + 2500 - 100, "record score")
|
|
eq(log.records[1][2], 80, "record berries picked")
|
|
check(count(sounds, "fade" .. Sim.MUS_VICTORY_WILD) == 1, "MUS_VICTORY_WILD after the fanfare")
|
|
run(1, "a")
|
|
check(sim.rs.show == "results", "A needs 30 frames first")
|
|
run(40)
|
|
run(1, "a")
|
|
run(5)
|
|
eq(sim.rs.show, "rankings", "rankings after A")
|
|
eq(sim.rs.rows[1].player, 0, "RED ranked first")
|
|
run(40)
|
|
run(1, "a")
|
|
run(3)
|
|
eq(sim.rs.show, "prize", "prize window when someone reached 3000")
|
|
check(count(sounds, "song" .. Sim.MUS_LEVEL_UP) == 1, "MUS_LEVEL_UP for the prize")
|
|
eq(sim.rs.prizeState, R.PRIZE_RECEIVED, "top scorer gets the prize")
|
|
eq(Bag.get(bag, R.prizeItem(sim.rs.prize)), 1, "prize berry in the bag")
|
|
run(40)
|
|
run(1, "a")
|
|
run(5)
|
|
eq(sim.flow, "standby1", "communication standby")
|
|
eq(sim:localInput(IDLE).r, 1, "results viewed: r = round")
|
|
run(200)
|
|
eq(sim.flow, "standby1", "waits for every player")
|
|
others[2].r, others[3].r = 1, 1
|
|
check(until_(function() return sim.flow == "save" end, 200), "then saves (someone reached 3000)")
|
|
check(until_(function() return sim.flow == "ask" end, 200), "then asks")
|
|
eq(log.saves, 1, "Task_LinkFullSave once")
|
|
check(until_(function() return sim.rs.show == "ask" end, 10), "Want to play again?")
|
|
check(sim.blank, "berries cleared for the prompt")
|
|
eq(sim:displayView().gray, 0, "status bar all yellow again")
|
|
run(1, "down")
|
|
eq(sim.rs.cursor, Sim.PLAY_AGAIN_NO, "cursor to NO")
|
|
run(1, "up")
|
|
eq(sim.rs.cursor, Sim.PLAY_AGAIN_YES, "back to YES")
|
|
run(1, "a")
|
|
eq(sim.flow, "standby2", "answered")
|
|
eq(sim:localInput(IDLE).v, 1 * 4 + Sim.PLAY_AGAIN_YES, "vote rides the input")
|
|
resetSounds()
|
|
run(1, "up")
|
|
eq(count(sounds, "se" .. Sim.SE_M_CHARM), 1, "heads move while waiting (SE_M_CHARM)")
|
|
eq(sim:dodrioPose(0), R.PICK_MIDDLE, "own Dodrio reaches up")
|
|
others[2].v, others[3].v = 5, 5
|
|
run(3)
|
|
eq(sim.L.rd, 2, "everyone said YES: round 2")
|
|
check(until_(function() return sim.round == 2 end, 300), "fade out, reset, fade in")
|
|
eq(sim.flow, "game", "new game")
|
|
eq(sim.intro.stage, "slide", "tree borders slide again")
|
|
check(not sim.started, "rules wait for the new countdown")
|
|
check(until_(function() return sim.started end, 900), "second game starts after its countdown")
|
|
endRound(sim, { { 1, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } })
|
|
check(until_(function() return sim.flow == "results" end, 400), "round 2 results")
|
|
local ok = until_(function() return sim.flow == "standby1" end, 400, "a")
|
|
check(ok, "results screens (no prize window below 3000)")
|
|
others[2].r, others[3].r = 2, 2
|
|
check(until_(function() return sim.flow == "ask" end, 300), "no save below 3000")
|
|
eq(log.saves, 1, "still one save")
|
|
run(5)
|
|
run(1, "b")
|
|
eq(sim:localInput(IDLE).v, 2 * 4 + Sim.PLAY_AGAIN_NO, "B answers NO")
|
|
others[2].v = 2 * 4 + Sim.PLAY_AGAIN_YES
|
|
others[3].v = 2 * 4 + Sim.PLAY_AGAIN_YES
|
|
run(3)
|
|
check(sim.L.ended, "someone said NO: the link ends")
|
|
sim:snapshot()
|
|
check(sim:finished(), "leader finishes after the last state")
|
|
sim:showResults({})
|
|
check(until_(function() return sim.flow == "dropped" end, 200), "Somebody dropped out")
|
|
check(until_(function() return sim:resultsDone() end, 200), "then MG leaves")
|
|
|
|
local bundle = Cache.bundle("dodrio_berry_picking/tables.lua")
|
|
if bundle then
|
|
local RomText = require("src.core.game3.rom_text")
|
|
for _, key in ipairs({ "gText_BerryPickingResults", "gText_10P30P50P50P", "gText_AnnouncingRankings",
|
|
"gText_AnnouncingPrizes", "gText_FirstPlacePrize", "gText_CantHoldAnyMore", "gText_FilledStorageSpace",
|
|
"gText_SpacePoints", "gText_1Colon", "gText_5Colon", "gText_CommunicationStandby3", "gText_SomeoneDroppedOut" }) do
|
|
check(RomText.has(key), key .. " is ROM text")
|
|
end
|
|
local t = RomText.plain("gText_FirstPlacePrize", { dynamic = { [0] = "RAZZ BERRY" } })
|
|
check(t:find("RAZZ BERRY", 1, true) ~= nil, "prize text takes the item name (" .. t:gsub("\n", " ") .. ")")
|
|
else
|
|
print("[skip] ROM text bundle: " .. tostring(Cache.reason))
|
|
end
|
|
end
|
|
|
|
local function roomSession(relay, s, roomId)
|
|
local rs = { paired = true, closed = false, left = false, seq = 0, target = roomId, lastSeq = 0 }
|
|
function rs:send(msg)
|
|
self.seq = self.seq + 1
|
|
relay:handle(s, { type = "room_msg", seq = self.seq, msg = msg })
|
|
end
|
|
local function inner(m, out)
|
|
if type(m.msg) ~= "table" then return end
|
|
local inn = Wire.sanitize(Json.decode(Json.encode(m.msg)))
|
|
if not inn then return end
|
|
if m.relay then
|
|
inn.relay = true
|
|
if type(inn.seat) ~= "number" then inn.seat = -1 end
|
|
else
|
|
inn.seat = m.seat
|
|
end
|
|
if (m.seq or 0) > rs.lastSeq then rs.lastSeq = m.seq end
|
|
out[#out + 1] = inn
|
|
end
|
|
function rs:poll()
|
|
local out = {}
|
|
for _, m in ipairs(s.transport.inbox) do
|
|
if m.type == "room_msg" then
|
|
inner(m, out)
|
|
elseif m.type == "room_replay" then
|
|
for _, e in ipairs(m.msgs or {}) do inner(e, out) end
|
|
end
|
|
end
|
|
s.transport.inbox = {}
|
|
return out
|
|
end
|
|
function rs:players()
|
|
local room = relay.rooms[roomId]
|
|
return room and room.players or {}
|
|
end
|
|
function rs:close()
|
|
self.left = true
|
|
self.closed = true
|
|
relay:handle(s, { type = "room_leave" })
|
|
end
|
|
return rs
|
|
end
|
|
|
|
local function bot(seat, votes)
|
|
local input = { want = nil, votes = votes or {}, roundAt = {} }
|
|
function input:wasPressed(b) return self.want == b end
|
|
function input:isDown() return false end
|
|
function input:plan(sim, frame)
|
|
self.want = nil
|
|
if not sim then return end
|
|
if sim.flow == "results" then
|
|
if frame % 40 == 0 then self.want = "a" end
|
|
return
|
|
end
|
|
if sim.flow == "ask" and sim.rs and sim.rs.show == "ask" then
|
|
self.want = (self.votes[sim.round] == "no") and "b" or "a"
|
|
return
|
|
end
|
|
if sim.flow ~= "game" or sim.view.gray >= R.NUM_STATUS_SQUARES then return end
|
|
self.roundAt[sim.round] = self.roundAt[sim.round] or frame
|
|
local limit = (sim.round == 1) and (1500 + seat * 200) or 200
|
|
if frame - self.roundAt[sim.round] > limit or (frame + seat * 7) % 97 < 6 then return end
|
|
local dirs = { [0] = "left", "up", "right" }
|
|
for pick = 0, 2 do
|
|
local col = R.headColumn(T, sim.n, sim.me, pick)
|
|
local f = sim.view.fall[col]
|
|
if (f == 6 or f == 7) and sim.view.ids[col] ~= R.BERRY_MISSED then
|
|
self.want = dirs[pick]
|
|
return
|
|
end
|
|
end
|
|
end
|
|
return input
|
|
end
|
|
|
|
local hookLogs = {}
|
|
G.hooksFor = function(ctx)
|
|
local seat = tonumber(ctx.seat) or 0
|
|
local entry = hookLogs[seat]
|
|
if not entry then
|
|
entry = recorderHooks(require("src.core.game3.bag").new())
|
|
hookLogs[seat] = entry
|
|
end
|
|
return entry.hooks
|
|
end
|
|
|
|
local function makeGame(n, ownCountdown, votes)
|
|
hookLogs = {}
|
|
local relay = FakeRelay.new()
|
|
local room = relay:newRoom({ intent = "minigame", seats = n, engine = 3 })
|
|
local seats = {}
|
|
local list = players(n)
|
|
local sizes = { state = 0, result = 0, bad = 0 }
|
|
for s = 0, n - 1 do
|
|
local sess = relay:seat(("%08x"):format(0xd0 + s), list[s + 1].name)
|
|
relay:seatPlayer(room, sess)
|
|
local rs = roomSession(relay, sess, room.room)
|
|
local send = rs.send
|
|
function rs:send(msg)
|
|
local bytes = #Json.encode(msg)
|
|
if msg.type == "game3_mg_state" then sizes.state = math.max(sizes.state, bytes) end
|
|
if msg.type == "game3_mg_result" then sizes.result = math.max(sizes.result, bytes) end
|
|
if bytes > ((msg.type == "game3_mg_result") and 1024 or 512) then sizes.bad = sizes.bad + 1 end
|
|
return send(self, msg)
|
|
end
|
|
local client = {
|
|
state = function() return sess.online and "online" or "offline" end,
|
|
room = function() return { room = room.room, leader = room.leader } end,
|
|
}
|
|
local session = { bag = require("src.core.game3.bag").new() }
|
|
local spec = { game = "pick", session = rs, seat = s, seats = n, players = list, seed = 424242, partySlot = 0,
|
|
returnToMap = false }
|
|
local m = MG.Match.new(spec, {
|
|
module = G, client = client, leader = 0,
|
|
partyMon = { species = 85, personality = 0x1234 + s, otId = 0x100 + s },
|
|
me = { name = list[s + 1].name, trainerId = 0x100 + s, gender = s % 2 },
|
|
countdown = function() return Countdown.new(120, 80, { playSe = function() end }) end,
|
|
onResult = function(msg, match) MG.applyResults(session, msg, match.seat, G) end,
|
|
})
|
|
if ownCountdown then
|
|
local onStart = m.onStart
|
|
m.onStart = function(self, msg)
|
|
onStart(self, msg)
|
|
if self.G.OWN_COUNTDOWN and self.sim then
|
|
self.countdown = nil
|
|
self.phase = "play"
|
|
self.sinceState = 0
|
|
end
|
|
end
|
|
end
|
|
seats[s] = { sess = sess, rs = rs, match = m, session = session, input = bot(s, votes and votes[s]), frame = 0 }
|
|
end
|
|
return relay, room, seats, sizes
|
|
end
|
|
|
|
local function stepSeats(seats, n)
|
|
for s = 0, n - 1 do
|
|
local st = seats[s]
|
|
if not st.frozen and not st.gone and not st.match:finished() then
|
|
st.frame = st.frame + 1
|
|
st.input:plan(st.match.sim, st.frame)
|
|
st.match:step(st.input)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function runUntil(seats, n, cond, limit)
|
|
for _ = 1, limit do
|
|
stepSeats(seats, n)
|
|
if cond() then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function allDone(seats, n)
|
|
for s = 0, n - 1 do
|
|
local st = seats[s]
|
|
if not st.gone and not st.match:finished() then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
print("[test] 11b. game3_mg_result -> records")
|
|
do
|
|
local rows = {
|
|
{ seat = 0, score = 3100, stats = { b = 10, g = 20, o = 50, m = 2, r = 7, p = 16 } },
|
|
{ seat = 1, score = 0, stats = { b = 5, g = 0, o = 0, m = 9, r = 7, p = 16 } },
|
|
}
|
|
local res = Wire.sanitize({ type = "game3_mg_result", game = "pick", results = rows, powder = {} })
|
|
local session = {}
|
|
check(G.applyResults(session, res, 0), "applyResults")
|
|
local rec = session.dodrioBerryPickingRecords
|
|
eq(rec.bestScore, 3100, "best score")
|
|
eq(rec.berriesPicked, 80, "berries picked")
|
|
eq(rec.berriesPickedInRow, 7, "berries in a row")
|
|
G.applyResults(session, res, 0)
|
|
eq(rec.bestScore, 3100, "idempotent")
|
|
local lower = { dodrioBerryPickingRecords = { bestScore = 5000, berriesPicked = 90, berriesPickedInRow = 9 } }
|
|
G.applyResults(lower, res, 0)
|
|
eq(lower.dodrioBerryPickingRecords.bestScore, 5000, "records only go up")
|
|
end
|
|
|
|
print("[test] 11c. prize bag rules (TryGivePrize)")
|
|
do
|
|
local Bag = require("src.core.game3.bag")
|
|
local function prizeFor(bag, seat, scores)
|
|
local sim = G.new({ game = "pick", seat = seat, seats = 3, players = players(3), leader = seat == 0, seed = 1,
|
|
rng = MG.rng(1), partyMon = { species = 85 }, hooks = recorderHooks(bag).hooks })
|
|
local res = R.newResults()
|
|
for p = 0, 2 do for k = 0, 3 do res[p][k] = scores[p + 1][k + 1] end end
|
|
sim.rs = { res = res, prize = 16 }
|
|
return sim:tryGivePrize()
|
|
end
|
|
local top = { { 10, 20, 50, 2 }, { 5, 0, 0, 9 }, { 1, 1, 1, 0 } }
|
|
local bag = Bag.new()
|
|
eq(prizeFor(bag, 0, top), R.PRIZE_RECEIVED, "top scorer gets it")
|
|
eq(prizeFor(Bag.new(), 1, top), R.NO_PRIZE, "second place gets nothing")
|
|
local full = Bag.new()
|
|
Bag.add(full, R.prizeItem(16), 998)
|
|
eq(prizeFor(full, 0, top), R.PRIZE_FILLED_BAG, "the 999th berry fills the storage space")
|
|
eq(prizeFor(full, 0, top), R.PRIZE_NO_ROOM, "then no room")
|
|
end
|
|
|
|
print("[test] 12. three seats over the fake relay: leader drops and comes back, play again, then NO")
|
|
do
|
|
local relay, room, seats, sizes = makeGame(3, false, { [2] = { [2] = "no" } })
|
|
check(runUntil(seats, 3, function() return seats[0].match.phase == "play" and seats[2].match.phase == "play" end, 400),
|
|
"ready -> start -> countdown -> play on every seat")
|
|
eq(#seats[1].match.startPlayers, 3, "three players in game3_mg_start")
|
|
runUntil(seats, 3, function() return false end, 900)
|
|
local picked = 0
|
|
for p = 0, 2 do picked = picked + R.berriesPicked(seats[0].match.sim.st.res[p]) end
|
|
check(picked > 0, "bots picked berries through the relay (" .. picked .. ")")
|
|
check(seats[1].match.sim.view.live, "member view comes from leader states")
|
|
|
|
seats[0].frozen = true
|
|
relay:drop(seats[0].sess)
|
|
runUntil(seats, 3, function() return false end, 150)
|
|
relay:migrateLeader(room.room)
|
|
check(runUntil(seats, 3, function() return seats[1].match:isLeader() end, 30), "relay migrated leadership to seat 1")
|
|
eq(seats[2].match.leader, 1, "seat 2 follows seat 1")
|
|
check(seats[1].match.sim.leader, "seat 1's sim took over from the last state")
|
|
runUntil(seats, 3, function() return false end, 120)
|
|
relay:reconnect(seats[0].sess)
|
|
seats[0].sess.online = true
|
|
relay:replay(seats[0].sess, seats[0].rs.lastSeq)
|
|
seats[0].frozen = false
|
|
check(runUntil(seats, 3, function() return seats[0].match.leader == 1 end, 30), "the old leader resumes as a member")
|
|
check(not seats[0].match.sim.leader, "its sim stepped down")
|
|
check(runUntil(seats, 3, function()
|
|
return seats[0].match.sim.round == 2 and seats[1].match.sim.round == 2 and seats[2].match.sim.round == 2
|
|
end, 60 * 60 * 6), "first game over, everyone said YES: second game on every seat")
|
|
local pickedGame1 = 0
|
|
for s = 0, 2 do
|
|
eq(#hookLogs[s].records, 1, "seat " .. s .. " TryUpdateRecords after game 1")
|
|
pickedGame1 = pickedGame1 + ((hookLogs[s].records[1] or {})[2] or 0)
|
|
end
|
|
check(pickedGame1 > 0, "game 1 berries picked, per the records (" .. pickedGame1 .. ")")
|
|
local ok = runUntil(seats, 3, function()
|
|
return seats[0].match.phase == "results" and seats[1].match.phase == "results" and seats[2].match.phase == "results"
|
|
end, 60 * 60 * 6)
|
|
check(ok, "seat 2 said NO after game 2: the link ends")
|
|
local r1 = canon(seats[1].match.result and seats[1].match.result.results)
|
|
eq(canon(seats[0].match.result and seats[0].match.result.results), r1, "seat 0 agrees on the results")
|
|
eq(canon(seats[2].match.result and seats[2].match.result.results), r1, "seat 2 agrees on the results")
|
|
print("[info] results " .. r1)
|
|
check(runUntil(seats, 3, function() return allDone(seats, 3) end, 3000), "every seat showed 'Somebody dropped out'")
|
|
for s = 0, 2 do
|
|
eq(seats[s].match.sim.flow, "done", "seat " .. s .. " flow done")
|
|
eq(#hookLogs[s].records, 2, "seat " .. s .. " records after both games")
|
|
local rec = seats[s].session.dodrioBerryPickingRecords
|
|
local mine
|
|
for _, r in ipairs(seats[1].match.result.results) do if r.seat == s then mine = r end end
|
|
check(rec ~= nil and mine ~= nil and rec.bestScore >= mine.score, "seat " .. s .. " MG result applied (records)")
|
|
eq(seats[s].session.berryPowder, nil, "no Berry Powder from Dodrio Berry Picking")
|
|
end
|
|
check(sizes.state > 0 and sizes.bad == 0, "every message under the relay caps (state max " .. sizes.state
|
|
.. ", result " .. sizes.result .. ")")
|
|
end
|
|
|
|
print("[test] 13. four seats, the leader leaves, three play on")
|
|
do
|
|
local relay, room, seats = makeGame(4)
|
|
runUntil(seats, 4, function() return seats[3].match.phase == "play" end, 400)
|
|
runUntil(seats, 4, function() return false end, 400)
|
|
seats[0].gone = true
|
|
seats[0].rs:close()
|
|
relay:migrateLeader(room.room)
|
|
check(runUntil(seats, 4, function() return seats[1].match:isLeader() end, 30), "seat 1 leads")
|
|
local ok = runUntil(seats, 4, function()
|
|
return seats[1].match.phase == "results" and seats[2].match.phase == "results" and seats[3].match.phase == "results"
|
|
end, 60 * 60 * 6)
|
|
check(ok, "three remaining seats finish the 4-player game")
|
|
local res = seats[1].match.result
|
|
eq(#(res and res.results or {}), 4, "results still list all four players")
|
|
end
|
|
|
|
print("[test] 14. three seats, one leaves: below the minimum")
|
|
do
|
|
local relay, room, seats = makeGame(3)
|
|
runUntil(seats, 3, function() return seats[2].match.phase == "play" end, 400)
|
|
seats[2].gone = true
|
|
seats[2].rs:close()
|
|
runUntil(seats, 3, function() return seats[0].match.phase == "error" end, 30)
|
|
eq(seats[0].match.phase, "error", "leader errors with two players left")
|
|
eq(seats[1].match.phase, "error", "member errors too")
|
|
eq(seats[0].session.dodrioBerryPickingRecords, nil, "no records written")
|
|
end
|
|
|
|
print("[test] 15. G.OWN_COUNTDOWN: intro and countdown per seat, leader drops during the intro")
|
|
do
|
|
local relay, room, seats = makeGame(3, true, { [1] = { [1] = "no" } })
|
|
check(runUntil(seats, 3, function() return seats[2].match.phase == "play" end, 60), "Match skips its countdown")
|
|
runUntil(seats, 3, function() return false end, 150)
|
|
check(not seats[0].match.sim.started, "leader still in its intro")
|
|
check(not seats[2].match.sim.view.live, "members see no berries before the game starts")
|
|
seats[0].frozen = true
|
|
relay:drop(seats[0].sess)
|
|
relay:migrateLeader(room.room)
|
|
check(runUntil(seats, 3, function() return seats[1].match:isLeader() end, 30), "seat 1 leads from inside the intro")
|
|
check(runUntil(seats, 3, function() return seats[1].match.sim.started end, 900), "the new leader starts after its own countdown")
|
|
check(runUntil(seats, 3, function() return seats[2].match.sim.view.live end, 30), "member 2 sees the berries")
|
|
relay:reconnect(seats[0].sess)
|
|
seats[0].sess.online = true
|
|
relay:replay(seats[0].sess, seats[0].rs.lastSeq)
|
|
seats[0].frozen = false
|
|
check(runUntil(seats, 3, function() return seats[0].match.leader == 1 end, 30), "old leader back as a member")
|
|
check(runUntil(seats, 3, function() return seats[0].match.sim.view.live end, 900),
|
|
"after its own countdown it renders seat 1's game")
|
|
local ok = runUntil(seats, 3, function()
|
|
return seats[0].match.phase == "results" and seats[1].match.phase == "results" and seats[2].match.phase == "results"
|
|
end, 60 * 60 * 6)
|
|
check(ok, "the game ends on every seat")
|
|
eq(canon(seats[0].match.result and seats[0].match.result.results),
|
|
canon(seats[2].match.result and seats[2].match.result.results), "results agree")
|
|
end
|
|
|
|
G.audio = nil
|
|
|
|
if failed > 0 then
|
|
print(("[FAIL] %d check(s) failed"):format(failed))
|
|
os.exit(1)
|
|
end
|
|
print("[PASS] game3_mg_dodrio_berry_picking_test")
|