Tech Details

Carrom Game - Day 16

Canvas + Physics

Table of Contents

01. Game Architecture & Constants

Carrom requires precise physics constants to achieve the authentic feel of pieces sliding on a polished wooden board. The friction coefficient is critical for realistic deceleration.

Core Constants

// Board dimensions const BOARD_SIZE = 600; // Base board size in pixels const POCKET_RADIUS = 22; // Corner pocket size const PIECE_RADIUS = 14; // Standard piece size const STRIKER_RADIUS = 18; // Striker is larger and heavier // Physics constants const FRICTION = 0.985; // Speed multiplier per frame const MIN_VELOCITY = 0.1; // Threshold to stop movement const MAX_POWER = 25; // Maximum shot force const BASELINE_OFFSET = 80; // Distance from edge to baseline

Game State Structure

// Core game state variables let pieces = []; // All carrom pieces let striker = null; // Player's striker let currentPlayer = 1; // 1 = black, 2 = white let scores = [0, 0]; // Player scores let piecesMoving = false; // Physics active flag let queenPocketed = false; // Queen state tracking let queenNeedsCover = [false, false]; // Cover rule // Piece data structure const piece = { x: 300, // X position y: 300, // Y position vx: 0, // X velocity vy: 0, // Y velocity radius: 14, // Collision radius type: 'black', // 'black', 'white', 'queen', 'striker' pocketed: false // In pocket flag };
Physics Tuning

The friction value of 0.985 means pieces retain 98.5% of their velocity each frame. At 60fps, this creates a natural deceleration curve that mimics real carrom board powder.

02. Piece Arrangement & Board Setup

Traditional Carrom has a specific starting arrangement with the Queen in the center, surrounded by alternating black and white pieces in concentric circles.

Circular Arrangement Algorithm

