To bring the initial concept of Russian Roulette to life, I decided to use generative AI for the images. I faced several issues sketching the artwork myself on my iPad, and I wanted a surreal realism and impact from the art. The images are therefore generated by DALL-E.
Here are the Game Mechanics:
Players
- Player: Player starts with 100 health, the bar is at the top-left (50, 50).
- Opponent: Also 100 health, bar is at top-right (650, 50).
- Health: Drops by 50 when hit. Game’s over if either of us hits 0.
Gun
- It’s a 6-shot revolver (shotsFired tracks how many times it’s fired).
- The chamber’s random—loaded or empty (isLoaded)—and resets on reload.
- Every shot counts toward the 6, whether it’s a bang or a click.
States
- “between”: Whether the choice is to shoot (S) or pass (N).
- “playerTurn”: Player shoots, showing playergun.gif.
- “opponentTurn”: Person shoots, showing persongun.gif.
- “reload”: After 6 shots, we reload, and they shoot at me!
- “gameOver”: One is down; hit R to restart.
My Journey Building It
I started with the basics—getting the images and health bars up. That was smooth, but then I hit a wall with sounds. I added gunshot.m4a for hits, and it worked sometimes, but other times—nothing. That was frustrating. Turns out, browsers block audio until you interact with the page, so I had to trigger it after a key press. Even then, emptyclick.m4a wouldn’t play right when the opponent fired an empty shot. I kept seeing “sound not loaded” in the console and realized the timing was off with setTimeout. I fixed it by storing the shot result in a variable and making sure the sound played every time—loaded or not. Adding the reload mechanic was tricky too; I wanted the opponent to shoot during it, but keeping the flow consistent took some trial and error.
Image:
The image (From DALL-E) I used to depict the person wearing a silk mask covering his facial features and creating a feeling of suspense:
Gameplay Flow
- Start: Game kicks off in “between” with nogun.gif.
- My Turn:
- S: I shoot.
- Loaded: gunshot.m4a, light flashes, opponent loses 50 health.
- Empty: emptyclick.m4a, no damage.
- N: Pass to the opponent.
- S: I shoot.
- Opponent’s Turn: They always shoot now (I made it consistent!).
- Loaded: gunshot.m4a, light flashes, I lose 50 health.
- Empty: emptyclick.m4a, no damage.
- Reload: After 6 shots:
- Switches to “reload”, shows nogun.gif.
- Gun resets with a random chamber.
- Opponent shoots at me:
- Loaded: gunshot.m4a, light flash, I take damage.
- Empty: emptyclick.m4a, I’m safe.
- Back to “between”.
- Game Over: When me or the opponent hits 0 health.
- “Game Over! Press ‘R’ to restart” shows up, and R starts it over.
Controls
- S: I shoot.
- N: Pass to the opponent.
- R: Restart when it’s over.
Visuals
- Canvas: 800×600—big enough to see everything.
- Images: Centered at (400, 300), 300×300 pixels.
- Health Bars: Red base (100 wide), green shrinks as health drops.
- Light Effect: A cool yellow-white flash when a shot lands—fades out fast.
- Instructions: Text at the bottom tells me what’s up.
Audio
- Gunshot: gunshot.m4a for a hit—loud and punchy.
- Empty Click: emptyclick.m4a for a miss—subtle but tense.
- Volume: Set both to 0.5 so my ears don’t hate me.
Overcoming Challenges
The sounds were my biggest headache. At first, gunshot.m4a only played after I clicked something—browser rules, ugh. I fixed that by tying it to key presses. Then emptyclick.m4a kept skipping when the opponent shot an empty chamber. I dug into the code and saw the random shoot chance was messing with the timing. I simplified it—stored the shot result, made the opponent shoot every time, and checked isLoaded() right before playing the sound. Now it’s rock-solid.
this.state = "playerTurn"; this.currentImg = playerGunImg; let shotFired = this.gun.shoot(); if (shotFired) { if (gunshotSound.isLoaded()) { gunshotSound.play(); } this.opponent.takeDamage(); this.flashAlpha = 255; } else { if (emptyClickSound.isLoaded()) { emptyClickSound.play(); } } setTimeout(() => this.checkReloadOrNext(), 1000); } opponentTurn() { this.state = "opponentTurn";
to show how I handled the opponent’s turn.
Key Gameplay Highlight
- Reload Mechanic: The reload’s risky and cool—opponent gets a free shot!
setTimeout(() => { let shotFired = this.gun.shoot(); if (shotFired) { if (gunshotSound.isLoaded()) { gunshotSound.play(); } this.player.takeDamage(); this.flashAlpha = 255; } else { if (emptyClickSound.isLoaded()) { emptyClickSound.play(); } } setTimeout(() => this.checkReloadOrNext(), 1000); }, 1000); } checkReloadOrNext() { if (this.gun.needsReload()) { this.reloadGun(); } else { this.nextRound(); } }
has the logic for resetting the gun and surviving that tense moment.
Technical Bits
- Classes:
- Player: Tracks my health and draws the bar.
- Gun: Manages the 6-shot limit and random chamber.
- Game: Runs the show—states, visuals, all of it.
- p5.js Stuff:
- preload(): Loads my assets.
- setup(): Sets up the 800×600 canvas and sound volumes.
- draw(): Keeps everything on screen.
- keyPressed(): Listens for S, N, R.
Endgame
It’s over when me or the opponent’s health hits 0. I see “Game Over! Press ‘R’ to restart”, hit R, and it’s back to square one—health full, gun reset.
What’s Next?
Maybe I’ll add a manual reload key or a score counter. Also, I would re-design the game with different artwork to make it more immersive.
Here is a snippet of the state handling, which was key to ensuring less redundancy by preventing procedural programming of the game. Also, the states handle the logic, and this was key in establishing how the game runs i.e if there is an issue with the Game Class, the rest of the gameplay mechanics are directly impacted.
class Game { constructor() { this.player = new Player("Player", 50, 50); // Player health bar at top-left this.opponent = new Player("Opponent", 650, 50); // Opponent health bar at top-right this.gun = new Gun(); this.state = "between"; // States: "between", "playerTurn", "opponentTurn", "reload", "gameOver" this.currentImg = noGunImg; // Start with no gun image this.flashAlpha = 0; // For light effect transparency }