mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
Merge pull request #1581 from 1Jamie/feat/pikachu-surf-and-gold-gamecorner
feat: surfing minigame overhaul + authentic Game Corner rendering
This commit is contained in:
@@ -1233,6 +1233,118 @@ check("a full wallet fills it", Chrome.money(999999), "\xc2\xa5999999")
|
||||
check("the coin field keeps its leading zeroes", Chrome.number(50, 4, true),
|
||||
"0050")
|
||||
|
||||
-- ============================================================ multi-game stress tests
|
||||
--
|
||||
-- Verify that 500 consecutive Slot Machine games and 500 consecutive Card Flip hands
|
||||
-- run to completion with zero softlocks, zero infinite spin loops (issue #1520),
|
||||
-- and correct coin accounting across all random biases and near-miss theatres.
|
||||
|
||||
local mockSave = {
|
||||
player = { name = "GOLD", coins = 5000 },
|
||||
}
|
||||
|
||||
local mockInput = {
|
||||
pressed = {},
|
||||
wasPressed = function(self, key)
|
||||
local v = self.pressed[key]
|
||||
self.pressed[key] = false
|
||||
return v or false
|
||||
end,
|
||||
press = function(self, key)
|
||||
self.pressed[key] = true
|
||||
end,
|
||||
}
|
||||
|
||||
local mockSlotGame = {
|
||||
save = mockSave,
|
||||
input = mockInput,
|
||||
data = {},
|
||||
}
|
||||
|
||||
-- Test Slot Machine 500-spin continuous loop
|
||||
local sm = SlotMachine.new(mockSlotGame, { lucky = true })
|
||||
local completedSpins = 0
|
||||
|
||||
for spin = 1, 500 do
|
||||
mockSave.player.coins = 5000 -- ensure test player always has coins
|
||||
if sm.phase == "quit" or sm.phase == "ranOut" then
|
||||
sm = SlotMachine.new(mockSlotGame, { lucky = true })
|
||||
end
|
||||
|
||||
-- Enter bet phase
|
||||
check("slot machine in bet phase at spin start", sm.phase, "bet")
|
||||
mockInput:press("a") -- bet 3 coins
|
||||
sm:update(1/60)
|
||||
|
||||
check("slot machine entered spinning phase", sm.phase, "spinning")
|
||||
|
||||
local frameCount = 0
|
||||
local maxFrames = 5000 -- safety bound per spin
|
||||
|
||||
while sm.phase == "spinning" and frameCount < maxFrames do
|
||||
frameCount = frameCount + 1
|
||||
if frameCount % 30 == 0 then
|
||||
mockInput:press("a") -- press stop button
|
||||
end
|
||||
sm:update(1/60)
|
||||
end
|
||||
|
||||
check(("spin %d must not hang in spinning"):format(spin), frameCount < maxFrames, true)
|
||||
|
||||
-- Resolve flash and payout phases
|
||||
while (sm.phase == "flash" or sm.phase == "payoutText") and frameCount < maxFrames do
|
||||
frameCount = frameCount + 1
|
||||
if sm.phase == "payoutText" and sm.matched == SlotMachine.NO_MATCH then
|
||||
mockInput:press("a")
|
||||
end
|
||||
sm:update(1/60)
|
||||
end
|
||||
|
||||
check(("spin %d reached again phase"):format(spin), sm.phase == "again" or sm.phase == "bet", true)
|
||||
if sm.phase == "again" then
|
||||
mockInput:press("a") -- choose YES to play again
|
||||
sm:update(1/60)
|
||||
completedSpins = completedSpins + 1
|
||||
elseif sm.phase == "bet" then
|
||||
completedSpins = completedSpins + 1
|
||||
end
|
||||
end
|
||||
|
||||
check("all 500 slot machine spins completed without softlock", completedSpins >= 490, true)
|
||||
|
||||
-- Test Card Flip 500-hand continuous loop
|
||||
local cf = CardFlip.new(mockSlotGame)
|
||||
local completedHands = 0
|
||||
|
||||
for hand = 1, 500 do
|
||||
mockSave.player.coins = 5000
|
||||
if cf.phase == "quit" then
|
||||
cf = CardFlip.new(mockSlotGame)
|
||||
end
|
||||
|
||||
local frameCount = 0
|
||||
local maxFrames = 500
|
||||
|
||||
while cf.phase ~= "again" and cf.phase ~= "quit" and frameCount < maxFrames do
|
||||
frameCount = frameCount + 1
|
||||
if cf.phase == "ask" or cf.phase == "message" or cf.phase == "result"
|
||||
or cf.phase == "choose" or cf.phase == "bet" then
|
||||
mockInput:press("a")
|
||||
end
|
||||
cf:update(1/60)
|
||||
end
|
||||
|
||||
check(("hand %d completed in bounds"):format(hand), frameCount < maxFrames, true)
|
||||
|
||||
if cf.phase == "again" then
|
||||
mockInput:press("a") -- play again
|
||||
cf:update(1/60)
|
||||
completedHands = completedHands + 1
|
||||
end
|
||||
end
|
||||
|
||||
check("all 500 card flip hands completed cleanly", completedHands >= 490, true)
|
||||
|
||||
print(("gen2 game corner: %d checks, %d failures"):format(checks, failures))
|
||||
-- Raise rather than os.exit: tests/run_tests.lua dofiles this file, so an exit
|
||||
-- here would take the whole tier down and silently skip every suite after it.
|
||||
|
||||
@@ -40,14 +40,16 @@ local mockGame = {
|
||||
|
||||
print("Running SurfingMinigame unit tests...")
|
||||
|
||||
-- Test 1: Initialization
|
||||
-- Test 1: Initialization & Title Screen transition
|
||||
local mg = SurfingMinigame.new(mockGame)
|
||||
assert_eq(mg.routine, -1, "Initial routine must be ROUTINE_TITLE (-1)")
|
||||
mg:startFromTitle()
|
||||
assert_eq(mg.routine, 0, "Routine must advance to ROUTINE_START_GAME (0) after startFromTitle()")
|
||||
assert_eq(mg.hp, 6000, "Initial HP must be 6000 (60.00s)")
|
||||
assert_eq(mg.speed, 0.25, "Initial speed must be 0.25")
|
||||
assert_eq(mg.distance, 0, "Initial distance must be 0")
|
||||
assert_eq(mg.routine, 0, "Initial routine must be ROUTINE_START_GAME (0)")
|
||||
assert_eq(mg.pikaState, 0, "Initial Pikachu state must be PIKA_STATE_RIDING (0)")
|
||||
print("✓ Initial state test passed")
|
||||
print("✓ Initial state & Title transition test passed")
|
||||
|
||||
-- Test 2: Start banner transition to RunGame
|
||||
for _ = 1, 40 do
|
||||
@@ -65,6 +67,8 @@ assert_eq(mg.hp, initialHp - 1, "HP should decrease by 1 each frame")
|
||||
print("✓ Auto acceleration and HP countdown test passed")
|
||||
|
||||
-- Test 4: Landing Evaluation Matrix
|
||||
local old_getWaveTile = mg.getWaveTileUnderPika
|
||||
mg.getWaveTileUnderPika = function() return 0x01 end -- force open water
|
||||
mg.frameSet = 5
|
||||
assert_eq(mg:evaluateLanding(), "rough", "Angle 5 on open water should be rough landing")
|
||||
mg.frameSet = 6
|
||||
@@ -77,6 +81,7 @@ for f = 8, 14 do
|
||||
mg.frameSet = f
|
||||
assert_eq(mg:evaluateLanding(), "wipeout", "Upside-down frame " .. f .. " must be wipeout")
|
||||
end
|
||||
mg.getWaveTileUnderPika = old_getWaveTile
|
||||
print("✓ Landing evaluation matrix test passed (including upside-down frames 8..14)")
|
||||
|
||||
-- Test 5: Stunt Scoring
|
||||
@@ -145,6 +150,128 @@ end
|
||||
assert_eq(mg.radness, 0, "Radness should be tallied down to 0")
|
||||
assert_eq(mg.totalScore, 300, "Total score should be 300 (100 HP + 200 Radness)")
|
||||
assert_eq(mg.routine, 10, "Routine should advance to ROUTINE_WAIT_LAST (10)")
|
||||
print("✓ Results tally countdown test passed")
|
||||
-- Test 8: Crossing finish line while jumping upside-down crashes into water and rights Pikachu before results
|
||||
local mg8 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg8.routine = 1 -- ROUTINE_RUN_GAME
|
||||
mg8.distanceFixed = (24 * 128 - 2) * 256
|
||||
mg8.speedFixed = 512
|
||||
mg8.pikaState = 1 -- PIKA_STATE_JUMPING
|
||||
mg8.frameSet = 11 -- Upside down
|
||||
mg8.pikaY = 60
|
||||
mg8.jumpDescending = true
|
||||
mg8.jumpArcMagnitude = 4
|
||||
mg8.radness = 150
|
||||
local preScore = mg8.radness
|
||||
|
||||
-- Update to cross the finish line
|
||||
mg8:update()
|
||||
assert_eq(mg8.routine, 2, "Routine should advance to ROUTINE_WAIT_RESULTS (2) upon crossing finish")
|
||||
assert_eq(mg8.pikaState, 1, "Pikachu should remain mid-air immediately after crossing line")
|
||||
|
||||
-- Update until Pikachu lands in water
|
||||
while mg8.pikaState == 1 do
|
||||
mg8:update()
|
||||
end
|
||||
assert_eq(mg8.pikaState, 3, "Upside-down landing post-finish line must trigger PIKA_STATE_CRASHED (3)")
|
||||
assert_eq(mg8.radness, preScore, "Radness score must NOT change after crossing finish line")
|
||||
assert_eq(mg8.crashTimer, 96, "Crash timer must be initialized to 96 frames")
|
||||
|
||||
-- Update while crashed to verify recovery
|
||||
while mg8.pikaState == 3 do
|
||||
mg8:update()
|
||||
end
|
||||
assert_eq(mg8.pikaState, 0, "Pikachu must recover back to PIKA_STATE_RIDING (0) and right itself on the board")
|
||||
assert_eq(mg8.frameSet, 4, "Pikachu frameSet must be reset to upright (4)")
|
||||
|
||||
-- Let coasting finish and verify transition to results
|
||||
while mg8.routine == 2 do
|
||||
mg8:update()
|
||||
end
|
||||
assert_eq(mg8.routine, 3, "Routine should advance to ROUTINE_SCROLL_RESULTS (3) only after Pikachu is upright")
|
||||
print("✓ Mid-air upside-down finish line crossing crash & recovery test passed")
|
||||
|
||||
-- Test 9: Crossing finish line while upright jumping lands cleanly and proceeds
|
||||
local mg9 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg9.routine = 1
|
||||
mg9.distanceFixed = (24 * 128 - 2) * 256
|
||||
mg9.speedFixed = 512
|
||||
mg9.pikaState = 1
|
||||
mg9.frameSet = 4 -- Clean flat
|
||||
mg9.pikaY = 60
|
||||
mg9.jumpDescending = true
|
||||
mg9.jumpArcMagnitude = 4
|
||||
mg9.radness = 200
|
||||
preScore = mg9.radness
|
||||
|
||||
mg9:update()
|
||||
assert_eq(mg9.routine, 2, "Routine should advance to ROUTINE_WAIT_RESULTS (2)")
|
||||
|
||||
while mg9.pikaState == 1 do
|
||||
mg9:update()
|
||||
end
|
||||
assert_eq(mg9.pikaState, 2, "Upright landing post-finish line must trigger PIKA_STATE_LANDING (2)")
|
||||
assert_eq(mg9.radness, preScore, "Radness score must NOT change post-finish")
|
||||
|
||||
while mg9.pikaState == 2 do
|
||||
mg9:update()
|
||||
end
|
||||
assert_eq(mg9.pikaState, 0, "Pikachu must return to PIKA_STATE_RIDING (0)")
|
||||
print("✓ Mid-air upright finish line crossing test passed")
|
||||
|
||||
-- Test 10: Crossing finish line while already crashed recovers before results
|
||||
local mg10 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg10.routine = 1
|
||||
mg10.distanceFixed = (24 * 128 - 2) * 256
|
||||
mg10.speedFixed = 512
|
||||
mg10.pikaState = 3 -- PIKA_STATE_CRASHED
|
||||
mg10.crashTimer = 50
|
||||
|
||||
mg10:update()
|
||||
assert_eq(mg10.routine, 2, "Routine should advance to ROUTINE_WAIT_RESULTS (2)")
|
||||
assert_eq(mg10.pikaState, 3, "Pikachu should still be crashed")
|
||||
|
||||
while mg10.pikaState == 3 do
|
||||
mg10:update()
|
||||
end
|
||||
assert_eq(mg10.pikaState, 0, "Pikachu must recover upright before proceeding to results")
|
||||
print("✓ Pre-crashed finish line crossing recovery test passed")
|
||||
|
||||
-- Test 11: Decoupled timestep accumulator (60Hz and 144Hz framerate consistency)
|
||||
local mg11_60 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg11_60.routine = 1 -- ROUTINE_RUN_GAME
|
||||
for _ = 1, 60 do
|
||||
mg11_60:update(1 / 60)
|
||||
end
|
||||
assert(mg11_60.t == 59 or mg11_60.t == 60, "60Hz update over 1s must produce approx 60 ticks (got " .. mg11_60.t .. ")")
|
||||
|
||||
local mg11_144 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg11_144.routine = 1 -- ROUTINE_RUN_GAME
|
||||
for _ = 1, 144 do
|
||||
mg11_144:update(1 / 144)
|
||||
end
|
||||
assert(mg11_144.t == 59 or mg11_144.t == 60, "144Hz update over 1s must produce approx 60 ticks (got " .. mg11_144.t .. ")")
|
||||
print("✓ Decoupled 59.7275Hz timestep accumulator test passed")
|
||||
|
||||
-- Test 12: Landing continuity on slopes (no position jumps while landing)
|
||||
local mg12 = SurfingMinigame.new(mockGame, nil, true)
|
||||
mg12.routine = 1
|
||||
mg12.pikaState = 2 -- PIKA_STATE_LANDING
|
||||
mg12.landingTimer = 20
|
||||
mg12.speedFixed = 256
|
||||
-- Place on a rising wave pattern
|
||||
mg12.cols[5] = { pat = SurfingMinigame.WAVE_PATTERNS[0x06], hl = 110, hr = 100 }
|
||||
mg12.distanceFixed = (5 * 16 - 80) * 256
|
||||
local startY = mg12.pikaY
|
||||
mg12:update()
|
||||
assert(mg12.pikaY ~= startY, "pikaY must continuously follow wave surface height while in PIKA_STATE_LANDING")
|
||||
print("✓ Landing slope height tracking continuity test passed")
|
||||
|
||||
-- Test 13: Fixed speed enforcement (minigames must always run at 1X speed)
|
||||
assert(mg12.isFixedSpeed == true, "SurfingMinigame must have isFixedSpeed flag enabled")
|
||||
assert(mg12.isMinigame == true, "SurfingMinigame must have isMinigame flag enabled")
|
||||
local mockStack = { states = { mg12 } }
|
||||
local Game = require("src.core.Game")
|
||||
assert(Game.isFixedSpeedInStack(mockStack) == true, "Game.isFixedSpeedInStack must return true for SurfingMinigame")
|
||||
print("✓ Minigame fixed speed enforcement test passed")
|
||||
|
||||
print("All SurfingMinigame unit tests passed successfully!")
|
||||
|
||||
Reference in New Issue
Block a user