diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..3d65862 --- /dev/null +++ b/logger.py @@ -0,0 +1,31 @@ +import logging + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) +formatter = logging.Formatter( + "%(asctime)s \033[1m%(levelname)s\033[0m\033[0m %(message)s" +) + +# stdout +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.DEBUG) +stream_handler.setFormatter(formatter) +log.addHandler(stream_handler) + + +# Add custom log level names with colors +logging.addLevelName( + logging.DEBUG, "\033[1;30m%s\033[1;0m" % logging.getLevelName(logging.DEBUG) +) # gray +logging.addLevelName( + logging.INFO, "\033[1;34m%s\033[1;0m" % logging.getLevelName(logging.INFO) +) # blue +logging.addLevelName( + logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING) +) # yellow +logging.addLevelName( + logging.ERROR, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.ERROR) +) # red +logging.addLevelName( + logging.CRITICAL, "\033[1;35m%s\033[1;0m" % logging.getLevelName(logging.CRITICAL) +) # \ No newline at end of file diff --git a/main.py b/main.py index aee98f3..04b49f7 100644 --- a/main.py +++ b/main.py @@ -1,97 +1,267 @@ - import random -import time +from typing import Dict, List, Optional +from dataclasses import dataclass +import sys +from logger import log -playerFile = open("Players.txt") -rolesFile = open ("Roles.txt") - -PlayersName = [] -RolesList = [] -KilledDuringNight = [] -PlayersInLove = [] -players_dict = {} +@dataclass +class Player: + name: str + role: str + alive: bool = True + protected: bool = False - -def GiveRoleToPlayers(): - if PlayersName and RolesList: - for player in PlayersName: - ChoseRole = random.choice(RolesList) - players_dict[player] = {'name': player, 'role': ChoseRole, 'alive': True, 'InLove': False, 'Protected': False} - RolesList.remove(ChoseRole) - else: - print("Players are not initialized") - - -def CreateLists(): - for line in playerFile: - PlayersName.append(line.strip()) - for line in rolesFile: - RolesList.append(line.strip()) - playerFile.close() - rolesFile.close() - - -def Kill(player): - if players_dict[player]['InLove'] == False: - players_dict[player]['alive'] = False - KilledDuringNight.append(player) - elif players_dict[player]['InLove'] == True: - print("in love check") - for player in players_dict: - if players_dict[player]['InLove'] == True: - print("found one") - players_dict[player]['alive'] = False - KilledDuringNight.append(player) - elif players_dict[player]['Protected'] == True: - return - +class Game: + def __init__(self): + # single source of truth: name -> Player + self.players: Dict[str, Player] = {} - -def Revive(player): - players_dict[player]["alive"] = True - KilledDuringNight.remove(player) -def PutInLove(player1, player2): - players_dict[player1]['InLove'] = True - players_dict[player2]['InLove'] = True - time.sleep(1) - print("The people selected wake up and show their role") - time.sleep(1) - print("They go back to sleep") + # lovers + self.lovers: Optional[Tuple[str, str]] = None -def Cupidon(): - print("You choose two people to be linked to the death") - player1 = SelectSomeone() - player2 = SelectSomeone() - PutInLove(player1, player2) + # ------------------------- + # setup / I/O + # ------------------------- + def load_lists(self) -> Optional[Dict[str, List[str]]]: + """ + Load players and roles from files and return them as lists. + Returns dict with keys 'players' and 'roles'. + """ + players_file = "players.txt" + roles_file = "roles.txt" -def Savior(): - print("You choose someone to protect") - ProtectedPlayer = SelectSomeone() - players_dict[ProtectedPlayer]['Protected'] = False + log.info("Loading lists...") + + try: + with open(players_file, "r", encoding="utf-8") as pf: + player_names = [line.strip() for line in pf if line.strip()] + except FileNotFoundError: + log.critical("Players file not found: %s", players_file) + return None + + try: + with open(roles_file, "r", encoding="utf-8") as rf: + roles_list = [line.strip() for line in rf if line.strip()] + except FileNotFoundError: + log.critical("Roles file not found: %s", roles_file) + return None + + log.info("Load complete!") + + return {"players": player_names, "roles": roles_list} + + # ------------------------- + # player / role assignment + # ------------------------- + def give_roles_to_players(self, player_names: Optional[List[str]] = None, + roles_list: Optional[List[str]] = None) -> None: + """ + Assign a random role to each player. Accepts optional lists (returned by load_lists). + If lists are not supplied, it will attempt to read files itself. + """ + if player_names is None or roles_list is None: + loaded = self.load_lists() + if not loaded: + return + player_names = loaded["players"] + roles_list = loaded["roles"] + + if not player_names: + log.info("Players are not initialized") + return + if not roles_list: + log.info("Roles are not initialized") + return + if len(roles_list) < len(player_names): + log.error("Not enough roles for players (roles: %d, players: %d)", + len(roles_list), len(player_names)) + return + + available_roles = list(roles_list) + random.shuffle(available_roles) + + # clear any existing players (safe to reassign) + self.players.clear() + + for name in player_names: + chosen_role = available_roles.pop() + self.players[name] = Player(name=name, role=chosen_role) + + log.debug("Assigned roles:") + for player in self.players.values(): + log.debug("%s: %s", player.name, player.role) + + # ------------------------- + # utilities + # ------------------------- + def select_someone(self, prompt: Optional[str] = None) -> Optional[str]: + """ + Prompt the user to enter a player name until a valid one is entered. + Returns None on EOF/KeyboardInterrupt. + """ + prompt = prompt or "Enter the name of the player: " + while True: + selected = input(prompt).strip() + if selected in self.players: + return selected + log.info("This player doesn't exist.") + + # ------------------------- + # game actions + # ------------------------- + def put_in_love(self, player1: str, player2: str) -> None: + """Link two players such that if one dies the other dies too.""" + if player1 not in self.players or player2 not in self.players: + log.error("One or both players do not exist: %s, %s", player1, player2) + return + + self.lovers = (player1, player2) + + log.debug("Players now put in love: %s <-> %s", player1, player2) + + log.info("They are now bounded by love.") + log.info("They wake up and reveal their role to each other.") + log.info("Those two go back to sleep.") + def kill(self, player: str) -> None: + """Kill a player.""" + if player not in self.players: + log.error("Couldn't kill unknown player: %s", player) + return -def SelectSomeone(): - while True: - SelectedPlayer = input("Enter the name of the player:") - if SelectedPlayer in PlayersName: - return SelectedPlayer + target = self.players[player] + + # already dead? + if not target.alive: + log.error("%s is already dead!", player) + return + + # protected? + if target.protected: + log.debug("%s was protected and survives.", player) + return + + # in love? + if target in self.lovers: + log.debug("%s is in love! Killing them and their lover.", target) + for p in self.lovers: + # kill them and their lover + log.debug("Killed %s", p) + p.alive = False + return + + # else just kill them + log.debug("Killed %s" % p) + target.alive = False + + def revive(self, player: str) -> None: + """Revive a player.""" + if player not in self.players: + log.error("Tried to revive unknown player: %s", player) + return + p = self.players[player] + if not p.alive: + p.alive = True + log.info("%s has been revived.", player) else: - print("This player don't exist") + log.info("%s is already alive.", player) -def FirstDayCycle(): - print("The villagers fall asleep") - time.sleep(1) - print("Cupidon get up") - Cupidon() - print("Cupidon go back to sleep") - time.sleep(1) - print("The savior get up") - Savior() + # ------------------------- + # helpers + # ------------------------- + def status(self) -> None: + """Log current players' statuses for debugging.""" + + # player values + for p in self.players.values(): + log.debug("%s -> role: %s, alive: %s, Protected: %s", + p.name, p.role, p.alive, p.protected) + # lovers + log.debug("Lovers: %s" % ",".join(self.lovers)) + + def role(func): + """Decorator for roles""" + def wrapper(*args, **kwargs): + # the role is the name of the function thats decorated + role = func.__name__.capitalize() + log.info("%s wakes up." % role) + func(*args, **kwargs) + log.info("%s goes back to sleep." % role) + + # workaround to call status from the instance + # is it ok? no idea but i dont have self anyway + instance = args[0] + instance.status() + + return wrapper + + # ------------------------- + # roles + # ------------------------- + + @role + def cupidon(self) -> None: + """Interactively pick two players to link by love.""" + log.info("Choose two people to be linked to death.") + log.info("Give me the first person that shall be bounded!") + p1 = self.select_someone() + if p1 is None: + return + log.info("What about the second one?") + p2 = self.select_someone() + if p2 is None: + return + if p1 == p2: + log.info("Cannot link a player to themselves.") + return + self.put_in_love(p1, p2) + + @role + def savior(self) -> None: + """Interactively choose someone to protect.""" + log.info("Choose someone to protect.") + protected = self.select_someone() + if protected is None: + return + + self.players[protected].protected = True + log.debug("Protected: %s", protected) + + + # ------------------------- + # game flow + # ------------------------- + def first_day_cycle(self) -> None: + log.info("All the villagers fall asleep.") + self.cupidon() + self.savior() -CreateLists() -GiveRoleToPlayers() -FirstDayCycle() \ No newline at end of file +if __name__ == "__main__": + print("---\nWerewolfGame\n---") + + # instantiate the game + game = Game() + + # I/O: read files and assign roles + loaded = game.load_lists() + try: + if loaded: + # start the game! + game.give_roles_to_players(loaded["players"], loaded["roles"]) + game.first_day_cycle() + + # CTRL+C + except KeyboardInterrupt: + print() # new line + log.info("Bye bye!") + sys.exit(0) + + # Any unhandled exception + except Exception as e: + log.exception("Unhandled exception: %s" % e) + sys.exit(1) + diff --git a/Players.txt b/players.txt similarity index 100% rename from Players.txt rename to players.txt diff --git a/Roles.txt b/roles.txt similarity index 100% rename from Roles.txt rename to roles.txt