Learn how to build a memory matching game in LiveCode using modern design patterns and best practices. This tutorial demonstrates UI Router architecture, state management, adaptive layouts, and Halloween-themed design.
This tutorial demonstrates how to build a memory matching game in LiveCode using modern design patterns and best practices. MR Spooky Match is a Halloween-themed card matching game that showcases proper architecture, state management, and user interface design principles.
Learning Objectives:
MR Spooky Match is a single-player memory game where players flip cards to find matching pairs of Halloween emoji icons. The game features three difficulty levels, tracks moves and completion time, and stores best times for each difficulty.
Key Features:
Rather than adding scripts to individual buttons, we implement a UI Router pattern where all mouseUp messages are handled at the stack level. This provides:
Implementation:
on mouseUp
local tTargetName
put the short name of the target into tTargetName
switch tTargetName
case "btnNewGame"
initializeGame
break
case "btnDiffEasy"
changeDifficulty "easy"
break
default
if char 1 to 8 of tTargetName is "btnCard_" then
cardClicked tTargetName
end if
break
end switch
end mouseUp
Consistent naming is critical for maintainability:
g prefix (e.g., gCards, gMoves)t prefix (e.g., tCardIndex, tIcon)p prefix (e.g., pLevel, pCardName)c_ prefix (e.g., c_bestTimeEasy, c_cardIndex)fld prefix (e.g., fldMessage, fldMovesValue)btn prefix (e.g., btnNewGame, btnCard_1)grc prefix (e.g., grcGameBoard)The game state is managed through global variables:
global gCards -- Array of all card data
global gFlippedCards -- Currently flipped card indices
global gMatchedPairs -- Count of matched pairs
global gMoves -- Total moves made
global gCurrentDifficulty -- "easy", "medium", or "hard"
global gIsChecking -- Prevents clicks during match check
global gStartTime -- Game start timestamp
global gBestTimes -- Array of best times per difficulty
global gSpookyIcons -- Line-delimited list of emoji
global gDifficultySettings -- Configuration array
The gCards array structure:
gCards[1]["icon"] -- The emoji icon
gCards[1]["isFlipped"] -- Boolean: face up?
gCards[1]["isMatched"] -- Boolean: matched?
gCards[1]["buttonName"] -- Associated button name
Objectives: Set up project structure, global variables, and UI Router
Key Components:
Custom properties need clear identification. We use a c_ prefix to make them stand out in code reviews:
command saveBestTimes
set the c_bestTimeEasy of this stack to gBestTimes["easy"]
set the c_bestTimeMedium of this stack to gBestTimes["medium"]
set the c_bestTimeHard of this stack to gBestTimes["hard"]
save this stack
end saveBestTimes
Objectives: Implement game logic, card management, and matching
command createCards
local tNumPairs, tPairIndex, tCardIndex, tIcon
put gDifficultySettings[gCurrentDifficulty]["pairs"] into tNumPairs
put empty into gCards
put 1 into tCardIndex
repeat with tPairIndex = 1 to tNumPairs
put line tPairIndex of gSpookyIcons into tIcon
-- Create pair
put tIcon into gCards[tCardIndex]["icon"]
put false into gCards[tCardIndex]["isFlipped"]
put false into gCards[tCardIndex]["isMatched"]
put "btnCard_" & tCardIndex into gCards[tCardIndex]["buttonName"]
add 1 to tCardIndex
-- Second card of pair
put tIcon into gCards[tCardIndex]["icon"]
put false into gCards[tCardIndex]["isFlipped"]
put false into gCards[tCardIndex]["isMatched"]
put "btnCard_" & tCardIndex into gCards[tCardIndex]["buttonName"]
add 1 to tCardIndex
end repeat
end createCards
command shuffleCards
local tNumCards, i, j, tTemp
put gDifficultySettings[gCurrentDifficulty]["pairs"] * 2 into tNumCards
repeat with i = tNumCards down to 2
put random(i) into j
put gCards[i] into tTemp
put gCards[j] into gCards[i]
put tTemp into gCards[j]
end repeat
end shuffleCards
The Fisher-Yates shuffle algorithm ensures a truly random distribution of cards. It works by iterating backwards through the array and swapping each element with a random element from the unshuffled portion.
The board layout adapts to difficulty by calculating card positions:
command renderBoard
local tNumCards, tCols, tRows, tBoardWidth, tBoardHeight
local tCardWidth, tCardHeight, tGap, tStartX, tStartY
local i, tRow, tCol, tCardX, tCardY, tBtnName
-- Get current difficulty settings
put gDifficultySettings[gCurrentDifficulty]["pairs"] * 2 into tNumCards
put gDifficultySettings[gCurrentDifficulty]["cols"] into tCols
put gDifficultySettings[gCurrentDifficulty]["rows"] into tRows
put gDifficultySettings[gCurrentDifficulty]["width"] into tBoardWidth
put gDifficultySettings[gCurrentDifficulty]["height"] into tBoardHeight
-- Resize board graphic
resizeGameBoard tBoardWidth, tBoardHeight
-- Calculate card dimensions
put 10 into tGap
put (tBoardWidth - (tCols + 1) * tGap) / tCols into tCardWidth
put (tBoardHeight - (tRows + 1) * tGap) / tRows into tCardHeight
-- Position each card
put the left of graphic "grcGameBoard" + tGap into tStartX
put the top of graphic "grcGameBoard" + tGap into tStartY
repeat with i = 1 to tNumCards
put ((i - 1) div tCols) into tRow
put ((i - 1) mod tCols) into tCol
put tStartX + (tCol * (tCardWidth + tGap)) into tCardX
put tStartY + (tRow * (tCardHeight + tGap)) into tCardY
put gCards[i]["buttonName"] into tBtnName
set the rect of button tBtnName to tCardX, tCardY, \
(tCardX + tCardWidth), (tCardY + tCardHeight)
-- Configure button
set the label of button tBtnName to "🎃"
set the visible of button tBtnName to true
set the c_cardIndex of button tBtnName to i
end repeat
-- Hide unused cards
repeat with i = (tNumCards + 1) to 32
set the visible of button ("btnCard_" & i) to false
end repeat
end renderBoard
command checkMatch
local tCard1Index, tCard2Index, tIcon1, tIcon2
put true into gIsChecking
put item 1 of gFlippedCards into tCard1Index
put item 2 of gFlippedCards into tCard2Index
put gCards[tCard1Index]["icon"] into tIcon1
put gCards[tCard2Index]["icon"] into tIcon2
if tIcon1 = tIcon2 then
-- Match found
put true into gCards[tCard1Index]["isMatched"]
put true into gCards[tCard2Index]["isMatched"]
add 1 to gMatchedPairs
applyMatchEffect tCard1Index, tCard2Index
showMessage "Spooky Match! 👻", true
updateDisplay
if gMatchedPairs = gDifficultySettings[gCurrentDifficulty]["pairs"] then
send "endGame" to me in 500 milliseconds
end if
else
-- No match - flip back
send "unflipCards" to me in 1000 milliseconds
end if
put empty into gFlippedCards
put false into gIsChecking
end checkMatch
Objectives: Apply consistent theming, add visual feedback, enhance user experience
command applyHalloweenTheme
set the backgroundColor of this stack to "26,0,51"
if there is a field "fldTitle" then
set the textColor of field "fldTitle" to "255,140,0"
set the textSize of field "fldTitle" to 32
set the textFont of field "fldTitle" to "Arial Black"
end if
styleControlButton "btnNewGame", "255,107,0"
styleControlButton "btnPeek", "139,0,255"
-- Additional styling...
end applyHalloweenTheme
Cards change appearance based on state:
command styleCard pButtonName, pIsFlipped, pIconSize
if there is a button pButtonName then
set the textSize of button pButtonName to pIconSize
set the textFont of button pButtonName to "Segoe UI Emoji"
set the borderWidth of button pButtonName to 3
set the borderColor of button pButtonName to "255,140,0"
if pIsFlipped is true then
-- Face up - dark purple
set the backgroundColor of button pButtonName to "26,0,51"
else
-- Face down - orange
set the backgroundColor of button pButtonName to "255,107,0"
end if
end if
end styleCard
Matched cards fade using blendLevel:
command applyMatchEffect pCard1Index, pCard2Index
local tBtn1, tBtn2
put gCards[pCard1Index]["buttonName"] into tBtn1
put gCards[pCard2Index]["buttonName"] into tBtn2
set the blendLevel of button tBtn1 to 60
set the blendLevel of button tBtn2 to 60
end applyMatchEffect
Displaying colorful emoji characters as card icons.
Solution: Use Unicode characters directly in LiveCode and set appropriate fonts:
put "👻" into gSpookyIcons
put return & "🎃" after gSpookyIcons
-- etc...
set the textFont of button pButtonName to "Segoe UI Emoji"
Windows uses Segoe UI Emoji, while macOS uses Apple Color Emoji. You may need conditional font setting based on platform.
Different difficulties require different grid configurations without overlapping controls.
Solution: Store grid specifications in a configuration array and dynamically calculate dimensions:
-- Easy: 4×4 grid with large cards
put 8 into gDifficultySettings["easy"]["pairs"]
put 4 into gDifficultySettings["easy"]["cols"]
put 4 into gDifficultySettings["easy"]["rows"]
put 520 into gDifficultySettings["easy"]["width"]
put 520 into gDifficultySettings["easy"]["height"]
-- Medium: 5×5 grid with smaller cards
put 12 into gDifficultySettings["medium"]["pairs"]
put 5 into gDifficultySettings["medium"]["cols"]
put 5 into gDifficultySettings["medium"]["rows"]
put 435 into gDifficultySettings["medium"]["width"]
put 435 into gDifficultySettings["medium"]["height"]
The renderBoard handler calculates card size based on available space.
Users clicking multiple cards rapidly during match checking can break game state.
Solution: Use a checking flag and validate card state:
command cardClicked pCardName
local tCardIndex
-- Prevent clicks during checking
if gIsChecking is true then exit cardClicked
put the c_cardIndex of button pCardName into tCardIndex
-- Validate card can be flipped
if gCards[tCardIndex]["isFlipped"] is true then exit cardClicked
if gCards[tCardIndex]["isMatched"] is true then exit cardClicked
-- Continue with flip logic...
end cardClicked
Converting milliseconds to readable MM:SS format.
Solution: Mathematical division and padding:
function formatTime pMilliseconds
local tSeconds, tMinutes, tSecs, tResult
put round(pMilliseconds / 1000) into tSeconds
put tSeconds div 60 into tMinutes
put tSeconds mod 60 into tSecs
if tSecs < 10 then
put tMinutes & ":0" & tSecs into tResult
else
put tMinutes & ":" & tSecs into tResult
end if
return tResult
end formatTime
Storing best times across sessions.
Solution: Custom properties with clear naming:
command saveBestTimes
set the c_bestTimeEasy of this stack to gBestTimes["easy"]
set the c_bestTimeMedium of this stack to gBestTimes["medium"]
set the c_bestTimeHard of this stack to gBestTimes["hard"]
save this stack
end saveBestTimes
command loadBestTimes
put empty into gBestTimes
put the c_bestTimeEasy of this stack into gBestTimes["easy"]
put the c_bestTimeMedium of this stack into gBestTimes["medium"]
put the c_bestTimeHard of this stack into gBestTimes["hard"]
end loadBestTimes
To build this game, create the following UI elements:
| Category | Count | Elements |
|---|---|---|
| Graphics | 2 | grcGameBoard, grcScoreBoard |
| Fields | 9 | fldTitle, fldMessage, fldMovesLabel, fldMovesValue, fldMatchesLabel, fldMatchesValue, fldBestTimeLabel, fldBestTimeValue, fldInstructions |
| Control Buttons | 5 | btnNewGame, btnPeek, btnDiffEasy, btnDiffMedium, btnDiffHard |
| Card Buttons | 32 | btnCard_1 through btnCard_32 |
| TOTAL | 48 |
All buttons should be scriptless — The stack script handles all interactions via the mouseUp router.
This architecture supports easy enhancements:
Additional Difficulty Levels:
Simply add new entries to gDifficultySettings and create corresponding buttons.
Different Icon Sets:
Replace gSpookyIcons with any line-delimited list of characters or image references.
Sound Effects:
Add play audioClip... commands in match/flip handlers.
Animations:
Use visual effect commands or timer-based property changes.
Multiplayer Mode:
Track separate scores in arrays indexed by player.
MR Spooky Match demonstrates effective LiveCode development practices through:
This tutorial provides a foundation for building sophisticated games in LiveCode. The patterns demonstrated here—particularly the UI Router, configuration-driven design, and state management—apply to many types of applications beyond games.