Deadly Danger Dungeon is a board game created by James Rolfe, aka the Angry Video Game Nerd, when he was little. In an episode 6 of the AVGN spin off Board James introduces and explains the game and attempts to play it.

Below is a scan of the game board.

Deadly Danger Dungeon board

The players progress through the game rolling a 6 sided die each turn and moving that number of squares and needs to acquire a key and then a talisman to escape and win the game. As seen in the episode though, success is very unlikely.

I wrote a python script to automate playing the game and get an idea of odds involved in escaping the dungeon.

Here is the link to the github repo

The game involves involves changing directions on the board at different times according to the in game states. I started by numbering each of the spaces on the board. Since the path required is predictable I chose to assign spots multiple numbers rather then deal with reversing the players direction. For instance, as far as the script is concerned, the first square is only passed over once, even though normally playing the game would require passing that spot three times. The other two times are represented as unique spots.

Below is the numbered board game. I used different colors to help keep track of the direction the player would travel.

Numbered Game Board

The first class in the script is the Player class which handles information about the player such as amount of health (self.hp) and spot on the game board (self.spot) etc, and functions for changing these attributes (move_to_spot, take_damage) and functions for returning the information (has_item, current_hp)

class Player:

    def __init__(self):
	    self.spot = 1
		self.hp = 3
		self.key = 0
		self.talisman = 0
		self.potion1 = 0
		self.potion2 = 0
		self.pole = 0

	def move_to_spot(self, spot):
		self.spot = spot
	
    def move_player(self, spots):
		self.spot += spots

	def take_damage(self, dmg):
		self.hp += dmg

	def add_item(self, item):
		setattr(self, item, True)
		if item.startswith('potion'):
		    self.hp += 2

	def lower_pole(self):
		self.pole = True

	def has_item(self, item):
		return getattr(self, item)

	def pole_down(self):
		return self.pole

	def current_spot(self):
		return self.spot

	def current_hp(self):
		return self.hp

The Game class is the largest and handles the game processes such as taking turns, moving the player and then checking and handling the results of the space the player has landed on.

class Game:

	def __init__(self):
		print
		print 'A new Doomed Soul enters the Deadly Danger Dungeon'
		self.player = Player()

	def check_spot(self):
		print self.player.current_spot()
		if self.player.current_spot() in dungeon:
			events[dungeon[self.player.current_spot()]].do(self.player)

	def move_spots(self):
		roll = roll_dice()
		return roll

	def check_win(self):
		if self.player.current_spot() in range(161, 170):
		    print 'Player escaped!'
			return True

	def check_alive(self):
		if self.player.current_hp() > 0:
			return True        
		print 'Player is dead'

The take_turn function handles the basic turn process, rolling the die and moving the player and then interpreting the result but also contains a large number of checks for implementing special instructions required for certain game states, ie checking if the player has the key (If the player does not land on the key they cannot continue on without because it is required to open a door later).

The bottom level (purple and brown numbers on the math) required more attention. The first time the player moves into the area, they have fallen from a spot above, they need to move to the right to the "pole" to lower the ladder on the left to climb out. This requires checking the state of the ladder to determine which path the player is on. Then when the player climbs up the ladder depending on how far a long they are, they may or may not need the key, so the script needs to check which way the player is headed and adjust their roll to put them on the appropriate track (red or green numbers).

The logic for these cases ended up being much more complicated then I expected when I started and I had to re-factor and fiddle with this part several times to account for everything.

	def take_turn(self):
		if not self.check_for_special():
			self.player.move_player(self.move_spots())
			self.check_spot()
		else:
			if self.pass_key():
				if self.player.current_spot() + 									self.move_spots() == 39:
					self.player.move_player(self.move_spots())
					self.check_spot()
                    
				if self.pass_talisman():
					if self.player.current_spot() + self.move_spots() == 191:                    
						self.player.move_player(self.move_spots())
						self.check_spot()

				if self.pass_door():
					print "Used key."
					self.player.move_player(self.move_spots())
					self.check_spot()

				if self.bottom_level():
					if not self.player.pole_down:
						if self.player.current_spot() + self.move_spots() < 195:
							self.player.move_player(self.move_spots())
							self.check_spot()

						if self.pass_ladder:
							if self.player.has_item('key') == True:
								overage = int((self.player.current_spot() + self.move_spots)) - 195
								self.player.move_to_spot((overage + 43))
							else:
								overage = int((self.player.current_spot() + self.move_spots)) - 195
								self.player.move_to_spot((overage + 35))
						else:
							self.player.move_player(self.move_spots())
							self.check_spot()

				if self.pass_use_talisman():
					print "Used talisman."
					self.player.move_player(self.move_spots())
					self.check_spot()
                    
				self.player.move_player(self.move_spots())
				self.check_spot()

	def check_for_special(self):
		return self.pass_key() or self.pass_talisman or self.pass_door() or self.bottom_level()

	def pass_key(self):
		return self.player.current_spot() + self.move_spots() in range(39, 44)

	def pass_talisman(self):
		return self.player.current_spot() + self.move_spots() in range(191, 197)

	def pass_door(self):
		return self.player.current_spot() + self.move_spots() in range(82, 86)

	def bottom_level(self):
		return self.player.current_spot() in range(172, 195)

	def pass_pole(self):
		return self.player.current_spot() + self.move_spots() in range(181,187)

	def pass_ladder(self):
		return self.player.current_spot() + self.move_spots() in range(195, 201)
        
	def pass_use_talisman(self):
		return self.player.current_spot() + self.move_spots() in range(146, 152)

