import random from typing import Dict, List, Optional from dataclasses import dataclass import sys from logger import log from fuzzywuzzy import process @dataclass class Player: name: str role: str alive: bool = True protected: bool = False class Game: def __init__(self): # single source of truth: name -> Player self.players: Dict[str, Player] = {} # lovers self.lovers: Optional[Tuple[str, str]] = None # ------------------------- # 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" 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 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() # exact match if selected in self.players: return selected # fuzzy matching match = process.extract(selected, self.players, limit=1) log.debug(match) if match: fuzz_player, fuzz_score = match[0][0], match[0][1] # player name, score log.debug(fuzz_player) log.debug(fuzz_score) if fuzz_score >= 60: log.info("You meant %s!", fuzz_player.name) log.debug("Fuzz score: %s" % fuzz_score) return fuzz_player # ------------------------- # 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 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: log.info("%s is already alive.", player) # ------------------------- # 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() 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)