ROSHCAM
Loading assets… 0%
Say hi 👋 · the match starts when you're both ready
You VS opponent
ROSHCAM
in queue
· 0 active matches
?
1
View profile
NOTIFICATIONS
No notifications yet
MESSAGES
No conversations yet. Add friends to start chatting.
developer · firebase not configured

SETUP FIREBASE.

This app is intended to ship with a Firebase config hard-coded. To configure it, follow these steps and paste your config into the HTML.

1

Create a Firebase project

Go to console.firebase.google.com, click Add project.

2

Enable Authentication

Build → Authentication → Get started. Enable Email/Password, Google, and Anonymous.

3

Create Firestore

Build → Firestore Database → Create. Paste these rules under the Rules tab:

rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // The moderation admin account. function isAdmin() { return request.auth != null && request.auth.uid == '9DURgNdTyUhtBso2L8Lo22R3Dru1'; } // ECONOMY GUARD: the browser may never change its own coin balance or the // list of owned cosmetics — only Cloud Functions (Admin SDK) can. Equipped // cosmetic SLOTS stay client-writable, so equipping still works offline of // the functions. function economyUntouched() { return request.resource.data.get('coins', 0) == resource.data.get('coins', 0) && request.resource.data.get('cosmetics', {}).get('owned', []) == resource.data.get('cosmetics', {}).get('owned', []); } match /users/{userId} { allow read: if request.auth != null; allow create: if request.auth != null && (request.auth.uid == userId || isAdmin()) && request.resource.data.get('coins', 0) == 0 && request.resource.data.get('cosmetics', {}).get('owned', []).size() == 0; allow update: if request.auth != null && (request.auth.uid == userId || isAdmin()) && (economyUntouched() || isAdmin()); } match /usernames/{name} { allow read: if request.auth != null; allow create: if request.auth != null && request.resource.data.uid == request.auth.uid; allow delete: if request.auth != null && resource.data.uid == request.auth.uid; } match /queue/{userId} { allow read, list: if request.auth != null; allow create, update: if request.auth != null && request.auth.uid == userId; // Allow deleting any queue entry (matchmaking transactions need this) allow delete: if request.auth != null; } match /matches/{matchId} { allow read, list: if request.auth != null; allow create: if request.auth != null; allow update, delete: if request.auth != null && (resource.data.caller == request.auth.uid || resource.data.callee == request.auth.uid); match /callerCandidates/{doc} { allow read, write: if request.auth != null; } match /calleeCandidates/{doc} { allow read, write: if request.auth != null; } } match /friendRequests/{reqId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.from == request.auth.uid; allow update, delete: if request.auth != null && (resource.data.from == request.auth.uid || resource.data.to == request.auth.uid); } match /invites/{inviteId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.from == request.auth.uid; allow update, delete: if request.auth != null && (resource.data.from == request.auth.uid || resource.data.to == request.auth.uid); } match /activity/{actId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.uid == request.auth.uid; } match /matchHistory/{histId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.uid == request.auth.uid; } match /reports/{reportId} { allow create: if request.auth != null && request.resource.data.reporter == request.auth.uid; // Only the admin can read / triage / dismiss (delete) reports. allow read, list, update, delete: if isAdmin(); } match /profileComments/{profileUid}/comments/{id} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.from == request.auth.uid; allow delete: if request.auth != null && (resource.data.from == request.auth.uid || profileUid == request.auth.uid || isAdmin()); } match /globalChat/{msgId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.uid == request.auth.uid && request.resource.data.text is string && request.resource.data.text.size() > 0 && request.resource.data.text.size() <= 300; allow delete: if request.auth != null && (resource.data.uid == request.auth.uid || isAdmin()); } // Direct messages: index doc per conversation + message subcollection. match /dmIndex/{convId} { allow read: if request.auth != null && request.auth.uid in resource.data.participants; allow create: if request.auth != null && request.auth.uid in request.resource.data.participants; allow update: if request.auth != null && request.auth.uid in resource.data.participants; } match /dms/{convId}/messages/{msgId} { allow read, list: if request.auth != null; allow create: if request.auth != null && request.resource.data.from == request.auth.uid; allow update: if request.auth != null; // reactions + soft-delete flags } match /notifications/{userId}/items/{id} { allow read, list, update, delete: if request.auth != null && request.auth.uid == userId; allow create: if request.auth != null; // anyone can notify you } // GIFTS + PURCHASES are function-only: readable by the people involved, // never writable from the browser. match /gifts/{giftId} { allow read, list: if request.auth != null && (resource.data.to == request.auth.uid || resource.data.from == request.auth.uid); } match /purchases/{purchaseId} { allow read: if request.auth != null && resource.data.uid == request.auth.uid; } } }
4

