👻 Developing MR Spooky Match 🎃

A LiveCode Memory Game Tutorial

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.

Project Metadata:
Author: Michael Roberts | License: MIT | Version: 0.3.0 | Platform: LiveCode | Difficulty: Intermediate

Introduction

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:

1. Project Overview

Game Concept

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.

Technical Specifications:

Key Features:

2. Design Decisions

2.1 UI Router Architecture

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

2.2 Naming Conventions

Consistent naming is critical for maintainability:

2.3 Data Structure

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

3. Implementation Phases

Phase 0-1: Foundation

Objectives: Set up project structure, global variables, and UI Router

Key Components:

  1. Header block with metadata (author, license, version)
  2. Global variable declarations
  3. Initialization of game constants (icons, difficulty settings)
  4. UI Router mouseUp handler
  5. Load/save handlers for best times
Challenge: Custom Property Management

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

Phase 2: Core Game Mechanics

Objectives: Implement game logic, card management, and matching

Card Creation

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

Card Shuffling - Fisher-Yates Algorithm

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
Tip:

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.

Dynamic Board Rendering

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

Match Checking Logic

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

Phase 3: UI Polish & Visual Effects

Objectives: Apply consistent theming, add visual feedback, enhance user experience

Theme Application

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

Card Styling

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

Visual Effects

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

4. Key Coding Challenges

Challenge 1: Unicode Emoji Support

Problem:

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"
Platform Considerations:

Windows uses Segoe UI Emoji, while macOS uses Apple Color Emoji. You may need conditional font setting based on platform.

Challenge 2: Adaptive Grid Layouts

Problem:

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.

Challenge 3: Preventing Click Races

Problem:

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

Challenge 4: Time Formatting

Problem:

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

Challenge 5: Data Persistence

Problem:

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

5. UI Element Checklist

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
Important:

All buttons should be scriptless — The stack script handles all interactions via the mouseUp router.

6. Best Practices Demonstrated

6.1 Code Organization

6.2 State Management

6.3 User Experience

6.4 Maintainability

7. Extensibility

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.

8. Conclusion

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.

Further Learning: