Source Code


What is Unconventional Russian Roulette?

Unconventional Russian Roulette introduces similarities as its origin Russian Roulette. The difference is that instead of using one bullet, it uses three bullets where they are loaded adjacently.

Let’s imagine this scenario:

Three bullets are loaded into the cylinder adjacently, Player A has taken a shot and survived. We, as Player B, need to decide whether to take a shot or not. For the sake of simplicity, the game ends this round and here are the conditions:

  • If we took a shot, and it was empty -> WE WON 1 MILLION DOLLARS
  • If we chose to stay, and it was loaded -> WE WON 1 MILLION DOLLARS

Q: If the main goal here is to be alive and to take 1 MILLION DOLLARS back home. Should Player B take a shot or not? and Why?

Indeed, there is a way to increase our chances of going back home alive with money.


The Winning Strategy

A cylinder consists of 6 rounds: 3 loaded, 3 empty. That’s 3/6 and 3/6 or 50% equal chance of surviving. However, the game begins the second round for us (as Player B). After the first round has been played out and Player A has survived, the chance of winning is dimming since an empty round has been shot; that leaves us 3/5 or 60% of catching a bullet.

loaded cylinder

A right-minded person would choose to pass since there’s a 60% chance of bullet existing in this round. However, this is not the case. By having three adjacent bullets loaded gives us a huge advantage of earning that lovely 1 million dollars and going home safely by taking a shot.

Considering this, Player A has survived the first round which means an empty round was fired; furthermore, the bullets are loaded adjacently. Therefore, there’s 2/3 chance that the cylinder is currently empty. The figure below depicted the aforementioned scenario in which the cylinder rotates in clockwise.

loaded cylinder
  • Green stars represent the possible first round shots

We can see that 2/3 will be empty and 1/3 will be a bullet. So, if we had to be choose between taking a shot or passing to get 1 MILLION DOLLARS, we’d have a better chance with taking a shot.


Experimental Procedures

Let’s create a simple script to simulate the game for us to prove the concept.

  • Create a GameEngine to simulate the game
# Import libs
import random
from typing import List, Dict, Any


# Core Game Engine
class GameEngine:
    """ Unlike its original rules, Unconventional Russian Roulett puts 2 bullets
        in the cylinder (n=6) **adjacently**. The game proceeds as usual in which the cylinder is initially spun,
        and each player takes turn to pull the trigger. The survival wins, obviously.
        Assume, we were "Player B" starting second round. After the first round had been played by Player A, and he/she survived.
        Now it is out turn to play; however, you were given a choice to take a shot or to stay. Here are rules
            - If we took a shot, and it was empty -> WE WON 1 MILIION DOLLARS
            - If we chose to stay, and it was loaded -> WE WON 1 MILIION DOLLARS

        The question raised here is that
            - Should we take a shot or not? and why?
            - Assuming that our main goal was to get 1 million dollars at all costs
    """

    def __init__(self) -> None:
        """ Generate cylinders (6 rounds)
            0   -> Empty (n=3)
            1   -> Loaded (n=3)
        """
        self._load_cylinder()

    def _load_cylinder(self) -> None:
        self.loaded_cylinder = [1, 1, 1] + [0 for _ in range(3)] # 3 loaded; 3 empty

    def _spin_cylinder(self) -> None:
        """ Simulate spinning cylinder """
        random_spin_num = random.randrange(1, 7) # random 1 - 6
        self.loaded_cylinder = self.loaded_cylinder[random_spin_num:] + self.loaded_cylinder[:random_spin_num]

    def _play(self, take_a_shot: bool = False) -> bool:
        """ Play the game
            1.) Player A will guarantee to survive the first round
            2.) Player B will need to choose to whether take a shoot or stay
            3.) The game will stop at the Player B's first round for simplicity
        """
        # Keep spinning to make sure the first round is not loaded
        self._load_cylinder()
        while self.loaded_cylinder[0] == 1:
            self._spin_cylinder()

        # Skip the first round since it is guaranteed to be empty for Player A
        has_player_won_million = False
        for bullet in self.loaded_cylinder[1:]:
            if take_a_shot and bullet == 0:
                has_player_won_million = True
            elif not take_a_shot and bullet == 1:
                has_player_won_million = True
            break
        return has_player_won_million

    def start_experiment(self, n: int = 1000) -> Dict[str, List[bool]]:
        experimental_results = {'game_num': n, 'take_a_shot_won': [], 'pass_won': []}
        if not isinstance(n, int):
            return experimental_results

        experimental_results['take_a_shot_won'] = sum([self._play(take_a_shot=True) for _ in range(n)])
        experimental_results['pass_won'] = sum([self._play(take_a_shot=False) for _ in range(n)])
        return experimental_results
  • Create a main function to play multiple games (n=1000) and visualize the result
# Import libraries
import matplotlib.pyplot as plt
import pandas as pd
import plotly.express as px

from game_engine import GameEngine

# Load Core GameEngine
game_engine = GameEngine()


# Main Function
def main() -> None:
    # Experimental Results
    experimental_res = game_engine.start_experiment(n=1000)

    # Prepare DataFrame
    df = pd.DataFrame({}, columns=['cat', 'num_of_winning'])
    df = df.append({'cat': 'take_a_shot_won', 'num_of_winning': experimental_res.get('take_a_shot_won')}, ignore_index=True)
    df = df.append({'cat': 'pass_won', 'num_of_winning': experimental_res.get('pass_won')}, ignore_index=True)
    print(df)

    # Visualisation
    fig = px.pie(df, values='num_of_winning', names='cat', hole=.3)
    fig.show()

    return None


if __name__ == '__main__':
    main()

Results

By experimenting 1000 games with a taking-a-shot method and a passing method, the results showed that taking-a-shot won 658 games; on the other hand, passing only won 312 games. The results matched our assumption.

               cat num_of_winning
0  take_a_shot_won            658
1         pass_won            312
loaded cylinder

Summary

Unconventional Russian Roulette offers something fresh and interesting to explore. We can always add more peculiar rules to make things complicated, yet insert hidden solutions to take advantages.

Finally, I do hope that this post sparks your interest in programming and data exploration. Cheers.


References