"""The controller used for all high level management of the gacha game.
Instances of this class should be created for each instance of a gacha game
you would like to run. These instances can then be used to access and manage
the Item, Banner, and Player objects in the game directly. Once accessed, the
objects can then be modified directly using class methods (see objects.py for
more info).
This object operates as the Controller of a gachapy game
Author: Jacob Kerr, 2021
"""
from typing import Dict, Optional
from .objects import *
import warnings
"""The default key function converting rarity to drop rate (1 / rarity)"""
def _sort_item_key(item: Item) -> float:
"""The key used to sort items in a list of items
Parameters
----------
item : Item
the item to extract the key from
Returns
-------
float
the key of the item
"""
return item.rarity
[docs]def _sort_player_key(player: Player) -> float:
"""The key used to sort players in a list of players
Parameters
----------
player : Player
the player to extract the key from
Returns
-------
float
the key of the player
"""
return player.get_net_worth()
[docs]class PullError(Exception):
"""An exception thrown when pulling from a banner"""
pass
[docs]class Controller:
"""A controller used to control an instance of a gacha game. Used to
perform all actions needed in a game and manage game information in the
form of gachapy objects
Fields
------
items : Dict[str,Item]
the list of items that are in the item pool for the gacha
banners : Dict[str,Banner]
the list of banners that are available for the gacha
players : Dict[str,Player]
the list of players enrolled in the gacha
"""
"""The list of items that are in the item pool for the gacha"""
[docs] banners: Dict[str, Banner]
"""The list of banners that are available for the gacha"""
[docs] players: Dict[str, Player]
"""The list of players enrolled in the gacha"""
def __init__(
self,
items: Dict[str, Item] = {},
banners: Dict[str, Banner] = {},
players: Dict[str, Player] = {},
) -> None:
"""Creates an instance of a gacha controller
Parameters
----------
items : Dict[str,Item]
the dict of id, item pairs that are in the item pool for the gacha
banners : Dict[str,Banner]
the dict of id, banner pairs that are available for the gacha
players : Dict,str[Player]
the dict of id, player pairs enrolled in the gacha
"""
self.items = items
self.banners = banners
self.players = players
[docs] def find_item_by_name(self, item_name: str) -> Optional[Item]:
"""Returns the Item object with the given name or None if not found
DEPRICATED: consider using find_item_by_id
Parameters
----------
item_name : str
the name of the item
Returns
-------
Optional[Item]
the item object with the given name or None if not found
"""
warnings.warn(
"find_item_by_name is depricated, consider using find_item_by_id",
DeprecationWarning)
item = [i for i in self.items.values() if i.name == item_name]
if len(item) < 1:
return None
return item[0]
[docs] def find_banner_by_name(self, banner_name: str) -> Optional[Banner]:
"""Returns the Banner object with the given name or None if not found
DEPRICATED: consider using find_banner_by_id
Parameters
----------
banner_name : str
the name of the banner
Returns
-------
Optional[Banner]
the banner object with the given name or None if not found
"""
warnings.warn(
"find_banner_by_name is depricated, consider using find_banner_by_id",
DeprecationWarning)
banner = [i for i in self.banners.values() if i.name == banner_name]
if len(banner) < 1:
return None
return banner[0]
[docs] def find_player_by_name(self, player_name: str) -> Optional[Player]:
"""Returns the Player object with the given name or None if not found
DEPRICATED: consider using find_player_by_id
Parameters
----------
player_name : str
the name of the player
Returns
-------
Optional[player]
the player object with the given name or None if not found
"""
warnings.warn(
"find_player_by_name is depricated, consider using find_player_by_id",
DeprecationWarning)
player = [i for i in self.players.values() if i.name == player_name]
if len(player) < 1:
return None
return player[0]
[docs] def find_item_by_id(self, item_id: str) -> Optional[Item]:
"""Returns the Item object with the given id or None if not found
Parameters
----------
item_id : str
the id of the item
Returns
-------
Optional[Item]
the item object with the given id or None if not found
"""
return self.items.get(item_id)
[docs] def find_banner_by_id(self, banner_id: str) -> Optional[Banner]:
"""Returns the Banner object with the given id or None if not found
Parameters
----------
banner_id : str
the id of the banner
Returns
-------
Optional[Banner]
the banner object with the given id or None if not found
"""
return self.banners.get(banner_id)
[docs] def find_player_by_id(self, player_id: str) -> Optional[Player]:
"""Returns the Player object with the given id or None if not found
Parameters
----------
player_id : str
the id of the player
Returns
-------
Optional[player]
the player object with the given id or None if not found
"""
return self.players.get(player_id)
[docs] def pull(self, player_id: str, banner_id: str) -> Optional[Item]:
"""Pulls and returns an item from the specified banner for the specified player
Parameters
----------
player_id : str
the id of the selected player
Preconditon: must be a valid id
banner_id : str
the id of the selected banner
Preconditon: must be a valid id
Returns
-------
Optional[Item]
the item if the pull is successful or None if the player does not have enough money
Raises
------
PullError if player or banner are not valid
"""
player = self.find_player_by_id(player_id)
if player == None:
raise PullError("Player not found")
banner = self.find_banner_by_id(banner_id)
if banner == None:
raise PullError("Banner not found")
if player.change_money(-1 * banner.price):
item = banner.pull()
player.add_item(item)
return item
return None
[docs] def add_new_item(self, name: str, id: str, rarity: float) -> Optional[Item]:
"""Adds a new item to the gacha game
Parameters
----------
name : str
the name of the new item
id : str
the id of the new item
Precondition: must be unique
rarity : float
rarity of the item where the higher the number, the higher the rarity
Precondition: must be >= 1
Returns
-------
Optional[Item]
the Item object representing the new item or None if an item
with the specified id already exists
"""
if id in self.items:
return None
new_item = Item(name, id, rarity)
self.items[id] = new_item
return new_item
[docs] def add_new_banner(
self,
name: str,
id: str,
items_str: List[str],
price: float,
key: str = DEFAULT_KEY,
) -> Optional[Banner]:
"""Adds a new banner to the gacha game
Parameters
----------
name : str
the name of the new banner
id : str
the id of the new banner
Precondition: must be unique
items_str : List[str]
the list of the ids of the items in the banner
price : float
the price of pulling from the banner
key : str
function that takes in rarity and returns the drop rate of the
item, written in KeyLang
Returns
-------
Optional[Banner]
the Banner object representing the new banner or None if a banner
with the specified id already exists
"""
if id in self.banners:
return None
items = [self.find_item_by_id(i) for i in items_str]
new_banner = Banner(name, id, items, price, key)
self.banners[id] = new_banner
return new_banner
[docs] def add_new_player(
self, name: str, id: str, start_money: float, items_str: List[str] = []
) -> Optional[Player]:
"""Adds a new player to the gacha game
Parameters
----------
name : str
the name of the new player
id : str
the id of the new player
start_money : float
the amount of money the new player will start with
items_str : List[str]
the list of the ids of the items the player has
Returns
-------
Optional[Player]
the Player object representing the new player or None if a player
with the specified id already exists
"""
if id in self.players:
return None
items_list = [self.find_item_by_id(i) for i in items_str]
new_player = Player(name, id, items_list, start_money)
self.players[id] = new_player
return new_player
[docs] def remove_item(self, item_id: str) -> Optional[Item]:
"""Removes the specified item from the gacha game
WARNING: Will also remove from banners and players if found,
costly operation
Parameters
----------
item_id : str
the name of the item to remove
Returns
-------
Optional[Item]
the removed item or None if item does not exist
"""
item = self.items.pop(item_id, None)
if item == None:
return item
for banner in self.banners:
while self.banners[banner].remove_item(item):
pass
for player in self.players:
while self.players[player].remove_item(item):
pass
return item
[docs] def remove_banner(self, banner_id: str) -> Optional[Banner]:
"""Removes the specified banner from the gacha game
Parameters
----------
banner_id : str
the id of the banner to remove
Returns
-------
Optional[Banner]
the removed banner or None if banner does not exist
"""
return self.banners.pop(banner_id, None)
[docs] def remove_player(self, player_id: str) -> Optional[Player]:
"""Removes the specified player from the gacha game
Parameters
----------
player_id : str
the id of the player to remove
Returns
-------
Optional[Player]
the removed player or None if player does not exist
"""
return self.players.pop(player_id, None)
[docs] def create_random_banner(
self,
name: str,
id: str,
num_items: int,
price: float = -1,
key: str = DEFAULT_KEY,
) -> Optional[Banner]:
"""Creates a random banner with the given name and number of items
The price is automatically determined by the average of the rarities of the items
selected if a price is not specified
Parameters
----------
name : str
the name of the random banner
id : str
the id of the random banner
Precondition: must be unique
num_items : int
the number of items in the banner
price : float
the price of the banner
key : str
function that takes in rarity and returns the drop rate of the item,
written in KeyLang
Returns
-------
Optional[Banner]
the banner created or None if a banner with the name specified already exists
"""
items = random.choices(list(self.items.values()), k=num_items)
items_str = [item.id for item in items]
if price < 0:
price = 0
for item in items:
price += item.rarity
price /= len(items)
return self.add_new_banner(name, id, items_str, price, key)
[docs] def remove_all_banners(self) -> None:
"""Removes all of the banners in the game
Returns
-------
None
"""
self.banners = {}
[docs] def top_items(self, num_items: int) -> List[Item]:
"""Returns the top specified number of items in the game sorted by rarity
Parameters
----------
num_items : int
the number of items to return
Precondition: must be >= 1
Returns
-------
List[Item]
the list of top items
"""
sort_list = sorted(list(self.items.values()), key=_sort_item_key, reverse=True)
return sort_list[:num_items]
[docs] def top_players(self, num_players: int) -> List[Player]:
"""Returns the top specified number of players in the game sorted by net worth
Parameters
----------
num_players : int
the number of players to return
Precondition: must be >= 1
Returns
-------
List[Player]
the list of top players
"""
sort_list = sorted(
list(self.players.values()), key=_sort_player_key, reverse=True
)
return sort_list[:num_players]
[docs] def __str__(self) -> str:
return f"""Items\n{[str(item) for item in self.items.values()]}\n
Banners\n{[str(banner) for banner in self.banners.values()]}\n
Players\n{[str(player) for player in self.players.values()]}"""