“SpinClick” is an interactive Russian Roulette simulation game that recreates the tension and mechanics of the classic game through precise physics modeling and engaging visual design. The application features a six-chamber revolver system with scroll-controlled spinning mechanics, realistic momentum physics, and explosive visual effects, providing an authentic gaming experience while maintaining educational value about probability and risk.
spinclick/
└── index.html # Complete Russian Roulette simulation (844 lines)
let chambers = []; /* Array to hold bullet assignments (true = bullet) */
let firedChambers = []; /* Array to track fired chambers (true = fired) */
function initializeChambers() {
chambers = [false, false, false, false, false, false];
firedChambers = [false, false, false, false, false, false];
// Place one bullet in random chamber
const bulletChamber = Math.floor(Math.random() * 6);
chambers[bulletChamber] = true;
}
function getAlignedChamber() {
const normalizedRotation = ((currentRotation % 360) + 360) % 360;
// When rotation = 0°, circle 0 is at top
// When rotation = 60°, circle 5 moves to top (rotating clockwise)
const alignedChamber = Math.round((6 - normalizedRotation / 60)) % 6;
return alignedChamber;
}
document.addEventListener('wheel', function(event) {
event.preventDefault();
const scrollDelta = event.deltaY;
const scrollSpeed = Math.abs(scrollDelta) / 10;
const direction = scrollDelta > 0 ? 1 : -1;
// Add to rotation velocity (proportional to scroll speed)
rotationVelocity += scrollSpeed * direction * 2;
// Cap maximum velocity
rotationVelocity = Math.max(-50, Math.min(50, rotationVelocity));
});
function animateRotation() {
const timeSinceLastScroll = Date.now() - lastScrollTime;
// Apply friction/momentum decay
if (timeSinceLastScroll > 50) {
const decayFactor = 0.98;
rotationVelocity *= decayFactor;
// Magnetic alignment for settling
const currentAngle = ((currentRotation % 360) + 360) % 360;
let targetPosition = findNearestUnfiredPosition(currentAngle);
let angleDifference = targetPosition - currentAngle;
// Handle wraparound
if (angleDifference > 180) angleDifference -= 360;
if (angleDifference < -180) angleDifference += 360;
// Apply gentle magnetic force when settling
if (Math.abs(rotationVelocity) < 3 && Math.abs(angleDifference) < 15) {
const magneticForce = angleDifference * 0.05;
rotationVelocity += magneticForce;
}
}
}
function findNearestUnfiredPosition(currentAngle) {
const unfiredPositions = [];
// Get angles for all unfired chambers
for (let i = 0; i < 6; i++) {
if (!firedChambers[i]) {
const chamberPosition = ((6 - i) * 60) % 360;
unfiredPositions.push(chamberPosition);
}
}
// Find closest unfired position
let bestPosition = unfiredPositions[0];
let bestDistance = getAngleDistance(currentAngle, bestPosition);
for (const position of unfiredPositions) {
const distance = getAngleDistance(currentAngle, position);
if (distance < bestDistance) {
bestDistance = distance;
bestPosition = position;
}
}
return bestPosition;
}
/* Circle positions in perfect hexagon around rotation center */
/* Radius = 12vh (distance from rotation center to stationary barrel) */
/* Circle 1: -90° (270°) - top position, aligns with stationary barrel */
.moving-circle:nth-child(1) {
top: calc(50% - 12vh - 30px);
left: calc(50% - 30px);
}
/* Circle 2: -30° (330°) - top-right */
.moving-circle:nth-child(2) {
top: calc(50% - 6vh - 30px);
left: calc(50% + 10.39vh - 30px);
}
/* Continues for all 6 chambers at 60° intervals */
.moving-circle.fired {
background-color: #cccccc;
opacity: 0.6;
}
.moving-circle.clicked {
background-color: #4CAF50;
}
.click-zone.active {
opacity: 0.5;
border-color: #00ff00;
}
.game-container {
position: relative;
width: 100vh;
height: 100vh;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
.explosion::before {
content: '';
background: radial-gradient(circle, #ffffff, #ffff00, #ff6600, #ff0000, #800000);
border-radius: 50%;
animation: mainBlast 0.6s ease-out forwards;
}
@keyframes mainBlast {
0% {
width: 0;
height: 0;
opacity: 1;
}
30% {
width: 200px;
height: 200px;
opacity: 1;
}
100% {
width: 800px;
height: 800px;
opacity: 0;
}
}
function createExplosion() {
// Create random particles
for (let i = 0; i < 20; i++) {
const particle = document.createElement('div');
particle.className = 'explosion-particle';
// Random position around explosion center
const angle = (Math.PI * 2 * i) / 20;
const distance = 50 + Math.random() * 150;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
particle.style.left = `calc(50% + ${x}px)`;
particle.style.top = `calc(50% + ${y}px)`;
particle.style.animationDelay = `${Math.random() * 0.2}s`;
document.querySelector('.game-container').appendChild(particle);
particle.classList.add('animate');
}
}
// Create fragments
for (let i = 0; i < 8; i++) {
const fragment = document.createElement('div');
fragment.className = 'explosion-fragment';
const angle = (Math.PI * 2 * i) / 8;
const distance = 100 + Math.random() * 200;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
fragment.style.setProperty('--fragment-x', `${x}px`);
fragment.style.setProperty('--fragment-y', `${y}px`);
}
let gameState = 'spinning'; // 'spinning', 'waiting', 'survived', 'gameOver'
function updateGameStatus() {
switch(gameState) {
case 'spinning':
gameStatus.textContent = 'Spinning chamber...';
break;
case 'waiting':
gameStatus.textContent = '';
clickZone.classList.add('active');
break;
case 'survived':
// Keep the survival message
break;
case 'gameOver':
gameStatus.textContent = '';
break;
}
}
function pullTrigger() {
if (gameState !== 'waiting') return;
roundsPlayed++;
const alignedChamber = getAlignedChamber();
const isLoaded = chambers[alignedChamber];
// Mark this chamber as fired and update visuals
firedChambers[alignedChamber] = true;
updateChamberVisuals();
if (isLoaded) {
// Game over - explosion!
gameState = 'gameOver';
createExplosion();
setTimeout(() => {
gameOver.classList.add('visible');
}, 400);
} else {
// Safe chamber - continue playing
luckyMessage.classList.add('animate');
gameStatus.textContent = `Safe! Round ${roundsPlayed} survived. Scroll to spin again...`;
gameState = 'survived';
}
}
// Scroll wheel control
document.addEventListener('wheel', function(event) {
event.preventDefault();
const scrollDelta = event.deltaY;
rotationVelocity += scrollSpeed * direction * 2;
});
// Mouse click trigger
document.addEventListener('click', function(event) {
event.preventDefault();
pullTrigger();
});
// Keyboard controls
document.addEventListener('keydown', function(event) {
if (event.code === 'Space') {
event.preventDefault();
pullTrigger();
}
});
let currentRotation = 0;
let rotationVelocity = 0;
let lastScrollTime = 0;
function animateRotation() {
// Update rotation only if there's velocity
if (rotationVelocity !== 0) {
currentRotation += rotationVelocity;
container.style.transform = `translate(-50%, -50%) rotate(${currentRotation}deg)`;
}
requestAnimationFrame(animateRotation);
}
.lucky-message.animate {
animation: luckyFlash 3s ease-out forwards;
}
@keyframes luckyFlash {
0% {
opacity: 0;
transform: translateX(-50%) scale(0.8);
}
10% {
opacity: 1;
transform: translateX(-50%) scale(1.2);
}
20% {
opacity: 1;
transform: translateX(-50%) scale(1);
}
80% {
opacity: 1;
transform: translateX(-50%) scale(1);
}
100% {
opacity: 0;
transform: translateX(-50%) scale(0.9);
}
}
// Clean up any remaining particles/fragments
document.querySelectorAll('.explosion-particle, .explosion-fragment').forEach(el => {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
});
SpinClick successfully recreates the psychological tension and mechanical precision of Russian Roulette through sophisticated physics modeling and engaging visual design. The scroll-controlled spinning mechanism provides an intuitive and realistic user experience while the probability-based gameplay offers educational value about risk assessment and statistical thinking.
Technical Rating: 9.1/10
The application demonstrates how classic concepts can be transformed into engaging digital experiences while maintaining both entertainment value and educational merit through precise technical implementation and thoughtful user experience design.