"""Base types for the Dork game"""
# -*- coding: utf-8 -*-
from abc import ABC, abstractmethod
import os
from random import randrange
from copy import deepcopy
from inspect import getfullargspec as argspec
import yaml
import dork.game_utils.factory_data as factory_data
from dork.game_utils.world_builder import MazeFactory
# pylint: disable=protected-access
[docs]class Grandparent:
"""common parent of holder, adjacent, and coord"""
[docs]class Holder(Grandparent):
"""A holder/container of items"""
def __init__(self):
super().__init__()
self.inventory = dict
[docs] def get_items(self, caller, data, verbose):
"""Print all inventory items
Displays all items held by self<player instance> or not. Verbose
print displays more information about inventory.
Args:
out (str): Inventory
verbose (str): Displays detailed inventory
returns:
verbose (str): Returns a formated/detailed inventory of player.
"""
if self.inventory:
out = f"\n{caller} inventory:"
else:
out = f"There's nothing here."
for name, item in data["inventory"].items():
out += "\n " + name
if verbose:
out += Game._verbose_print(item)
return out
[docs]class Stats:
"""stats for items"""
def __init__(self):
self.data = dict
self.attack = int
self.strength = int
self.weight = int
self.luck = int
self.equipable = bool
[docs]class Item(Stats):
"""An obtainable/usable item"""
def __init__(self):
super().__init__()
self.data = dict
self.name = str
self.description = str
self.type = str
self.usable = NotUsable
[docs] def make(self, item):
"""Make an item
Creates the name, description, and type of item in game world
Args:
name (str): Generates a random item name
description (str): Generates a random item description
type (str): Generates a definition what type of item is generated
usable(str): Is the item usable or not
returns:
name (str): returns the item name
description (str): returns item description
type (str): returns the type of item
usable (str): returns the usability of item
"""
self.name = item.get("name", "")
self.description = item.get("description", "")
self.type = item.get("type", "filler")
if not isinstance(self.type, str) or self.type is None:
self.usable = NotUsable
elif len(self.type) > 1:
self.set_usable(self.type)
else:
self.usable = NotUsable
[docs] def set_usable(self, new_use):
"""method that sets usable on runtime
This method changes the use behavior,
provide usable class as argument
Defines whether an item's type is usable or not
Args:
new_use (dict): checks if item's type is usable or not
"""
uses = {"filler": NotUsable,
"weapon": Attackable,
"key": Openable,
"gold": Payable,
"magic items": Statable,
"jewelry": Statable,
"armor": Statable,
"magic consumables": Statable}
if not isinstance(new_use, str):
self.usable = NotUsable
elif "legendary" in new_use:
self.usable = Attackable
elif new_use not in uses:
self.usable = NotUsable
else:
self.usable = uses[new_use]
[docs] def use(self, target, name):
"""Strategy pattern call
This method is called by items to be used
Calls its own behavior class called a Usable
Args:
target (Player): passes Player as target to be used on
name (str): passed as name of item used
returns:
blurb (str): returns output for repl from specific usage
"""
return self.usable.use(target, name)
[docs]class Usable(ABC):
"""Strategy pattern inspired by refactoring.guru"""
[docs] @staticmethod
@abstractmethod
def use(target, name):
"""use method defaults to doing nothing
This method is the parent method inherited by
all behavior classes' uses
Args:
target (Player): passes Player as target to
be used on children
name (str): passed as name of item used in children
"""
[docs]class Attackable(Usable):
"""Any object that can be swung will say it was swung"""
[docs] @staticmethod
def use(target, name):
"""
Concrete use call for weapons
This method is called by weapons to be attack.
Changes state of target player by calling
the target's damage method.
Args:
target (Player): passes Player as target to attack
name (str): passed as name of item used
returns:
blurb (str): returns that player swings weapon at target
"""
out = target.damage() + "\n"
return out + "You swing the " + name + " at " + target.name
[docs]class NotUsable(Usable):
"""Any object that cannot be used"""
[docs] @staticmethod
def use(target, name):
"""Useless use method
This method is called by filler items to be used
Concrete unusable item class use method
Args:
target (Player): passes Player as target to be used on
name (str): passed as name of item used
returns:
blurb (str): returns "You find no use of this item"
"""
return "You find no use of this item"
[docs]class Openable(Usable):
"""Object opening behavior class"""
[docs] @staticmethod
def use(target, name):
"""Opens object targeted if possible
This method is called by key items to be used
Args:
target (Player): passes Player as target to be used on
name (str): passed as name of item used
returns:
blurbs (str): returns item has been inserted
"""
return "You insert the " + name + " into " + target.name
[docs]class Payable(Usable):
"""Any object that can be used as gold"""
[docs] @staticmethod
def use(target, name):
"""Gold use method
This method is called by gold items to be used
Args:
target (Player): passes Player as target to be used on
name (str): passed as name of item used
returns:
blurb (str): returns that you pay with item
"""
return "You use the " + name + " to pay " + target.name
[docs]class Statable(Usable):
"""Any object that can change stats"""
[docs] @staticmethod
def use(target, name):
"""Stat change use method
This method is called by items to be used
Args:
target (Player): passes Player as target to be used on
name (str): passed as name of item used
returns:
blurb (str): states that item took effect
"""
return "The " + name + " takes effect on " + target.name
[docs]class Adjacent(Grandparent):
"""adjacency object for rooms"""
def __init__(self):
super().__init__()
self.north = str
self.south = str
self.east = str
self.west = str
[docs]class Coord(Grandparent):
"""coordinate object for rooms"""
def __init__(self):
super().__init__()
self.x = int
self.y = int
[docs]class Player(Holder):
"""A player or npc in the game"""
states = {"damage": {"Calm": "Hostile", "Hostile": "Dead", "Dead": "Dead"},
"talk": {"Calm": "Calm", "Hostile": "Calm", "Dead": "Dead"}}
blurbs = {"Calm": {"talk": "Hello", "damage": "Ouch..Your gonna get it!"},
"Hostile": {"talk": "I guess you are ok...I'll calm down",
"damage": "UGH\nYou dealt a death blow"},
"Dead": {"talk": "That person is dead...blab away",
"damage": "You monster, stop hitting the dead!"}}
instances = []
def __init__(self):
super().__init__()
self.data = dict
self.name = str
self.description = str
self.location = Room
self.equipped = list
self.state = "Calm"
def _new_instance(self):
self.instances.append(self)
[docs] def move(self, cardinal, maze):
"""walk this way
Moves the player from one location to another in the game/maze
Args:
Out (str): describes where the player is located
returns:
out (str): returns if the player can move to next location
"""
adjacent_room = getattr(self.location, cardinal)
if not adjacent_room:
out = f"You cannot go {cardinal} from here."
else:
adjacent_room.data["players"][self.name] = \
self.location.data["players"].pop(self.name)
adjacent_room.players[self.name] = \
self.location.players.pop(self.name)
maze[self.location.x][self.location.y] = MazeFactory.room_color
self.location = adjacent_room
maze[self.location.x][self.location.y] = MazeFactory.player_color
out = "You have entered " + self.location.description
MazeFactory.update(maze)
return out
[docs] def next_state(self, action):
"""Method that changes state of Player
This method updates players state based on name of method called
Args:
uses (str): passed as name action or method used on player
"""
self.state = self.states[action][self.state]
[docs] def talk(self):
"""Talk method called by players
This method is called by user to produce
a text blurb based on player's state
returns:
uses (str): returns applicable blurb
"""
out = (self.blurbs[self.state]["talk"])
self.next_state("talk")
return out
[docs] def damage(self):
"""damage method called by items to inflict
damage on players
This method is by items and changes the
state of the callee player and returns blurb associated.
returns:
uses (str): returns blurb from hitting player
"""
out = (self.blurbs[self.state]["damage"])
self.next_state("damage")
return out
[docs]class Room(Adjacent, Coord, Holder):
"""A room on the worldmap"""
# pylint: disable=too-many-instance-attributes
instances = []
def __init__(self):
super().__init__()
self.name = str
self.data = dict
self.description = str
self.players = dict
def _new_instance(self):
self.instances.append(self)
[docs] def get_players(self, hero, data, verbose):
"""display a printout of the players present in this room"""
if len(self.players) > 1:
out = f"\n\nplayers:"
for name, player in data["players"].items():
if name != hero:
out += "\n " + name
if verbose:
out += Game._verbose_print(player)
else:
out = f"\n\nThere's nobody else here."
return out
[docs]class Game:
"""An instance of Dork"""
verbose = False
dataaa = {}
def __init__(self):
self.data = {}
self.maze = []
self.rooms = {}
self.hero = Player()
self.points = 10
def __call__(self, cmd, arg):
do_func = getattr(self, cmd)
func_args = argspec(do_func).args
if arg:
if not func_args or ("self" in func_args and len(func_args) == 1):
out = self._repl_error("This command takes no arguments")
else:
out = do_func(arg)
else:
out = do_func()
return out
def _toggle_verbose(self) -> (str, bool):
self.verbose = not self.verbose
out = {
True: "verbose inventory: ON",
False: "verbose inventory: OFF"
}[self.verbose]
return out, False
def _gtfo(self):
return f"Thanks for playing DORK, {self.hero.name}!", True
def _draw_maze(self):
MazeFactory.draw(self.maze)
return "\b", False
def _move(self, cardinal):
return self.hero.move(cardinal, self.maze), False
def _points(self, user_input):
if '_repl_error' in user_input:
self.points = 0
elif '_get_points' in user_input:
pass
else:
self.points += 1
return self.points
def _get_points(self):
point = self.points
if point == 0:
return f"Booooooo! you suck.\nYou have {point} points.", False
return f"you have: {point}", False
def _examine(self):
return self.hero.location.get_items(
caller=self.hero.location.name,
data=self.hero.location.data,
verbose=self.verbose
) + self.hero.location.get_players(
hero=self.hero.name,
verbose=self.verbose,
data=self.hero.location.data
), False
def _inventory(self):
return self.hero.get_items(
caller=self.hero.name,
data=self.hero.data,
verbose=self.verbose), False
def _look(self):
return self.hero.location.description, False
def _save_game(self):
self._get_state()
Gamebuilder.save_game(self.hero.name, self.data)
return "game saved successfully!", False
# item_name defaults to None, so we take all items in room
def _take_item(self, item_name=None):
out = ""
hero = self.hero
room = hero.location
if not item_name:
room_copy = deepcopy(room.inventory)
for item in room_copy:
this_item = room.inventory.pop(item)
this_data = room.data["inventory"].pop(item)
hero.inventory[item] = this_item
hero.data["inventory"][item] = this_data
out += f"You took {item}\n"
elif item_name in room.inventory:
this_item = room.inventory.pop(item_name)
this_data = room.data["inventory"].pop(item_name)
hero.inventory[item_name] = this_item
hero.data["inventory"][item_name] = this_data
out += f"You took {item_name}. You took it well."
else:
out = f"There is no {item_name} here."
self._update_room_inv_description(room)
return out, False
def _drop_item(self, item_name=None):
out = ""
hero = self.hero
room = hero.location
if not item_name:
player_copy = deepcopy(hero.inventory)
for item in player_copy:
this_item = hero.inventory.pop(item)
this_data = hero.data["inventory"].pop(item)
room.inventory[item] = this_item
room.data["inventory"][item] = this_data
out += f"You dropped {item}\n"
elif item_name in hero.inventory:
this_item = hero.inventory.pop(item_name)
this_data = hero.data["inventory"].pop(item_name)
room.inventory[item_name] = this_item
room.data["inventory"][item_name] = this_data
out += f"You dropped {item_name}. How clumsy."
else:
out = f"There is no {item_name} in your inventory."
self._update_room_inv_description(room)
return out, False
def _use_item(self, item="Nothing"):
if item in self.hero.inventory.keys():
target = str.casefold(input("What do you want to use it on? "))
for player in self.hero.location.players:
if player == target.title():
target_obj = self.hero.location.players[target.title()]
return self.hero.inventory[item].use(target_obj,
item), False
if player == target:
target_obj = self.hero.location.players[target]
return self.hero.inventory[item].use(target_obj,
item), False
return "Invalid target", False
return "You don't have that item...", False
def _start_over(self):
if self._confirm():
out = "new game"
else:
out = "Guess you changed your mind!"
return out, False
def _get_state(self):
for name, room in self.rooms.items():
self.data["rooms"][name] = room.data
def _talk(self, target="nobody"):
if target.casefold() == self.hero.name.casefold():
return self.hero.talk(), False
if target.title() in self.hero.location.players:
target_player = self.hero.location.players[target.title()]
return target_player.talk(), False
return "Who are you talking to?", False
@staticmethod
def _update_room_inv_description(location):
inv_list = location.inventory
num = len(inv_list)
description = location.description.splitlines()
des = location.description
if num > 1:
rand_ind = randrange(4)
des = description[0] + "\n" + description[1] + "\n" \
+ factory_data.ROOM_INV_DESCRIPTIONS["1"][rand_ind]
if num == 1:
des = description[0] + "\n" + description[1] \
+ "\n" + factory_data.ROOM_INV_DESCRIPTIONS["2"]
elif num == 0:
des = description[0] + "\n" + description[1] \
+ "\n" + factory_data.ROOM_INV_DESCRIPTIONS["3"]
location.description = des
return 0
@staticmethod
def _verbose_print(data, calls=2):
out = ""
spc = " "
for key, val in data.items():
if isinstance(val, dict):
out += "\n" + spc*calls + \
f"{key}:{Game._verbose_print(val, calls+1)}"
elif val not in (0, ''):
out += "\n" + spc*calls + f"{key}: {val}"
return out
@staticmethod
def _confirm():
print("\n!!!WARNING!!! You will lose unsaved data!\n")
conf = False
while True:
conf = str.casefold(
input("Would you like to proceed? Y/N: ")
)
conf = {
"y": True,
"n": False
}.get(conf, None)
if conf is None:
print("That is not a valid response!")
else:
break
return conf
@staticmethod
def _repl_error(arg):
return f"{arg}", False
@staticmethod
def _zork():
return "holy *%&#@!!! a wild zork appeared!", False
[docs]class Gamebuilder:
"""Build an instance of Game"""
[docs] @staticmethod
def build(player_name):
"""Instantiate a game of Dork from dictionary
Creates an instance of a game from a dictionary of game data
Args:
data (dict): Saves the state of the game, player, and items
game (): Class instance of the game
returns:
data (dict): returns the state of the current game
game (): returns the updated location, player, and items
"""
data = Gamebuilder.load_game(player_name)
if not data:
data = MazeFactory.build()
hero_data = {
"name": player_name,
"description": "the hero of dork!",
"location": "Entrance",
"inventory": {},
"equipped": []
}
data["rooms"]["room 0"]["players"][player_name] = hero_data
game = Gamebuilder._instantiate(Game, **data)
setattr(game, "maze", data["maze"])
setattr(
game, "rooms", Gamebuilder._make_rooms(
deepcopy(data["rooms"])
)
)
Gamebuilder._place_players(game)
Gamebuilder._make_paths(game)
Gamebuilder._get_adj_description(game)
Gamebuilder._get_room_inv_description(game)
for player in Player.instances:
if player.name == player_name:
hero = player
game.hero = hero
game.maze[hero.location.x][hero.location.y] = MazeFactory.player_color
return game
@staticmethod
def _make_rooms(rooms):
factories = {
"inventory": Gamebuilder._make_item,
"players": Gamebuilder._make_player,
}
for name, room in rooms.items():
new_room = Gamebuilder._instantiate(Room, **room)
for field, data in room.items():
if field == "adjacent":
Gamebuilder._make_adjacent(new_room, data)
elif field == "coordinates":
Gamebuilder._make_coord(new_room, data)
elif isinstance(data, dict):
room_field = getattr(new_room, field)
for sub in data:
room_field[sub] = factories[field](data[sub])
else:
setattr(new_room, field, data)
rooms[name] = new_room
new_room._new_instance()
return rooms
@staticmethod
def _make_player(player):
new_player = Gamebuilder._instantiate(Player, **player)
for field, data in player.items():
if isinstance(data, dict):
inventory = getattr(new_player, field)
for sub in data:
inventory[sub] = Gamebuilder._make_item(data[sub])
else:
setattr(new_player, field, data)
new_player._new_instance()
return new_player
@staticmethod
def _make_item(item):
new_item = Gamebuilder._instantiate(Item, **item)
for field, data in item.items():
if field == "stats":
Gamebuilder._make_stats(new_item, data)
if field == "type":
new_item.set_usable(data)
else:
setattr(new_item, field, data)
return new_item
@staticmethod
def _make_adjacent(room, adjacent):
for key, val in adjacent.items():
setattr(room, key, val)
@staticmethod
def _make_coord(room, coord):
setattr(room, "x", coord[0])
setattr(room, "y", coord[1])
@staticmethod
def _make_stats(item, stats):
for key, val in stats.items():
setattr(item, key, val)
@staticmethod
def _place_players(game):
for _, room in game.rooms.items():
for _, player in room.players.items():
player.location = room
@staticmethod
def _make_paths(game):
adj = ["north", "south", "east", "west"]
for _, room in game.rooms.items():
for direction, room_name in vars(room).items():
if room_name and direction in adj:
setattr(room, direction, game.rooms[room_name])
@staticmethod
def _get_room_inv_description(worldmapp):
worldmap = worldmapp.rooms
worldmap_length = len(worldmap)
iterator = 0
for rooms in worldmap:
if iterator != worldmap_length - 1:
inv_list = worldmap[rooms].inventory
num = len(inv_list)
if num >= 2:
rand_ind = randrange(4)
first_desc = worldmap[rooms].description + "\n"
desc = factory_data.ROOM_INV_DESCRIPTIONS["1"][rand_ind]
worldmap[rooms].description = first_desc+desc
elif num == 1:
first_desc = worldmap[rooms].description + "\n"
desc = factory_data.ROOM_INV_DESCRIPTIONS["2"]
worldmap[rooms].description = first_desc+desc
iterator += 1
return 0
@staticmethod
def _get_adj_description(worldmapp):
worldmap = worldmapp.rooms
for rooms in worldmap:
desc = ""
adj_list = list()
adj_possibilities = {"north", "east", "south", "west"}
for pos in adj_possibilities:
if worldmap[rooms].data["adjacent"][pos] is not None:
adj_list.append(pos)
adj_string = ""
for adj in adj_list:
if adj_list[0] == adj:
adj_string += " "+adj
else:
adj_string += ", "+adj
adj_string += "..."
if((len(adj_list) == 1)
and rooms != "room 0" and rooms != "room "+str(len(worldmap))):
desc = factory_data.ADJ_ROOM_DESCRIPTIONS["1"]
elif len(adj_list) == 2:
rand_ind = randrange(8)
desc = factory_data.ADJ_ROOM_DESCRIPTIONS["2"][rand_ind] \
+ adj_string
elif len(adj_list) == 3:
rand_ind = randrange(5)
desc = factory_data.ADJ_ROOM_DESCRIPTIONS["3"][rand_ind] \
+ adj_string
first_desc = worldmap[rooms].description + "\n"
worldmap[rooms].description = first_desc+desc
return 0
@staticmethod
def _instantiate(clz, **data):
"""return an object of type clz with attributes given by data"""
new_obj = clz()
setattr(new_obj, "data", data)
for key, val in deepcopy(data).items():
setattr(new_obj, key, val)
return new_obj
[docs] @staticmethod
def load_game(player):
"""Load the save file associated with player
Loads a saved yaml file based on what the user named their player
Args:
data (dict): dictionary of the game state from yaml file
returns:
data (dict): returns the game state based upon the player's name
"""
save_files = []
with os.scandir("./dork/saves") as saves:
for entry in saves:
save_files.append(entry.name)
if player + ".yml" in save_files:
print(f"loading {player}'s save file...")
file_path = f"./dork/saves/{player}.yml"
with open(file_path) as file:
data = yaml.safe_load(file.read())
else:
data = dict()
return data
[docs] @staticmethod
def save_game(player, data):
"""Save a game instance to a yaml file if it exists, else create one
Gets data on game state from saved yaml file
Args:
data (dict): dictionary of game state
player (str): string of the player's name
returns:
data (dict): returns the dictionary state of the game from yaml
player (str): returns the yaml file depending on the player's name
"""
data = {
"rooms": data["rooms"],
"maze": data["maze"],
}
file_name = f"./dork/saves/{player}.yml"
with open(file_name, "w") as save_file:
yaml.safe_dump(
data, save_file,
indent=4, width=80,
)
return f"Your game was successfully saved as {player}.yml!"