function createPieces() { pieces = []; const center = canvas.width / 2; const scale = canvas.width / BOARD_SIZE; const pieceR = PIECE_RADIUS * scale; // Queen in center (red piece) pieces.push({ x: center, y: center, vx: 0, vy: 0, radius: pieceR, type: 'queen', color: '#dc2626', pocketed: false }); // Inner ring - 6 pieces at radius 2.5x piece size const innerRadius = pieceR * 2.5; for (let i = 0; i < 6; i++) { const angle = (i / 6) * Math.PI * 2 - Math.PI / 2; const type = i % 2 === 0 ? 'black' : 'white'; pieces.push({ x: center + Math.cos(angle) * innerRadius, y: center + Math.sin(angle) * innerRadius, vx: 0, vy: 0, radius: pieceR, type: type, pocketed: false }); } // Outer ring - 12 pieces at radius 4.5x piece size const outerRadius = pieceR * 4.5; for (let i = 0; i < 12; i++) { // Offset by half to stagger with inner ring const angle = (i / 12) * Math.PI * 2 + Math.PI / 12; const type = i % 2 === 0 ? 'black' : 'white'; pieces.push({ x: center + Math.cos(angle) * outerRadius, y: center + Math.sin(angle) * outerRadius, radius: pieceR, type: type }); } }

Piece Arrangement Pattern

Queen (Center) + 6 Inner Ring + 12 Outer Ring

Pocket Positioning

function updatePockets() { const s = canvas.width; const offset = 30; // Distance from corner pockets.length = 0; pockets.push( { x: offset, y: offset }, // Top-left { x: s - offset, y: offset }, // Top-right { x: offset, y: s - offset }, // Bottom-left { x: s - offset, y: s - offset } // Bottom-right ); }

03. Physics Simulation

The physics loop updates positions based on velocity, applies friction to simulate the powder-coated board surface, and handles boundary collisions with the board frame.

Main Update Loop

function update() { if (!gameRunning) return; piecesMoving = false; // Update all active pieces for (const piece of pieces) { if (piece.pocketed) continue; // Apply velocity to position piece.x += piece.vx; piece.y += piece.vy; // Apply friction (simulates board powder) piece.vx *= FRICTION; piece.vy *= FRICTION; // Check if still moving if (Math.abs(piece.vx) > MIN_VELOCITY || Math.abs(piece.vy) > MIN_VELOCITY) { piecesMoving = true; } else { // Stop completely below threshold piece.vx = 0; piece.vy = 0; } // Handle boundary and pocket collisions boundaryCollision(piece); checkPocketCollision(piece); } // Check piece-to-piece collisions checkAllCollisions(); }

Boundary Collision (Board Frame)

function boundaryCollision(obj) { const s = canvas.width; const margin = 35 * (s / BOARD_SIZE); // Frame width const boardMin = margin; const boardMax = s - margin; // Left wall if (obj.x - obj.radius < boardMin) { obj.x = boardMin + obj.radius; obj.vx *= -0.8; // Energy loss on bounce } // Right wall if (obj.x + obj.radius > boardMax) { obj.x = boardMax - obj.radius; obj.vx *= -0.8; } // Top wall if (obj.y - obj.radius < boardMin) { obj.y = boardMin + obj.radius; obj.vy *= -0.8; } // Bottom wall if (obj.y + obj.radius > boardMax) { obj.y = boardMax - obj.radius; obj.vy *= -0.8; } }
Bounce Coefficient

The -0.8 multiplier creates an 80% energy retention on wall bounces. This simulates the rubber cushions on a real Carrom board frame.

04. Elastic Collision Response

When two circular pieces collide, we calculate the collision normal and apply impulse-based physics to achieve realistic momentum transfer.

Circle-Circle Collision Detection

function checkCollision(a, b) { // Distance between centers const dx = b.x - a.x; const dy = b.y - a.y; const dist = Math.sqrt(dx * dx + dy * dy); const minDist = a.radius + b.radius; // No collision if not overlapping if (dist >= minDist || dist === 0) return; // Calculate collision normal (unit vector) const nx = dx / dist; const ny = dy / dist; // Relative velocity along collision normal const dvx = a.vx - b.vx; const dvy = a.vy - b.vy; const dvn = dvx * nx + dvy * ny; // Only resolve if objects are approaching if (dvn <= 0) return; // Mass-based impulse (striker is heavier) const ma = a.type === 'striker' ? 2 : 1; const mb = b.type === 'striker' ? 2 : 1; const totalMass = ma + mb; // Calculate impulse magnitude const impulse = (2 * dvn) / totalMass; const restitution = 0.95; // Near-elastic collision // Apply impulse to velocities a.vx -= impulse * mb * nx * restitution; a.vy -= impulse * mb * ny * restitution; b.vx += impulse * ma * nx * restitution; b.vy += impulse * ma * ny * restitution; }

Position Separation

// After velocity resolution, separate overlapping pieces const overlap = minDist - dist; const separationX = (overlap / 2 + 0.5) * nx; const separationY = (overlap / 2 + 0.5) * ny; // Push objects apart equally a.x -= separationX; a.y -= separationY; b.x += separationX; b.y += separationY;

Collision Resolution Pipeline

Detect Overlap -> Calculate Normal -> Apply Impulse -> Separate
Penetration Fix

The +0.5 in separation ensures pieces don't get stuck together. This small buffer prevents numerical precision issues from causing repeated collisions.

05. Pocket Detection & Scoring

Pockets are circular regions in the corners. When a piece's center enters the pocket radius, it's considered pocketed and removed from play.

Pocket Collision Check

function checkPocketCollision(obj) { const pocketR = POCKET_RADIUS * (canvas.width / BOARD_SIZE); for (const pocket of pockets) { const dx = obj.x - pocket.x; const dy = obj.y - pocket.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < pocketR) { // Piece has entered the pocket! obj.pocketed = true; obj.vx = 0; obj.vy = 0; // Handle based on piece type if (obj.type === 'striker') { foul = true; // Pocketing striker is a foul } else { pocketedThisTurn.push(obj); handleScore(obj); } break; } } } function handleScore(piece) { if (piece.type === 'black') { scores[0]++; } else if (piece.type === 'white') { scores[1]++; } else if (piece.type === 'queen') { queenPocketed = true; // Mark that current player needs to cover queenNeedsCover[currentPlayer - 1] = true; } }

Foul Penalty - Return Piece

function returnPieceToCenter() { const ownColor = currentPlayer === 1 ? 'black' : 'white'; // Find a pocketed piece of player's color const pocketedPieces = pieces.filter( p => p.pocketed && p.type === ownColor ); if (pocketedPieces.length > 0) { // Return the most recently pocketed piece const piece = pocketedPieces[pocketedPieces.length - 1]; piece.pocketed = false; piece.x = canvas.width / 2; piece.y = canvas.height / 2; piece.vx = 0; piece.vy = 0; scores[currentPlayer - 1]--; } }

06. Turn-Based Game Rules

Carrom has specific rules about turn continuation, the Queen's "cover" requirement, and win conditions that must be carefully implemented.

Turn End Logic

function handleTurnEnd() { if (piecesMoving) return; // Check win conditions first const blackLeft = pieces.filter( p => p.type === 'black' && !p.pocketed ).length; const whiteLeft = pieces.filter( p => p.type === 'white' && !p.pocketed ).length; if (blackLeft === 0 || whiteLeft === 0) { endGame(); return; } // Handle striker foul (pocketed striker) if (striker && striker.pocketed) { foul = true; returnPieceToCenter(); } // Queen cover rule check checkQueenCover(); // Determine if turn continues const ownColor = currentPlayer === 1 ? 'black' : 'white'; const pocketedOwn = pocketedThisTurn.some( p => p.type === ownColor ); // Switch turns if no own piece pocketed or foul if (!pocketedOwn || foul) { currentPlayer = currentPlayer === 1 ? 2 : 1; } // Reset for next turn foul = false; pocketedThisTurn = []; createStriker(); }

Queen Cover Rule

function checkQueenCover() { for (let p = 0; p < 2; p++) { if (!queenNeedsCover[p]) continue; const ownColor = p === 0 ? 'black' : 'white'; // Check if they covered the queen this turn const coveredQueen = pocketedThisTurn.some( piece => piece.type === ownColor ); if (!coveredQueen) { // Queen goes back to center! const queen = pieces.find( piece => piece.type === 'queen' ); if (queen) { queen.pocketed = false; queen.x = canvas.width / 2; queen.y = canvas.height / 2; } queenPocketed = false; } else { // Queen successfully covered - bonus points! scores[p] += 3; } queenNeedsCover[p] = false; } }
Queen Cover Rule

In traditional Carrom, after pocketing the Queen, you must "cover" it by pocketing one of your own pieces in the same turn or the next. If you fail, the Queen returns to the center. Successfully covering awards bonus points!

Striker Positioning

function positionStriker(pos) { const s = canvas.width; const scale = s / BOARD_SIZE; const borderWidth = 35 * scale; // Each player has their own baseline const baselineY = currentPlayer === 1 ? s - BASELINE_OFFSET * scale // Bottom for player 1 : BASELINE_OFFSET * scale; // Top for player 2 // Constrain striker to baseline striker.y = baselineY; // Limit horizontal movement within baseline circles striker.x = Math.max( borderWidth + 60 * scale + striker.radius, Math.min( s - borderWidth - 60 * scale - striker.radius, pos.x ) ); }

Turn Flow

Position Striker -> Aim & Shoot -> Physics Run -> Score & Rules