RosettaCodeData/Task/Hunt-the-Wumpus/Python/hunt-the-wumpus-1.py
2023-07-01 13:44:08 -04:00

275 lines
9.4 KiB
Python

import random
class WumpusGame(object):
def __init__(self, edges=[]):
# Create arbitrary caves from a list of edges (see the end of the script for example).
if edges:
cave = {}
N = max([edges[i][0] for i in range(len(edges))])
for i in range(N):
exits = [edge[1] for edge in edges if edge[0] == i]
cave[i] = exits
# If no edges are specified, play in the standard cave: a dodecahedron.
else:
cave = {1: [2,3,4], 2: [1,5,6], 3: [1,7,8], 4: [1,9,10], 5:[2,9,11],
6: [2,7,12], 7: [3,6,13], 8: [3,10,14], 9: [4,5,15], 10: [4,8,16],
11: [5,12,17], 12: [6,11,18], 13: [7,14,18], 14: [8,13,19],
15: [9,16,17], 16: [10,15,19], 17: [11,20,15], 18: [12,13,20],
19: [14,16,20], 20: [17,18,19]}
self.cave = cave
self.threats = {}
self.arrows = 5
self.arrow_travel_distance = 5 # As in the original game. I don't like this choice:
# a bow should not cover a whole cave.
self.player_pos = -1
"""
HELPER: These methods wrap processes that are useful or called often.
"""
def get_safe_rooms(self):
""" Returns a list containing all numbers of rooms that
do not contain any threats
"""
return list(set(self.cave.keys()).difference(self.threats.keys()))
def populate_cave(self):
""" Drop player and threats into random rooms in the cave.
"""
for threat in ['bat', 'bat', 'pit', 'pit', 'wumpus']:
pos = random.choice(self.get_safe_rooms())
self.threats[pos] = threat
self.player_pos = random.choice(self.get_safe_rooms())
def breadth_first_search(self, source, target, max_depth=5):
""" The game board (whether custom or standard dodecahedron) is an undirected graph.
The rooms are the vertices and the tunnels are the edges of this graph. To find
out whether a target room can be reached from a source room using a given amount
of tunnels, one can do a breadth first search on the underlying undirected graph.
BFS works like this: start with the source vertex, maybe it is already the target?
If not, then go a level deeper and find out, if one of the children (also called
successors) of the source vertex is the wanted target. If not, then for each child,
go a level deeper and find out if one of the grand-children is the wanted target.
If not, then for each grand-child go a level deeper and so on.
The following is a recursive implementation of BFS. You will not find any loops
(for, while). Instead you manage two lists. The first one ('stack') contains all
the vertices of the current depth-level (e.g. all grand children). The second
('visited') contains all vertices that you already checked. Now there are three
possibilites: Either stack is empty, then all vertices have been checked unsuccessfully;
or the target vertex is a member of the stack, then you are happy; or the target is
not a member of the stack, but there are still some vertices that you did not visit,
then you append to the stack, all successors of the members of the stack and the old
stack now belongs to the visited vertices.
"""
# Set up some initial values.
graph = self.cave
depth = 0
def search(stack, visited, target, depth):
if stack == []: # The whole graph was searched, but target was not found.
return False, -1
if target in stack:
return True, depth
visited = visited + stack
stack = list(set([graph[v][i] for v in stack for i in range(len(graph[v]))]).difference(visited))
depth += 1
if depth > max_depth: # Target is too far away from the source.
return False, depth
else: # Visit all successors of vertices in the stack.
return search(stack, visited, target, depth)
return search([source], [], target, depth)
"""
INPUT / OUTPUT: The player interacts with the game.
"""
def print_warning(self, threat):
""" Called when entering a new room. Shows threats in adjacent rooms.
"""
if threat == 'bat':
print("You hear a rustling.")
elif threat == 'pit':
print("You feel a cold wind blowing from a nearby cavern.")
elif threat == 'wumpus':
print("You smell something terrible nearby.")
def get_players_input(self):
""" Queries input until valid input is given.
"""
while 1: # Query the action.
inpt = input("Shoot or move (S-M)? ")
try: # Ensure that the player choses a valid action (shoot or move)
mode = str(inpt).lower()
assert mode in ['s', 'm', 'q']
break
except (ValueError, AssertionError):
print("This is not a valid action: pick 'S' to shoot and 'M' to move.")
if mode == 'q': # I added a 'quit-button' for convenience.
return 'q', 0
while 1: # Query the target of the action.
inpt = input("Where to? ")
try: # Ensure that the chosen target is convertable to an integer.
target = int(inpt)
except ValueError:
print("This is not even a real number.")
continue # Restart the while loop, to get a valid integer as target.
if mode == 'm':
try: # When walking, the target must be adjacent to the current room.
assert target in self.cave[self.player_pos]
break
except AssertionError:
print("You cannot walk that far. Please use one of the tunnels.")
elif mode == 's':
try: # When shooting, the target must be reachable within 5 tunnels.
bfs = self.breadth_first_search(self.player_pos, target)
assert bfs[0] == True
break
except AssertionError:
if bfs[1] == -1: # The target is outside cave.
print("There is no room with this number in the cave. Your arrow travels randomly.")
target = random.choice(self.cave.keys())
if bfs[1] > self.arrow_travel_distance: # The target is too far.
print("Arrows aren't that croocked.")
return mode, target
"""
CORE / GAME LOGIC
"""
def enter_room(self, room_number):
""" Controls the process of entering a new room.
"""
print("Entering room {}...".format(room_number))
# Maybe a threat waits in the new room.
if self.threats.get(room_number) == 'bat':
# The bat teleports the player to random empty room
print("You encounter a bat, it transports you to a random empty room.")
new_pos = random.choice(self.get_safe_rooms())
return self.enter_room(new_pos)
elif self.threats.get(room_number) == 'wumpus':
print("Wumpus eats you.")
return -1
elif self.threats.get(room_number) == 'pit':
print("You fall into a pit.")
return -1
# The room is safe; collect information about adjacent rooms.
for i in self.cave[room_number]:
self.print_warning(self.threats.get(i))
# Only if nothing else happens, the player enters the room of his choice.
return room_number
def shoot_room(self, room_number):
""" Controls the process of shooting in a room.
"""
print("Shooting an arrow into room {}...".format(room_number))
# Fire an arrow and see if something is hit by it.
self.arrows -= 1
threat = self.threats.get(room_number)
if threat in ['bat', 'wumpus']:
del self.threats[room_number]
if threat == 'wumpus':
print("Hurra, you killed the wumpus!")
return -1
elif threat == 'bat':
print("You killed a bat.")
elif threat in ['pit', None]:
print("This arrow is lost.")
# If this was your last arrow and it did not hit the wumpus...
if self.arrows < 1: # This (or the updating of self.arrows) seems to be broken...
print("Your quiver is empty.")
return -1
# If you shoot into another room, the Wumpus has a 75% of chance of waking up and moving into an adjacent room.
if random.random() < 0.75:
#print("DEBUG: Wumpus moved.")
for room_number, threat in self.threats.items():
if threat == 'wumpus':
wumpus_pos = room_number
new_pos = random.choice(list(set(self.cave[wumpus_pos]).difference(self.threats.keys())))
del self.threats[room_number]
self.threats[new_pos] = 'wumpus'
if new_pos == self.player_pos: # Wumpus entered players room.
print("Wumpus enters your room and eats you!")
return -1
return self.player_pos
def gameloop(self):
print("HUNT THE WUMPUS")
print("===============")
print()
self.populate_cave()
self.enter_room(self.player_pos)
while 1:
#print("DEBUG: Your quiver holds {} arrows.".format(self.arrows))
#print("DEBUG: Rooms with no threats are: {}.".format(self.get_safe_rooms()))
#print("DEBUG: Threats are located in the following rooms: {}".format(self.threats))
print("You are in room {}.".format(self.player_pos), end=" ")
print("Tunnels lead to: {0} {1} {2}".format(*self.cave[self.player_pos]))
inpt = self.get_players_input() # Player choses move or shoot.
print() # Visual separation of rounds.
if inpt[0] == 'm': # Move.
target = inpt[1]
self.player_pos = self.enter_room(target)
elif inpt[0] == 's': # Shoot.
target = inpt[1]
self.player_pos = self.shoot_room(target)
elif inpt[0] == 'q': # Quit.
self.player_pos = -1
if self.player_pos == -1: # E.g. Deadly threat, quiver empty, etc.
break # If any of the game loosing conditions are True,
# then player_pos will be -1.
print()
print("Game over!")
if __name__ == '__main__':
# Only executed if you start this script as the main script,
# i.e. you enter 'python path/to/wumpus.py' in a terminal.
# Assuming you saved the script in the directory 'path/to'
# and named it 'wumpus.py'.
# TODO: In the original game you can replay a dungeon (same positions of you and the threats)
WG = WumpusGame()
WG.gameloop()