RSS Amplifier

Andy Brudtkuhl · Sep 26, 2025

Building Snooji

0
Sign in to vote or save

Andy Brudtkuhl · brudtkuhl.com

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

  • 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.

Links

Read the original on brudtkuhl.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.