Files
wow_bejeweled/tools/test-runtime.lua
T

2773 lines
126 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/Fonts.lua", addon)
LoadAddonFile("Bejeweled/UI/Backdrops.lua", addon)
LoadAddonFile("Bejeweled/UI/GemPool.lua", addon)
LoadAddonFile("Bejeweled/UI/Animations.lua", addon)
LoadAddonFile("Bejeweled/UI/HUD.lua", addon)
LoadAddonFile("Bejeweled/UI/Summary.lua", addon)
LoadAddonFile("Bejeweled/UI/Skills.lua", addon)
LoadAddonFile("Bejeweled/UI/Options.lua", addon)
LoadAddonFile("Bejeweled/UI/About.lua", addon)
LoadAddonFile("Bejeweled/UI/Legal.lua", addon)
LoadAddonFile("Bejeweled/UI/MainWindow.lua", addon)
LoadAddonFile("Bejeweled/UI/Compartment.lua", addon)
LoadAddonFile("Bejeweled/UI/Minimap.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")
do
local fontAttempts = {}
local rejectingBundledFont = {}
function rejectingBundledFont:SetFont(path, size, flags)
fontAttempts[#fontAttempts + 1] = { path, size, flags }
return #fontAttempts > 1
end
local selectedFont, usedFallback = addon.Fonts:Set(rejectingBundledFont, 14, "OUTLINE", "Fonts\\TestFallback.ttf")
AssertEqual(fontAttempts[1][1], addon.Fonts.BUNDLED_FONT_PATH, "bundled font was not attempted first")
AssertEqual(fontAttempts[2][1], "Fonts\\TestFallback.ttf", "standard font fallback path")
AssertEqual(selectedFont, "Fonts\\TestFallback.ttf", "selected fallback font path")
assert(usedFallback, "rejected bundled font did not report fallback use")
end
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:ClearAllPoints()
self.points = {}
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:SetVertexColor(red, green, blue, alpha)
self.vertexColor = { red, green, blue, 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 CreateMockFontString(layer)
local fontString = CreateMockTexture(layer)
function fontString:ClearAllPoints()
self.points = {}
end
function fontString:SetText(text)
self.text = text
end
function fontString:SetTextColor(red, green, blue, alpha)
self.textColor = { red, green, blue, alpha }
end
function fontString:SetFont(path, size, flags)
self.font = { path, size, flags }
return true
end
function fontString:SetJustifyH(justify)
self.justifyH = justify
end
return fontString
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:SetMinMaxValues(minimum, maximum)
self.minimum = minimum
self.maximum = maximum
end
function frame:SetValueStep(step)
self.valueStep = step
end
function frame:SetObeyStepOnDrag(obey)
self.obeyStepOnDrag = obey
end
function frame:SetOrientation(orientation)
self.orientation = orientation
end
function frame:SetThumbTexture(path)
self.thumbTexture = CreateMockTexture("ARTWORK")
self.thumbTexture.path = path
end
function frame:GetThumbTexture()
return self.thumbTexture
end
function frame:SetValue(value)
if self.minimum then
value = math.max(self.minimum, math.min(self.maximum, value))
end
if self.valueStep then
value = math.floor(value / self.valueStep + 0.5) * self.valueStep
end
self.value = value
if self.scripts.OnValueChanged then
self.scripts.OnValueChanged(self, value, false)
end
end
function frame:GetValue()
return self.value
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:CreateFontString(name, layer)
local fontString = CreateMockFontString(layer)
self.createdFontStrings = self.createdFontStrings or {}
self.createdFontStrings[#self.createdFontStrings + 1] = fontString
return fontString
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:SetBackdrop(descriptor)
self.descriptor = descriptor
end
function frame:SetBackdropColor(red, green, blue, alpha)
self.backgroundColor = { red, green, blue, alpha }
end
function frame:SetBackdropBorderColor(red, green, blue, alpha)
self.borderColor = { red, green, blue, alpha }
end
function frame:SetFrameLevel(frameLevel)
self.frameLevel = frameLevel
end
function frame:SetFrameStrata(frameStrata)
self.frameStrata = frameStrata
end
function frame:GetWidth()
return self.width
end
function frame:GetHeight()
return self.height
end
function frame:IsShown()
return self.shown and true or false
end
function frame:GetFrameLevel()
if self.frameLevel then
return self.frameLevel
end
if self.parent and type(self.parent.GetFrameLevel) == "function" then
return self.parent:GetFrameLevel()
end
return 0
end
function frame:EnableMouse(enabled)
self.mouseEnabled = enabled
end
function frame:SetMovable(movable)
self.movable = movable
end
function frame:SetClampedToScreen(clamped)
self.clamped = clamped
end
function frame:StartMoving()
self.moving = true
end
function frame:StopMovingOrSizing()
self.moving = false
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,
GetEffectiveScale = function()
return 1
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")
animations:ClearTransientEffects()
assert(#animations.shardPool >= 30, "cascade clears did not emit pooled ten-shard bursts")
animations.random = MakeRandom(7007)
animations.hintDelay = 0.05
local hint = animations:ShowHint(2, 3)
assert(not hint.shown, "new hint bypassed its delay")
AssertEqual(hint.texture.path, addon.Constants.IMAGE_ROOT .. "hintArrow", "hint texture")
animations:UpdateEffects(0.025)
animations:UpdateEffects(0.025)
assert(not hint.shown, "hint appeared at rather than after its delay")
animations:UpdateEffects(0.025)
assert(hint.shown, "hint did not appear after its delay")
AssertEqual(hint.bounceY, 1, "hint initial bounce step")
local pausedHintBounce = hint.bounceY
animations:Pause()
assert(not animations:UpdateEffects(1), "paused effects clock advanced")
AssertEqual(hint.bounceY, pausedHintBounce, "paused hint continued bouncing")
animations:Resume()
assert(animations:HideHint(), "active hint was not hidden")
assert(not hint.shown, "hidden hint remained visible")
local shard = animations:PlayShard(2, 3, 4, {
xVelocity = 1,
yVelocity = 11,
effectFrame = 12,
})
AssertEqual(shard.texture.path, addon.Constants.IMAGE_ROOT .. "gemshards", "shard texture")
AssertEqual(shard.texture.vertexColor[1], addon.Constants.GEM_EFFECT_COLORS[4][1], "shard color")
local shardX = shard.x
animations:UpdateEffects(0.025)
AssertEqual(shard.effectFrame, 1, "shard atlas wrap")
AssertEqual(shard.x, shardX + 1, "shard horizontal integration")
AssertEqual(shard.yVelocity, 11.5, "shard gravity integration")
AssertEqual(shard.alpha, 0.85, "shard fade threshold")
for _ = 1, 20 do
animations:UpdateEffects(0.025)
end
AssertEqual(#animations.activeShards, 0, "finished shard remained active")
local pooledShard = animations.shardPool[#animations.shardPool]
local reusedShard = animations:PlayShard(1, 1, 3, { effectFrame = 1 })
assert(reusedShard == pooledShard, "shard pool did not reuse its frame")
animations:ClearTransientEffects()
local scoreText = animations:PlayFloatingText(100, 100, 30, 3, false)
AssertEqual(scoreText.text, "30", "floating score text")
AssertEqual(scoreText.font[1], addon.Constants.IMAGE_ROOT .. "Contb___.ttf", "floating text font")
AssertEqual(scoreText.textColor[3], addon.Constants.GEM_EFFECT_COLORS[3][3], "floating score color")
animations:UpdateEffects(0.025)
AssertEqual(scoreText.y, 99.75, "floating score upward motion")
for _ = 1, 80 do
animations:UpdateEffects(0.025)
end
AssertEqual(#animations.activeFloatingText, 0, "finished floating score remained active")
AssertEqual(#animations.floatingTextPool, 1, "floating score was not pooled")
local skillText = animations:PlayFloatingText(120, 80, "+1 Skill", 3, true)
assert(skillText == scoreText, "floating text pool did not reuse its font string")
AssertEqual(skillText.textColor[1], 1, "nonscore floating text red channel")
AssertEqual(skillText.textColor[2], 0.4, "nonscore floating text green channel")
animations:UpdateEffects(0.025)
AssertEqual(skillText.y, 80.25, "nonscore floating text downward motion")
animations:ClearTransientEffects()
local lightwave = animations:PlayLightwave(1, 1, 0)
AssertEqual(lightwave.effectFrame, -1, "lightwave initial delay frame")
AssertEqual(lightwave.texture.texCoord[1], 0.33, "lightwave atlas cell")
animations:UpdateEffects(0.025)
animations:UpdateEffects(0.025)
AssertEqual(lightwave.effectFrame, 1, "lightwave activation frame")
AssertEqual(lightwave.alpha, 0.9, "lightwave activation alpha")
AssertEqual(#animations.activeLightwaves, 2, "lightwave did not propagate right")
AssertEqual(animations.activeLightwaves[2].column, 2, "propagated lightwave column")
animations:ClearTransientEffects()
animations.lightwavePeriod = 0.05
assert(animations:SetAmbientLightwaves(true), "ambient lightwaves were not enabled")
animations:UpdateEffects(0.025)
animations:UpdateEffects(0.025)
animations:UpdateEffects(0.025)
AssertEqual(#animations.activeLightwaves, addon.Constants.GRID_HEIGHT, "ambient lightwave row count")
assert(animations:SetAmbientLightwaves(false), "ambient lightwaves were not disabled")
AssertEqual(#animations.activeLightwaves, 0, "disabled ambient lightwaves remained active")
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,
detectGameOver = false,
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
FillStablePattern(sessionGrid)
sessionGrid:Set(1, 1, addon.Constants.HYPER_CONTENTS)
local transitionPowerContents = sessionGrid:Get(2, 1).contents
sessionGrid:Set(2, 1, transitionPowerContents, true)
sessionPool:Project(sessionGrid, 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(completedLevelTransition.boardReset, "level transition did not regenerate the board")
AssertEqual(completedLevelTransition.preservedHyperGems, 1, "level transition hyper preservation record")
AssertEqual(completedLevelTransition.preservedPowerGems, 1, "level transition power preservation record")
local resetHyperCount = 0
local resetPowerCount = 0
for row = 1, addon.Constants.GRID_HEIGHT do
for column = 1, addon.Constants.GRID_WIDTH do
local resetCell = sessionGrid:Get(column, row)
if resetCell.contents == addon.Constants.HYPER_CONTENTS then
resetHyperCount = resetHyperCount + 1
end
if resetCell.bigStar then
resetPowerCount = resetPowerCount + 1
end
end
end
AssertEqual(resetHyperCount, 1, "regenerated level board hyper count")
AssertEqual(resetPowerCount, 1, "regenerated level board power count")
assert(sessionGrid:FindLegalMove(), "regenerated level board has no legal move")
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
local function TestGameOverTransitions()
local gameOverGrid = addon.Grid:New()
FillStablePattern(gameOverGrid)
assert(not gameOverGrid:FindLegalMove(), "game-over fixture unexpectedly has a legal move")
local gameOverPool = addon.GemPool:New(gemPoolParent, {
createFrame = CreateGemPoolFrame,
createBoardTiles = false,
})
gameOverPool:Project(gameOverGrid, true)
local gameOverAnimations = addon.Animations:New(gameOverPool, {
createFrame = CreateGemPoolFrame,
})
local gameOverProfile = addon.SavedVariables:CreateDefaultProfile()
gameOverProfile.skill.skillPoints = 75
local gameOverAccount = addon.SavedVariables:CreateDefaultAccount()
gameOverAccount.played.OtherCharacter = 99
local gameOverState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, {
score = 4321,
level = 5,
pointsToLevelUp = 8000,
pointMultiplier = 3,
largestCascade = 12,
largestCombo = 6,
moves = 88,
})
local gameOverStarted = {}
local gameOverCompleted = {}
local gameOverSounds = {}
local gameOverSession = addon.Session:New(gameOverGrid, gameOverPool, gameOverAnimations, {
profile = gameOverProfile,
accountData = gameOverAccount,
playerName = "Nighthawk",
scoringState = gameOverState,
timerElapsed = 75.5,
deferGameOverTransitions = true,
inputOptions = {
random = function() return 1 end,
audio = {
Play = function(_, soundName)
gameOverSounds[#gameOverSounds + 1] = soundName
end,
},
},
onGameOverStarted = function(result)
gameOverStarted[#gameOverStarted + 1] = result
end,
onGameOverComplete = function(result)
gameOverCompleted[#gameOverCompleted + 1] = result
end,
})
assert(not gameOverSession:GetInput().requireLegalMove, "runtime input still forces legal cascade refills")
gameOverSession:SaveClassicGame("game-over-fixture")
assert(gameOverSession:HasClassicGame(), "game-over fixture did not create a resumable game")
local terminalMove = { status = "complete" }
gameOverSession:HandleMoveComplete(terminalMove)
assert(gameOverSession:IsGameOver(), "no-legal-move board did not end the session")
assert(gameOverSession:IsGameOverTransitionPending(), "game-over presentation was not deferred")
assert(not gameOverSession.active, "game-over session remained active")
assert(gameOverSession:IsLocked(), "game-over transition did not retain the input lock")
assert(not gameOverPool:GetFrame(1, 1).mouseEnabled, "game-over transition left gems interactive")
AssertEqual(#gameOverStarted, 1, "game-over start callback count")
AssertEqual(gameOverStarted[1].cause, "no-legal-move", "game-over cause")
AssertEqual(gameOverStarted[1].kind, "no-more-moves", "classic game-over presentation kind")
AssertEqual(gameOverStarted[1].score, 4321, "game-over final score snapshot")
AssertEqual(gameOverStarted[1].totalGames, 100, "account-wide completed-game count")
AssertEqual(gameOverProfile.skill.games, 1, "profile completed-game count")
AssertEqual(gameOverAccount.played.Nighthawk, 1, "account character completed-game count")
assert(gameOverProfile.skill.gainAchieve3, "100-game achievement was not checked")
AssertEqual(gameOverSounds[1], "NoMoreMoves", "classic game-over sound")
AssertEqual(gameOverSounds[2], "WipeBoard", "game-over board-wipe sound")
assert(not gameOverSession:HasClassicGame(), "completed Classic game remained resumable")
for row = 1, addon.Constants.GRID_HEIGHT + 1 do
for column = 1, #(gameOverProfile.settings.savedState[row]) do
AssertEqual(gameOverProfile.settings.savedState[row][column], 0, "cleared Classic save wire value")
end
end
gameOverStarted[1].score = 0
terminalMove.gameOverTransition.level = 0
AssertEqual(gameOverSession:GetGameOverTransition().score, 4321, "callback mutated active game-over record")
AssertEqual(gameOverSession:GetGameOverTransition().level, 5, "source move mutated active game-over record")
gameOverSession:Pause("game-over")
gameOverSession:Resume("game-over")
assert(gameOverSession:IsLocked(), "pause cycle released the terminal input lock")
local classicSummary = gameOverSession:CompleteGameOver()
AssertEqual(classicSummary.status, "complete", "game-over completion status")
AssertEqual(classicSummary.metricName, "score", "classic summary metric name")
AssertEqual(classicSummary.metric, 4321, "classic summary metric")
assert(classicSummary.personalBest.updated, "classic personal best was not updated")
AssertEqual(gameOverProfile.stats.classic.score, 4321, "persisted Classic personal best")
local classicBestPayload = addon.SavedVariables:VerifyAuthenticatedPayload(
gameOverProfile.stats.classic.data,
addon.SavedVariables:ByteSum("Nighthawk")
)
AssertEqual(addon.SavedVariables:DecodeBase70(classicBestPayload), 4321, "authenticated Classic personal best")
local retainedClassicData = gameOverProfile.stats.classic.data
local lowerClassicBest = addon.SavedVariables:UpdatePersonalBest(
gameOverProfile,
addon.Constants.GAME_MODE_CLASSIC,
4000,
"Nighthawk"
)
assert(not lowerClassicBest.updated, "lower Classic score replaced the personal best")
AssertEqual(gameOverProfile.stats.classic.data, retainedClassicData, "lower Classic score replaced authenticated data")
assert(not gameOverSession:IsGameOverTransitionPending(), "completed game-over transition remained pending")
assert(gameOverSession:IsLocked(), "completed game-over transition released terminal input")
AssertEqual(gameOverSession:HandleCell(1, 1).status, "locked", "completed session accepted input")
AssertEqual(#gameOverCompleted, 1, "game-over completion callback count")
AssertEqual(terminalMove.gameOverComplete.metric, 4321, "source move summary handoff")
classicSummary.score = 0
AssertEqual(gameOverSession:GetGameOverSummary().score, 4321, "returned summary mutated retained handoff")
local timedGrid = addon.Grid:New()
FillStablePattern(timedGrid)
local timedPool = addon.GemPool:New(gemPoolParent, {
createFrame = CreateGemPoolFrame,
createBoardTiles = false,
})
timedPool:Project(timedGrid, true)
local timedAnimations = addon.Animations:New(timedPool, {
createFrame = CreateGemPoolFrame,
})
local timedProfile = addon.SavedVariables:CreateDefaultProfile()
timedProfile.skill.skillPoints = 225
local timedAccount = addon.SavedVariables:CreateDefaultAccount()
local timedSounds = {}
local timedState = addon.Scoring:NewState(addon.Constants.GAME_MODE_TIMED, {
score = 3000,
level = 2,
largestCascade = 7,
largestCombo = 4,
moves = 25,
})
local timedSession = addon.Session:New(timedGrid, timedPool, timedAnimations, {
profile = timedProfile,
accountData = timedAccount,
playerName = "Nighthawk",
gameMode = addon.Constants.GAME_MODE_TIMED,
scoringState = timedState,
timerElapsed = 9,
timeLimit = 10,
detectGameOver = false,
inputOptions = {
random = function() return 1 end,
audio = {
Play = function(_, soundName)
timedSounds[#timedSounds + 1] = soundName
end,
},
},
})
AssertEqual(timedSession:AdvanceElapsed(2), 10, "timed session did not clamp at its limit")
assert(timedSession:IsGameOver(), "expired timed session did not end")
assert(not timedSession:IsGameOverTransitionPending(), "automatic timed summary remained deferred")
local timedSummary = timedSession:GetGameOverSummary()
AssertEqual(timedSummary.cause, "time-expired", "timed game-over cause")
AssertEqual(timedSummary.kind, "time-up", "timed game-over presentation kind")
AssertEqual(timedSummary.metricName, "points-per-second", "timed summary metric name")
AssertEqual(timedSummary.metric, 300, "timed points-per-second metric")
assert(timedSummary.personalBest.updated, "timed personal best was not updated")
AssertEqual(timedProfile.stats.timed.score, 300, "persisted timed personal best")
AssertEqual(timedProfile.stats.timed.played, 1, "timed playtime persistence")
AssertEqual(timedProfile.stats.played, 1, "total playtime persistence")
AssertEqual(#timedSummary.skillEvents, 2, "timed PPS skill-check count")
AssertEqual(timedSummary.skillEvents[1].index, addon.Constants.SKILL_PPS250, "250-PPS skill check")
AssertEqual(timedSummary.skillEvents[2].index, addon.Constants.SKILL_PPS300, "300-PPS skill check")
local timedBestPayload = addon.SavedVariables:VerifyAuthenticatedPayload(
timedProfile.stats.timed.data,
addon.SavedVariables:ByteSum("Nighthawk")
)
AssertEqual(addon.SavedVariables:DecodeBase70(timedBestPayload), 30000, "authenticated timed personal best")
AssertEqual(timedSounds[1], "TimesUp", "timed game-over sound")
AssertEqual(timedSounds[2], "WipeBoard", "timed board-wipe sound")
timedPool:SetInteractive(true)
timedSession:GetInput():SetSessionLocked(true, "terminal-refresh")
assert(not timedPool:GetFrame(1, 1).mouseEnabled, "idempotent terminal lock did not reassert interaction state")
end
function addon:TestHUDPresentationForTest()
local hudGrid = addon.Grid:New()
FillStablePattern(hudGrid)
hudGrid:Set(2, 8, 1)
hudGrid:Set(3, 8, 1)
hudGrid:Set(1, 7, 1)
assert(hudGrid:FindLegalMove(), "HUD fixture has no hintable move")
local hudPool = addon.GemPool:New(gemPoolParent, {
createFrame = CreateGemPoolFrame,
createBoardTiles = false,
})
hudPool:Project(hudGrid, true)
local hudAnimations = addon.Animations:New(hudPool, {
createFrame = CreateGemPoolFrame,
hintDelay = 0.05,
})
local hudProfile = addon.SavedVariables:CreateDefaultProfile()
local hudChatMessages = {}
local hudState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, {
score = 12345,
level = 3,
pointsToLevelUp = 16000,
pointMultiplier = 2,
largestCascade = 7,
largestCombo = 4,
moves = 20,
})
local userPauseEvents = 0
local userLevelEvents = 0
local hud = addon.HUD:New(gemPoolParent, hudAnimations, {
createFrame = CreateGemPoolFrame,
statusDuration = 0.1,
achievementDuration = 0.1,
profile = hudProfile,
chatMessage = function(message)
hudChatMessages[#hudChatMessages + 1] = message
return true
end,
skillLabel = function(event)
return event.index == addon.Constants.ACHIEVEMENT_POWER100 and "I've Got The Power!" or nil
end,
skillDescription = function(event)
return event.index == addon.Constants.ACHIEVEMENT_POWER100 and "Get a Power Gem total of 100+" or nil
end,
})
local hudSession = hud:CreateSession(hudGrid, hudPool, {
profile = hudProfile,
playerName = "Nighthawk",
scoringState = hudState,
autoSave = false,
detectGameOver = false,
deferLevelTransitions = true,
deferGameOverTransitions = true,
inputOptions = {
random = MakeRandom(9201),
requireLegalMove = false,
},
onPauseChanged = function()
userPauseEvents = userPauseEvents + 1
end,
onLevelTransitionComplete = function()
userLevelEvents = userLevelEvents + 1
end,
})
assert(hud.session == hudSession, "HUD did not attach its created session")
AssertEqual(hud.levelPanel.caption.text, "LVL", "classic HUD level caption")
AssertEqual(hud.levelPanel.value.text, "3", "classic HUD level value")
AssertEqual(hud.dataPanel.value.text, "12,345", "classic HUD formatted score")
AssertEqual(hud.progress.ratio, 12345 / 16000, "classic HUD progress ratio")
AssertEqual(hud.levelPanel.width, 94, "classic HUD level capsule width")
AssertEqual(hud.dataPanel.width, 128, "classic HUD score capsule width")
AssertEqual(hud.progress.width, 236, "classic HUD progress width")
AssertEqual(hud.progress.fill.points[1][1], "TOPLEFT", "classic HUD progress fill anchor")
AssertEqual(hud.progress.fill.points[1][5], -5, "classic HUD progress fill vertical offset")
assert(hudAnimations.hint and hudAnimations.hint.active, "HUD did not schedule an idle hint")
hudSession:Pause("hud-test")
assert(hud.pausedFrame.shown, "paused HUD overlay remained hidden")
assert(not hudAnimations.hint.active, "paused HUD retained its hint")
hudSession:Resume("hud-test")
assert(not hud.pausedFrame.shown, "resumed HUD retained its pause overlay")
assert(hudAnimations.hint.active, "resumed HUD did not reschedule its hint")
AssertEqual(userPauseEvents, 2, "HUD callback wiring replaced user pause callbacks")
local skillEvent = {
type = addon.Constants.SKILL_TYPE_ACHIEVEMENT,
index = addon.Constants.ACHIEVEMENT_POWER100,
gained = 5,
completed = true,
pointsAfter = 80,
}
local floatingBefore = #hudAnimations.activeFloatingText
hud:OnCascadeResolved({
scoringResult = {
points = 75,
scoreEvents = { { contents = 3 } },
skillEvents = { skillEvent },
},
})
AssertEqual(#hudAnimations.activeFloatingText, floatingBefore + 2, "HUD omitted score or achievement floating text")
AssertEqual(hud.achievementFrame.text.text, "Achievement: I've Got The Power!", "HUD achievement message")
AssertEqual(#hudChatMessages, 1, "HUD local skill chat message count")
AssertEqual(
hudChatMessages[1],
'[Bejeweled Addon] You just completed "Get a Power Gem total of 100+." +5 Skill!',
"HUD local skill chat message"
)
AssertEqual(hud:PresentSkillEvents({ skillEvent }), 0, "HUD repeated an already-presented skill event")
AssertEqual(#hudChatMessages, 1, "HUD repeated local skill chat output")
local rankMessages = hud:BuildSkillChatMessages({
gained = 1,
pointsAfter = 75,
rankUp = true,
rankAfter = 2,
})
AssertEqual(#rankMessages, 2, "HUD rank-up local message count")
AssertEqual(rankMessages[1], "Your skill in Bejeweling has increased to 75.", "HUD rank-up skill message")
AssertEqual(rankMessages[2], "Your Bejeweling skill is now of the Journeyman rank.", "HUD rank-up message")
hud:Update(0.11)
assert(not hud.achievementFrame.shown, "HUD achievement notice did not expire")
hudAnimations:ClearTransientEffects()
hudState.score = hudState.pointsToLevelUp
hudState.levelPending = true
hudSession:HandleMoveComplete({ status = "complete" })
assert(hudSession:IsLevelTransitionPending(), "HUD level fixture did not defer its transition")
AssertEqual(hud.statusFrame.text.text, "Level up", "HUD level-start status")
assert(hud.statusFrame.shown, "HUD level-start status remained hidden")
local hudLevelResult = hudSession:CompleteLevelTransition()
AssertEqual(hudLevelResult.level, 4, "HUD level transition result")
AssertEqual(hud.levelPanel.value.text, "4", "HUD did not refresh the completed level")
AssertEqual(hud.statusFrame.text.text, "Level 4", "HUD level-complete status")
AssertEqual(userLevelEvents, 1, "HUD callback wiring replaced user level callbacks")
hud:Update(0.11)
assert(not hud.statusFrame.shown, "HUD temporary status did not expire")
local gameOverStarted = hudSession:BeginGameOver("manual-test")
AssertEqual(gameOverStarted.kind, "no-more-moves", "HUD classic game-over kind")
AssertEqual(hud.statusFrame.text.text, "No More Moves", "HUD game-over status")
hud:Update(10)
assert(hud.statusFrame.shown, "persistent HUD game-over status expired")
local hudSummary = hudSession:CompleteGameOver()
assert(hudSummary.personalBest, "HUD game-over fixture omitted personal-best data")
assert(hud.summaryFrame.shown, "HUD final summary remained hidden")
assert(string.find(hud.summaryFrame.text.text, "Score: 16,000", 1, true), "HUD summary omitted the formatted score")
assert(string.find(hud.summaryFrame.text.text, "Largest cascade: 7", 1, true), "HUD summary omitted cascade data")
assert(not hud.statusFrame.shown, "HUD final summary retained the game-over banner")
local timedGrid = addon.Grid:New()
FillStablePattern(timedGrid)
local timedPool = addon.GemPool:New(gemPoolParent, {
createFrame = CreateGemPoolFrame,
createBoardTiles = false,
})
timedPool:Project(timedGrid, true)
local timedAnimations = addon.Animations:New(timedPool, { createFrame = CreateGemPoolFrame })
local timedHUD = addon.HUD:New(gemPoolParent, timedAnimations, { createFrame = CreateGemPoolFrame })
local timedProfile = addon.SavedVariables:CreateDefaultProfile()
local timedState = addon.Scoring:NewState(addon.Constants.GAME_MODE_TIMED, {
score = 300,
pointMultiplier = 2.5,
})
local timedSession = timedHUD:CreateSession(timedGrid, timedPool, {
profile = timedProfile,
playerName = "Nighthawk",
gameMode = addon.Constants.GAME_MODE_TIMED,
scoringState = timedState,
timerElapsed = 30,
timeLimit = 60,
detectGameOver = false,
autoSave = false,
inputOptions = { requireLegalMove = false },
})
AssertEqual(timedHUD.levelPanel.caption.text, "PPS", "timed HUD PPS caption")
AssertEqual(timedHUD.levelPanel.value.text, "10.00", "timed HUD PPS value")
AssertEqual(timedHUD.dataPanel.value.text, "2.5x", "timed HUD multiplier")
AssertEqual(timedHUD.progress.text.text, "0:30", "timed HUD countdown")
AssertEqual(timedHUD.progress.ratio, 0.5, "timed HUD progress ratio")
timedSession:SetElapsed(60)
timedHUD:Update(0)
AssertEqual(timedHUD.progress.text.text, "0:00", "timed HUD zero countdown")
AssertEqual(timedHUD.progress.ratio, 0, "timed HUD empty progress")
end
function addon:TestMainWindowForTest()
local runtimeProfile = addon.SavedVariables:CreateDefaultProfile()
local runtimeAccount = addon.SavedVariables:CreateDefaultAccount()
local runtimeAudio = {
played = {},
elapsed = 0,
}
function runtimeAudio:Play(soundName)
self.played[#self.played + 1] = soundName
return true
end
function runtimeAudio:Update(elapsed)
self.elapsed = self.elapsed + elapsed
return {}
end
local sessionStarts = {}
local sessionStops = {}
local settingsChanges = {}
local flightState
local flightRequests = {}
local runtime = addon.MainWindow:New(gemPoolParent, {
createFrame = CreateGemPoolFrame,
profile = runtimeProfile,
accountData = runtimeAccount,
audio = runtimeAudio,
playerName = "Nighthawk",
random = MakeRandom(12001),
onSessionStarted = function(result)
sessionStarts[#sessionStarts + 1] = result
end,
onSessionStopped = function(result)
sessionStops[#sessionStops + 1] = result
end,
onSettingsChanged = function(key, value)
settingsChanges[#settingsChanges + 1] = { key = key, value = value }
end,
flightOptionProvider = function()
return flightState
end,
onFlightTimedRequested = function(state, window)
flightRequests[#flightRequests + 1] = state
return window:StartTimed(state.seconds)
end,
})
AssertEqual(runtime.frame.width, addon.MainWindow.WINDOW_WIDTH, "runtime window width")
AssertEqual(runtime.frame.height, addon.MainWindow.WINDOW_HEIGHT, "runtime window height")
AssertEqual(runtime.boardSurface.width, 400, "runtime board width")
AssertEqual(runtime.boardSurface.height, 400, "runtime board height")
AssertEqual(#runtime.gemPool.tiles, 16, "runtime board tile count")
AssertEqual(runtime.summary.frame.width, 400, "runtime summary width")
AssertEqual(runtime.summary.frame.height, 400, "runtime summary height")
AssertEqual(runtime.skills.frame.width, 400, "runtime skill-screen width")
AssertEqual(runtime.skills.frame.height, 400, "runtime skill-screen height")
AssertEqual(runtime.options.frame.width, 400, "runtime options width")
AssertEqual(runtime.about.frame.width, 400, "runtime About width")
AssertEqual(runtime.legal.frame.width, 400, "runtime legal width")
runtime:Show()
assert(runtime.frame.shown, "runtime window did not show")
assert(runtime:IsShown(), "runtime visibility state did not follow Show")
AssertEqual(runtime.activeOverlay, "legal", "runtime initial legal notice")
assert(runtime.legal:IsShown(), "runtime initial legal notice remained hidden")
assert(string.find(runtime.legal.frame.text.text, "PopCap Games", 1, true), "runtime legal notice omitted PopCap attribution")
runtime.legal.okayButton.scripts.OnClick()
assert(runtimeAccount.legalDisplayed, "runtime legal acknowledgement was not persisted")
AssertEqual(runtime.activeOverlay, "menu", "runtime initial menu")
assert(not runtime.overlays.menu.resume.shown, "runtime initial menu exposed Resume")
runtimeProfile.skill.rank = 3
runtimeProfile.skill.skillPoints = 175
runtimeProfile.skill.gainFun1 = true
runtimeProfile.skill.gainAchieve1 = true
runtimeProfile.stats.classic.score = 123456
runtimeProfile.stats.timed.score = 42.5
runtimeProfile.stats.gemMatch[4] = 99
runtimeAccount.played.Nighthawk = 12
runtime.overlays.menu.skills.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "skills", "runtime Feats action did not open the skill screen")
assert(runtime.skills:IsShown(), "runtime skill screen remained hidden")
AssertEqual(runtime.skills.frame.rank.text, "Bejeweling Skill Rank: Expert", "runtime skill rank caption")
AssertEqual(runtime.skills.progress.text.text, "175 / 225", "runtime skill progress caption")
AssertEqual(runtime.skills.progress.ratio, 25 / 75, "runtime skill rank progress")
AssertEqual(runtime.skills.progress.fill.path, addon.Constants.IMAGE_ROOT .. "barArt", "runtime skill progress texture")
AssertEqual(runtime.skills.progress.fill.points[1][1], "LEFT", "runtime skill progress fill anchor")
AssertEqual(runtime.skills.progress.fill.points[1][5], 0, "runtime skill progress fill alignment")
local visibleSkills = runtime.skills:GetVisibleRecords()
AssertEqual(visibleSkills[1].category, "Match Gems", "runtime first skill category")
AssertEqual(visibleSkills[1].name, "Match 5 Gems (Create a |cFFA335EE[Hyper Cube]|r)", "runtime first skill challenge")
visibleSkills[1].name = "changed by caller"
AssertEqual(runtime.skills:GetVisibleRecords()[1].name, "Match 5 Gems (Create a |cFFA335EE[Hyper Cube]|r)", "runtime skill records were not copied")
runtime.skills.statisticsTab.scripts.OnClick()
AssertEqual(runtime.skills.activeTab, "statistics", "runtime statistics tab selection")
local visibleStatistics = runtime.skills:GetVisibleRecords()
AssertEqual(visibleStatistics[1].label, "Classic High Score", "runtime first personal statistic")
AssertEqual(visibleStatistics[1].value, "123,456", "runtime formatted Classic high score")
AssertEqual(runtime.skills:GetPageCount(), 3, "runtime statistics page count")
runtime.skills.leaderboardsTab.scripts.OnClick()
AssertEqual(runtime.skills.activeTab, "leaderboards", "runtime leaderboard tab selection")
AssertEqual(runtime.skills.scopeButton.label.text, "Guild", "runtime default leaderboard scope")
AssertEqual(runtime.skills.modeButton.label.text, "Classic", "runtime default leaderboard mode")
local visibleScores = runtime.skills:GetVisibleRecords()
AssertEqual(visibleScores[1].name, "PopCap Games", "runtime first local leaderboard name")
AssertEqual(visibleScores[1].formattedScore, "1,000", "runtime first local leaderboard score")
runtime.skills.scopeButton.scripts.OnClick()
AssertEqual(runtime.skills.leaderboardScope, "friends", "runtime leaderboard scope toggle")
runtime.skills.modeButton.scripts.OnClick()
AssertEqual(runtime.skills.leaderboardMode, "timed", "runtime leaderboard mode toggle")
runtime.skills.achievementsTab.scripts.OnClick()
AssertEqual(runtime.skills.activeTab, "achievements", "runtime achievement tab selection")
AssertEqual(runtime.skills.frame.status.text, "Unlocked 13 / 26 Completed 2", "runtime achievement counts")
AssertEqual(runtime.skills:GetPageSize(), addon.Skills.ACHIEVEMENT_PAGE_SIZE, "runtime achievement page size")
AssertEqual(runtime.skills.rows[1].height, 38, "runtime achievement row height")
AssertEqual(runtime.skills.rows[1].icon.width, 32, "runtime achievement icon width")
AssertEqual(runtime.skills.rows[1].icon.height, 32, "runtime achievement icon height")
AssertEqual(runtime.skills.rows[1].title.points[1][4], 42, "runtime achievement title inset")
AssertEqual(runtime.skills.rows[1].description.points[1][4], 42, "runtime achievement description inset")
local visibleAchievements = runtime.skills:GetVisibleRecords()
assert(visibleAchievements[1].completed, "runtime completed achievements were not sorted first")
AssertEqual(runtime.skills:GetPageCount(), 3, "runtime achievement page count")
runtime.skills.nextButton.scripts.OnClick()
AssertEqual(runtime.skills.page, 2, "runtime achievement next-page action")
assert(runtime.skills.previousButton.shown, "runtime achievement previous-page action remained hidden")
runtime.skills.backButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "menu", "runtime skill-screen Back action")
assert(not runtime.skills:IsShown(), "runtime skill-screen Back action retained the screen")
runtimeProfile.skill.rank = 1
runtimeProfile.skill.skillPoints = 0
runtimeProfile.skill.gainFun1 = nil
runtimeProfile.skill.gainAchieve1 = nil
runtimeProfile.stats.classic.score = 0
runtimeProfile.stats.timed.score = 0
runtimeProfile.stats.gemMatch[4] = 0
runtimeAccount.played.Nighthawk = nil
runtime.overlays.menu.settings.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "options", "runtime Settings action")
assert(runtime.options:IsShown(), "runtime Settings screen remained hidden")
runtime.options.rows[1].scripts.OnClick()
AssertEqual(runtimeProfile.settings.gameAlpha, 0.9, "runtime game-opacity setting")
AssertEqual(runtime.frame.alpha, 0.9, "runtime game-opacity application")
runtime.options.rows[3].scripts.OnClick()
AssertEqual(runtime.options:GetSoundMode(), "Quiet", "runtime sound-mode setting")
runtime.options.rows[4].scripts.OnClick()
assert(runtimeProfile.settings.disableHints, "runtime hint setting")
assert(not runtime.hud:AreHintsEnabled(), "runtime hint setting did not reach the HUD")
runtime.options.rows[6].scripts.OnClick()
assert(runtimeProfile.settings.hideMinimap, "runtime minimap visibility setting")
AssertEqual(settingsChanges[#settingsChanges].key, "hideMinimap", "runtime minimap setting callback")
runtime.options.rows[6].scripts.OnClick()
assert(not runtimeProfile.settings.hideMinimap, "runtime minimap visibility setting did not toggle off")
runtime.options.backButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "menu", "runtime Settings Back action")
runtimeProfile.settings.gameAlpha = 1
runtimeProfile.settings.disableHints = nil
runtimeProfile.settings.quietSounds = nil
runtimeProfile.settings.enableSounds = 1
runtime:ApplySettings("gameAlpha")
runtime.overlays.menu.about.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "about", "runtime About action")
AssertEqual(runtime.about.activeTab, "tutorial", "runtime default About tab")
runtime.about.tabs.story.scripts.OnClick()
AssertEqual(runtime.about.activeTab, "story", "runtime About story tab")
assert(string.find(runtime.about.contents.story.text.text, "github.com/Nighthawk42", 1, true), "runtime About story omitted project home")
runtime.about.tabs.credits.scripts.OnClick()
AssertEqual(runtime.about.activeTab, "credits", "runtime About credits tab")
runtime.about.backButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "menu", "runtime About Back action")
runtime.overlays.menu.legal.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "legal", "runtime manual Legal action")
runtime.legal.okayButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "menu", "runtime Legal acknowledgement action")
runtime.overlays.menu.newGame.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "mode", "runtime New Game did not show mode selection")
runtime.overlays.mode.classic.scripts.OnClick()
local firstClassic = runtime.session
assert(firstClassic and firstClassic.active, "runtime Classic session did not start")
assert(not runtime.animations:IsPaused(), "runtime Classic session inherited the menu pause")
AssertEqual(firstClassic.gameMode, addon.Constants.GAME_MODE_CLASSIC, "runtime Classic mode")
assert(runtime.grid:FindLegalMove(), "runtime Classic board has no legal move")
assert(runtimeProfile.settings.classicInProgress, "new Classic runtime was not resumable")
assert(type(runtime.gemPool:GetFrame(1, 1).scripts.OnMouseDown) == "function", "runtime did not attach gem input")
AssertEqual(sessionStarts[1].status, "started", "runtime Classic start callback")
runtime:Update(1.5)
AssertEqual(firstClassic.timerElapsed, 1.5, "runtime update did not advance session time")
AssertEqual(runtimeAudio.elapsed, 1.5, "runtime update did not flush audio")
runtime:ShowMenu()
assert(firstClassic:IsPaused(), "runtime menu did not pause Classic play")
assert(runtime.overlays.menu.resume.shown, "active runtime menu hid Resume")
assert(not runtime.hud.statusFrame.shown, "runtime menu retained the HUD status banner")
assert(not runtime.hud.achievementFrame.shown, "runtime menu retained the HUD achievement banner")
assert(not runtime.hud.pausedFrame.shown, "runtime menu retained the HUD paused banner")
runtime.overlays.menu.resume.scripts.OnClick()
assert(not firstClassic:IsPaused(), "runtime Resume left Classic paused")
assert(runtime.activeOverlay == nil, "runtime Resume retained a menu overlay")
firstClassic.scoringState.score = 2468
firstClassic:SaveClassicGame("continue-fixture")
runtime:ShowMenu()
runtime.overlays.menu.newGame.scripts.OnClick()
runtime.overlays.mode.classic.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "classic", "saved Classic did not show Continue/New Game")
runtime.overlays.classic.continue.scripts.OnClick()
local restoredClassic = runtime.session
assert(restoredClassic ~= firstClassic, "runtime Continue reused the abandoned session")
assert(not runtime.animations:IsPaused(), "runtime Continue inherited the menu pause")
assert(not firstClassic.active and firstClassic:IsLocked(), "runtime replacement did not deactivate the old session")
AssertEqual(restoredClassic.scoringState.score, 2468, "runtime Continue score")
AssertEqual(sessionStarts[2].status, "restored", "runtime Continue callback")
AssertEqual(#sessionStops, 1, "runtime Continue omitted session-stop callback")
runtime:ShowMenu()
runtime.overlays.menu.newGame.scripts.OnClick()
runtime.overlays.mode.timed.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "timed", "runtime Timed choice did not show setup")
AssertEqual(runtime.overlays.timed.slider.minimum, addon.MainWindow.MIN_TIMED_MINUTES, "runtime Timed minimum minutes")
AssertEqual(runtime.overlays.timed.slider.maximum, addon.MainWindow.MAX_TIMED_MINUTES, "runtime Timed maximum minutes")
AssertEqual(runtime.overlays.timed.slider.valueStep, 1, "runtime Timed minute step")
assert(runtime.overlays.timed.slider.obeyStepOnDrag, "runtime Timed slider did not obey its minute step")
AssertEqual(runtime.overlays.timed.slider.value, 5, "runtime Timed default minutes")
AssertEqual(runtime.overlays.timed.durationValue.text, "5 Minutes", "runtime Timed default caption")
assert(not runtime.overlays.timed.flightToggle.shown, "runtime exposed flight timing without adapter state")
runtime.overlays.timed.slider:SetValue(10)
AssertEqual(runtime.overlays.timed.durationValue.text, "10 Minutes", "runtime Timed upper-bound caption")
runtime.overlays.timed.slider:SetValue(2)
AssertEqual(runtime.overlays.timed.durationValue.text, "2 Minutes", "runtime Timed lower-bound caption")
runtime.overlays.timed.slider:SetValue(5)
runtime.overlays.timed.go.scripts.OnClick()
local timedRuntime = runtime.session
AssertEqual(timedRuntime.gameMode, addon.Constants.GAME_MODE_TIMED, "runtime Timed mode")
assert(not runtime.animations:IsPaused(), "runtime Timed session inherited the menu pause")
assert(runtime.gemPool:GetFrame(1, 1).mouseEnabled, "runtime Timed replacement left gem input disabled")
AssertEqual(timedRuntime.timeLimit, addon.MainWindow.DEFAULT_TIMED_DURATION, "runtime default Timed duration")
AssertEqual(runtime.hud.levelPanel.caption.text, "PPS", "runtime Timed HUD mode")
AssertEqual(sessionStarts[3].timeLimit, addon.MainWindow.DEFAULT_TIMED_DURATION, "runtime Timed callback duration")
assert(not restoredClassic.active, "runtime Timed start left Classic active")
timedRuntime.scoringState.score = 900
timedRuntime.scoringState.level = 2
timedRuntime.scoringState.largestCascade = 8
timedRuntime.scoringState.largestCombo = 3
timedRuntime.scoringState.moves = 12
timedRuntime:SetElapsed(60)
timedRuntime:BeginGameOver("runtime-restart-fixture")
assert(timedRuntime:IsGameOver() and timedRuntime:IsLocked(), "runtime Timed fixture did not reach terminal state")
assert(timedRuntime:GetGameOverSummary(), "runtime Timed summary handoff is unavailable")
AssertEqual(runtime.activeOverlay, "summary", "completed runtime did not open the summary")
assert(runtime.summary:IsShown(), "runtime summary frame remained hidden")
assert(not runtime.hud.summaryFrame.shown, "runtime retained the compact HUD summary")
AssertEqual(runtime.summary.frame.mode.text, "Timed Mode", "runtime summary mode")
AssertEqual(runtime.summary.metrics.primary.caption.text, "Points per Second", "runtime summary primary caption")
AssertEqual(runtime.summary.metrics.primary.value.text, "15.00", "runtime summary primary value")
AssertEqual(runtime.summary.metrics.time.value.text, "1 min 0 sec", "runtime summary elapsed time")
AssertEqual(runtime.summary.metrics.level.value.text, "2", "runtime summary level")
AssertEqual(runtime.summary.metrics.cascade.value.text, "8", "runtime summary largest cascade")
AssertEqual(runtime.summary.metrics.combo.value.text, "3", "runtime summary largest combo")
AssertEqual(runtime.summary.metrics.moves.value.text, "12", "runtime summary moves")
AssertEqual(runtime.summary.metrics.best.value.text, "15.00", "runtime summary personal best")
runtime.summary.menuButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "menu", "runtime summary Menu action")
assert(not runtime.summary:IsShown(), "runtime summary Menu action retained the summary")
assert(not runtime.overlays.menu.resume.shown, "terminal runtime menu exposed Resume")
local classicSummaryFixture = {
gameMode = addon.Constants.GAME_MODE_CLASSIC,
metricName = "score",
metric = 1234567,
elapsed = 75.9,
level = 9,
largestCascade = 14,
largestCombo = 6,
moves = 44,
personalBest = { best = 1234567 },
}
runtime:ShowSummary(classicSummaryFixture)
AssertEqual(runtime.summary.frame.mode.text, "Classic Mode", "runtime Classic summary mode")
AssertEqual(runtime.summary.metrics.primary.caption.text, "Final Score", "runtime Classic summary primary caption")
AssertEqual(runtime.summary.metrics.primary.value.text, "1,234,567", "runtime Classic summary formatted score")
AssertEqual(runtime.summary.metrics.time.value.text, "1 min 15 sec", "runtime Classic summary floored time")
classicSummaryFixture.metric = 0
AssertEqual(runtime.summary:GetResult().metric, 1234567, "runtime summary retained provider-owned result state")
runtime.summary.newGameButton.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "mode", "runtime summary New Game action")
assert(not runtime.summary:IsShown(), "runtime summary New Game action retained the summary")
timedRuntime = runtime:StartTimed(60)
assert(runtime.gemPool:GetFrame(1, 1).mouseEnabled, "runtime terminal replacement left gem input disabled")
AssertEqual(timedRuntime.timeLimit, 60, "runtime custom Timed duration")
flightState = { seconds = 59, learning = false, routeToken = "short-route" }
runtime:ShowModeMenu()
runtime.overlays.mode.timed.scripts.OnClick()
assert(runtime.overlays.timed.flightWarning.shown, "short flight time warning was hidden")
assert(not runtime.overlays.timed.flightToggle.shown, "short flight time remained selectable")
flightState = { seconds = 12, learning = true, routeToken = "learning-route" }
runtime:RefreshTimedSetup()
assert(runtime.overlays.timed.flightToggle.shown, "flight-learning option was hidden")
AssertEqual(runtime.overlays.timed.flightStatus.text, "Recording flight time: 0 min 12 sec", "flight-learning status")
flightState = { seconds = 125, learning = false, routeToken = "known-route" }
runtime:RefreshTimedSetup()
assert(runtime.overlays.timed.flightToggle.shown, "known flight time option was hidden")
AssertEqual(runtime.overlays.timed.flightStatus.text, "Remaining flight time: 2 min 5 sec", "known flight status")
runtime.overlays.timed.flightToggle.scripts.OnClick()
assert(runtime.flightOptionSelected, "flight timing option did not select")
AssertEqual(runtime.overlays.timed.flightToggle.label.text, "[x] Use flight path time", "flight timing selected caption")
runtime:RefreshTimedSetup()
assert(runtime.flightOptionSelected, "flight timing refresh discarded an active selection")
runtime.overlays.timed.go.scripts.OnClick()
timedRuntime = runtime.session
AssertEqual(#flightRequests, 1, "flight timing request callback count")
assert(flightRequests[1] ~= flightState, "flight timing request leaked provider state")
AssertEqual(flightRequests[1].routeToken, "known-route", "flight timing request context")
AssertEqual(timedRuntime.timeLimit, 125, "flight timing request duration")
assert(runtime.activeOverlay == nil, "accepted flight timing request retained setup")
runtime:Hide()
assert(timedRuntime:IsPaused(), "hidden runtime window did not pause play")
assert(not runtime:IsShown(), "runtime visibility state did not follow Hide")
runtime:Show()
assert(not timedRuntime:IsPaused(), "shown runtime window did not resume its owned pause")
runtime.frame.scripts.OnDragStart(runtime.frame)
assert(runtime.frame.moving, "unlocked runtime window did not start moving")
runtime.frame.scripts.OnDragStop(runtime.frame)
assert(not runtime.frame.moving, "runtime drag stop left the window moving")
runtimeProfile.settings.lockWindow = true
runtime.frame.scripts.OnDragStart(runtime.frame)
assert(not runtime.frame.moving, "locked runtime window started moving")
runtime:ShowMenu()
runtime.overlays.menu.newGame.scripts.OnClick()
runtime.overlays.mode.classic.scripts.OnClick()
AssertEqual(runtime.activeOverlay, "classic", "runtime Classic save chooser was skipped")
runtime.overlays.classic.newGame.scripts.OnClick()
local freshClassic = runtime.session
AssertEqual(freshClassic.scoringState.score, 0, "runtime fresh Classic retained prior score")
assert(freshClassic ~= restoredClassic, "runtime fresh Classic reused a prior session")
AssertEqual(#sessionStarts, 6, "runtime session-start callback count")
AssertEqual(#sessionStops, 5, "runtime session-stop callback count")
end
function addon:TestMinimapButtonForTest()
local minimap = {
left = 100,
bottom = 200,
width = 140,
height = 140,
}
function minimap:GetLeft()
return self.left
end
function minimap:GetBottom()
return self.bottom
end
function minimap:GetWidth()
return self.width
end
function minimap:GetHeight()
return self.height
end
local uiParent = {
GetEffectiveScale = function()
return 2
end,
}
local tooltip = { lines = {} }
function tooltip:SetOwner(owner, anchor)
self.owner = owner
self.anchor = anchor
end
function tooltip:SetText(value)
self.text = value
end
function tooltip:AddLine(value)
self.lines[#self.lines + 1] = value
end
function tooltip:Show()
self.shown = true
end
function tooltip:Hide()
self.shown = false
end
local runtime = { shown = true }
function runtime:Toggle()
self.shown = not self.shown
end
function runtime:IsShown()
return self.shown
end
local cursorX = 340
local cursorY = 680
local settings = {}
local button = self.MinimapButton:New({
minimap = minimap,
uiParent = uiParent,
settings = settings,
createFrame = CreateGemPoolFrame,
tooltip = tooltip,
cursorPosition = function()
return cursorX, cursorY
end,
runtimeProvider = function()
return runtime
end,
})
AssertEqual(button.frame.width, self.MinimapButton.BUTTON_SIZE, "minimap button width")
AssertEqual(button.frame.height, self.MinimapButton.BUTTON_SIZE, "minimap button height")
AssertEqual(button.frame.icon.width, self.MinimapButton.ICON_SIZE, "minimap icon width")
AssertEqual(button.frame.icon.path, self.Constants.IMAGE_ROOT .. "windowIcon", "minimap icon texture")
assert(button.frame.shown, "default minimap button remained hidden")
AssertEqual(button.frame.points[1][2], minimap, "default minimap button parent anchor")
AssertEqual(button.frame.points[1][4], -self.MinimapButton.ATTACHED_RADIUS, "default minimap button radius")
button.frame.scripts.OnMouseDown(button.frame, "LeftButton")
button.frame.scripts.OnMouseUp(button.frame, "LeftButton")
assert(not runtime.shown, "minimap left-click did not hide the runtime")
button.frame.scripts.OnMouseDown(button.frame, "LeftButton")
button.frame.scripts.OnMouseUp(button.frame, "LeftButton")
assert(runtime.shown, "minimap left-click did not restore the runtime")
button.frame.scripts.OnEnter(button.frame)
assert(button.frame.highlight.shown, "minimap hover highlight remained hidden")
AssertEqual(tooltip.owner, button.frame, "minimap tooltip owner")
AssertEqual(tooltip.anchor, "ANCHOR_LEFT", "minimap tooltip anchor")
AssertEqual(tooltip.lines[1], "Left-click to show or hide the game.", "minimap tooltip click help")
button.frame.scripts.OnLeave(button.frame)
assert(not button.frame.highlight.shown and not tooltip.shown, "minimap tooltip or highlight remained visible")
button.frame.scripts.OnMouseDown(button.frame, "RightButton")
button.frame.scripts.OnUpdate(button.frame)
button.frame.scripts.OnMouseUp(button.frame, "RightButton")
assert(not settings.minimapDetached, "near-minimap drag detached the button")
assert(math.abs(settings.minimapAngle - 90) < 0.001, "attached minimap angle was not persisted")
AssertEqual(button.frame.points[1][2], minimap, "attached minimap drag anchor")
cursorX = 1000
cursorY = 1000
button.frame.scripts.OnMouseDown(button.frame, "RightButton")
button.frame.scripts.OnUpdate(button.frame)
button.frame.scripts.OnMouseUp(button.frame, "RightButton")
assert(settings.minimapDetached, "distant minimap drag did not detach the button")
AssertEqual(settings.minimapX, 500, "detached minimap X position")
AssertEqual(settings.minimapY, 500, "detached minimap Y position")
AssertEqual(button.frame.points[1][2], uiParent, "detached minimap parent anchor")
AssertEqual(button.frame.points[1][3], "BOTTOMLEFT", "detached minimap relative anchor")
button:SetHidden(true)
assert(not button.frame.shown and settings.hideMinimap, "minimap visibility setting did not hide the button")
button:SetHidden(false)
assert(button.frame.shown and not settings.hideMinimap, "minimap visibility setting did not restore the button")
end
TestSessionRestore()
TestGameOverTransitions()
addon:TestHUDPresentationForTest()
addon.TestHUDPresentationForTest = nil
addon:TestMainWindowForTest()
addon.TestMainWindowForTest = nil
addon:TestMinimapButtonForTest()
addon.TestMinimapButtonForTest = nil
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.fonts == addon.Fonts, "addon initialization did not install Fonts")
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.hudFactory == addon.HUD, "addon initialization did not install HUD")
assert(addon.summaryFactory == addon.Summary, "addon initialization did not install Summary")
assert(addon.skillsFactory == addon.Skills, "addon initialization did not install Skills")
assert(addon.optionsFactory == addon.Options, "addon initialization did not install Options")
assert(addon.aboutFactory == addon.About, "addon initialization did not install About")
assert(addon.legalFactory == addon.Legal, "addon initialization did not install Legal")
assert(addon.mainWindowFactory == addon.MainWindow, "addon initialization did not install MainWindow")
assert(addon.compartment == addon.Compartment, "addon initialization did not install Compartment")
assert(addon.minimapButtonFactory == addon.MinimapButton, "addon initialization did not install MinimapButton")
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 initializedHUDFactory = addon.hudFactory
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.fonts == addon.Fonts, "Fonts 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.hudFactory == initializedHUDFactory, "HUD initialization is not idempotent")
assert(addon.summaryFactory == addon.Summary, "Summary initialization is not idempotent")
assert(addon.skillsFactory == addon.Skills, "Skills initialization is not idempotent")
assert(addon.optionsFactory == addon.Options, "Options initialization is not idempotent")
assert(addon.aboutFactory == addon.About, "About initialization is not idempotent")
assert(addon.legalFactory == addon.Legal, "Legal initialization is not idempotent")
assert(addon.mainWindowFactory == addon.MainWindow, "MainWindow initialization is not idempotent")
assert(addon.compartment == addon.Compartment, "Compartment initialization is not idempotent")
assert(addon.minimapButtonFactory == addon.MinimapButton, "MinimapButton initialization is not idempotent")
assert(addon.inputFactory == initializedInputFactory, "Input initialization is not idempotent")
assert(addon.sessionFactory == initializedSessionFactory, "Session initialization is not idempotent")
addon.runtimeMinimapForTest = {
left = 100,
bottom = 200,
width = 140,
height = 140,
}
function addon.runtimeMinimapForTest:GetLeft()
return self.left
end
function addon.runtimeMinimapForTest:GetBottom()
return self.bottom
end
function addon.runtimeMinimapForTest:GetWidth()
return self.width
end
function addon.runtimeMinimapForTest:GetHeight()
return self.height
end
addon.runtimeMinimapTooltipForTest = { lines = {} }
function addon.runtimeMinimapTooltipForTest:SetOwner(owner, anchor)
self.owner = owner
self.anchor = anchor
end
function addon.runtimeMinimapTooltipForTest:SetText(value)
self.text = value
end
function addon.runtimeMinimapTooltipForTest:AddLine(value)
self.lines[#self.lines + 1] = value
end
function addon.runtimeMinimapTooltipForTest:Show()
self.shown = true
end
function addon.runtimeMinimapTooltipForTest:Hide()
self.shown = false
end
addon.runtimeForTest = addon:StartRuntime({
uiParent = gemPoolParent,
createFrame = CreateGemPoolFrame,
playerName = "Nighthawk",
random = MakeRandom(13001),
minimap = addon.runtimeMinimapForTest,
minimapTooltip = addon.runtimeMinimapTooltipForTest,
cursorPosition = function()
return 170, 270
end,
})
addon.runtimeMinimapForTest = nil
addon.runtimeMinimapTooltipForTest = nil
assert(addon.runtimeForTest == addon.runtime, "StartRuntime did not retain the playable shell")
assert(addon.minimapButton and addon.minimapButton.frame.shown, "StartRuntime did not create the minimap launcher")
AssertEqual(addon.runtimeForTest.activeOverlay, "legal", "StartRuntime did not open the first-run legal notice")
addon.runtimeForTest.legal.okayButton.scripts.OnClick()
AssertEqual(addon.runtimeForTest.activeOverlay, "menu", "StartRuntime legal acknowledgement did not open the menu")
addon.runtimeForTest:ShowOptions()
addon.runtimeForTest.options.rows[6].scripts.OnClick()
assert(not addon.minimapButton.frame.shown, "live Hide Minimap setting did not hide the launcher")
addon.runtimeForTest.options.rows[6].scripts.OnClick()
assert(addon.minimapButton.frame.shown, "live Hide Minimap setting did not restore the launcher")
addon.runtimeForTest:ShowMenu()
assert(addon:StartRuntime() == addon.runtimeForTest, "StartRuntime is not idempotent")
function addon:TestCompartmentForTest()
local mockRuntime = { visible = false, toggleCount = 0 }
function mockRuntime:Toggle()
self.visible = not self.visible
self.toggleCount = self.toggleCount + 1
return self
end
function mockRuntime:IsShown()
return self.visible
end
local providerCalls = 0
local tooltip = { lines = {} }
function tooltip:SetOwner(owner, anchor)
self.owner = owner
self.anchor = anchor
end
function tooltip:SetText(value)
self.text = value
end
function tooltip:AddLine(...)
self.lines[#self.lines + 1] = { ... }
end
function tooltip:Show()
self.shown = true
end
function tooltip:Hide()
self.shown = false
end
local compartment = self.Compartment:New({
addon = self,
tooltip = tooltip,
runtimeProvider = function()
providerCalls = providerCalls + 1
return mockRuntime
end,
})
local shown = compartment:OnClick("Bejeweled", "LeftButton")
AssertEqual(shown.status, "shown", "pre-runtime compartment show status")
assert(shown.runtime == mockRuntime, "pre-runtime compartment did not retain the provided runtime")
AssertEqual(providerCalls, 1, "pre-runtime provider call count")
local hidden = compartment:OnClick("Bejeweled", "LeftButton")
AssertEqual(hidden.status, "hidden", "compartment hide status")
AssertEqual(mockRuntime.toggleCount, 2, "compartment runtime toggle count")
local ignored = compartment:OnClick("Bejeweled", "RightButton")
AssertEqual(ignored.status, "ignored", "compartment non-left click status")
AssertEqual(providerCalls, 2, "ignored compartment click started a runtime")
local menuButton = {}
assert(compartment:OnEnter("Bejeweled", menuButton), "compartment hover did not show its tooltip")
assert(tooltip.owner == menuButton, "compartment tooltip owner changed")
AssertEqual(tooltip.anchor, "ANCHOR_LEFT", "compartment tooltip anchor")
AssertEqual(tooltip.text, "Bejeweled", "compartment tooltip title")
AssertEqual(tooltip.lines[1][1], "Left-click to show or hide the game.", "compartment tooltip instruction")
assert(tooltip.shown, "compartment tooltip did not show")
assert(compartment:OnLeave("Bejeweled", menuButton), "compartment hover leave was ignored")
assert(not tooltip.shown, "compartment tooltip did not hide")
assert(type(Bejeweled_OnAddonCompartmentClick) == "function", "compartment click global is unavailable")
assert(type(Bejeweled_OnAddonCompartmentEnter) == "function", "compartment enter global is unavailable")
assert(type(Bejeweled_OnAddonCompartmentLeave) == "function", "compartment leave global is unavailable")
local liveHidden = Bejeweled_OnAddonCompartmentClick("Bejeweled", "LeftButton")
AssertEqual(liveHidden.status, "hidden", "live compartment hide status")
assert(not self.runtimeForTest:IsShown(), "live compartment did not hide the runtime")
local liveShown = Bejeweled_OnAddonCompartmentClick("Bejeweled", "LeftButton")
AssertEqual(liveShown.status, "shown", "live compartment show status")
assert(self.runtimeForTest:IsShown(), "live compartment did not restore the runtime")
end
addon:TestCompartmentForTest()
addon.TestCompartmentForTest = nil
print("Runtime verification passed: minimap/addon-compartment access, local skill chat, aligned skill/footer presentation, full local summary, Timed setup/flight boundary, playable Classic/Timed window shell, HUD, pause/restore/level/game-over sessions, input, cascade/effect animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.")