The roll_dice function is pretty straight forward and gets a random number using random.randit to handle the die rolling for player movement.

def roll_dice():
	d6 = random.randint(1, 6)
	return d6

The final class Event handles implementing the events when a player lands on a spot with instructions. This could be taking damage, falling into an alligator pit and dieing or falling down onto a spot deeper in the dungeon and taking damage.

class Event:

	def __init__(self, message, func = None):
		self.message = message
		self.func = func

	def do(self, player):
		if self.func:
			self.func(player)
			print self.message

The board is represented in the script in the dungeon and events dictionary. events contains all the possible results from landing on a spot in the game and dungeon maps the spots to the results.

Some of the events are repeated on multiple spots in the game, and by giving each spot a multiple numbers so I decided to create the dungeon dictionary to map out the dungeon, rather then repeat the multiple events. The key corresponds to the spot on the board and the value corresponds to the key in the events dictionary.

events = {
    1: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(25)),
    2: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(8)),
    3: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(13)),
    4: Event('Fell into crocodile pit and died', lambda p: p.take_damage(-p.current_hp())),
    5: Event('Fell into spikes and died', lambda p: p.take_damage(-p.current_hp())),
    6: Event('Found potion (+2 HP)', lambda p: p.add_item('potion1')),
    7: Event('Hit by falling boulder (-1 HP)', lambda p: p.take_damage(-1)),
    8: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(177)),
    9: Event('Shot by arrows (-1 HP)', lambda p: p.take_damage(-1)),
    10: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(174)),
    11: Event('Found potion (+2 HP)', lambda p: p.add_item('potion2')),
    12: Event('Found the key', lambda p: p.add_item('key')),
    13: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(64)),
    14: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(69)),
    15: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(53)),
    16: Event('Choked on poisonous gas (-1 HP)', lambda p: p.take_damage(-1)),
    17: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(78)),
    18: Event('Fell into fire and burned to death', lambda p: p.take_damage(-p.current_hp())),
    19: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(74)),
    21: Event('Found the Talisman', lambda p: p.add_item('talisman')),
    22: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(128)),
    23: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(124)),
    24: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(53)),
    25: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(133)),
    26: Event('Fell down deeper into the dungeon (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(137)),
    28: Event('Fell from the ladder (-1 HP)', lambda p: p.take_damage(-1) and p.move_to_spot(146)),
    29: Event('Fell from the ladder and died', lambda p: p.take_damage(-p.current_hp())),
    31: Event('Cave in (-1 HP)', lambda p: p.take_damage(-1)),
    32: Event('Touched the border and lowered the pole', lambda p: p.lower_pole())
}

dungeon = {
    4: 1,
    5: 2,
    10: 3,
    15: 4,
    18: 5,
    19: 6,
	20: 5,
	23: 7,
	28: 8,
	30: 9,
	31: 9,
	32: 10,
	34: 11,
	39: 12,
	44: 11,
	46: 10,
	50: 8,
	54: 7,
	57: 5,
	58: 6,
	59: 5,
	62: 4,
	67: 13,
	72: 14,
	73: 15,
	79: 5,
	80: 5,
	82: 16,
	84: 17,
	86: 18,
	89: 18,
	91: 19,
	92: 16,
	94: 3,
	101: 21,
	108: 3,
	110: 16,
	111: 22,
	113: 18,
	113: 18,
	118: 23,
	120: 16,
	122: 5,
	123: 5,
	129: 24,
	130: 25,
	135: 26,
	140: 4,
	143: 5,
	149: 28,
	151: 28,
	153: 28,
	155: 29,
	157: 29,
	179: 31,
	181: 32,
	183: 31
} 

Finally before the main loop I set some variables to keep track of the results of each play through. The script will take an argument for number of times to play through the game or will default to a single play through.

wins = 0
deaths = 0
games = 0
finished_games = 0
turn = 0

try:
	games = sys.argv[1]
except:
	games = 1

The main loop plays the game having the play take turns until they are no longer alive or escape and then incrementing the result and playing again until the specified number of play throughs has been finished, and finally the results are printed.

while finished_games < int(games):
	# Begin the game
	game = Game()
	while not game.check_win() and game.check_alive():
		game.take_turn()
	finished_games +=1
	if game.check_win():
		wins +=1
	else:
		deaths +=1

print str(games) + ' games played'
print str(deaths) + ' deaths'
print str(wins) + ' wins'

Once the script was working I had it play through 1,000,000 games resulting in 999,732 and only 268 wins. Each game played has 0.0268% chance that the player will win.

In the episode James recalls that he and his brother never were able to escape the dungeon so I knew that the chances of winning were going to be low, but it turns out the game is much harder then i expected, it probably could have used some more play testing.