

Carrom - Free Online Carrom Board Game: Flick, Pocket, Cover the Queen
3.0
Penilaian
0
0
import {useEffect, useRef, useState} from 'react'; export const PIECE_ART = { striker: '/img/games/carrom/pieces/pieceStriker.webp', white: '/img/games/carrom/pieces/pieceWhite.webp', black: '/img/games/carrom/pieces/pieceBlack.webp', queen: '/img/games/carrom/pieces/pieceQueen.webp', };
export function Disc({kind, size = 28, className = ''}) { // The real in-game piece sprites, so the widgets look exactly like the game. return ( <img src={PIECE_ART[kind] ?? PIECE_ART.white} alt="" draggable={false} className={'inline-block select-none drop-shadow-[0_2px_1px_rgba(30,15,5,0.4)] ' + className} style={{width: size, height: size}} /> ); }
export function BoardSetupDiagram() { // Same math as the shared rules engine (CarromPoolGame#getStartingBallLayout): an inner ring of // 6 men touching the queen, an outer ring of 12, whites at inner angles 0/120/240 and outer // slots k where k % 4 <= 1, forming the traditional "Y". Rendered on the real board art. const size = 300; const c = size / 2; const d = 30; const inner = Array.from({length: 6}, (, i) => { const a = (i * Math.PI) / 3; return {x: c + d * 1.02 * Math.cos(a), y: c - d * 1.02 * Math.sin(a), white: i % 2 === 0}; }); const outer = Array.from({length: 12}, (, k) => { const a = (k * Math.PI) / 6; return {x: c + 2 * d * 1.02 * Math.cos(a), y: c - 2 * d * 1.02 * Math.sin(a), white: k % 4 <= 1}; }); return (
{inner.concat(outer).map((p, i) => (
<span key={i} className="absolute" style={{left: p.x - 11, top: p.y - 11}}>
<Disc kind={p.white ? 'white' : 'black'} size={22} />
))}
<span className="absolute" style={{left: c - 11, top: c - 11}}>
The opening setup: the red queen in the middle, 6 pieces around her, 12 more around those, and your striker on the baseline.
export function QueenExplainer() { const [step, setStep] = useState(0); const steps = [ { label: '1. Pocket the queen', text: 'You may only pocket the red queen after you have pocketed at least one of your own pieces. Sink her and you keep your turn, but she is not yours yet.', board: ['queen'], }, { label: '2a. Cover her', text: 'Pocket one of your own pieces on that same shot or the very next one. That covers the queen: she stays down for good and the pressure is off.', board: ['queen', 'white'], }, { label: '2b. Miss the cover', text: 'If your next shot pockets nothing of yours, the queen comes back to the center circle and your turn ends. Someone has to win her all over again.', board: [], }, ]; const active = steps[step]; return (
{active.text}
export function MiniCarrom() {
// A self-contained toy sim, not the real engine: circles with friction, elastic bounces, and
// corner pockets are enough to teach flicking and the queen rules. The real game runs the full
// tailuge physics server-side.
const SIZE = 300;
const CENTER = SIZE / 2;
const MAN_R = 11;
const STRIKER_R = 14;
const POCKET_R = 15;
// Calibrated to the real board art: the playing surface is the inner 6.4%..93.6% of the image
// and the pocket holes are painted 10.2% in from each corner.
const SURFACE_MIN = SIZE * 0.064;
const SURFACE_MAX = SIZE * 0.936;
const POCKET_AT = SIZE * 0.102;
const POCKETS = [
[POCKET_AT, POCKET_AT],
[SIZE - POCKET_AT, POCKET_AT],
[POCKET_AT, SIZE - POCKET_AT],
[SIZE - POCKET_AT, SIZE - POCKET_AT],
];
// Centered on the baseline strip painted in the board art (measured at 82.76% down the image).
const BASELINE_Y = SIZE * 0.8276;
const START_MESSAGE = 'Press the striker, drag back, and let go to flick.';
function makeStartingBalls() {
// Mini version of the real setup: queen in the middle, one ring of 6 touching her,
// whites and browns alternating. You play white.
const ring = Array.from({length: 6}, (_, i) => {
const a = (i * Math.PI) / 3;
return {
id: i + 2,
kind: i % 2 === 0 ? 'white' : 'black',
x: CENTER + 23 * Math.cos(a),
y: CENTER - 23 * Math.sin(a),
vx: 0,
vy: 0,
r: MAN_R,
sunk: false,
};
});
return [
{id: 0, kind: 'striker', x: CENTER, y: BASELINE_Y, vx: 0, vy: 0, r: STRIKER_R, sunk: false},
{id: 1, kind: 'queen', x: CENTER, y: CENTER, vx: 0, vy: 0, r: MAN_R, sunk: false},
...ring,
];
}
const [balls, setBalls] = useState(makeStartingBalls);
const [message, setMessage] = useState(START_MESSAGE);
const [pointer, setPointer] = useState(null);
const boardRef = useRef(null);
const sim = useRef({balls: null, raf: 0, last: 0, dragging: false, moving: false, hasPottedWhite: false, queenPending: false, queenCovered: false});
useEffect(() => () => cancelAnimationFrame(sim.current.raf), []);
function findFreeSpot(list, r) {
// First free spot at the center, spiraling outward, used when a piece returns to the board.
for (let ringIndex = 0; ringIndex < 8; ringIndex++) {
for (let slot = 0; slot < Math.max(1, ringIndex * 6); slot++) {
const a = (slot * 2 * Math.PI) / Math.max(1, ringIndex * 6);
const x = CENTER + ringIndex * (MAN_R * 2 + 2) * Math.cos(a);
const y = CENTER + ringIndex * (MAN_R * 2 + 2) * Math.sin(a);
if (list.every((b) => b.sunk || (b.x - x) ** 2 + (b.y - y) ** 2 > (b.r + r + 1) ** 2)) {
return {x, y};
}
}
}
return {x: CENTER, y: CENTER};
}
function returnToBoard(ball, list) {
const spot = findFreeSpot(list, ball.r);
ball.sunk = false;
ball.x = spot.x;
ball.y = spot.y;
ball.vx = 0;
ball.vy = 0;
}
function respawnStriker(list) {
const striker = list[0];
striker.sunk = false;
striker.vx = 0;
striker.vy = 0;
striker.y = BASELINE_Y;
for (let offset = 0; offset < 8; offset++) {
for (const sign of [1, -1]) {
const x = CENTER + sign * offset * 18;
if (list.slice(1).every((b) => b.sunk || (b.x - x) ** 2 + (b.y - BASELINE_Y) ** 2 > (b.r + striker.r + 1) ** 2)) {
striker.x = x;
return;
}
}
}
striker.x = CENTER;
}
function settle(sunkBefore) {
const s = sim.current;
const list = s.balls;
const newlySunk = list.filter((b) => b.sunk && !sunkBefore.has(b.id));
const strikerFoul = newlySunk.some((b) => b.kind === 'striker');
const whites = newlySunk.filter((b) => b.kind === 'white');
const queenSunk = newlySunk.some((b) => b.kind === 'queen');
const browns = newlySunk.filter((b) => b.kind === 'black');
let msg;
if (strikerFoul) {
for (const w of whites) {
returnToBoard(w, list);
}
if (queenSunk || s.queenPending) {
const queen = list[1];
if (queen.sunk) {
returnToBoard(queen, list);
}
s.queenPending = false;
}
const penalty = list.find((b) => b.kind === 'white' && b.sunk);
if (penalty) {
returnToBoard(penalty, list);
}
msg = 'Foul! The striker went in. Your pieces from this shot come back, plus one already-pocketed piece as a penalty.';
} else if (queenSunk && whites.length === 0 && !s.hasPottedWhite) {
returnToBoard(list[1], list);
msg = 'The queen only counts after you have pocketed one of your own. She goes back to the center.';
} else if (queenSunk && whites.length > 0) {
s.queenCovered = true;
s.hasPottedWhite = true;
msg = 'Queen down and covered in one shot. She stays down for good.';
} else if (s.queenPending && whites.length > 0) {
s.queenPending = false;
s.queenCovered = true;
s.hasPottedWhite = true;
msg = 'Covered! The queen stays down for good.';
} else if (s.queenPending) {
s.queenPending = false;
returnToBoard(list[1], list);
msg = 'No cover. The queen returns to the center, and in a real board your turn would end.';
} else if (queenSunk) {
s.queenPending = true;
msg = 'Queen down! Now cover her: pocket a white piece with your next flick or she comes back.';
} else if (whites.length > 0) {
s.hasPottedWhite = true;
msg = browns.length > 0 ? 'One of yours dropped, so you keep your turn. The brown one stays down too, a gift to your opponent.' : 'That one is yours, so you keep your turn. Flick again.';
} else if (browns.length > 0) {
msg = 'That piece belongs to your opponent. It stays down, and your turn would pass.';
} else {
msg = 'Nothing dropped, so the turn would pass. Try again.';
}
if (list.every((b) => b.kind !== 'white' || b.sunk) && s.queenCovered) {
msg = 'Board cleared, queen covered. That is the win!';
}
respawnStriker(list);
setMessage(msg);
setBalls(list.map((b) => ({...b})));
}
function tick(now) {
const s = sim.current;
const dt = Math.min(32, now - s.last) / 1000;
s.last = now;
const list = s.balls;
// Constant sliding deceleration, like a real disc on a powdered board. Exponential drag
// reads as air hockey: fast pieces glide too far and slow ones crawl forever.
const speedDrop = 1000 * dt;
for (const b of list) {
if (b.sunk) {
continue;
}
// Sleep: snap sub-visible speeds to a dead stop, or resting pieces jiggle forever on
// micro-velocities. A collision impulse from a moving piece wakes them.
const speed = Math.hypot(b.vx, b.vy);
if (speed < 8 || speed <= speedDrop) {
b.vx = 0;
b.vy = 0;
continue;
}
b.x += b.vx * dt;
b.y += b.vy * dt;
b.vx *= 1 - speedDrop / speed;
b.vy *= 1 - speedDrop / speed;
for (const [px, py] of POCKETS) {
if ((b.x - px) ** 2 + (b.y - py) ** 2 < POCKET_R ** 2) {
b.sunk = true;
}
}
if (b.sunk) {
continue;
}
if (b.x < SURFACE_MIN + b.r) { b.x = SURFACE_MIN + b.r; b.vx = -b.vx * 0.72; }
if (b.x > SURFACE_MAX - b.r) { b.x = SURFACE_MAX - b.r; b.vx = -b.vx * 0.72; }
if (b.y < SURFACE_MIN + b.r) { b.y = SURFACE_MIN + b.r; b.vy = -b.vy * 0.72; }
if (b.y > SURFACE_MAX - b.r) { b.y = SURFACE_MAX - b.r; b.vy = -b.vy * 0.72; }
}
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
const a = list[i];
const b = list[j];
if (a.sunk || b.sunk) {
continue;
}
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 0.001;
const overlap = a.r + b.r - dist;
if (overlap > 0) {
const nx = dx / dist;
const ny = dy / dist;
// Soft positional correction with a small tolerance: full-strength separation every
// frame visibly shoves resting neighbors apart step by step.
if (overlap > 0.3) {
const push = overlap * 0.6;
a.x -= (nx * push) / 2;
a.y -= (ny * push) / 2;
b.x += (nx * push) / 2;
b.y += (ny * push) / 2;
}
const ma = a.kind === 'striker' ? 1.6 : 1;
const mb = b.kind === 'striker' ? 1.6 : 1;
const rel = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny;
if (rel < 0) {
// Slow contacts are nearly dead so clusters settle; only real hits stay springy.
const e = -rel < 30 ? 0.2 : 0.93;
const impulse = (-(1 + e) * rel) / (1 / ma + 1 / mb);
a.vx -= (impulse / ma) * nx;
a.vy -= (impulse / ma) * ny;
b.vx += (impulse / mb) * nx;
b.vy += (impulse / mb) * ny;
}
}
}
}
setBalls(list.map((b) => ({...b})));
if (list.some((b) => !b.sunk && Math.hypot(b.vx, b.vy) > 8)) {
s.raf = requestAnimationFrame(tick);
} else {
s.moving = false;
settle(s.settleBaseline);
}
}
function localPoint(e) {
const rect = boardRef.current.getBoundingClientRect();
return {x: e.clientX - rect.left, y: e.clientY - rect.top};
}
function onPointerDown(e) {
const s = sim.current;
if (s.moving) {
return;
}
const p = localPoint(e);
const striker = balls[0];
if (Math.hypot(p.x - striker.x, p.y - striker.y) < STRIKER_R + 14) {
s.dragging = true;
e.currentTarget.setPointerCapture(e.pointerId);
setPointer(p);
}
}
function onPointerMove(e) {
if (sim.current.dragging) {
setPointer(localPoint(e));
}
}
function onPointerUp(e) {
const s = sim.current;
if (!s.dragging) {
return;
}
s.dragging = false;
setPointer(null);
const p = localPoint(e);
const striker = balls[0];
let vx = (striker.x - p.x) * 7;
let vy = (striker.y - p.y) * 7;
const speed = Math.hypot(vx, vy);
if (speed < 70) {
return;
}
if (speed > 950) {
vx *= 950 / speed;
vy = 950 / speed;
}
s.balls = balls.map((b) => ({...b}));
s.balls[0].vx = vx;
s.balls[0].vy = vy;
s.settleBaseline = new Set(s.balls.filter((b) => b.sunk).map((b) => b.id));
s.moving = true;
s.last = performance.now();
s.raf = requestAnimationFrame(tick);
}
function reset() {
const s = sim.current;
cancelAnimationFrame(s.raf);
s.moving = false;
s.dragging = false;
s.hasPottedWhite = false;
s.queenPending = false;
s.queenCovered = false;
setPointer(null);
setBalls(makeStartingBalls());
setMessage(START_MESSAGE);
}
const striker = balls[0];
const whitesLeft = balls.filter((b) => b.kind === 'white' && !b.sunk).length;
const queenState = sim.current.queenCovered ? 'covered' : sim.current.queenPending ? 'needs cover' : balls[1].sunk ? 'pocketed' : 'on the board';
const aim = pointer && {dx: striker.x - pointer.x, dy: striker.y - pointer.y};
return (
{message}
Your pieces left: {whitesLeft} · Queen: {queenState}
{aim && Math.hypot(aim.dx, aim.dy) > 8 && (
<span
className="absolute h-[5px] origin-left rounded bg-gradient-to-r from-amber-300 to-orange-600 shadow-[0_0_6px_rgba(251,146,60,0.7)]"
style={{
left: striker.x,
top: striker.y,
width: Math.min(135, Math.hypot(aim.dx, aim.dy)),
transform: rotate(${Math.atan2(aim.dy, aim.dx)}rad),
}}
/>
)}
{balls.map((b) => (
<span key={b.id} className="absolute" style={{transform: translate(${b.x - b.r}px, ${b.y - b.r}px)}}>
<span className={'block transition-transform duration-200 ' + (b.sunk ? 'scale-0' : 'scale-100')}>
<Disc kind={b.kind === 'black' ? 'black' : b.kind} size={b.r * 2} />
))}
Carrom Game Online
Foony Carrom is a free online carrom game with realistic striker physics, the full queen and cover rules enforced automatically, and private rooms you can share with one link. You flick a striker across a square wooden board, pocket your nine pieces in the corner pockets, and race your opponent to clear the board. It runs in your browser on desktop or phone with nothing to install.
Carrom started in India and is played across South Asia and far beyond, usually on a living room floor with talcum powder and strong opinions. This version keeps the feel of the real board, the slide of the pieces, the rebounds off the frame, and the tension of the last shot, without anyone needing to own 74 centimeters of plywood.
Play Carrom Online with Friends
Click Play with Friends to open a private room and share the invite link. Anyone with the link joins from a modern browser, no account or install needed. A board of carrom is a head-to-head duel between two players, and extra people in the room can hang out, chat, and rotate in between boards.
Prefer solo practice? Add a bot instead. There are four tiers, Easy, Medium, iCheater, and iCheater Pro Max. Easy leaves you room to learn the aim controls, Medium punishes lazy shots, and the two iCheater tiers test a spread of striker positions and angles before every flick. Hosts can also set a per-turn time limit from 5 to 60 seconds, or turn the timer off entirely.
Carrom Board Setup
The board is a square with a pocket in each corner. Nineteen pieces start in the center: the red queen in the exact middle, an inner circle of six pieces, and an outer circle of twelve. The white pieces form a Y shape pointing out from the queen, which is the traditional arrangement. Each player flicks from their own baseline, the red strip near their edge of the board.
Whoever breaks plays white and goes first. You place the striker anywhere on your baseline before every shot, so picking the right spot along that strip is as much a part of aiming as the angle itself.
How to Play Carrom
On your turn, slide the striker along your baseline, aim, set power, and release. Pocket one of your own pieces and you shoot again. Pocket nothing and the turn passes. Pocket one of your opponent's pieces and it stays down, a gift they will thank you for. The first player to pocket all nine of their pieces wins the board.
Here is a small practice board you can flick right now. You play the light pieces, and it calls out what each result would mean in a real game:
Pocketing the striker itself is a foul. Any of your pieces that fell on that shot come back to the center, one of your previously pocketed pieces returns as a penalty, and your turn ends. There is one more rule that trips up new players: you cannot pocket your last piece while the queen is still on the board. She always has to be settled first.
The Carrom Queen and Covering
The queen is worth nothing on her own. You may only pocket her after sinking at least one of your own pieces, and you must then cover her by pocketing another of your own pieces on the same shot or the very next one. Fail the cover and she returns to the center circle.
Timing the queen is the heart of carrom strategy. Take her too early and you risk handing her back. Leave her too late and your opponent may clear their pieces while you babysit the middle of the board.
Carrom Striker Tips and Strategy
- Move the striker before you aim. The baseline is wide. Sliding the striker two piece-widths left often turns an impossible cut into a straight shot at a corner pocket.
- Break toward a corner, not straight ahead. An angled break scatters the formation toward two pockets at once and often drops a piece immediately.
- Use the frame. A piece hugging a cushion is easiest to cut in along the rail. The rebound angles are honest, so a bank off the frame into a corner is a real weapon, and it earns the Off the Frame achievement.
- Count before you take the queen. Pocket her when you still have two or three easy pieces left, so the cover shot is a formality instead of a prayer.
- Mind the last-piece rule. If the queen is still up, plan your order so your ninth piece is not the only shot you have left.
Leaderboards, Achievements, and Progression
Every board feeds the Carrom leaderboards across day, week, month, year, and all-time windows. There are 8 Carrom achievements to chase, from First Flick and Board Winner up to Crowned for covering the queen, Hat Trick for three or more pockets in one turn, Steady Hand for winning without a single striker foul, and Untouchable for winning before your opponent pockets anything. Playing also levels your carrom rank and earns currency drops you can spend across Foony.
Carrom vs Pool
Carrom and pool share the same core idea, send the round thing into the hole, but the feel is completely different. Carrom uses your finger and a flat striker on a dry square board where pieces slide, while 8-ball pool uses a cue stick and rolling balls on cloth. Carrom shoots from a fixed baseline every turn, pool plays the cue ball from wherever it stops. And where pool has solids and stripes, carrom has the queen, a single shared piece both players fight over. Foony Carrom runs on the same physics engine as our pool and snooker tables, so if you can judge an angle in one, you can judge it in all three. Fans of Carrom Pool style mobile games will feel at home here, with the classic rules kept intact.