buckshot-roulette-sim
Running experiments on different algorithms to play the indie video game Buckshot Roulette.
- Two players take turns shooting the shotgun at themselves or at their opponent.
- Player One (Dealer) always starts first.
- The shotgun is loaded with blank and live shells, in a sequence unknown to the players.
- However, the number of each type of shell is visible to both players.
- When the shotgun runs out of shells, it is reloaded with a new load of shells identical to the previous load.
- Whomever a player shoots with a live shell takes a point of damage. Blank shells deal no damage.
- If a player chooses to fire upon themself and a blank is loaded, they do not end their turn.
- A player ends their turn when they do one of two things:
- Fire a blank shell at their opponent
- Fire a live shell at their opponent or self
- The game ends when a player has no more health points left, and the player left standing wins.
When I played Buckshot Roulette for the first time (and lost many, many times), I found myself wondering what the most optimal strategy was. The idea came to mind to experimentally test algorithms in a simulated environment for the game, and thus the idea for this experiment was born.
The goal of the experiment is to see how the win rate of each strategy varies as opposed to other strategies in differing conditions, such as different starting health and blank to live ratio.
🐛 Found a bug? Leave an Issue or PR with a bug fix. |
---|
To install and run this repo to gather data for yourself, follow these steps:
- If not installed, install Python >=3.12
- Run
git clone https://github.com/EightBitByte/buckshot-roulette-sim.git
cd
into this new directory- Run
python -m venv venv
- Activate the virtual environment (run
./venv/scripts/activate
) - Run
pip install -r requirements.txt
From here, you can choose to implement your own stratagems in src/stratagem.py
, alter the experiment variables in src/gather.data.py
, and/or gather data.
In src/stratagem.py
, you should define a class that inherits from the Stratagem
base class. It should implement one function: get_move
, which takes a GameState
object and returns a Move
object.
The GameState
object represents the simulated player counting the rounds in the gun; from this information, the simulated player should be able to make a decision.
For instance, one could implement a Stupid
stratagem that shoots itself when there are more live shells than blanks in the gun, like so:
# src/stratagem.py
class Stupid(Stratagem):
"""
This player shoots itself when there are more lives in the gun than blanks,
and shoots the opponent when there are more blanks in the gun than lives.
"""
def get_move(self, game_state: GameState) -> Move:
if (game_state.live_shells > game_state.blank_shells):
return Move.SHOOT_SELF
elif (game_state.live_shells < game_state.blank_shells):
return Move.SHOOT_OPPO
else:
return Move.SHOOT_SELF
As implemented in this repo, you should be able to run the code in gather_data.py
and see your stratagem against the others implemented. If you want to test only a selection of stratagems, see line 26 in gather_data.py
.
To gather data and plot on a graph:
- Run
python ./src/gather_data.py
to gather data per your experiments - TBD
Each simulated player adopts a different strategy. Those implemented are highlighted below. Those not yet implemented (but are planned) have an ❌ next to their names.
This player shoots the opponent if the shell in the chamber is more likely to be live. When the odds are even (50:50), this player will shoot itself in an effort to keep control of the gun (if the current shell is blank).
Like Greedy, this player shoots the opponent if the shell in the chamber is more likely to be live. When the odds are even (50:50), this player will shoot the other player in effort to avoid shooting itself.
Like Greedy and Safe, this player shoots the opponent if the shell in the chamber is more likely to be live. When the odds are even (50:50), this player will flip a coin and shoot the opponent if the coin lands on heads, shooting itself otherwise (similar to Random).
This player will flip a coin and shoot the opponent if the coin lands on heads, shooting itself otherwise.
Opposite of Scared. This player will always shoot itself until it is sure that the gun only contains live shells. Then, it will shoot the other player until the next load.
Opposite of Reckless. This player will always shoot the opponent until it is sure that the gun only contains blank shells.
This player plays the same as Safe until brought down to 1 HP. Then, it plays like Scared.
The following experiments will be run against every single pair of strategies outlined above. The experiment will continue for a fixed number of trials.
Experiment # | Load | Number of Trials | Health | Completed? |
---|---|---|---|---|
1 | 4 Blank, 4 Live | 100000 | 3 | ❌ |
2 | 2 Blank, 4 Live | 100000 | 3 | ❌ |
3 | 1 Blank, 4 Live | 100000 | 3 | ❌ |
4 | 4 Blank, 4 Live | 100000 | 5 | ❌ |
5 | 2 Blank, 4 Live | 100000 | 5 | ❌ |
6 | 1 Blank, 4 Live | 100000 | 5 | ❌ |
I defined a few terms in the context of this project that I refer to in this README
and in the code. They are defined below.
Term | Definition |
---|---|
blank |
A blank shell that deals no damage to the target. |
health |
The amount of damage points that a player can take before ending the game. |
live |
A live shell that deals 1 health point to the target. |
load |
A randomized sequence of shells consisting of an enumerated amount of blanks and lives. |
match/trial |
A single game played between two simulated players. |
pairing |
A unique pair of two stratagems. |