mirror of
https://github.com/Nighthawk42/wow_bejeweled.git
synced 2026-08-30 04:30:22 +00:00
1558 lines
69 KiB
Lua
1558 lines
69 KiB
Lua
local function LoadAddonFile(path, addon)
|
|
local chunk, reason = loadfile(path)
|
|
assert(chunk, reason)
|
|
chunk("Bejeweled", addon)
|
|
end
|
|
|
|
local function AssertEqual(actual, expected, message)
|
|
if actual ~= expected then
|
|
error((message or "values differ") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
|
|
end
|
|
end
|
|
|
|
local function MakeRandom(seed)
|
|
local state = seed
|
|
return function(minimum, maximum)
|
|
state = (state * 1103515245 + 12345) % 2147483648
|
|
return minimum + (state % (maximum - minimum + 1))
|
|
end
|
|
end
|
|
|
|
local eventFrame = { scripts = {} }
|
|
function eventFrame:RegisterEvent(event)
|
|
self.registeredEvent = event
|
|
return true
|
|
end
|
|
function eventFrame:UnregisterEvent(event)
|
|
if self.registeredEvent == event then
|
|
self.registeredEvent = nil
|
|
end
|
|
return true
|
|
end
|
|
function eventFrame:SetScript(scriptName, callback)
|
|
self.scripts[scriptName] = callback
|
|
end
|
|
|
|
function CreateFrame(frameType)
|
|
AssertEqual(frameType, "Frame", "initializer frame type")
|
|
return eventFrame
|
|
end
|
|
|
|
local addon = {}
|
|
LoadAddonFile("Bejeweled/Core/Init.lua", addon)
|
|
LoadAddonFile("Bejeweled/Core/Constants.lua", addon)
|
|
LoadAddonFile("Bejeweled/Core/Audio.lua", addon)
|
|
LoadAddonFile("Bejeweled/Core/SavedVariables.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Grid.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Matches.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Cascade.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Scoring.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Input.lua", addon)
|
|
LoadAddonFile("Bejeweled/Engine/Session.lua", addon)
|
|
LoadAddonFile("Bejeweled/UI/Backdrops.lua", addon)
|
|
LoadAddonFile("Bejeweled/UI/GemPool.lua", addon)
|
|
LoadAddonFile("Bejeweled/UI/Animations.lua", addon)
|
|
|
|
AssertEqual(eventFrame.registeredEvent, "ADDON_LOADED", "initializer event registration")
|
|
AssertEqual(addon.Constants.GRID_WIDTH, 8, "grid width")
|
|
AssertEqual(addon.Constants.GRID_HEIGHT, 8, "grid height")
|
|
AssertEqual(addon.Constants.GEM_COLOR_COUNT, 7, "gem color count")
|
|
|
|
local windowBackdrop = addon.Backdrops:CreateDescriptor("window")
|
|
AssertEqual(windowBackdrop.bgFile, addon.Constants.IMAGE_ROOT .. "windowBackground", "window backdrop texture")
|
|
AssertEqual(windowBackdrop.edgeFile, addon.Constants.IMAGE_ROOT .. "windowBorder", "window backdrop border")
|
|
AssertEqual(windowBackdrop.insets.right, 3, "window backdrop right inset")
|
|
windowBackdrop.insets.right = 99
|
|
AssertEqual(addon.Backdrops:CreateDescriptor("window").insets.right, 3, "backdrop preset shared mutable insets")
|
|
|
|
local customPanelBackdrop = addon.Backdrops:CreateDescriptor("panel", {
|
|
edgeSize = 24,
|
|
insets = { left = 7 },
|
|
})
|
|
AssertEqual(customPanelBackdrop.edgeSize, 24, "backdrop scalar override")
|
|
AssertEqual(customPanelBackdrop.insets.left, 7, "backdrop inset override")
|
|
AssertEqual(customPanelBackdrop.insets.top, 3, "backdrop inset default preservation")
|
|
|
|
local createdBackdropFrame
|
|
local backdropCreateArguments
|
|
local function CreateBackdropFrame(frameType, name, parent, template)
|
|
backdropCreateArguments = { frameType, name, parent, template }
|
|
createdBackdropFrame = {}
|
|
function createdBackdropFrame:SetBackdrop(descriptor)
|
|
self.descriptor = descriptor
|
|
end
|
|
function createdBackdropFrame:SetBackdropColor(red, green, blue, alpha)
|
|
self.backgroundColor = { red, green, blue, alpha }
|
|
end
|
|
function createdBackdropFrame:SetBackdropBorderColor(red, green, blue, alpha)
|
|
self.borderColor = { red, green, blue, alpha }
|
|
end
|
|
return createdBackdropFrame
|
|
end
|
|
|
|
local backdropParent = {}
|
|
local appliedBackdropFrame, appliedDescriptor = addon.Backdrops:CreateFrame({
|
|
name = "BejeweledBackdropTest",
|
|
parent = backdropParent,
|
|
template = "UIPanelButtonTemplate",
|
|
preset = "panel",
|
|
backgroundColor = { 0.6, 0.6, 0.6, 1 },
|
|
borderColor = { 1, 0.8, 0.45 },
|
|
createFrame = CreateBackdropFrame,
|
|
})
|
|
assert(appliedBackdropFrame == createdBackdropFrame, "backdrop frame identity changed")
|
|
assert(appliedDescriptor == createdBackdropFrame.descriptor, "created backdrop descriptor was not applied")
|
|
AssertEqual(backdropCreateArguments[1], "Frame", "default backdrop frame type")
|
|
AssertEqual(backdropCreateArguments[2], "BejeweledBackdropTest", "backdrop frame name")
|
|
assert(backdropCreateArguments[3] == backdropParent, "backdrop frame parent changed")
|
|
AssertEqual(backdropCreateArguments[4], "UIPanelButtonTemplate,BackdropTemplate", "backdrop template composition")
|
|
AssertEqual(createdBackdropFrame.backgroundColor[4], 1, "backdrop background alpha")
|
|
AssertEqual(createdBackdropFrame.borderColor[3], 0.45, "backdrop border blue")
|
|
assert(createdBackdropFrame.borderColor[4] == nil, "three-channel border color gained an alpha")
|
|
|
|
local existingTemplateFrame = addon.Backdrops:CreateFrame({
|
|
template = "BackdropTemplate",
|
|
createFrame = CreateBackdropFrame,
|
|
})
|
|
assert(existingTemplateFrame, "existing BackdropTemplate frame was not created")
|
|
AssertEqual(backdropCreateArguments[4], "BackdropTemplate", "BackdropTemplate was duplicated")
|
|
|
|
local function CreateMockTexture(layer)
|
|
local texture = { layer = layer }
|
|
function texture:SetWidth(width)
|
|
self.width = width
|
|
end
|
|
function texture:SetHeight(height)
|
|
self.height = height
|
|
end
|
|
function texture:SetPoint(...)
|
|
self.points = self.points or {}
|
|
self.points[#self.points + 1] = { ... }
|
|
end
|
|
function texture:SetTexture(path)
|
|
self.path = path
|
|
return true
|
|
end
|
|
function texture:SetTexCoord(left, right, bottom, top)
|
|
self.texCoord = { left, right, bottom, top }
|
|
end
|
|
function texture:SetBlendMode(blendMode)
|
|
self.blendMode = blendMode
|
|
end
|
|
function texture:SetAlpha(alpha)
|
|
self.alpha = alpha
|
|
end
|
|
function texture:SetRotation(radians)
|
|
self.rotation = radians
|
|
end
|
|
function texture:Show()
|
|
self.shown = true
|
|
end
|
|
function texture:Hide()
|
|
self.shown = false
|
|
end
|
|
return texture
|
|
end
|
|
|
|
local function CreateMockLine(layer)
|
|
local line = CreateMockTexture(layer)
|
|
function line:ClearAllPoints()
|
|
self.startPoint = nil
|
|
self.endPoint = nil
|
|
end
|
|
function line:SetStartPoint(...)
|
|
self.startPoint = { ... }
|
|
end
|
|
function line:SetEndPoint(...)
|
|
self.endPoint = { ... }
|
|
end
|
|
function line:SetThickness(thickness)
|
|
self.thickness = thickness
|
|
end
|
|
function line:SetColorTexture(red, green, blue, alpha)
|
|
self.color = { red, green, blue, alpha }
|
|
end
|
|
return line
|
|
end
|
|
|
|
local gemPoolCreatedFrames = {}
|
|
local function CreateGemPoolFrame(frameType, name, parent, template)
|
|
local frame = {
|
|
frameType = frameType,
|
|
name = name,
|
|
parent = parent,
|
|
template = template,
|
|
scripts = {},
|
|
}
|
|
function frame:SetPoint(...)
|
|
self.points = self.points or {}
|
|
self.points[#self.points + 1] = { ... }
|
|
end
|
|
function frame:ClearAllPoints()
|
|
self.points = {}
|
|
end
|
|
function frame:SetWidth(width)
|
|
self.width = width
|
|
end
|
|
function frame:SetHeight(height)
|
|
self.height = height
|
|
end
|
|
function frame:CreateTexture(name, layer)
|
|
local texture = CreateMockTexture(layer)
|
|
self.createdTextures = self.createdTextures or {}
|
|
self.createdTextures[#self.createdTextures + 1] = texture
|
|
return texture
|
|
end
|
|
function frame:CreateLine(name, layer)
|
|
local line = CreateMockLine(layer)
|
|
self.createdLines = self.createdLines or {}
|
|
self.createdLines[#self.createdLines + 1] = line
|
|
return line
|
|
end
|
|
function frame:CreateAnimationGroup()
|
|
local group = {
|
|
animations = {},
|
|
scripts = {},
|
|
playing = false,
|
|
}
|
|
function group:CreateAnimation(animationType)
|
|
local animation = { animationType = animationType }
|
|
function animation:SetDuration(duration)
|
|
self.duration = duration
|
|
end
|
|
function animation:SetOrder(order)
|
|
self.order = order
|
|
end
|
|
function animation:SetOffset(offsetX, offsetY)
|
|
self.offsetX = offsetX
|
|
self.offsetY = offsetY
|
|
end
|
|
function animation:SetFromAlpha(alpha)
|
|
self.fromAlpha = alpha
|
|
end
|
|
function animation:SetToAlpha(alpha)
|
|
self.toAlpha = alpha
|
|
end
|
|
self.animations[#self.animations + 1] = animation
|
|
return animation
|
|
end
|
|
function group:SetScript(scriptName, callback)
|
|
self.scripts[scriptName] = callback
|
|
end
|
|
function group:Play()
|
|
self.playing = true
|
|
self.paused = false
|
|
end
|
|
function group:Pause()
|
|
if self.playing then
|
|
self.playing = false
|
|
self.paused = true
|
|
end
|
|
end
|
|
function group:IsPaused()
|
|
return self.paused and true or false
|
|
end
|
|
function group:Stop()
|
|
self.playing = false
|
|
self.paused = false
|
|
self.stopped = true
|
|
end
|
|
function group:FinishForTest()
|
|
if not self.playing then
|
|
return false
|
|
end
|
|
self.playing = false
|
|
local callback = self.scripts.OnFinished
|
|
if callback then
|
|
callback(self)
|
|
end
|
|
return true
|
|
end
|
|
self.animationGroups = self.animationGroups or {}
|
|
self.animationGroups[#self.animationGroups + 1] = group
|
|
return group
|
|
end
|
|
function frame:SetAlpha(alpha)
|
|
self.alpha = alpha
|
|
end
|
|
function frame:SetFrameLevel(frameLevel)
|
|
self.frameLevel = frameLevel
|
|
end
|
|
function frame:EnableMouse(enabled)
|
|
self.mouseEnabled = enabled
|
|
end
|
|
function frame:RegisterForDrag(button)
|
|
self.dragButton = button
|
|
end
|
|
function frame:SetScript(scriptName, handler)
|
|
self.scripts[scriptName] = handler
|
|
end
|
|
function frame:Show()
|
|
self.shown = true
|
|
end
|
|
function frame:Hide()
|
|
self.shown = false
|
|
end
|
|
gemPoolCreatedFrames[#gemPoolCreatedFrames + 1] = frame
|
|
return frame
|
|
end
|
|
|
|
local gemPoolParent = {
|
|
GetFrameLevel = function()
|
|
return 10
|
|
end,
|
|
}
|
|
local enteredGem
|
|
local gemPool = addon.GemPool:New(gemPoolParent, {
|
|
createFrame = CreateGemPoolFrame,
|
|
handlers = {
|
|
onEnter = function(frame)
|
|
enteredGem = frame
|
|
end,
|
|
},
|
|
})
|
|
AssertEqual(#gemPoolCreatedFrames, 80, "board tile and gem frame allocation count")
|
|
AssertEqual(#gemPool.tiles, 16, "board tile count")
|
|
AssertEqual(gemPool.tiles[1].texture.path, addon.Constants.IMAGE_ROOT .. "board", "board tile texture")
|
|
AssertEqual(gemPool.tiles[1].texture.texCoord[1], 0.046875, "board tile left coordinate")
|
|
AssertEqual(gemPool.tiles[16].points[1][2], 300, "last board tile x position")
|
|
AssertEqual(gemPool.tiles[16].points[1][3], -300, "last board tile y position")
|
|
|
|
local bottomRightGem = gemPool:GetFrame(8, 8)
|
|
AssertEqual(bottomRightGem.x, 350, "bottom-right gem x position")
|
|
AssertEqual(bottomRightGem.y, 350, "bottom-right gem y position")
|
|
AssertEqual(bottomRightGem.frameLevel, 12, "gem frame level")
|
|
AssertEqual(bottomRightGem.dragButton, "LeftButton", "gem drag registration")
|
|
AssertEqual(bottomRightGem.highlight.blendMode, "ADD", "gem highlight blend mode")
|
|
assert(not bottomRightGem.selector.shown, "gem selector started visible")
|
|
bottomRightGem.scripts.OnEnter(bottomRightGem)
|
|
assert(enteredGem == bottomRightGem, "gem enter handler wiring")
|
|
|
|
local projectionGrid = addon.Grid:New()
|
|
projectionGrid:Set(2, 3, 4)
|
|
projectionGrid:Set(3, 3, addon.Constants.HYPER_CONTENTS)
|
|
projectionGrid:Set(4, 3, 6, true)
|
|
local firstProjection = gemPool:Project(projectionGrid)
|
|
AssertEqual(firstProjection.changedCount, 3, "initial nonempty gem projection count")
|
|
local redGemFrame = gemPool:GetFrame(2, 3)
|
|
AssertEqual(redGemFrame.texture.path, addon.Constants.IMAGE_ROOT .. "gem_red", "normal gem texture projection")
|
|
AssertEqual(redGemFrame.texture.texCoord[2], 49 / 255, "normal gem idle coordinate")
|
|
AssertEqual(redGemFrame.glow.path, addon.Constants.IMAGE_ROOT .. "highlight_red", "normal gem glow projection")
|
|
local hyperGemFrame = gemPool:GetFrame(3, 3)
|
|
AssertEqual(hyperGemFrame.texture.path, addon.Constants.IMAGE_ROOT .. "hypergem", "hyper gem texture projection")
|
|
AssertEqual(hyperGemFrame.texture.texCoord[2], 0.1, "hyper gem idle coordinate")
|
|
local powerGemFrame = gemPool:GetFrame(4, 3)
|
|
assert(powerGemFrame.projectedBigStar, "power-gem projection marker")
|
|
AssertEqual(powerGemFrame.texture.path, addon.Constants.IMAGE_ROOT .. "gem_orange", "power-gem base texture")
|
|
AssertEqual(gemPool:Project(projectionGrid).changedCount, 0, "unchanged grid projection")
|
|
|
|
projectionGrid:Set(2, 3, 7)
|
|
local changedProjection = gemPool:Project(projectionGrid)
|
|
AssertEqual(changedProjection.changedCount, 1, "single-cell projection change count")
|
|
AssertEqual(redGemFrame.texture.path, addon.Constants.IMAGE_ROOT .. "gem_green", "changed gem texture projection")
|
|
gemPool:SetInteractive(false)
|
|
assert(not gemPool:GetFrame(1, 1).mouseEnabled, "gem interaction disable")
|
|
gemPool:SetHandlers({ onLeave = function() end })
|
|
assert(gemPool:GetFrame(1, 1).scripts.OnEnter == nil, "removed gem handler remained installed")
|
|
assert(type(gemPool:GetFrame(1, 1).scripts.OnLeave) == "function", "replacement gem handler was not installed")
|
|
redGemFrame.selector:Show()
|
|
redGemFrame.glow:SetAlpha(0.75)
|
|
local resetProjection = gemPool:ResetPresentation(projectionGrid)
|
|
AssertEqual(resetProjection.changedCount, 64, "forced projection after presentation reset")
|
|
assert(not redGemFrame.selector.shown, "presentation reset left selector visible")
|
|
AssertEqual(redGemFrame.glow.alpha, 0, "presentation reset left gem glow visible")
|
|
AssertEqual(redGemFrame.points[1][1], "TOPLEFT", "presentation reset gem anchor")
|
|
AssertEqual(redGemFrame.points[1][2], 50, "presentation reset gem x position")
|
|
AssertEqual(redGemFrame.points[1][3], -100, "presentation reset gem y position")
|
|
gemPool:SetSelection(2, 3)
|
|
assert(gemPool.selectedFrame == redGemFrame, "gem selection frame was not retained")
|
|
assert(redGemFrame.selector.shown, "selected gem selector was not shown")
|
|
gemPool:SetSelection(nil)
|
|
assert(gemPool.selectedFrame == nil, "cleared gem selection was retained")
|
|
assert(not redGemFrame.selector.shown, "cleared gem selector remained visible")
|
|
|
|
local playedFiles = {}
|
|
local playedSoundKits = {}
|
|
local audioSettings = {}
|
|
local audioVisible = true
|
|
local audio = addon.Audio:New(audioSettings, {
|
|
isVisible = function()
|
|
return audioVisible
|
|
end,
|
|
playSoundFile = function(path)
|
|
playedFiles[#playedFiles + 1] = path
|
|
return true, #playedFiles
|
|
end,
|
|
playSound = function(soundKitID)
|
|
playedSoundKits[#playedSoundKits + 1] = soundKitID
|
|
return true, #playedSoundKits
|
|
end,
|
|
soundKit = {
|
|
UI_SCENARIO_STAGE_END = 31757,
|
|
UI_AUTO_QUEST_COMPLETE = 23404,
|
|
},
|
|
})
|
|
|
|
assert(audio:Play("Select"), "select sound request was rejected")
|
|
assert(audio:Play("Invalid"), "invalid-move sound request was rejected")
|
|
assert(audio:Play("Select"), "duplicate select sound request was rejected")
|
|
local ordinarySounds = audio:Update(0.1)
|
|
AssertEqual(#ordinarySounds, 2, "ordinary sound coalescing")
|
|
AssertEqual(ordinarySounds[1].name, "Invalid", "ordinary sound playback order")
|
|
AssertEqual(ordinarySounds[2].name, "Select", "ordinary sound playback order")
|
|
AssertEqual(playedFiles[1], addon.Constants.SOUND_ROOT .. "bad2.mp3", "invalid-move sound path")
|
|
AssertEqual(playedFiles[2], addon.Constants.SOUND_ROOT .. "select.mp3", "selection sound path")
|
|
|
|
audioSettings.quietSounds = 1
|
|
assert(audio:Play("PowerCreate"), "quiet power-create sound request was rejected")
|
|
audio:Update(0)
|
|
AssertEqual(playedFiles[3], addon.Constants.SOUND_ROOT .. "q_multishot.mp3", "quiet sound path")
|
|
|
|
assert(audio:Play("Combo", 9), "capped combo sound request was rejected")
|
|
assert(audio:Play("Combo", 2), "second combo sound request was rejected")
|
|
local firstComboSounds = audio:Update(0)
|
|
AssertEqual(#firstComboSounds, 1, "one combo per update")
|
|
AssertEqual(firstComboSounds[1].comboIndex, 2, "lowest pending combo tier")
|
|
AssertEqual(playedFiles[4], addon.Constants.SOUND_ROOT .. "q_combo32.mp3", "combo-two sound path")
|
|
local secondComboSounds = audio:Update(0)
|
|
AssertEqual(secondComboSounds[1].comboIndex, 6, "capped combo tier")
|
|
AssertEqual(playedFiles[5], addon.Constants.SOUND_ROOT .. "q_combo72.mp3", "combo-six sound path")
|
|
|
|
assert(not audio:Play("GemClick"), "initial gem click bypassed throttle")
|
|
audio:Update(0.11)
|
|
assert(audio:Play("GemClick"), "elapsed gem click was rejected")
|
|
audio:Update(0)
|
|
AssertEqual(playedFiles[6], addon.Constants.SOUND_ROOT .. "q_gemongem2.mp3", "gem-click sound path")
|
|
assert(not audio:Play("GemClick"), "immediate repeated gem click bypassed throttle")
|
|
|
|
audioVisible = false
|
|
assert(not audio:Play("Go"), "hidden-window sound request was accepted")
|
|
audioVisible = true
|
|
audioSettings.disableSounds = 1
|
|
assert(not audio:Play("Go"), "disabled sound request was accepted")
|
|
local clickTimeBeforeDisabledUpdate = audio.lastClick
|
|
audio:Update(1)
|
|
AssertEqual(audio.lastClick, clickTimeBeforeDisabledUpdate, "disabled audio advanced its click timer")
|
|
audioSettings.disableSounds = nil
|
|
|
|
assert(audio:Play("LevelUp"), "level-up sound request was rejected")
|
|
local levelSounds = audio:Update(0)
|
|
AssertEqual(levelSounds[1].name, "LevelUp", "level-up sound event")
|
|
AssertEqual(playedSoundKits[1], 31757, "supported level-up SoundKit")
|
|
|
|
local existingSettings = { gameAlpha = 0.65, customSetting = "preserved" }
|
|
local account = { customAccountField = true }
|
|
local profile = { settings = existingSettings, customProfileField = true }
|
|
local initializedAccount, initializedProfile = addon.SavedVariables:Initialize(account, profile)
|
|
assert(initializedAccount == account, "account table identity changed")
|
|
assert(initializedProfile == profile, "profile table identity changed")
|
|
assert(initializedProfile.settings == existingSettings, "settings table identity changed")
|
|
AssertEqual(initializedProfile.settings.gameAlpha, 0.65, "existing setting")
|
|
AssertEqual(initializedProfile.settings.defaultPublish, "GUILD", "default publish channel")
|
|
AssertEqual(initializedProfile.settings.customSetting, "preserved", "unknown setting")
|
|
AssertEqual(initializedProfile.stats.gemMatch[8], 0, "gem-match wire width")
|
|
AssertEqual(initializedProfile.skill.friendList.c, 0, "friend counter")
|
|
AssertEqual(initializedProfile.scoreList.friends.classic[1][4], "9VW``nt", "classic signed fallback")
|
|
AssertEqual(initializedProfile.scoreList.guild.timed[10][4], "8H4`a3", "timed signed fallback")
|
|
assert(initializedProfile.scoreList.friends.classic ~= initializedProfile.scoreList.guild.classic, "leaderboard defaults share mutable tables")
|
|
assert(BejeweledData == account, "account SavedVariable was not installed")
|
|
assert(BejeweledProfile == profile, "profile SavedVariable was not installed")
|
|
do
|
|
local popCapSeed = addon.SavedVariables:ByteSum("PopCap Games")
|
|
local signedFallbackPayload = addon.SavedVariables:VerifyAuthenticatedPayload("9VW``nt", popCapSeed)
|
|
AssertEqual(signedFallbackPayload, "``nt", "legacy signed fallback payload")
|
|
AssertEqual(addon.SavedVariables:DecodeBase70(signedFallbackPayload), 1000, "legacy signed fallback score")
|
|
AssertEqual(addon.SavedVariables:EncodeBase70(1000, 4), "``nt", "legacy base-70 score encoding")
|
|
assert(addon.SavedVariables:VerifyAuthenticatedPayload("9VX``nt", popCapSeed) == nil, "tampered fallback signature was accepted")
|
|
end
|
|
|
|
local firstGrid = addon.Grid:New(MakeRandom(8675309))
|
|
local secondGrid = addon.Grid:New(MakeRandom(8675309))
|
|
assert(firstGrid:Fill(), "first deterministic fill failed")
|
|
assert(secondGrid:Fill(), "second deterministic fill failed")
|
|
assert(not firstGrid:HasAnyMatch(), "generated board contains an immediate match")
|
|
assert(firstGrid:FindLegalMove(), "generated board has no legal move")
|
|
|
|
local firstWire = firstGrid:ExportLegacyBoard()
|
|
local secondWire = secondGrid:ExportLegacyBoard()
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(firstWire[y][x], secondWire[y][x], "deterministic board cell")
|
|
end
|
|
end
|
|
|
|
for seed = 1, 50 do
|
|
local generatedGrid = addon.Grid:New(MakeRandom(seed))
|
|
assert(generatedGrid:Fill(), "seeded board generation failed")
|
|
assert(not generatedGrid:HasAnyMatch(), "seeded board contains an immediate match")
|
|
assert(generatedGrid:FindLegalMove(), "seeded board has no legal move")
|
|
end
|
|
|
|
local swapGrid = addon.Grid:New()
|
|
swapGrid:Set(1, 1, 1)
|
|
swapGrid:Set(2, 1, 2)
|
|
swapGrid:Set(3, 1, 1)
|
|
swapGrid:Set(2, 2, 1)
|
|
swapGrid:Set(4, 1, 4)
|
|
swapGrid:Set(5, 1, 5)
|
|
assert(swapGrid:IsLegalSwap(2, 1, 2, 2), "known match-producing swap was rejected")
|
|
assert(not swapGrid:IsLegalSwap(4, 1, 5, 1), "known non-matching swap was accepted")
|
|
local moveFrom, moveTo = swapGrid:FindLegalMove()
|
|
assert(moveFrom and moveTo, "known legal move was not found")
|
|
|
|
swapGrid:Set(7, 7, addon.Constants.HYPER_CONTENTS)
|
|
swapGrid:Set(8, 7, 3)
|
|
assert(swapGrid:IsLegalSwap(7, 7, 8, 7), "hyper-gem swap was rejected")
|
|
|
|
local legacyRows = {}
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
legacyRows[y] = {}
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
legacyRows[y][x] = 0
|
|
end
|
|
end
|
|
legacyRows[1][1] = 13
|
|
legacyRows[1][2] = 9
|
|
local loadedGrid = addon.Grid:New()
|
|
loadedGrid:LoadLegacyBoard(legacyRows)
|
|
AssertEqual(loadedGrid:Get(1, 1).contents, 3, "big-star color decode")
|
|
assert(loadedGrid:Get(1, 1).bigStar, "big-star marker decode")
|
|
AssertEqual(loadedGrid:Get(2, 1).contents, 9, "hyper-gem decode")
|
|
AssertEqual(loadedGrid:ExportLegacyBoard()[1][1], 13, "big-star wire round trip")
|
|
|
|
local invalidRows = {}
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
invalidRows[y] = {}
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
invalidRows[y][x] = 1
|
|
end
|
|
end
|
|
invalidRows[8][8] = 10
|
|
local beforeInvalidLoad = loadedGrid:ExportLegacyBoard()
|
|
local loadedInvalid = pcall(function()
|
|
loadedGrid:LoadLegacyBoard(invalidRows)
|
|
end)
|
|
assert(not loadedInvalid, "invalid legacy big-star value was accepted")
|
|
local afterInvalidLoad = loadedGrid:ExportLegacyBoard()
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(afterInvalidLoad[y][x], beforeInvalidLoad[y][x], "invalid legacy load was not atomic")
|
|
end
|
|
end
|
|
|
|
local matchGrid = addon.Grid:New()
|
|
matchGrid:Set(2, 2, 3)
|
|
matchGrid:Set(3, 2, 3)
|
|
matchGrid:Set(4, 2, 3)
|
|
local threeMatch = addon.Matches:Find(matchGrid)
|
|
assert(threeMatch.hasMatches, "three-gem match was not found")
|
|
AssertEqual(#threeMatch.groups, 1, "three-gem group count")
|
|
AssertEqual(threeMatch.cellCount, 3, "three-gem matched cell count")
|
|
AssertEqual(threeMatch.groups[1].axis, "horizontal", "three-gem primary axis")
|
|
assert(not threeMatch.groups[1].special, "three-gem match created a special gem")
|
|
assert(threeMatch.marks[2][2].horizontal, "horizontal match mark was not reported")
|
|
assert(not matchGrid:Get(2, 2).markX, "match discovery mutated a grid cell")
|
|
|
|
matchGrid:Reset()
|
|
for x = 2, 5 do
|
|
matchGrid:Set(x, 3, 4)
|
|
end
|
|
local preferredPowerCell = matchGrid:Get(4, 3)
|
|
local fourMatch = addon.Matches:Find(matchGrid, { preferredCell = preferredPowerCell })
|
|
AssertEqual(fourMatch.groups[1].special.kind, "power", "four-gem classification")
|
|
assert(fourMatch.groups[1].special.cell == preferredPowerCell, "preferred power-gem position was ignored")
|
|
|
|
matchGrid:Reset()
|
|
for y = 1, 5 do
|
|
matchGrid:Set(6, y, 5)
|
|
end
|
|
local fiveMatch = addon.Matches:Find(matchGrid, { random = function() return 2 end })
|
|
AssertEqual(fiveMatch.groups[1].special.kind, "hyper", "five-gem classification")
|
|
assert(fiveMatch.groups[1].special.cell == matchGrid:Get(6, 2), "hyper-gem fallback position was not deterministic")
|
|
|
|
matchGrid:Reset()
|
|
matchGrid:Set(3, 1, 6)
|
|
matchGrid:Set(3, 2, 6)
|
|
matchGrid:Set(3, 3, 6)
|
|
matchGrid:Set(2, 3, 6)
|
|
matchGrid:Set(4, 3, 6)
|
|
local tMatch = addon.Matches:Find(matchGrid)
|
|
AssertEqual(#tMatch.groups, 1, "T-match group count")
|
|
AssertEqual(tMatch.groups[1].clearCount, 5, "T-match clear count")
|
|
AssertEqual(tMatch.groups[1].special.kind, "power", "T-match classification")
|
|
assert(tMatch.groups[1].special.cell == matchGrid:Get(3, 3), "T-match special was not placed at the intersection")
|
|
|
|
matchGrid:Set(1, 3, 6)
|
|
matchGrid:Set(5, 3, 6)
|
|
local longCrossMatch = addon.Matches:Find(matchGrid)
|
|
AssertEqual(longCrossMatch.groups[1].special.kind, "hyper", "five-wide cross classification")
|
|
assert(longCrossMatch.groups[1].special.cell == matchGrid:Get(3, 3), "cross hyper gem was not placed at the intersection")
|
|
matchGrid:Set(3, 3, 6, true)
|
|
local powerOccupiedCross = addon.Matches:Find(matchGrid, { random = function() return 1 end })
|
|
assert(powerOccupiedCross.groups[1].special.cell == matchGrid:Get(3, 1), "hyper gem did not fall back from a power-gem intersection")
|
|
|
|
matchGrid:Reset()
|
|
for y = 1, 3 do
|
|
matchGrid:Set(2, y, 2)
|
|
matchGrid:Set(4, y, 2)
|
|
end
|
|
matchGrid:Set(3, 3, 2)
|
|
local overlappingMatches = addon.Matches:Find(matchGrid)
|
|
AssertEqual(#overlappingMatches.groups, 2, "overlapping match group count")
|
|
AssertEqual(overlappingMatches.groups[1].clearCount, 5, "first overlapping group count")
|
|
AssertEqual(overlappingMatches.groups[2].clearCount, 5, "second overlapping group count")
|
|
AssertEqual(overlappingMatches.cellCount, 7, "overlapping unique cell count")
|
|
|
|
matchGrid:Reset()
|
|
matchGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
matchGrid:Set(2, 8, addon.Constants.HYPER_CONTENTS)
|
|
matchGrid:Set(3, 8, addon.Constants.HYPER_CONTENTS)
|
|
local hyperOnly = addon.Matches:Find(matchGrid)
|
|
assert(not hyperOnly.hasMatches, "hyper gems were treated as an ordinary color match")
|
|
|
|
for seed = 1, 100 do
|
|
local random = MakeRandom(seed * 97)
|
|
matchGrid:Reset()
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
matchGrid:Set(x, y, random(1, addon.Constants.GEM_COLOR_COUNT))
|
|
end
|
|
end
|
|
local discovered = addon.Matches:Find(matchGrid, { random = random })
|
|
local discoveredCells = {}
|
|
for index = 1, #discovered.cells do
|
|
discoveredCells[discovered.cells[index]] = true
|
|
end
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
local cell = matchGrid:Get(x, y)
|
|
AssertEqual(discoveredCells[cell] and true or false, matchGrid:HasMatchAt(x, y), "random-board match coverage")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function FillStablePattern(grid)
|
|
grid:Reset()
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
grid:Set(x, y, ((x + (y * 2)) % addon.Constants.GEM_COLOR_COUNT) + 1)
|
|
end
|
|
end
|
|
assert(not grid:HasAnyMatch(), "stable cascade test pattern contains a match")
|
|
end
|
|
|
|
local cascadeGrid = addon.Grid:New(MakeRandom(424242))
|
|
FillStablePattern(cascadeGrid)
|
|
for x = 1, 3 do
|
|
cascadeGrid:Set(x, 8, 1)
|
|
end
|
|
local simpleCascade = addon.Cascade:Step(cascadeGrid, addon.Matches:Find(cascadeGrid), {
|
|
random = MakeRandom(1234),
|
|
requireLegalMove = false,
|
|
})
|
|
AssertEqual(simpleCascade.matchedCellCount, 3, "simple cascade match count")
|
|
AssertEqual(simpleCascade.clearCount, 3, "simple cascade clear count")
|
|
AssertEqual(simpleCascade.removedCount, 3, "simple cascade removed count")
|
|
AssertEqual(#simpleCascade.hyperAwards, 0, "normal cascade hyper award count")
|
|
AssertEqual(#simpleCascade.lightningLinks, 0, "normal cascade lightning-link count")
|
|
AssertEqual(#simpleCascade.moves, 21, "simple cascade gravity move count")
|
|
AssertEqual(#simpleCascade.refills, 3, "simple cascade refill count")
|
|
AssertEqual(cascadeGrid:Get(1, 8).contents, 2, "first compacted bottom gem")
|
|
AssertEqual(cascadeGrid:Get(2, 8).contents, 3, "second compacted bottom gem")
|
|
AssertEqual(cascadeGrid:Get(3, 8).contents, 4, "third compacted bottom gem")
|
|
|
|
local function FinishAllGemAnimations()
|
|
local finishedAny = true
|
|
local passes = 0
|
|
while finishedAny do
|
|
finishedAny = false
|
|
passes = passes + 1
|
|
assert(passes <= 20, "animation completion did not converge")
|
|
for frameIndex = 1, #gemPoolCreatedFrames do
|
|
local frame = gemPoolCreatedFrames[frameIndex]
|
|
for groupIndex = 1, #(frame.animationGroups or {}) do
|
|
if frame.animationGroups[groupIndex]:FinishForTest() then
|
|
finishedAny = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function FinishAnimationRunner(runner)
|
|
for pass = 1, 100 do
|
|
FinishAllGemAnimations()
|
|
if not runner:IsPlaying() then
|
|
return true
|
|
end
|
|
runner:UpdateEffects(0.025)
|
|
end
|
|
error("animation runner did not finish effects", 2)
|
|
end
|
|
|
|
local animationGrid = addon.Grid:New(MakeRandom(9876))
|
|
FillStablePattern(animationGrid)
|
|
for x = 1, 3 do
|
|
animationGrid:Set(x, 8, 1)
|
|
end
|
|
local animationPool = addon.GemPool:New(gemPoolParent, {
|
|
createFrame = CreateGemPoolFrame,
|
|
createBoardTiles = false,
|
|
})
|
|
animationPool:Project(animationGrid, true)
|
|
local animationStep = addon.Cascade:Step(animationGrid, addon.Matches:Find(animationGrid), {
|
|
random = MakeRandom(8765),
|
|
requireLegalMove = false,
|
|
})
|
|
local animationPhases = {}
|
|
local animationCompleted = false
|
|
local animations = addon.Animations:New(animationPool, {
|
|
clearDuration = 0.12,
|
|
fallPerCell = 0.04,
|
|
minimumFallDuration = 0.08,
|
|
})
|
|
assert(type(animations.effectFrame.scripts.OnUpdate) == "function", "persistent effect driver was not installed")
|
|
local animationPlan = animations:BuildPlan(animationStep)
|
|
AssertEqual(#animationPlan.steps, 1, "single-step animation plan count")
|
|
AssertEqual(#animationPlan.steps[1].clear, 3, "animation clear plan count")
|
|
AssertEqual(#animationPlan.steps[1].moves, 21, "animation move plan count")
|
|
AssertEqual(#animationPlan.steps[1].refills, 3, "animation refill plan count")
|
|
AssertEqual(animationPlan.steps[1].refills[1].fromY, 0, "refill entry row")
|
|
AssertEqual(animationPlan.steps[1].refills[1].offsetY, -50, "refill translation offset")
|
|
AssertEqual(animationPlan.steps[1].moves[1].duration, 0.08, "minimum movement duration")
|
|
local animationRun
|
|
animationRun = animations:Play(animationStep, animationGrid, {
|
|
onPhase = function(phase, stepIndex)
|
|
animationPhases[#animationPhases + 1] = phase .. ":" .. stepIndex
|
|
end,
|
|
onComplete = function(run)
|
|
assert(run == animationRun, "animation completion run changed")
|
|
animationCompleted = true
|
|
end,
|
|
})
|
|
assert(animations:IsPlaying(), "animation runner did not enter playing state")
|
|
assert(not animationPool:GetFrame(1, 1).mouseEnabled, "animation runner left gem interaction enabled")
|
|
AssertEqual(animationPhases[1], "clear:1", "animation clear phase notification")
|
|
AssertEqual(animationPool:GetFrame(1, 8).bejeweledClearAnimation.alpha.duration, 0.12, "clear animation duration")
|
|
FinishAllGemAnimations()
|
|
assert(animationCompleted, "animation completion callback was not invoked")
|
|
assert(not animations:IsPlaying(), "animation runner stayed active after completion")
|
|
assert(animationPool:GetFrame(1, 1).mouseEnabled, "animation completion left gem interaction disabled")
|
|
AssertEqual(animationPhases[2], "settle:1", "animation settle phase notification")
|
|
AssertEqual(animationPool:GetFrame(1, 8).contents, animationGrid:Get(1, 8).contents, "animation final grid projection")
|
|
AssertEqual(animationPool:GetFrame(1, 8).points[1][2], 0, "animation final frame x anchor")
|
|
AssertEqual(animationPool:GetFrame(1, 8).points[1][3], -350, "animation final frame y anchor")
|
|
|
|
local cancelledReason
|
|
local cancelledRun = animations:Play(animationStep, animationGrid, {
|
|
onCancel = function(reason)
|
|
cancelledReason = reason
|
|
end,
|
|
})
|
|
assert(animations:Cancel("test-cancel"), "active animation cancellation failed")
|
|
assert(cancelledRun.cancelled, "cancelled animation run was not marked")
|
|
AssertEqual(cancelledReason, "test-cancel", "animation cancellation reason")
|
|
assert(not animations:IsPlaying(), "cancelled animation runner stayed active")
|
|
assert(animationPool:GetFrame(1, 1).mouseEnabled, "animation cancellation left gem interaction disabled")
|
|
assert(not animations:Cancel(), "inactive animation cancellation succeeded")
|
|
|
|
animationPool:RenderCell(4, 4, { contents = addon.Constants.HYPER_CONTENTS }, true)
|
|
animationPool:RenderCell(5, 5, { contents = 6, bigStar = true }, true)
|
|
animations:SyncPersistentEffects(false)
|
|
local persistentHyper = animationPool:GetFrame(4, 4)
|
|
local persistentPower = animationPool:GetFrame(5, 5)
|
|
AssertEqual(persistentHyper.texture.texCoord[2], 0.1, "hyper atlas initial frame")
|
|
assert(persistentPower.bejeweledPowerEffect.texture.shown, "power effect texture was not shown")
|
|
assert(persistentPower.bejeweledPowerEffect.highlight.shown, "power effect highlight was not shown")
|
|
AssertEqual(persistentPower.bejeweledPowerEffect.texture.width, 90, "power effect width")
|
|
AssertEqual(persistentPower.bejeweledPowerEffect.highlight.blendMode, "ADD", "power effect blend mode")
|
|
animations.effectFrame.scripts.OnUpdate(animations.effectFrame, 0.025)
|
|
AssertEqual(persistentHyper.bejeweledHyperFrame, 2, "hyper atlas frame advance")
|
|
AssertEqual(persistentHyper.texture.texCoord[1], 0.1, "hyper atlas second-frame coordinate")
|
|
AssertEqual(persistentPower.bejeweledPowerEffect.alpha, 97, "power effect cross-fade advance")
|
|
assert(persistentPower.bejeweledPowerEffect.texture.rotation > 0, "power effect rotation did not advance")
|
|
animationPool:RenderCell(5, 5, { contents = 6 }, true)
|
|
animations:SyncPersistentEffects(false)
|
|
assert(not persistentPower.bejeweledPowerEffect.texture.shown, "removed power effect texture remained visible")
|
|
assert(not persistentPower.bejeweledPowerEffect.highlight.shown, "removed power effect highlight remained visible")
|
|
|
|
local explosionCompleted = false
|
|
local explosionStep = {
|
|
removedCells = {},
|
|
spawnedSpecials = {},
|
|
triggeredPowerRecords = { { x = 2, y = 3, contents = 4 } },
|
|
lightningLinks = {},
|
|
moves = {},
|
|
refills = {},
|
|
}
|
|
local explosionPlan = animations:BuildPlan(explosionStep)
|
|
AssertEqual(#explosionPlan.steps[1].explosions, 1, "triggered explosion plan count")
|
|
local explosionRun = animations:Play(explosionStep, animationGrid, {
|
|
onComplete = function()
|
|
explosionCompleted = true
|
|
end,
|
|
})
|
|
assert(animations:IsPlaying(), "explosion barrier did not hold animation playback")
|
|
AssertEqual(#animations.activeExplosions, 1, "active explosion count")
|
|
local explosionFrame = animations.activeExplosions[1]
|
|
AssertEqual(explosionFrame.texture.path, addon.Constants.IMAGE_ROOT .. "explosion", "explosion texture")
|
|
AssertEqual(explosionFrame.texture.width, 150, "explosion texture width")
|
|
AssertEqual(explosionFrame.points[1][4], 50, "explosion x anchor")
|
|
AssertEqual(explosionFrame.points[1][5], -100, "explosion y anchor")
|
|
animations:UpdateEffects(0.025)
|
|
AssertEqual(explosionFrame.effectFrame, 2, "explosion atlas frame advance")
|
|
AssertEqual(explosionFrame.texture.texCoord[1], 49 / 255, "explosion atlas overlap coordinate")
|
|
for index = 2, 15 do
|
|
animations:UpdateEffects(0.025)
|
|
end
|
|
assert(not explosionCompleted, "explosion barrier completed before the final atlas frame")
|
|
AssertEqual(explosionFrame.effectFrame, 16, "explosion final atlas frame")
|
|
animations:UpdateEffects(0.025)
|
|
assert(explosionCompleted, "explosion barrier did not complete")
|
|
assert(explosionRun.completed, "explosion animation run was not marked complete")
|
|
assert(not explosionFrame.shown, "completed explosion remained visible")
|
|
AssertEqual(#animations.explosionPool, 1, "completed explosion was not pooled")
|
|
|
|
local pooledExplosion = animations:PlayExplosion(1, 1)
|
|
assert(pooledExplosion == explosionFrame, "explosion pool did not reuse its frame")
|
|
for index = 1, 16 do
|
|
animations:UpdateEffects(0.025)
|
|
end
|
|
AssertEqual(#animations.explosionPool, 1, "standalone explosion was not returned to its pool")
|
|
|
|
local cancelledExplosionRun = animations:Play(explosionStep, animationGrid)
|
|
local cancelledExplosionFrame = animations.activeExplosions[1]
|
|
assert(animations:Cancel("effect-cancel"), "explosion animation cancellation failed")
|
|
assert(cancelledExplosionRun.cancelled, "cancelled explosion run was not marked")
|
|
assert(not cancelledExplosionFrame.shown, "cancelled explosion remained visible")
|
|
AssertEqual(#animations.activeExplosions, 0, "cancelled explosion remained active")
|
|
AssertEqual(#animations.explosionPool, 1, "cancelled explosion was not pooled")
|
|
|
|
local inputGrid = addon.Grid:New()
|
|
FillStablePattern(inputGrid)
|
|
inputGrid:Set(2, 8, 1)
|
|
inputGrid:Set(3, 8, 1)
|
|
inputGrid:Set(1, 7, 1)
|
|
assert(not inputGrid:HasAnyMatch(), "input test board started with a match")
|
|
assert(inputGrid:IsLegalSwap(1, 7, 1, 8), "input test swap was not legal")
|
|
local inputPool = addon.GemPool:New(gemPoolParent, {
|
|
createFrame = CreateGemPoolFrame,
|
|
createBoardTiles = false,
|
|
})
|
|
inputPool:Project(inputGrid, true)
|
|
local inputAnimations = addon.Animations:New(inputPool, {
|
|
createFrame = CreateGemPoolFrame,
|
|
swapDuration = 0.2,
|
|
})
|
|
local inputProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local inputScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local inputRandomValues = { 2, 3, 4 }
|
|
local inputRandomIndex = 0
|
|
local function InputRandom(minimum, maximum)
|
|
inputRandomIndex = inputRandomIndex + 1
|
|
local value = inputRandomValues[((inputRandomIndex - 1) % #inputRandomValues) + 1]
|
|
return math.max(minimum, math.min(maximum, value))
|
|
end
|
|
local inputSounds = {}
|
|
local inputAudio = {
|
|
Play = function(_, soundName)
|
|
inputSounds[#inputSounds + 1] = soundName
|
|
return true
|
|
end,
|
|
}
|
|
local inputCompletions = {}
|
|
local inputCascades = {}
|
|
local input = addon.Input:New(inputGrid, inputPool, inputAnimations, {
|
|
audio = inputAudio,
|
|
scoringState = inputScoringState,
|
|
profile = inputProfile,
|
|
random = InputRandom,
|
|
requireLegalMove = false,
|
|
onMoveComplete = function(result)
|
|
inputCompletions[#inputCompletions + 1] = result
|
|
end,
|
|
onCascadeResolved = function(result)
|
|
inputCascades[#inputCascades + 1] = result.cascadeResult
|
|
end,
|
|
})
|
|
local inputHandlers = input:CreateGemHandlers()
|
|
assert(type(inputHandlers.onMouseDown) == "function", "input gem handler was not created")
|
|
local firstSelection = inputHandlers.onMouseDown(inputPool:GetFrame(8, 1), "LeftButton")
|
|
AssertEqual(firstSelection.status, "selected", "input first selection status")
|
|
assert(inputPool:GetFrame(8, 1).selector.shown, "input selection did not show selector")
|
|
local clearedSelection = input:HandleCell(1, 7)
|
|
AssertEqual(clearedSelection.reason, "nonadjacent", "nonadjacent selection clear reason")
|
|
assert(input:GetSelection() == nil, "nonadjacent click retained selection")
|
|
|
|
AssertEqual(input:HandleCell(1, 7).status, "selected", "legal swap source selection")
|
|
local acceptedMove = input:HandleCell(1, 8)
|
|
assert(acceptedMove.valid, "legal input move was rejected")
|
|
AssertEqual(acceptedMove.status, "animating", "accepted input move status")
|
|
assert(input:IsLocked(), "accepted input move did not lock input")
|
|
AssertEqual(input:HandleCell(4, 4).status, "locked", "locked input accepted another cell")
|
|
AssertEqual(inputScoringState.moves, 1, "accepted move count")
|
|
assert(inputGrid:HasAnyMatch(), "accepted swap was not applied before presentation")
|
|
AssertEqual(acceptedMove.swapRun.kind, "swap", "accepted swap animation kind")
|
|
AssertEqual(inputPool:GetFrame(1, 7).bejeweledSwapForwardAnimation.outbound.duration, 0.2, "accepted swap duration")
|
|
FinishAllGemAnimations()
|
|
assert(not input:IsLocked(), "cascade completion left input locked")
|
|
AssertEqual(acceptedMove.status, "complete", "accepted move completion status")
|
|
AssertEqual(#inputCascades, 1, "accepted move cascade callback count")
|
|
AssertEqual(#inputCompletions, 1, "accepted move completion callback count")
|
|
assert(inputCascades[1].stable, "accepted move cascade did not stabilize")
|
|
assert(not inputGrid:HasAnyMatch(), "accepted move cascade left a match")
|
|
assert(inputPool:GetFrame(1, 1).mouseEnabled, "accepted move completion left gem input disabled")
|
|
|
|
local invalidFirstX, invalidFirstY, invalidSecondX, invalidSecondY
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH - 1 do
|
|
if not inputGrid:IsLegalSwap(x, y, x + 1, y) then
|
|
invalidFirstX, invalidFirstY = x, y
|
|
invalidSecondX, invalidSecondY = x + 1, y
|
|
break
|
|
end
|
|
end
|
|
if invalidFirstX then
|
|
break
|
|
end
|
|
end
|
|
assert(invalidFirstX, "input test could not find an invalid adjacent swap")
|
|
local invalidBefore = inputGrid:ExportLegacyBoard()
|
|
input:HandleCell(invalidFirstX, invalidFirstY)
|
|
local rejectedMove = input:HandleCell(invalidSecondX, invalidSecondY)
|
|
assert(not rejectedMove.valid, "invalid input move was accepted")
|
|
assert(input:IsLocked(), "invalid rollback did not lock input")
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(inputGrid:EncodeLegacyValue(inputGrid:Get(x, y)), invalidBefore[y][x], "invalid swap changed authoritative grid")
|
|
end
|
|
end
|
|
local rollbackAnimation = inputPool:GetFrame(invalidFirstX, invalidFirstY).bejeweledSwapRollbackAnimation
|
|
AssertEqual(rollbackAnimation.outbound.order, 1, "rollback outbound animation order")
|
|
AssertEqual(rollbackAnimation.returnTranslation.order, 2, "rollback return animation order")
|
|
FinishAllGemAnimations()
|
|
AssertEqual(rejectedMove.status, "rejected", "invalid move completion status")
|
|
assert(not input:IsLocked(), "invalid rollback left input locked")
|
|
AssertEqual(inputScoringState.moves, 1, "invalid move changed move count")
|
|
AssertEqual(inputSounds[#inputSounds], "Invalid", "invalid move sound")
|
|
|
|
FillStablePattern(inputGrid)
|
|
inputGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
local inputHyperTarget = inputGrid:Get(2, 8).contents
|
|
local inputHyperTargetCount = 0
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
if inputGrid:Get(x, y).contents == inputHyperTarget then
|
|
inputHyperTargetCount = inputHyperTargetCount + 1
|
|
end
|
|
end
|
|
end
|
|
inputPool:Project(inputGrid, true)
|
|
AssertEqual(input:HandleCell(1, 8).status, "selected", "hyper swap source selection")
|
|
local hyperMove = input:HandleCell(2, 8)
|
|
assert(hyperMove.valid and hyperMove.hyperPlan, "hyper input move was not accepted")
|
|
AssertEqual(inputScoringState.moves, 1, "legacy hyper move changed move count")
|
|
FinishAnimationRunner(inputAnimations)
|
|
AssertEqual(hyperMove.status, "complete", "hyper input move completion status")
|
|
assert(hyperMove.cascadeResult.hyper, "hyper input move used the normal cascade path")
|
|
AssertEqual(#hyperMove.cascadeResult.steps[1].hyperAwards, inputHyperTargetCount + 1, "hyper input award count")
|
|
AssertEqual(#inputAnimations.lightningPool, inputHyperTargetCount - 1, "hyper lightning pool count")
|
|
local pooledLightning = inputAnimations.lightningPool[1]
|
|
AssertEqual(pooledLightning.base.path, addon.Constants.IMAGE_ROOT .. "lightning", "hyper lightning texture")
|
|
AssertEqual(pooledLightning.base.thickness, 10, "hyper lightning thickness")
|
|
AssertEqual(pooledLightning.highlight.color[1], addon.Constants.GEM_EFFECT_COLORS[inputHyperTarget][1], "hyper lightning color")
|
|
assert(not pooledLightning.base.shown and not pooledLightning.highlight.shown, "completed lightning remained visible")
|
|
AssertEqual(inputSounds[#inputSounds - 1], "HyperDestroy", "hyper destruction sound")
|
|
AssertEqual(inputSounds[#inputSounds], "ElectroExplode", "hyper electro sound")
|
|
assert(not input:IsLocked(), "hyper input move left input locked")
|
|
|
|
local function TestSessionRestore()
|
|
local sessionGrid = addon.Grid:New()
|
|
FillStablePattern(sessionGrid)
|
|
sessionGrid:Set(4, 4, addon.Constants.HYPER_CONTENTS)
|
|
local savedPowerContents = sessionGrid:Get(5, 5).contents
|
|
sessionGrid:Set(5, 5, savedPowerContents, true)
|
|
local sessionPool = addon.GemPool:New(gemPoolParent, {
|
|
createFrame = CreateGemPoolFrame,
|
|
createBoardTiles = false,
|
|
})
|
|
sessionPool:Project(sessionGrid, true)
|
|
local sessionAnimations = addon.Animations:New(sessionPool, {
|
|
createFrame = CreateGemPoolFrame,
|
|
swapDuration = 0.2,
|
|
})
|
|
local sessionProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local sessionState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, {
|
|
score = 12345,
|
|
pointsToLevelUp = 16000,
|
|
level = 4,
|
|
pointMultiplier = 2.5,
|
|
largestCascade = 9,
|
|
largestCombo = 4,
|
|
moves = 37,
|
|
})
|
|
local sessionPauseEvents = {}
|
|
local sessionSaveEvents = {}
|
|
local sessionRestoreEvents = {}
|
|
local sessionLevelStartedEvents = {}
|
|
local sessionLevelCompleteEvents = {}
|
|
local sessionSounds = {}
|
|
local session = addon.Session:New(sessionGrid, sessionPool, sessionAnimations, {
|
|
profile = sessionProfile,
|
|
playerName = "Nighthawk",
|
|
scoringState = sessionState,
|
|
timerElapsed = 42.75,
|
|
deferLevelTransitions = true,
|
|
inputOptions = {
|
|
random = MakeRandom(4810),
|
|
requireLegalMove = false,
|
|
audio = {
|
|
Play = function(_, soundName)
|
|
sessionSounds[#sessionSounds + 1] = soundName
|
|
end,
|
|
},
|
|
},
|
|
onPauseChanged = function(result)
|
|
sessionPauseEvents[#sessionPauseEvents + 1] = result
|
|
end,
|
|
onSaved = function(result)
|
|
sessionSaveEvents[#sessionSaveEvents + 1] = result
|
|
end,
|
|
onRestored = function(result)
|
|
sessionRestoreEvents[#sessionRestoreEvents + 1] = result
|
|
end,
|
|
onLevelTransitionStarted = function(result)
|
|
sessionLevelStartedEvents[#sessionLevelStartedEvents + 1] = result
|
|
end,
|
|
onLevelTransitionComplete = function(result)
|
|
sessionLevelCompleteEvents[#sessionLevelCompleteEvents + 1] = result
|
|
end,
|
|
})
|
|
local manualSessionSave = session:SaveClassicGame("test-save")
|
|
local savedSessionState = manualSessionSave.savedState
|
|
assert(session:HasClassicGame(), "saved classic session was not discoverable")
|
|
assert(sessionProfile.settings.classicInProgress, "classic in-progress wire flag was not set")
|
|
AssertEqual(savedSessionState[4][4], addon.Constants.HYPER_CONTENTS, "saved hyper wire value")
|
|
AssertEqual(savedSessionState[5][5], savedPowerContents + addon.Constants.BIG_STAR_WIRE_OFFSET, "saved power wire value")
|
|
AssertEqual(savedSessionState[9][1], 12345, "saved score metadata")
|
|
AssertEqual(savedSessionState[9][7], 42.75, "saved elapsed metadata")
|
|
local savedScorePayload = addon.SavedVariables:VerifyAuthenticatedPayload(
|
|
savedSessionState[9][9],
|
|
addon.SavedVariables:ByteSum("Nighthawk")
|
|
)
|
|
AssertEqual(addon.SavedVariables:DecodeBase70(savedScorePayload), 12345, "saved authenticated score")
|
|
|
|
sessionGrid:Reset()
|
|
sessionState.score = 1
|
|
sessionState.moves = 0
|
|
session:SetElapsed(0)
|
|
AssertEqual(session:Pause("pre-restore").status, "paused", "session pause status")
|
|
assert(session:IsPaused() and sessionAnimations:IsPaused(), "session pause did not freeze animations")
|
|
assert(not sessionPool:GetFrame(1, 1).mouseEnabled, "session pause left input interactive")
|
|
AssertEqual(session:HandleCell(1, 1).status, "paused", "paused session accepted input")
|
|
local restoredSession = session:RestoreClassicGame()
|
|
assert(restoredSession.state.signatureValid, "restored score signature was rejected")
|
|
assert(restoredSession.paused, "restore did not preserve the prior pause state")
|
|
AssertEqual(sessionGrid:Get(4, 4).contents, addon.Constants.HYPER_CONTENTS, "restored hyper contents")
|
|
assert(sessionGrid:Get(5, 5).bigStar, "restored power marker")
|
|
AssertEqual(sessionState.score, 12345, "restored authenticated score")
|
|
AssertEqual(sessionState.moves, 37, "restored move count")
|
|
AssertEqual(session.timerElapsed, 42.75, "restored elapsed time")
|
|
local pausedHyperFrame = sessionPool:GetFrame(4, 4)
|
|
local pausedHyperAtlasFrame = pausedHyperFrame.bejeweledHyperFrame
|
|
assert(not sessionAnimations:UpdateEffects(0.025), "paused session advanced effect timing")
|
|
AssertEqual(pausedHyperFrame.bejeweledHyperFrame, pausedHyperAtlasFrame, "paused hyper atlas advanced")
|
|
AssertEqual(session:AdvanceElapsed(5), 42.75, "paused session advanced elapsed time")
|
|
session:Resume("test-resume")
|
|
assert(not session:IsPaused() and not sessionAnimations:IsPaused(), "session resume left animations paused")
|
|
assert(sessionPool:GetFrame(1, 1).mouseEnabled, "session resume left input disabled")
|
|
sessionAnimations:UpdateEffects(0.025)
|
|
AssertEqual(pausedHyperFrame.bejeweledHyperFrame, pausedHyperAtlasFrame + 1, "resumed hyper atlas did not advance")
|
|
AssertEqual(session:AdvanceElapsed(1.25), 44, "running session elapsed time")
|
|
AssertEqual(#sessionPauseEvents, 2, "session pause callback count")
|
|
AssertEqual(#sessionRestoreEvents, 1, "session restore callback count")
|
|
|
|
local validSessionSignature = savedSessionState[9][9]
|
|
savedSessionState[9][9] = "000" .. string.sub(validSessionSignature, 4)
|
|
sessionGrid:Reset()
|
|
local invalidSignatureRestore = session:RestoreClassicGame({ paused = false })
|
|
assert(not invalidSignatureRestore.state.signatureValid, "tampered saved score signature was accepted")
|
|
AssertEqual(sessionState.score, 0, "tampered saved score did not restore as zero")
|
|
AssertEqual(invalidSignatureRestore.state.savedScore, 12345, "raw saved score evidence changed")
|
|
savedSessionState[9][9] = validSessionSignature
|
|
|
|
local invalidSavedCell = savedSessionState[1][1]
|
|
savedSessionState[1][1] = 99
|
|
local beforeFailedRestore = sessionGrid:ExportLegacyBoard()
|
|
local malformedRestoreSucceeded = pcall(function()
|
|
session:RestoreClassicGame()
|
|
end)
|
|
assert(not malformedRestoreSucceeded, "malformed saved board was accepted")
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(
|
|
sessionGrid:EncodeLegacyValue(sessionGrid:Get(x, y)),
|
|
beforeFailedRestore[y][x],
|
|
"failed session restore changed the grid"
|
|
)
|
|
end
|
|
end
|
|
savedSessionState[1][1] = invalidSavedCell
|
|
|
|
FillStablePattern(sessionGrid)
|
|
sessionGrid:Set(2, 8, 1)
|
|
sessionGrid:Set(3, 8, 1)
|
|
sessionGrid:Set(1, 7, 1)
|
|
sessionPool:Project(sessionGrid, true)
|
|
AssertEqual(session:HandleCell(1, 7).status, "selected", "session move source selection")
|
|
local sessionMove = session:HandleCell(1, 8)
|
|
assert(sessionMove.valid, "session legal move was rejected")
|
|
session:Pause("mid-swap")
|
|
local pausedSwapGroup = sessionPool:GetFrame(1, 7).bejeweledSwapForwardAnimation.group
|
|
assert(pausedSwapGroup:IsPaused(), "session pause did not pause the active swap group")
|
|
FinishAllGemAnimations()
|
|
assert(sessionAnimations:IsPlaying(), "paused swap completed while frozen")
|
|
session:Resume("mid-swap")
|
|
FinishAnimationRunner(sessionAnimations)
|
|
AssertEqual(sessionMove.status, "complete", "resumed session move completion status")
|
|
assert(sessionMove.saveResult and sessionMove.saveResult.status == "saved", "stable session move was not auto-saved")
|
|
AssertEqual(#sessionSaveEvents, 2, "session save callback count")
|
|
AssertEqual(sessionProfile.settings.savedState[9][4], sessionState.moves, "auto-saved move count")
|
|
|
|
sessionState.score = 500
|
|
sessionState.pointsToLevelUp = 500
|
|
sessionState.level = 1
|
|
sessionState.pointMultiplier = 1
|
|
sessionState.levelPending = true
|
|
local levelSourceMove = { status = "complete" }
|
|
session:HandleMoveComplete(levelSourceMove)
|
|
assert(session:IsLevelTransitionPending(), "session did not retain the presentation handoff")
|
|
assert(session:IsLocked(), "pending level transition did not lock input")
|
|
assert(not sessionPool:GetFrame(1, 1).mouseEnabled, "pending level transition left gems interactive")
|
|
AssertEqual(sessionState.level, 1, "level advanced before presentation completion")
|
|
assert(sessionState.levelPending, "pending level flag was consumed before presentation completion")
|
|
AssertEqual(#sessionLevelStartedEvents, 1, "level-transition start callback count")
|
|
AssertEqual(sessionLevelStartedEvents[1].kind, "level-up", "classic level-transition kind")
|
|
AssertEqual(sessionLevelStartedEvents[1].oldLevel, 1, "level-transition starting level")
|
|
AssertEqual(sessionLevelStartedEvents[1].level, 2, "level-transition target level")
|
|
AssertEqual(sessionSounds[#sessionSounds], "LevelUp", "level-transition sound")
|
|
sessionLevelStartedEvents[1].level = 99
|
|
levelSourceMove.levelTransition.oldLevel = 99
|
|
AssertEqual(session:GetLevelTransition().level, 2, "callback mutated active level-transition record")
|
|
AssertEqual(session:GetLevelTransition().oldLevel, 1, "source move mutated active level-transition record")
|
|
local transitionSaveSucceeded = pcall(function()
|
|
session:SaveClassicGame("during-level-transition")
|
|
end)
|
|
assert(not transitionSaveSucceeded, "session saved an incomplete level transition")
|
|
session:Pause("level-transition")
|
|
session:Resume("level-transition")
|
|
assert(session:IsLocked(), "pause cycle released the level-transition lock")
|
|
assert(not sessionPool:GetFrame(1, 1).mouseEnabled, "pause cycle re-enabled input during level transition")
|
|
|
|
local completedLevelTransition = session:CompleteLevelTransition()
|
|
AssertEqual(completedLevelTransition.status, "complete", "level-transition completion status")
|
|
AssertEqual(completedLevelTransition.level, 2, "session-advanced level")
|
|
AssertEqual(completedLevelTransition.pointMultiplier, 1.5, "session-advanced point multiplier")
|
|
AssertEqual(completedLevelTransition.pointsToLevelUp, 1975, "session-advanced level threshold")
|
|
assert(not sessionState.levelPending, "completed transition retained the pending level flag")
|
|
assert(not session:IsLevelTransitionPending(), "completed level transition remained active")
|
|
assert(not session:IsLocked(), "completed level transition left input locked")
|
|
assert(sessionPool:GetFrame(1, 1).mouseEnabled, "completed level transition left gems disabled")
|
|
AssertEqual(#sessionLevelCompleteEvents, 1, "level-transition completion callback count")
|
|
AssertEqual(#sessionSaveEvents, 3, "level-transition autosave callback count")
|
|
assert(levelSourceMove.saveResult and levelSourceMove.saveResult.status == "saved", "level transition was not auto-saved")
|
|
AssertEqual(sessionProfile.settings.savedState[9][2], 1975, "auto-saved level threshold")
|
|
AssertEqual(sessionProfile.settings.savedState[9][3], 2, "auto-saved advanced level")
|
|
|
|
session.deferLevelTransitions = false
|
|
sessionState.score = 1975
|
|
sessionState.levelPending = true
|
|
local automaticLevelMove = { status = "complete" }
|
|
session:HandleMoveComplete(automaticLevelMove)
|
|
assert(not session:IsLevelTransitionPending(), "automatic level transition remained deferred")
|
|
AssertEqual(sessionState.level, 3, "automatic session-advanced level")
|
|
AssertEqual(sessionState.pointMultiplier, 2, "automatic session-advanced point multiplier")
|
|
AssertEqual(sessionState.pointsToLevelUp, 5550, "automatic session-advanced threshold")
|
|
AssertEqual(#sessionLevelStartedEvents, 2, "automatic level-transition start callback count")
|
|
AssertEqual(#sessionLevelCompleteEvents, 2, "automatic level-transition completion callback count")
|
|
AssertEqual(#sessionSaveEvents, 4, "automatic level-transition autosave callback count")
|
|
assert(automaticLevelMove.saveResult, "automatic level transition omitted stable-state autosave")
|
|
end
|
|
TestSessionRestore()
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
for x = 2, 5 do
|
|
cascadeGrid:Set(x, 8, 7)
|
|
end
|
|
local powerTarget = cascadeGrid:Get(4, 8)
|
|
local powerCascade = addon.Cascade:Step(cascadeGrid, addon.Matches:Find(cascadeGrid, {
|
|
preferredCell = powerTarget,
|
|
}), {
|
|
random = MakeRandom(2345),
|
|
requireLegalMove = false,
|
|
})
|
|
AssertEqual(#powerCascade.spawnedSpecials, 1, "power-gem spawn count")
|
|
AssertEqual(powerCascade.spawnedSpecials[1].kind, "power", "power-gem cascade kind")
|
|
AssertEqual(powerCascade.removedCount, 3, "power-gem preserved removal count")
|
|
AssertEqual(cascadeGrid:Get(4, 8).contents, 7, "power-gem preserved color")
|
|
assert(cascadeGrid:Get(4, 8).bigStar, "power gem was not preserved")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
for x = 2, 6 do
|
|
cascadeGrid:Set(x, 8, 5)
|
|
end
|
|
local hyperTarget = cascadeGrid:Get(4, 8)
|
|
local hyperCascade = addon.Cascade:Step(cascadeGrid, addon.Matches:Find(cascadeGrid, {
|
|
preferredCell = hyperTarget,
|
|
}), {
|
|
random = MakeRandom(3456),
|
|
requireLegalMove = false,
|
|
})
|
|
AssertEqual(#hyperCascade.spawnedSpecials, 1, "hyper-gem spawn count")
|
|
AssertEqual(hyperCascade.spawnedSpecials[1].kind, "hyper", "hyper-gem cascade kind")
|
|
AssertEqual(hyperCascade.removedCount, 4, "hyper-gem preserved removal count")
|
|
AssertEqual(cascadeGrid:Get(4, 8).contents, addon.Constants.HYPER_CONTENTS, "hyper gem was not preserved")
|
|
assert(not cascadeGrid:Get(4, 8).bigStar, "hyper gem retained a power marker")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
for x = 3, 5 do
|
|
cascadeGrid:Set(x, 8, 6, x == 4)
|
|
end
|
|
cascadeGrid:Set(5, 7, cascadeGrid:Get(5, 7).contents, true)
|
|
local explosionCascade = addon.Cascade:Step(cascadeGrid, addon.Matches:Find(cascadeGrid), {
|
|
random = MakeRandom(4567),
|
|
requireLegalMove = false,
|
|
})
|
|
AssertEqual(explosionCascade.triggeredPowerCount, 2, "chained power-gem trigger count")
|
|
AssertEqual(explosionCascade.clearCount, 11, "chained power-gem neighborhood clear count")
|
|
AssertEqual(explosionCascade.removedCount, 11, "chained power-gem neighborhood removal count")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
cascadeGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
local hyperTargetContents = cascadeGrid:Get(2, 8).contents
|
|
local hyperTargetCount = 0
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
if cascadeGrid:Get(x, y).contents == hyperTargetContents then
|
|
hyperTargetCount = hyperTargetCount + 1
|
|
end
|
|
end
|
|
end
|
|
cascadeGrid:Swap(1, 8, 2, 8)
|
|
local hyperResolution = addon.Cascade:ResolveHyper(cascadeGrid, {
|
|
targetContents = hyperTargetContents,
|
|
doubleHyper = false,
|
|
activations = {
|
|
{ x = 1, y = 8, consumedX = 2, consumedY = 8 },
|
|
},
|
|
}, {
|
|
random = MakeRandom(4680),
|
|
requireLegalMove = false,
|
|
})
|
|
local hyperActivationStep = hyperResolution.steps[1]
|
|
assert(hyperResolution.hyper and hyperResolution.stable, "hyper resolution did not stabilize")
|
|
AssertEqual(#hyperActivationStep.hyperActivations, 1, "colored hyper activation count")
|
|
AssertEqual(#hyperActivationStep.hyperAwards, hyperTargetCount + 1, "colored hyper award count")
|
|
AssertEqual(hyperActivationStep.clearCount, hyperTargetCount + 1, "colored hyper clear count")
|
|
AssertEqual(#hyperActivationStep.lightningLinks, hyperTargetCount - 1, "colored hyper lightning-link count")
|
|
AssertEqual(hyperActivationStep.hyperAwards[1].kind, "hyper-destroy", "colored hyper destruction award kind")
|
|
AssertEqual(hyperActivationStep.hyperAwards[2].kind, "hyper-chain", "colored hyper chain award kind")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
cascadeGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
local hyperPowerTarget = cascadeGrid:Get(2, 8).contents
|
|
local hyperPowerCell
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
local cell = cascadeGrid:Get(x, y)
|
|
if cell.contents == hyperPowerTarget and not (x == 2 and y == 8) then
|
|
hyperPowerCell = cell
|
|
break
|
|
end
|
|
end
|
|
if hyperPowerCell then
|
|
break
|
|
end
|
|
end
|
|
assert(hyperPowerCell, "hyper power-target test could not find a matching color")
|
|
cascadeGrid:Set(hyperPowerCell.gridX, hyperPowerCell.gridY, hyperPowerTarget, true)
|
|
cascadeGrid:Swap(1, 8, 2, 8)
|
|
local hyperPowerResolution = addon.Cascade:ResolveHyper(cascadeGrid, {
|
|
targetContents = hyperPowerTarget,
|
|
doubleHyper = false,
|
|
activations = {
|
|
{ x = 1, y = 8, consumedX = 2, consumedY = 8 },
|
|
},
|
|
}, {
|
|
random = MakeRandom(4682),
|
|
requireLegalMove = false,
|
|
})
|
|
local hyperPowerStep = hyperPowerResolution.steps[1]
|
|
AssertEqual(hyperPowerStep.triggeredPowerCount, 1, "hyper-targeted power-gem trigger count")
|
|
assert(hyperPowerStep.clearCount > #hyperPowerStep.hyperAwards, "hyper-targeted power gem did not expand its clear")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
cascadeGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
cascadeGrid:Set(2, 8, addon.Constants.HYPER_CONTENTS)
|
|
cascadeGrid:Set(4, 4, addon.Constants.HYPER_CONTENTS)
|
|
cascadeGrid:Swap(1, 8, 2, 8)
|
|
local doubleHyperResolution = addon.Cascade:ResolveHyper(cascadeGrid, {
|
|
targetContents = addon.Constants.HYPER_CONTENTS,
|
|
doubleHyper = true,
|
|
activations = {
|
|
{ x = 1, y = 8, consumedX = 2, consumedY = 8 },
|
|
{ x = 2, y = 8, consumedX = 1, consumedY = 8 },
|
|
},
|
|
}, {
|
|
random = MakeRandom(4681),
|
|
requireLegalMove = false,
|
|
})
|
|
local doubleHyperStep = doubleHyperResolution.steps[1]
|
|
AssertEqual(#doubleHyperStep.hyperActivations, 2, "double-hyper activation count")
|
|
AssertEqual(#doubleHyperStep.hyperAwards, 5, "double-hyper award count")
|
|
AssertEqual(doubleHyperStep.clearCount, 3, "double-hyper clear count")
|
|
AssertEqual(#doubleHyperStep.lightningLinks, 1, "double-hyper lightning-link count")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
cascadeGrid:Set(1, 8, addon.Constants.HYPER_CONTENTS)
|
|
cascadeGrid:Swap(1, 8, 2, 8)
|
|
local staleHyperBoard = cascadeGrid:ExportLegacyBoard()
|
|
local staleHyperSucceeded = pcall(function()
|
|
addon.Cascade:ResolveHyper(cascadeGrid, {
|
|
targetContents = 1,
|
|
doubleHyper = false,
|
|
activations = {
|
|
{ x = 1, y = 8, consumedX = 2, consumedY = 8 },
|
|
},
|
|
}, { requireLegalMove = false })
|
|
end)
|
|
assert(not staleHyperSucceeded, "stale hyper plan was accepted")
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(
|
|
cascadeGrid:EncodeLegacyValue(cascadeGrid:Get(x, y)),
|
|
staleHyperBoard[y][x],
|
|
"failed hyper resolution was not atomic"
|
|
)
|
|
end
|
|
end
|
|
|
|
local cancelledLightningRun = animations:Play(hyperActivationStep, cascadeGrid)
|
|
AssertEqual(#animations.activeLightning, hyperTargetCount - 1, "active hyper lightning count")
|
|
assert(animations:Cancel("lightning-cancel"), "hyper lightning cancellation failed")
|
|
assert(cancelledLightningRun.cancelled, "cancelled hyper lightning run was not marked")
|
|
AssertEqual(#animations.activeLightning, 0, "cancelled hyper lightning remained active")
|
|
AssertEqual(#animations.lightningPool, hyperTargetCount - 1, "cancelled hyper lightning was not pooled")
|
|
|
|
FillStablePattern(cascadeGrid)
|
|
for x = 1, 3 do
|
|
cascadeGrid:Set(x, 8, 1)
|
|
end
|
|
local resolvedCascade = addon.Cascade:Resolve(cascadeGrid, {
|
|
random = MakeRandom(5678),
|
|
requireLegalMove = false,
|
|
})
|
|
assert(resolvedCascade.stable, "cascade resolution did not report stability")
|
|
assert(resolvedCascade.cascadeCount >= 1, "cascade resolution skipped an existing match")
|
|
assert(not cascadeGrid:HasAnyMatch(), "cascade resolution left a match on the board")
|
|
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
cascadeGrid:Set(x, y, 1)
|
|
end
|
|
end
|
|
local limitedCascadeSucceeded = pcall(function()
|
|
addon.Cascade:Resolve(cascadeGrid, {
|
|
random = function() return 1 end,
|
|
requireLegalMove = false,
|
|
maximumCascades = 1,
|
|
})
|
|
end)
|
|
assert(not limitedCascadeSucceeded, "cascade limit did not reject an endless refill")
|
|
for y = 1, addon.Constants.GRID_HEIGHT do
|
|
for x = 1, addon.Constants.GRID_WIDTH do
|
|
AssertEqual(cascadeGrid:Get(x, y).contents, 1, "failed cascade resolution was not atomic")
|
|
end
|
|
end
|
|
|
|
local scoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local scoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local simpleScoring = addon.Scoring:ApplyCascade(scoringState, { steps = { simpleCascade } }, scoringProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
AssertEqual(simpleScoring.points, 10, "classic three-match score")
|
|
AssertEqual(scoringState.score, 10, "classic score state")
|
|
AssertEqual(simpleScoring.combo, 1, "classic three-match combo")
|
|
AssertEqual(scoringState.combo, 0, "combo was not reset after a stable move")
|
|
AssertEqual(scoringState.largestCombo, 1, "largest combo state")
|
|
AssertEqual(scoringState.largestCascade, 3, "largest cascade state")
|
|
AssertEqual(scoringProfile.stats.totalGemsMatched, 3, "legacy refill-backed gem statistic")
|
|
AssertEqual(scoringProfile.stats.gemMatch[1], 1, "per-color match statistic")
|
|
AssertEqual(scoringProfile.skill.skillPoints, 1, "match-three skill gain")
|
|
|
|
local moveProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local moveState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, { moves = 99 })
|
|
local recordedMove = addon.Scoring:RecordMove(moveState, moveProfile, { random = function() return 1 end })
|
|
AssertEqual(recordedMove.moves, 100, "classic recorded move count")
|
|
AssertEqual(recordedMove.skillEvents[1].index, addon.Constants.SKILL_MOVE100, "classic move-100 skill index")
|
|
local timedMoveProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local timedMoveState = addon.Scoring:NewState(addon.Constants.GAME_MODE_TIMED, { moves = 4 })
|
|
addon.Scoring:RecordMove(timedMoveState, timedMoveProfile)
|
|
AssertEqual(timedMoveProfile.stats.timed.mostMoves, 5, "timed most-moves statistic")
|
|
|
|
local powerScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local powerScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local powerScoring = addon.Scoring:ApplyCascade(powerScoringState, { steps = { powerCascade } }, powerScoringProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
AssertEqual(powerScoring.points, 45, "four-match power-gem score")
|
|
AssertEqual(powerScoringProfile.stats.totalPowerGems, 1, "power-gem creation statistic")
|
|
|
|
local hyperScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local hyperScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local hyperScoring = addon.Scoring:ApplyCascade(hyperScoringState, { steps = { hyperCascade } }, hyperScoringProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
AssertEqual(hyperScoring.points, 30, "five-match hyper-gem score")
|
|
AssertEqual(hyperScoringProfile.stats.totalHyperGems, 1, "hyper-gem creation statistic")
|
|
|
|
local explosionScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local explosionScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local explosionScoring = addon.Scoring:ApplyCascade(
|
|
explosionScoringState,
|
|
{ steps = { explosionCascade } },
|
|
explosionScoringProfile,
|
|
{ random = function() return 1 end }
|
|
)
|
|
AssertEqual(explosionScoring.points, 75, "chained power-gem score")
|
|
AssertEqual(#explosionScoring.scoreEvents, 2, "chained power-gem score event count")
|
|
AssertEqual(explosionScoring.scoreEvents[2].kind, "power-trigger", "chained power-gem score event kind")
|
|
AssertEqual(explosionScoring.scoreEvents[2].points, 40, "second power-gem explosion score")
|
|
|
|
local activationScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local activationScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local activationScoring = addon.Scoring:ApplyCascade(
|
|
activationScoringState,
|
|
{ steps = { hyperActivationStep } },
|
|
activationScoringProfile,
|
|
{ random = function() return 1 end }
|
|
)
|
|
AssertEqual(activationScoring.points, 75 + 20 * hyperTargetCount, "colored hyper activation score")
|
|
AssertEqual(activationScoring.scoreEvents[1].kind, "hyper-destroy", "colored hyper first score kind")
|
|
AssertEqual(activationScoring.scoreEvents[2].kind, "hyper-chain", "colored hyper electro score kind")
|
|
|
|
local doubleHyperScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local doubleHyperScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local doubleHyperScoring = addon.Scoring:ApplyCascade(
|
|
doubleHyperScoringState,
|
|
{ steps = { doubleHyperStep } },
|
|
doubleHyperScoringProfile,
|
|
{ random = function() return 1 end }
|
|
)
|
|
AssertEqual(doubleHyperScoring.points, 210, "double-hyper activation score")
|
|
AssertEqual(#doubleHyperScoring.scoreEvents, 5, "double-hyper score event count")
|
|
|
|
local crossPowerGrid = addon.Grid:New()
|
|
crossPowerGrid:Set(3, 1, 6)
|
|
crossPowerGrid:Set(3, 2, 6)
|
|
crossPowerGrid:Set(3, 3, 6, true)
|
|
crossPowerGrid:Set(2, 3, 6)
|
|
crossPowerGrid:Set(4, 3, 6)
|
|
local crossPowerCascade = addon.Cascade:Step(crossPowerGrid, addon.Matches:Find(crossPowerGrid), {
|
|
random = MakeRandom(6789),
|
|
requireLegalMove = false,
|
|
})
|
|
AssertEqual(crossPowerCascade.matchAwards[1].powerTriggerCount, 2, "cross intersection power mark count")
|
|
local crossPowerProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local crossPowerState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
|
local crossPowerScoring = addon.Scoring:ApplyCascade(
|
|
crossPowerState,
|
|
{ steps = { crossPowerCascade } },
|
|
crossPowerProfile,
|
|
{ random = function() return 1 end }
|
|
)
|
|
AssertEqual(crossPowerScoring.points, 95, "cross-intersection power-gem score")
|
|
|
|
local timedScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local timedScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_TIMED)
|
|
local timedScoring = addon.Scoring:ApplyCascade(timedScoringState, { steps = { simpleCascade } }, timedScoringProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
AssertEqual(timedScoring.points, 150, "timed three-match score")
|
|
AssertEqual(timedScoringState.pointsToLevelUp, 7500, "timed initial level threshold")
|
|
|
|
local tieredSkillProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
tieredSkillProfile.skill.skillPoints = 10
|
|
local failedTieredSkill = addon.Scoring:AttemptSkill(
|
|
tieredSkillProfile,
|
|
addon.Constants.SKILL_TYPE_MATCH,
|
|
addon.Constants.SKILL_MATCH3,
|
|
{ random = function() return 100 end }
|
|
)
|
|
AssertEqual(failedTieredSkill.gained, 0, "tier-one skill failure")
|
|
AssertEqual(tieredSkillProfile.skill.skillPoints, 10, "failed skill changed points")
|
|
local gainedTieredSkill = addon.Scoring:AttemptSkill(
|
|
tieredSkillProfile,
|
|
addon.Constants.SKILL_TYPE_MATCH,
|
|
addon.Constants.SKILL_MATCH3,
|
|
{ random = function() return 1 end }
|
|
)
|
|
AssertEqual(gainedTieredSkill.gained, 1, "tier-one skill gain")
|
|
AssertEqual(tieredSkillProfile.skill.skillPoints, 11, "successful skill points")
|
|
|
|
local achievementProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
achievementProfile.skill.skillPoints = 150
|
|
local achievement = addon.Scoring:AttemptSkill(
|
|
achievementProfile,
|
|
addon.Constants.SKILL_TYPE_ACHIEVEMENT,
|
|
addon.Constants.ACHIEVEMENT_POWER100,
|
|
{ random = function() return 1 end }
|
|
)
|
|
assert(achievement.completed, "achievement was not completed")
|
|
AssertEqual(achievement.awardAmount, 5, "achievement award amount")
|
|
assert(achievementProfile.skill.gainAchieve4, "achievement wire flag was not set")
|
|
|
|
local levelingProfile = addon.SavedVariables:CreateDefaultProfile()
|
|
local levelingState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, { score = 490 })
|
|
local thresholdScoring = addon.Scoring:ApplyCascade(levelingState, { steps = { simpleCascade } }, levelingProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
assert(thresholdScoring.levelPending, "level threshold was not detected")
|
|
local levelAdvance = addon.Scoring:AdvanceLevel(levelingState, levelingProfile, {
|
|
random = function() return 1 end,
|
|
})
|
|
AssertEqual(levelAdvance.level, 2, "advanced level")
|
|
AssertEqual(levelAdvance.pointMultiplier, 1.5, "advanced level point multiplier")
|
|
AssertEqual(levelAdvance.pointsToLevelUp, 1975, "advanced level threshold")
|
|
AssertEqual(levelingProfile.stats.classic.highestLevel, 2, "classic highest-level statistic")
|
|
|
|
eventFrame.scripts.OnEvent(eventFrame, "ADDON_LOADED", "AnotherAddon", false)
|
|
assert(not addon.initialized, "foreign ADDON_LOADED initialized the addon")
|
|
eventFrame.scripts.OnEvent(eventFrame, "ADDON_LOADED", "Bejeweled", false)
|
|
assert(addon.initialized, "addon initialization did not complete")
|
|
assert(addon.grid, "addon initialization did not create a grid")
|
|
assert(addon.audio, "addon initialization did not create audio")
|
|
assert(addon.backdrops == addon.Backdrops, "addon initialization did not install backdrops")
|
|
assert(addon.gemPoolFactory == addon.GemPool, "addon initialization did not install GemPool")
|
|
assert(addon.animationFactory == addon.Animations, "addon initialization did not install Animations")
|
|
assert(addon.inputFactory == addon.Input, "addon initialization did not install Input")
|
|
assert(addon.sessionFactory == addon.Session, "addon initialization did not install Session")
|
|
assert(eventFrame.registeredEvent == nil, "initializer event was not unregistered")
|
|
local initializedGrid = addon.grid
|
|
local initializedAudio = addon.audio
|
|
local initializedBackdrops = addon.backdrops
|
|
local initializedGemPoolFactory = addon.gemPoolFactory
|
|
local initializedAnimationFactory = addon.animationFactory
|
|
local initializedInputFactory = addon.inputFactory
|
|
local initializedSessionFactory = addon.sessionFactory
|
|
addon:Initialize({}, {})
|
|
assert(addon.grid == initializedGrid, "addon initialization is not idempotent")
|
|
assert(addon.audio == initializedAudio, "audio initialization is not idempotent")
|
|
assert(addon.backdrops == initializedBackdrops, "backdrop initialization is not idempotent")
|
|
assert(addon.gemPoolFactory == initializedGemPoolFactory, "GemPool initialization is not idempotent")
|
|
assert(addon.animationFactory == initializedAnimationFactory, "Animations initialization is not idempotent")
|
|
assert(addon.inputFactory == initializedInputFactory, "Input initialization is not idempotent")
|
|
assert(addon.sessionFactory == initializedSessionFactory, "Session initialization is not idempotent")
|
|
|
|
print("Runtime verification passed: pause/restore/level-transition sessions, input, cascade animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.")
|