Building Snooji cover image

Building Snooji

September 26, 2025

projects

I 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

Performance Considerations

The game runs at 60fps on modern devices by:

Next Steps

Future enhancements include:

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.

Links