Add your domain

Authentication → Settings → Authorized domains. Add your GitHub Pages domain (e.g. yourname.github.io).

5

Paste config into the HTML

Project settings → Your apps → </> → register, then copy the firebaseConfig object. Open rosham.html, find the FIREBASE_CONFIG constant near the top of the <script> section, and replace the placeholder object with your real config.

// Find this near the top of the <script type="module"> section: const FIREBASE_CONFIG = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_PROJECT.firebaseapp.com", projectId: "YOUR_PROJECT", appId: "1:..." };
ROSHCAM

Rock. Paper. Scissors.
Against real people.

Get matched with a stranger worldwide over live webcam, read their eyes, and throw first. Climb a ranked ladder from Bronze all the way to Legend.

Live webcam duelsReal opponents, real reflexes — your hand is the controller.
Ranked & ELOWin to climb 9 ranks and 50 levels. Build streaks for big jumps.
Friends & challengesAdd friends, message, and challenge them to private duels.
Private by designVideo is peer-to-peer — we never record it. Streamer mode hides names.
playing now · free to play · 18+

Get started

Free account, or jump straight in as a guest — no email needed.

Secured by Google Video never recorded
or with email
or

No email, no commitment — try a match, make an account later to keep your rank.

one last step

PICK YOUR handle.

This is how the world will know you in the arena.

?
your_handle 🌍
1000 ELO · Level 1 · Rookie
PREVIEW
You can change both any time in Settings → Privacy.

You can change your username, country and avatar any time in Settings.

1000
SEARCHING online
?

FINDING OPPONENT...

scanning the globe…
MATCH RANGE±90
◄ lower ELOwidening the net as you wait…higher ELO ►
🌍 Worldwide Spot #1 ~15s wait
Hold a clear gesture before the timer ends.
searching 0s
?
you
local · webcam 0ms
0 · 0
BEST OF 3
?
connecting…
In-match options
Opponent stepped away…
Camera pausedTap "Pause my camera" to resume
show hand
VS
?
NO PEEKING
opponent hidden during round
waiting
opponent disconnected · finding a new one…
PLAYERS — GET READY!
3
SHOW YOUR HAND
rock, paper, or scissors · hold steady
vs

PREDICTED!
🎤 Let's see it!
?
YOU
Bronze III
VS
?
OPP
Bronze III
first time facing this player
BEST OF 3 · WINNER TAKES ALL
ON FIRE 3
❄️ ICE COLD 3
⚠️SUSPICIOUS TIMING
PREDICTtheir throw · bonus XP
PROMOTION
Bronze III
Silver I

SILVER

1100 RATING
Promoted from Bronze III
Tap or press SPACE to continue
FORFEIT PENALTY

RATING LOST

−49 ELO
1893 1844
You left an active match by closing or refreshing the browser. This counts as a forfeit and costs −25 ELO on top of the normal loss.
2
LEVEL UP
You reached Level 2
LEVEL UP
2
Level 1 Level 2
SPACE to continue
PRESTIGE
1

PRESTIGE 1

Gold
You hit the pinnacle and prestiged!
Tap to continue
VICTORY
Best of 3 · won 2–0
?
you
2 0
?
@stranger
Bronze III
1000 +12
1
Rookie +0 XP
0 / 200 XP
OPENING
Press SPACE or tap to skip
LEGENDARY
GIFT INCOMING
@player sent you a gift!
🎁

Open it, or decline to send the coins back.

You are banned

Your account has been suspended.