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.")