import random from typing import Dict, List, Optional from dataclasses import dataclass import sys from logger import log from finger_count import * from playsound import playsound import wave from piper import PiperVoice import os from art import * from translation import * @dataclass class Player: name: str alias: 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 # Dead this night self.dead_this_night: [List[str]] = [] # Used potions self.used_potions: [list[str]] = [] # Language self.language: str = "fr" # ------------------------- # 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" words_file = "randoms_words.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 try: with open(words_file, "r", encoding="utf-8") as rf: alias_list = [line.strip() for line in rf if line.strip()] except FileNotFoundError: log.critical("Roles file not found: %s", words_file) return None log.info("Load complete!") return {"players": player_names, "roles": roles_list, "alias": alias_list} # ------------------------- # player / role assignment # ------------------------- def give_roles_to_players(self, player_names: Optional[List[str]] = None, roles_list: Optional[List[str]] = None, alias_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. """ number = 0 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 if len(alias_list) < len(player_names): log.error("Not enough randoms words for players (words: %d, players: %d)", len(alias_list), len(player_names)) return available_roles = list(roles_list) random.shuffle(available_roles) available_alias = list(alias_list) random.shuffle(alias_list) # clear any existing players (safe to reassign) self.players.clear() # clear any player name audio if not os.path.exists("./voicesline/playersname"): os.makedirs("./voicesline/playersname") for filename in os.listdir("./voicesline/playersname"): file_path = os.path.join("./voicesline/playersname", filename) # Check if it is a file (not a subdirectory) if os.path.isfile(file_path): os.remove(file_path) # Remove the file for filename in os.listdir("./voicesline/alias"): file_path = os.path.join("./voicesline/alias", filename) # Check if it is a file (not a subdirectory) if os.path.isfile(file_path): os.remove(file_path) # Remove the file if self.language == "fr": voice = PiperVoice.load("./voices/fr/fr_FR-tom-medium.onnx") elif self.language == "en": voice = PiperVoice.load("./voices/en/en_GB-alan-medium.onnx") else: log.info("Unknow language") for name in player_names: chosen_role = available_roles.pop() chosen_alias = available_alias.pop() with wave.open(f"./voicesline/playersname/{name}.wav", "wb") as wav_file: voice.synthesize_wav(name, wav_file) with wave.open(f"./voicesline/alias/{chosen_alias}.wav", "wb") as wav_file: voice.synthesize_wav(chosen_alias, wav_file) number += 1 self.players[number] = Player(name=name, role=chosen_role, alias=chosen_alias) self.play_sound("players", name.capitalize()) self.play_sound("prompts", "first_wakes") if name == "Dan": self.play_sound("prompts", "cheater") self.show_text(chosen_role) self.show_text(chosen_alias) time.sleep(3) os.system('clear') self.play_sound("players", name.capitalize()) self.play_sound("prompts", "sleep_again") log.debug("Assigned roles:") for player in self.players.values(): log.debug("%s: %s", player.name, player.role) # ------------------------- # utilities # ------------------------- def clear_data(self) -> None: for player in self.players: self.players[player].protected = False def couple(self) -> None: os.system('clear') self.play_sound("prompts", "lovers") log.debug(self.lovers) for player in self.lovers: log.info(self.players[player].alias) self.play_sound("alias", self.players[player].alias) self.show_text(self.players[player].role) time.sleep(10) def show_text(self, text: str) -> None: if self.language == "fr": if text in FR: Art=text2art(FR.get(text)) print(Art) else: Art=text2art(text) print(Art) if self.language == "en": Art=text2art(text) print(Art) def get_remaining_roles(self) -> List[str]: remaining_roles = [] for player in self.players: if self.players[player].alive: remaining_roles.append(self.players[player].role) log.DEBUG(remaining_roles) return remaining_roles def number_2_name(self, player_number: int): player_name = self.players[player_number].name return player_name def play_sound(self, type, soundinfo) -> None: if type == "prompts": f = f"./voicesline/prompts/{self.language}/{soundinfo}.wav" elif type == "roles": f = f"./voicesline/roles/{self.language}/{soundinfo}.wav" elif type == "players": f = f"./voicesline/playersname/{soundinfo}.wav" elif type == "alias": f = f"./voicesline/alias/{soundinfo}.wav" else: log.debug("An error occurred") return time.sleep(0.2) playsound(f) 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 "show_number" #self.play_sound("prompts", prompt) while True: selected = count_fingers() # exact match if selected in self.players: return selected def choose_between(self, options): #Prompt the user to choose between options until a valid one is entered numbered_options = " ".join([f"{i+1}: {option}" for i, option in enumerate(options)]) log.info(f"Choose between {numbered_options}:") while True: selected = count_fingers() try: index = int(selected) - 1 if 0 <= index < len(options): chosen_option = options[index] log.debug("You chose %s!", chosen_option) return chosen_option else: log.warning("Invalid number entered.") except ValueError: log.warning("Invalid input. Please enter an option or number.") # ------------------------- # 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", self.players[player1], self.players[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 player in self.lovers: log.debug("%s is in love! Killing them and their lover.", player) for p in self.lovers: # kill them and their lover log.debug("Killed %s", p) self.dead_this_night.append(p) self.players[p].alive = False return # else just kill them log.info("Killed %s" % player) self.dead_this_night.append(player) target.alive = False def revive(self, player: str) -> None: """Revive a player.""" log.info("Players that will die this night are: %s", self.dead_this_night) 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(f"Lovers: {self.lovers}") def role(func): """Decorator for roles""" def wrapper(self, *args, **kwargs): # the role is the name of the function thats decorated role = func.__name__.capitalize() os.system('clear') if role in self.get_remaining_roles(): self.play_sound("roles", role) self.play_sound("prompts", "wakes") func(self, *args, **kwargs) self.play_sound("roles", role) self.play_sound("prompts", "sleep_again") log.INFO("%s goes back to sleep." % role) self.status() return wrapper # ------------------------- # roles # ------------------------- @role def cupidon(self) -> None: """Interactively pick two players to link by love.""" self.play_sound("prompts", "linked_death") self.play_sound("prompts", "first_bound") p1 = self.select_someone() if p1 is None: return self.play_sound("prompts", "second_bound") 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.""" self.play_sound("prompts", "choose_protect") protected = self.select_someone() if protected is None: return self.players[protected].protected = True log.debug("Protected: %s", protected) @role def witch(self) -> None: """Interactively choose to kill or revive someone""" self.play_sound("prompts", "potions_choice") self.play_sound("prompts", "might_die") for player in self.dead_this_night: self.show_text(self.players[player].name) if len(self.used_potions) >= 2: self.show_text("Vous avez utilisé toutes vos potions. ") time.sleep(5) else: while True: options = ("Death", "Revive", "Nothing") potionchoice = self.choose_between(options).capitalize() if potionchoice == "Revive" and "Revive" not in self.used_potions: player = self.select_someone() if player in self.dead_this_night: self.used_potions.append("Revive") self.revive(player) return elif player not in self.dead_this_night and not self.players[player].alive: self.show_text("Tu ne peux pas faire revivre cette personne.") elif self.players[player].alive: self.show_text("Cette personne n'est pas morte.") else: log.info("Unknown error: Invalid player choice") return elif potionchoice == "Death" and "Death" not in self.used_potions: player = self.select_someone() if self.players[player].alive: self.used_potions.append("Death") self.kill(player) time.sleep(1) return elif not self.players[player].alive: self.show_text("Ce joueur est déjà mort.") else: log.info("Unknown error: Invalid player choice") return elif potionchoice == "Nothing": self.show_text("Vous ne faites rien cette nuit.") return elif potionchoice in self.used_potions: log.info("You already used this potion.") else: log.critical("Unknown error: Invalid potion choice.") return @role def werewolf(self) -> None: self.play_sound("prompts", "werewolf_choice") player = self.select_someone() self.kill(player) @role def seer(self) -> None: log.info("Choose a player to discover their role.") while True: player = self.select_someone() if "Seer" == self.players[player].role: self.show_text("You can't see your own role.") else: self.show_text(self.players[player].role) time.sleep(3) return @role def hunter(self) -> None: self.play_sound("prompts", "hunter") player = self.select_someone() self.kill(player) # ------------------------- # game flow # ------------------------- def first_night_cycle(self) -> None: self.play_sound("prompts", "fall_asleep_all") self.cupidon() self.couple() self.savior() self.werewolf() self.witch() self.seer() self.day() def night(self) -> None: self.dead_this_night = [] self.play_sound("prompts", "fall_asleep_all") self.savior() self.werewolf() self.witch() self.seer() self.day() def day(self) -> None: self.play_sound("prompts", "wake_up_all") self.play_sound("prompts", "night_deaths") for player in self.dead_this_night: self.play_sound("players", self.number_2_name(player)) for player in self.dead_this_night: if self.players[player].role == "Hunter": self.hunter() self.play_sound("prompts", "village_vote") while True: if count_fingers(stable_duration=4) == 6: break self.play_sound("prompts", "now_show_number") player = self.select_someone() self.kill(player) self.night() 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"], loaded["alias"]) game.first_night_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)