
Building Snooji
September 26, 2025
projectsI recently built Snooji, a modern take on the classic Snood puzzle game using Laravel 12 and HTML5 Canvas. In this post, I'll share how I implemented the core game mechanics, level system, and scoring backend.
The Problem
Snood was a fantastic puzzle game from the 90s, but there wasn't a modern web version that captured the same physics-based gameplay. I wanted to build something that maintained the original's strategic depth while working seamlessly on modern devices.
The Solution
Snooji recreates Snood's core mechanics using emojis instead of the original character sprites. Players launch emojis from a cannon, match three or more of the same type, and try to clear the board before the danger line fills up.
Technical Implementation
Game Engine
The core game runs on HTML5 Canvas with vanilla JavaScript. No game frameworks needed for this type of puzzle game.
class SnoojiGame {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.grid = this.initializeGrid();
this.currentEmoji = this.getRandomEmoji();
this.nextEmoji = this.getRandomEmoji();
}
initializeGrid() {
// 14x14 grid with 5-8 starting rows
const grid = Array(14).fill().map(() => Array(14).fill(null));
const startRows = Math.floor(Math.random() * 4) + 5;
for (let row = 0; row < startRows; row++) {
for (let col = 0; col < 14; col++) {
if (Math.random() < 0.8) {
grid[row][col] = this.getRandomEmoji();
}
}
}
return grid;
}
}
Collision Detection
The physics system handles grid-based snapping and cluster detection:
handleCollision(shot) {
const gridX = Math.floor(shot.x / CELL_SIZE);
const gridY = Math.floor(shot.y / CELL_SIZE);
// Snap to grid
this.grid[gridY][gridX] = shot.emoji;
// Check for matches
const matches = this.findMatches(gridX, gridY);
if (matches.length >= 3) {
this.clearMatches(matches);
this.applyGravity();
}
}
Laravel Backend
The scoring system uses a simple Laravel setup with Redis for caching:
// app/Models/Game.php
class Game extends Model
{
protected $casts = [
'params' => 'array',
'completed_at' => 'datetime',
];
public function scores()
{
return $this->hasMany(Score::class);
}
}
// app/Http/Controllers/ScoreController.php
class ScoreController extends Controller
{
public function store(Request $request)
{
$score = Score::create([
'user_id' => auth()->id(),
'points' => $request->points,
'shots_taken' => $request->shots_taken,
'difficulty' => $request->difficulty,
]);
Cache::put("leaderboard_{$request->difficulty}",
$this->getLeaderboard($request->difficulty), 300);
return response()->json($score);
}
}
Level System
Levels are stored as JSON in the database, allowing for both hand-crafted and generated content:
// database/migrations/create_levels_table.php
Schema::create('levels', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('chapter')->nullable();
$table->integer('order')->default(0);
$table->json('params');
$table->timestamps();
});
Level parameters include grid size, emoji types, density, and scoring targets:
{
"name": "Level 1",
"rows": 7,
"cols": 7,
"emoji_types": ["๐", "๐", "๐", "๐"],
"density": 0.85,
"danger_row": 6,
"score_targets": [500, 1000, 1500],
"moves_limit": 25
}
Scoring Algorithm
The scoring system rewards larger clusters and efficient play:
calculateScore(matches, drops = 0) {
const BASE_POINTS = 10;
const BONUS_PER_EXTRA = 4;
const DROP_BONUS = 5;
let score = 0;
matches.forEach(match => {
const size = match.length;
const points = BASE_POINTS + BONUS_PER_EXTRA * Math.pow(size - 3, 2);
score += points;
});
score += drops * DROP_BONUS;
return Math.floor(score);
}
Key Features
- Physics-based gameplay: Shots bounce off walls and snap to grid
- Cluster detection: 8-way adjacency matching with flood fill algorithm
- Chain reactions: Unconnected emojis fall and can create new matches
- Danger meter: Increases with each shot, game over when full
- Level progression: Hand-crafted and generated levels
- Real-time scoring: Laravel API with Redis caching
Performance Considerations
The game runs at 60fps on modern devices by:
- Using
requestAnimationFrame
for smooth rendering - Implementing dirty rectangle updates
- Caching rendered emoji sprites
- Debouncing API calls for score submission
Next Steps
Future enhancements include:
- Real-time multiplayer with Laravel Reverb
- Daily challenge mode with server-signed seeds
- Progressive Web App features
- Mobile-optimized touch controls
Snooji demonstrates how Laravel can power more than just traditional web applicationsโit's equally capable of handling game backends, real-time features, and complex scoring systems.
Try It Out
You can play Snooji at snooji.laravel.cloud. The game is fully playable in your browser with no installation required.