Casual Games Mini Program Case
This case showcases a collection of casual puzzle games designed for quick entertainment and mental stimulation, featuring multiple game modes, social features, and progressive difficulty levels to engage users of all ages.
Project Overview
Project Background
Casual gaming has become increasingly popular as people seek quick entertainment during breaks and commutes. This mini program addresses the need for accessible, engaging games that can be played instantly without downloads, while providing social interaction and achievement systems to maintain long-term engagement.
Core Features
- Multiple Game Modes: Puzzle, match-3, word games, and brain teasers
- Progressive Difficulty: Adaptive difficulty based on player performance
- Social Features: Leaderboards, friend challenges, and multiplayer modes
- Achievement System: Unlockable rewards and progress tracking
- Daily Challenges: Fresh content to encourage daily engagement
- Offline Play: Games available without internet connection
- Cross-Platform Sync: Progress saved across devices
Technical Implementation
Game Engine Architecture
// utils/game-engine.js
class GameEngine {
constructor(gameConfig) {
this.config = gameConfig
this.state = 'idle'
this.score = 0
this.level = 1
this.lives = 3
this.gameData = {}
this.eventListeners = new Map()
}
init() {
this.setupCanvas()
this.loadAssets()
this.setupEventListeners()
this.setState('ready')
}
setupCanvas() {
const query = wx.createSelectorQuery()
query.select('#game-canvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.canvas = canvas
this.ctx = ctx
this.canvasWidth = res[0].width
this.canvasHeight = res[0].height
})
}
async loadAssets() {
const assets = this.config.assets || []
const loadPromises = assets.map(asset => this.loadAsset(asset))
try {
await Promise.all(loadPromises)
this.emit('assetsLoaded')
} catch (error) {
console.error('Failed to load assets:', error)
this.emit('assetsError', error)
}
}
loadAsset(asset) {
return new Promise((resolve, reject) => {
const image = this.canvas.createImage()
image.onload = () => {
this.assets = this.assets || {}
this.assets[asset.name] = image
resolve(image)
}
image.onerror = reject
image.src = asset.url
})
}
start() {
if (this.state !== 'ready') return
this.setState('playing')
this.gameLoop()
this.emit('gameStart')
}
pause() {
if (this.state === 'playing') {
this.setState('paused')
this.emit('gamePause')
}
}
resume() {
if (this.state === 'paused') {
this.setState('playing')
this.gameLoop()
this.emit('gameResume')
}
}
gameLoop() {
if (this.state !== 'playing') return
this.update()
this.render()
requestAnimationFrame(() => this.gameLoop())
}
update() {
// Override in specific game implementations
}
render() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// Override in specific game implementations
}
setState(newState) {
const oldState = this.state
this.state = newState
this.emit('stateChange', { oldState, newState })
}
addScore(points) {
this.score += points
this.emit('scoreUpdate', this.score)
// Check for level up
const newLevel = Math.floor(this.score / 1000) + 1
if (newLevel > this.level) {
this.level = newLevel
this.emit('levelUp', this.level)
}
}
loseLife() {
this.lives--
this.emit('lifeUpdate', this.lives)
if (this.lives <= 0) {
this.gameOver()
}
}
gameOver() {
this.setState('gameOver')
this.emit('gameOver', {
score: this.score,
level: this.level
})
}
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, [])
}
this.eventListeners.get(event).push(callback)
}
emit(event, data) {
const listeners = this.eventListeners.get(event) || []
listeners.forEach(callback => callback(data))
}
}
export default GameEngine
Match-3 Puzzle Game Implementation
// games/match3/match3-game.js
import GameEngine from '../../utils/game-engine'
class Match3Game extends GameEngine {
constructor() {
super({
assets: [
{ name: 'gem1', url: '/images/games/gem1.png' },
{ name: 'gem2', url: '/images/games/gem2.png' },
{ name: 'gem3', url: '/images/games/gem3.png' },
{ name: 'gem4', url: '/images/games/gem4.png' },
{ name: 'gem5', url: '/images/games/gem5.png' }
]
})
this.gridSize = 8
this.gemSize = 40
this.grid = []
this.selectedGem = null
this.animating = false
this.matches = []
this.combo = 0
}
init() {
super.init()
this.initializeGrid()
this.setupTouchHandlers()
}
initializeGrid() {
this.grid = []
for (let row = 0; row < this.gridSize; row++) {
this.grid[row] = []
for (let col = 0; col < this.gridSize; col++) {
this.grid[row][col] = {
type: Math.floor(Math.random() * 5) + 1,
x: col * this.gemSize,
y: row * this.gemSize,
row,
col
}
}
}
// Ensure no initial matches
this.removeInitialMatches()
}
removeInitialMatches() {
let hasMatches = true
while (hasMatches) {
hasMatches = false
for (let row = 0; row < this.gridSize; row++) {
for (let col = 0; col < this.gridSize; col++) {
if (this.hasMatchAt(row, col)) {
this.grid[row][col].type = Math.floor(Math.random() * 5) + 1
hasMatches = true
}
}
}
}
}
setupTouchHandlers() {
this.on('canvasTouch', (e) => {
if (this.animating) return
const touch = e.touches[0]
const rect = e.target.getBoundingClientRect()
const x = touch.clientX - rect.left
const y = touch.clientY - rect.top
const col = Math.floor(x / this.gemSize)
const row = Math.floor(y / this.gemSize)
if (row >= 0 && row < this.gridSize && col >= 0 && col < this.gridSize) {
this.handleGemTouch(row, col)
}
})
}
handleGemTouch(row, col) {
const gem = this.grid[row][col]
if (!this.selectedGem) {
this.selectedGem = gem
this.highlightGem(gem)
} else {
if (this.selectedGem === gem) {
// Deselect
this.selectedGem = null
this.clearHighlight()
} else if (this.areAdjacent(this.selectedGem, gem)) {
// Attempt swap
this.swapGems(this.selectedGem, gem)
} else {
// Select new gem
this.selectedGem = gem
this.highlightGem(gem)
}
}
}
areAdjacent(gem1, gem2) {
const rowDiff = Math.abs(gem1.row - gem2.row)
const colDiff = Math.abs(gem1.col - gem2.col)
return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1)
}
async swapGems(gem1, gem2) {
this.animating = true
// Temporarily swap
const tempType = gem1.type
gem1.type = gem2.type
gem2.type = tempType
// Check for matches
const matches1 = this.findMatches(gem1.row, gem1.col)
const matches2 = this.findMatches(gem2.row, gem2.col)
if (matches1.length > 0 || matches2.length > 0) {
// Valid move
await this.animateSwap(gem1, gem2)
this.selectedGem = null
this.clearHighlight()
await this.processMatches()
} else {
// Invalid move - swap back
gem1.type = gem2.type
gem2.type = tempType
await this.animateSwap(gem1, gem2, true) // Reverse animation
}
this.animating = false
}
animateSwap(gem1, gem2, reverse = false) {
return new Promise((resolve) => {
const duration = 300
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
// Animate gem positions
if (!reverse) {
gem1.animX = gem1.x + (gem2.x - gem1.x) * progress
gem1.animY = gem1.y + (gem2.y - gem1.y) * progress
gem2.animX = gem2.x + (gem1.x - gem2.x) * progress
gem2.animY = gem2.y + (gem1.y - gem2.y) * progress
} else {
gem1.animX = gem2.x + (gem1.x - gem2.x) * progress
gem1.animY = gem2.y + (gem1.y - gem2.y) * progress
gem2.animX = gem1.x + (gem2.x - gem1.x) * progress
gem2.animY = gem1.y + (gem2.y - gem1.y) * progress
}
if (progress < 1) {
requestAnimationFrame(animate)
} else {
gem1.animX = gem1.x
gem1.animY = gem1.y
gem2.animX = gem2.x
gem2.animY = gem2.y
resolve()
}
}
animate()
})
}
async processMatches() {
let totalMatches = 0
this.combo = 0
while (true) {
const allMatches = this.findAllMatches()
if (allMatches.length === 0) break
totalMatches += allMatches.length
this.combo++
// Calculate score with combo multiplier
const baseScore = allMatches.length * 10
const comboBonus = this.combo > 1 ? (this.combo - 1) * 5 : 0
this.addScore(baseScore + comboBonus)
// Remove matched gems
await this.removeMatches(allMatches)
// Drop gems down
await this.dropGems()
// Fill empty spaces
await this.fillEmptySpaces()
}
if (totalMatches > 0) {
this.emit('matchesProcessed', { totalMatches, combo: this.combo })
}
}
findAllMatches() {
const matches = []
const processed = new Set()
for (let row = 0; row < this.gridSize; row++) {
for (let col = 0; col < this.gridSize; col++) {
const key = `${row}-${col}`
if (!processed.has(key)) {
const gemMatches = this.findMatches(row, col)
if (gemMatches.length >= 3) {
matches.push(...gemMatches)
gemMatches.forEach(match => {
processed.add(`${match.row}-${match.col}`)
})
}
}
}
}
return matches
}
findMatches(row, col) {
const gem = this.grid[row][col]
const matches = [gem]
// Check horizontal matches
let left = col - 1
while (left >= 0 && this.grid[row][left].type === gem.type) {
matches.unshift(this.grid[row][left])
left--
}
let right = col + 1
while (right < this.gridSize && this.grid[row][right].type === gem.type) {
matches.push(this.grid[row][right])
right++
}
if (matches.length >= 3) {
return matches
}
// Check vertical matches
const verticalMatches = [gem]
let up = row - 1
while (up >= 0 && this.grid[up][col].type === gem.type) {
verticalMatches.unshift(this.grid[up][col])
up--
}
let down = row + 1
while (down < this.gridSize && this.grid[down][col].type === gem.type) {
verticalMatches.push(this.grid[down][col])
down++
}
if (verticalMatches.length >= 3) {
return verticalMatches
}
return []
}
render() {
super.render()
// Draw grid background
this.ctx.fillStyle = '#2a2a2a'
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
// Draw gems
for (let row = 0; row < this.gridSize; row++) {
for (let col = 0; col < this.gridSize; col++) {
const gem = this.grid[row][col]
this.drawGem(gem)
}
}
// Draw selection highlight
if (this.selectedGem) {
this.drawSelection(this.selectedGem)
}
// Draw UI
this.drawUI()
}
drawGem(gem) {
const x = gem.animX !== undefined ? gem.animX : gem.x
const y = gem.animY !== undefined ? gem.animY : gem.y
const asset = this.assets[`gem${gem.type}`]
if (asset) {
this.ctx.drawImage(asset, x, y, this.gemSize, this.gemSize)
} else {
// Fallback colored rectangle
this.ctx.fillStyle = this.getGemColor(gem.type)
this.ctx.fillRect(x, y, this.gemSize, this.gemSize)
}
}
getGemColor(type) {
const colors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff']
return colors[type - 1] || '#ffffff'
}
drawSelection(gem) {
this.ctx.strokeStyle = '#ffffff'
this.ctx.lineWidth = 3
this.ctx.strokeRect(gem.x, gem.y, this.gemSize, this.gemSize)
}
drawUI() {
this.ctx.fillStyle = '#ffffff'
this.ctx.font = '20px Arial'
this.ctx.fillText(`Score: ${this.score}`, 10, 30)
this.ctx.fillText(`Level: ${this.level}`, 10, 60)
this.ctx.fillText(`Lives: ${this.lives}`, 10, 90)
if (this.combo > 1) {
this.ctx.fillStyle = '#ffff00'
this.ctx.fillText(`Combo x${this.combo}!`, 10, 120)
}
}
}
export default Match3Game
Social Features Integration
// utils/social-features.js
class SocialFeatures {
static async getLeaderboard(gameType, period = 'weekly') {
try {
const res = await wx.request({
url: `/api/games/leaderboard/${gameType}`,
method: 'GET',
data: { period }
})
return res.data.leaderboard
} catch (error) {
console.error('Failed to get leaderboard:', error)
return []
}
}
static async submitScore(gameType, score, gameData = {}) {
try {
const res = await wx.request({
url: '/api/games/scores',
method: 'POST',
data: {
gameType,
score,
gameData,
userId: wx.getStorageSync('userId')
}
})
if (res.data.newRecord) {
this.showNewRecordDialog(res.data.rank, score)
}
return res.data
} catch (error) {
console.error('Failed to submit score:', error)
throw error
}
}
static showNewRecordDialog(rank, score) {
wx.showModal({
title: 'New Record!',
content: `Congratulations! You achieved rank #${rank} with a score of ${score}!`,
confirmText: 'Share',
cancelText: 'OK',
success: (res) => {
if (res.confirm) {
this.shareScore(score, rank)
}
}
})
}
static async challengeFriend(friendId, gameType, score) {
try {
const res = await wx.request({
url: '/api/games/challenges',
method: 'POST',
data: {
friendId,
gameType,
challengeScore: score,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
wx.showToast({
title: 'Challenge sent!',
icon: 'success'
})
}
return res.data
} catch (error) {
console.error('Failed to send challenge:', error)
throw error
}
}
static async getFriendsList() {
try {
const res = await wx.request({
url: `/api/users/${wx.getStorageSync('userId')}/friends`,
method: 'GET'
})
return res.data.friends
} catch (error) {
console.error('Failed to get friends list:', error)
return []
}
}
static async shareScore(score, rank) {
try {
wx.shareAppMessage({
title: `I just scored ${score} points and ranked #${rank}!`,
path: '/pages/games/index',
imageUrl: '/images/share-game.png'
})
} catch (error) {
console.error('Failed to share score:', error)
}
}
static async getAchievements(userId) {
try {
const res = await wx.request({
url: `/api/games/achievements/${userId}`,
method: 'GET'
})
return res.data.achievements
} catch (error) {
console.error('Failed to get achievements:', error)
return []
}
}
static async unlockAchievement(achievementId) {
try {
const res = await wx.request({
url: `/api/games/achievements/${achievementId}/unlock`,
method: 'POST',
data: {
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
this.showAchievementUnlocked(res.data.achievement)
}
return res.data
} catch (error) {
console.error('Failed to unlock achievement:', error)
throw error
}
}
static showAchievementUnlocked(achievement) {
wx.showModal({
title: 'Achievement Unlocked!',
content: `🏆 ${achievement.name}\n${achievement.description}`,
showCancel: false,
confirmText: 'Awesome!'
})
}
}
export default SocialFeatures
Daily Challenge System
// utils/daily-challenges.js
class DailyChallenges {
static async getTodaysChallenge() {
try {
const res = await wx.request({
url: '/api/games/daily-challenge',
method: 'GET',
data: {
userId: wx.getStorageSync('userId'),
date: new Date().toISOString().split('T')[0]
}
})
return res.data.challenge
} catch (error) {
console.error('Failed to get daily challenge:', error)
return null
}
}
static async completeDailyChallenge(challengeId, result) {
try {
const res = await wx.request({
url: `/api/games/daily-challenge/${challengeId}/complete`,
method: 'POST',
data: {
userId: wx.getStorageSync('userId'),
result
}
})
if (res.data.success) {
this.showChallengeComplete(res.data.reward)
}
return res.data
} catch (error) {
console.error('Failed to complete daily challenge:', error)
throw error
}
}
static showChallengeComplete(reward) {
wx.showModal({
title: 'Challenge Complete!',
content: `Great job! You earned ${reward.points} points and ${reward.coins} coins!`,
showCancel: false,
confirmText: 'Collect'
})
}
static async getChallengeHistory(userId) {
try {
const res = await wx.request({
url: `/api/games/daily-challenge/history/${userId}`,
method: 'GET'
})
return res.data.history
} catch (error) {
console.error('Failed to get challenge history:', error)
return []
}
}
static async getChallengeStreak(userId) {
try {
const res = await wx.request({
url: `/api/games/daily-challenge/streak/${userId}`,
method: 'GET'
})
return res.data.streak
} catch (error) {
console.error('Failed to get challenge streak:', error)
return 0
}
}
static generateChallengeTypes() {
return [
{
type: 'score_target',
name: 'Score Master',
description: 'Reach a target score',
difficulty: 'medium'
},
{
type: 'time_limit',
name: 'Speed Demon',
description: 'Complete game within time limit',
difficulty: 'hard'
},
{
type: 'moves_limit',
name: 'Efficiency Expert',
description: 'Complete with limited moves',
difficulty: 'hard'
},
{
type: 'combo_master',
name: 'Combo King',
description: 'Achieve specific combo count',
difficulty: 'medium'
},
{
type: 'perfect_game',
name: 'Perfectionist',
description: 'Complete without losing lives',
difficulty: 'expert'
}
]
}
}
export default DailyChallenges
Game Analytics and Progression
// utils/game-analytics.js
class GameAnalytics {
static async trackGameSession(gameType, sessionData) {
try {
await wx.request({
url: '/api/games/analytics/session',
method: 'POST',
data: {
gameType,
userId: wx.getStorageSync('userId'),
...sessionData,
timestamp: Date.now()
}
})
} catch (error) {
console.error('Failed to track game session:', error)
}
}
static async trackGameEvent(gameType, event, data = {}) {
try {
await wx.request({
url: '/api/games/analytics/event',
method: 'POST',
data: {
gameType,
event,
data,
userId: wx.getStorageSync('userId'),
timestamp: Date.now()
}
})
} catch (error) {
console.error('Failed to track game event:', error)
}
}
static async getPlayerStats(userId, gameType) {
try {
const res = await wx.request({
url: `/api/games/analytics/stats/${userId}/${gameType}`,
method: 'GET'
})
return res.data.stats
} catch (error) {
console.error('Failed to get player stats:', error)
return null
}
}
static async getGameInsights(userId) {
try {
const res = await wx.request({
url: `/api/games/analytics/insights/${userId}`,
method: 'GET'
})
return res.data.insights
} catch (error) {
console.error('Failed to get game insights:', error)
return null
}
}
static calculateSkillRating(gameStats) {
const {
gamesPlayed,
averageScore,
bestScore,
winRate,
averageTime
} = gameStats
// Base rating from games played (experience factor)
let rating = Math.min(gamesPlayed * 2, 200)
// Score performance factor
const scoreRatio = averageScore / (bestScore || 1)
rating += scoreRatio * 300
// Win rate factor
rating += winRate * 200
// Time efficiency factor (lower is better for most games)
const timeEfficiency = Math.max(0, 1 - (averageTime / 300)) // 5 minutes baseline
rating += timeEfficiency * 100
return Math.round(Math.max(0, Math.min(1000, rating)))
}
static getSkillLevel(rating) {
if (rating < 100) return { level: 'Beginner', color: '#8BC34A' }
if (rating < 300) return { level: 'Novice', color: '#FFC107' }
if (rating < 500) return { level: 'Intermediate', color: '#FF9800' }
if (rating < 700) return { level: 'Advanced', color: '#F44336' }
if (rating < 900) return { level: 'Expert', color: '#9C27B0' }
return { level: 'Master', color: '#3F51B5' }
}
}
export default GameAnalytics
Project Results
Key Metrics
- User Engagement: 75% daily active user rate with average session time of 12 minutes
- Game Completion: 68% of started games completed successfully
- Social Interaction: 45% of users participate in friend challenges and leaderboards
- Retention Rate: 82% 7-day retention, 65% 30-day retention
Business Impact
- User Growth: 300% increase in registered users within 6 months
- Revenue Growth: 180% increase through in-app purchases and premium features
- User Satisfaction: 4.5/5.0 average rating with 85% recommendation rate
- Daily Engagement: Average of 3.2 game sessions per active user per day
This casual games collection successfully demonstrates how engaging gameplay mechanics, social features, and progressive difficulty can create a compelling gaming experience that keeps users coming back for more entertainment and social interaction.