From 8fa8db77eaebef14bd8ac53eaf4d553b3e471609 Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 14 Nov 2025 19:57:47 +0100 Subject: [PATCH 1/7] Fixed issue with naming and added support for update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed special charachter issue with reminder and role token naming * added new features to update * added built-in json instead of normal url, as BotC webpage changed * added bloodstar-url option, to be able to use tool with homebrew scripts easily * added small change to name and ability texts, to be able to handle special charcters better, now only ő and ű * added id for roles, to avoid issue with naming, from now on id is used for token names, not the name, so it is possible to keep id in english but name in localized text --- src/botc_tokens/commands/create.py | 9 +- src/botc_tokens/commands/update.py | 49 +- src/botc_tokens/data/known_roles.json | 2550 +++++++++++++++++++++++++ src/botc_tokens/helpers/role.py | 2 + src/botc_tokens/helpers/text_tools.py | 9 + src/botc_tokens/helpers/wiki_soup.py | 24 +- tests/commands/test_update.py | 146 +- tests/helpers/test_role.py | 2 +- tests/helpers/test_wiki_soup.py | 2 +- tests/testhelpers.py | 53 +- 10 files changed, 2769 insertions(+), 77 deletions(-) create mode 100755 src/botc_tokens/data/known_roles.json diff --git a/src/botc_tokens/commands/create.py b/src/botc_tokens/commands/create.py index 5fd7d62..5959224 100644 --- a/src/botc_tokens/commands/create.py +++ b/src/botc_tokens/commands/create.py @@ -72,7 +72,8 @@ def run(): overall_task = overall_progress.add_task("Creating Tokens...", total=len(roles)) step_task = step_progress.add_task("Reading roles...") for role in roles: - role_slug = ''.join(e for e in role.name.lower() if e.isalnum()) + role_base = role.id if role.id is not None else role.name + role_slug = ''.join(e for e in role_base.lower() if e.isalnum()) step_progress.update(step_task, description=f"Creating Token for: {role.name}") # Make sure our target directory exists role_output_path = output_path / str(role.home_script) / str(role.type) @@ -98,7 +99,7 @@ def run(): reminder_icon.transform(resize=f"{target_width}x{target_height}") for reminder_text in role.reminders: step_progress.update(step_task, description=f"Creating Token for: {role.name}") - reminder_name = format_filename(f"{role_slug}-Reminder-{reminder_text}") + reminder_name = role_slug + format_filename(f"-Reminder-{reminder_text}") reminder_output_path = role_output_path / f"{reminder_name}.png" duplicate_counter = 1 while reminder_output_path.exists(): @@ -135,7 +136,7 @@ def load_components(component_package): print(f"\n[red]Error:[/][bold] Could not load component: {str(e)}[/]") return None except BadZipFile: - print(f"\n[red]Error:[/][bold] Could not load components from '{component_package}' it does not appear to be a " + print(f"\n[red]Error:[/][bold] Could not load components from '{component_package}' it does not appear to be a \n" "valid components package.[/]") return None except FileNotFoundError as e: @@ -155,7 +156,7 @@ def find_roles_from_json(json_files): icon = data.get('icon') if icon: data['icon'] = str(json_file.parent / str(data.get('icon'))) - role = Role(data.get('name', "Unknown")) + role = Role(id=data.get('id'), name=data.get('name', "Unknown")) for att in dir(role): if att in data: setattr(role, att, data[att]) diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 6b42645..885caa7 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -3,13 +3,13 @@ """Download story from the requested url.""" # Standard library import argparse -from dataclasses import asdict import json -from pathlib import Path import sys +import urllib.parse +from dataclasses import asdict +from pathlib import Path from time import sleep from urllib.error import HTTPError -import urllib.parse from urllib.request import Request, urlopen # Third-party libraries @@ -33,12 +33,16 @@ def _parse_args(): help="Directory in which to write the json and icon files (Default: 'inputs')") parser.add_argument('--script-filter', type=str, default='', help="Filter for scripts to pull (Example: 'Experimental')") + parser.add_argument('-b', '--bloodstar-url', type=str, default='', + help="Link to bloodstar hosted script json") parser.add_argument('--reminders', type=str, help="JSON file to override reminder guesses from the wiki.") parser.add_argument('-c', '--custom-list', type=str, default=None, help="JSON file with a custom list of roles to update.") parser.add_argument('--use-playtest', action='store_true', help="Use the playtest icon source instead of the wiki.") + parser.add_argument('--use-built-in', action='store_true', + help="Use the built-in role list as of date 2025-11-13.") args = parser.parse_args(sys.argv[2:]) return args @@ -82,7 +86,7 @@ def run(): role_task = overall_progress.add_task("Updating role data...", total=None) step_task = step_progress.add_task("Grabbing role data") - wiki = prep_wiki(args.script_filter, args.custom_list) + wiki = prep_wiki(args.script_filter, args.bloodstar_url, args.custom_list, args.use_built_in) if wiki is None: return 1 @@ -123,11 +127,12 @@ def run(): role_file = role_output_path / f"{format_filename(role['id'])}.json" found_role = \ - process_role(role, role_file, wiki, step_progress, step_task, role_output_path, args.use_playtest) + process_role(role, role_file, wiki, step_progress, step_task, role_output_path, + len(args.bloodstar_url) > 0 or args.use_built_in, args.use_playtest or args.use_built_in) if found_role is not None: # Check if the role is in our forced_setup list - if found_role.name.lower() in forced_setup: + if found_role.id.lower() in forced_setup: found_role.affects_setup = True # Write it out @@ -140,16 +145,22 @@ def run(): step_progress.stop_task(step_task) -def prep_wiki(script_filter, custom_list=None): +def prep_wiki(script_filter, bloodstar_url, custom_list=None, use_built_in=False): """Prepare the wiki object, loading the data from the web or a custom list. Args: script_filter (str): The filter to use when downloading the data. + bloodstar_url (str): The URL of the bloodstar site. custom_list (str): The path to a custom list of roles to use. + use_built_in (bool): Whether to load roles from built in file. """ # Gather the requested role data wiki = WikiSoup(script_filter) - if custom_list: + if use_built_in: + wiki.role_data = json.load(open(data_dir / "known_roles.json", encoding="utf-8")) + elif bloodstar_url.startswith("https://www.bloodstar.xyz"): + wiki.load_from_bloodstar(bloodstar_url) + elif custom_list: custom_list_path = Path(custom_list) if not custom_list_path.exists(): print(f"[red]Error:[/] Could not find '{custom_list}'") @@ -169,7 +180,7 @@ def prep_wiki(script_filter, custom_list=None): return wiki -def process_role(role, file, wiki, step_progress, step_task, role_output_path, use_playtest=False): +def process_role(role, file, wiki, step_progress, step_task, role_output_path, skip_reminders=False, use_playtest=False): """Process a role, grabbing the relevant data and returning a Role object. Args: @@ -182,7 +193,8 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u use_playtest (bool): Whether to use the playtest icon source instead of the wiki. """ name = role['name'] - found_role = Role(name=name) + role_id = role['id'] + found_role = Role(id= role_id, name=name) # Check if we have a json file for the role if file.exists(): @@ -205,15 +217,15 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u if role.get("remindersGlobal"): found_role.reminders.extend(role.get("remindersGlobal")) if not found_role.reminders: - found_role.reminders = get_role_reminders(name, wiki) + found_role.reminders = get_role_reminders(name, wiki, skip_reminders) # Determine night actions - if role.get("firstNight"): + if role.get("firstNight") or has_value(role.get("firstNightReminder")): found_role.first_night = True else: found_role.first_night = True if role['id'] in wiki.night_data['firstNight'] else False - if role.get("otherNight"): + if role.get("otherNight") or has_value(role.get("otherNightReminder")): found_role.other_nights = True else: found_role.other_nights = True if role['id'] in wiki.night_data['otherNight'] else False @@ -239,6 +251,10 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u return found_role +def has_value(value): + if isinstance(value, str): + return len(value) > 0 + return False def get_role_icon(found_role, role, role_output_path, wiki, use_playtest=False): """Get the icon for a role, using the wiki if needed. @@ -274,7 +290,7 @@ def get_role_icon(found_role, role, role_output_path, wiki, use_playtest=False): print(f"[red]Error:[/] No icon found for {found_role.name}: {str(e)}") return icon_url = urllib.parse.urljoin("https://wiki.bloodontheclocktower.com", icon_url) - icon_path = role_output_path / f"{format_filename(found_role.name)}{Path(icon_url).suffix}" + icon_path = role_output_path / f"{format_filename(found_role.id)}{Path(icon_url).suffix}" icon_path.parent.mkdir(parents=True, exist_ok=True) if not save_icon(found_role, icon_path, icon_url): return @@ -320,15 +336,16 @@ def get_role_ability(name, wiki): return "" -def get_role_reminders(name, wiki): +def get_role_reminders(name, wiki, skip_reminders=False): """Get the reminders for a role, using the wiki if needed. Args: name (str): The name of the role to search. wiki (WikiSoup): The wiki soup object. + skip_reminders (bool): Whether to skip the reminder search in case of built-in and bloodstar homebrew characters. """ try: - return wiki.get_reminders(name) + return wiki.get_reminders(name, skip_reminders) except RuntimeError: print(f"[red]Error:[/] No reminder info found for {name}") return [] diff --git a/src/botc_tokens/data/known_roles.json b/src/botc_tokens/data/known_roles.json new file mode 100755 index 0000000..3c87bc3 --- /dev/null +++ b/src/botc_tokens/data/known_roles.json @@ -0,0 +1,2550 @@ +[ + { + "id": "steward", + "name": "Steward", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "Point to the good player marked *KNOW*.", + "reminders": [ + "Know" + ], + "setup": false, + "ability": "You start knowing 1 good player.", + "flavor": "How DARE you accuse Her Ladyship of wrongdoing? I’ve known her my entire life! All nine years!" + }, + { + "id": "knight", + "name": "Knight", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "Point to the two non-Demon players marked *KNOW*.", + "reminders": [ + "Know", + "Know" + ], + "setup": false, + "ability": "You start knowing 2 players that are not the Demon.", + "flavor": "When a man lies, he murders some part of the world." + }, + { + "id": "chef", + "name": "Chef", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "Give a finger signal.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You start knowing how many pairs of evil players there are.", + "flavor": "This evening's reservations seem odd. Never before has Mrs. Mayweather kept company with that scamp from Hudson Lane. Yet, tonight, they have a table for two. Strange." + }, + { + "id": "noble", + "name": "Noble", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Point to all three players marked *KNOW*.", + "otherNightReminder": "", + "reminders": [ + "Know", + "Know", + "Know" + ], + "setup": false, + "ability": "You start knowing 3 players, 1 and only 1 of which is evil.", + "flavor": "Sarcasm is indeed the lowest form of wit. But speaking in response to your criticism, Sir, it is, nevertheless, a form of wit." + }, + { + "id": "investigator", + "name": "Investigator", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "Show the Minion character token. Point to both the *MINION* and *WRONG* players.", + "otherNightReminder": "", + "reminders": [ + "Minion", + "Wrong" + ], + "setup": false, + "ability": "You start knowing that 1 of 2 players is a particular Minion.", + "flavor": "It is a fine night for a stroll, wouldn't you say, Mister Morozov? Or should I say... BARON Morozov?" + }, + { + "id": "washerwoman", + "name": "Washerwoman", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "Show the Townsfolk character token. Point to both the *TOWNSFOLK* and *WRONG* players.", + "otherNightReminder": "", + "reminders": [ + "Townsfolk", + "Wrong" + ], + "setup": false, + "ability": "You start knowing that 1 of 2 players is a particular Townsfolk.", + "flavor": "Bloodstains on a dinner jacket? No, this is cooking sherry. How careless." + }, + { + "id": "clockmaker", + "name": "Clockmaker", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "Give a finger signal.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You start knowing how many steps from the Demon to its nearest Minion.", + "flavor": "Do not disturb me. The tick must continue, for the circle is a symbol of life and contains all things - all answers - in its divine machinery. I must work." + }, + { + "id": "grandmother", + "name": "Grandmother", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "Point to the grandchild player & show their character token.", + "otherNightReminder": "If the grandchild was killed by the Demon, the Grandmother dies too. :reminder:", + "reminders": [ + "Grandchild", + "Dead" + ], + "setup": false, + "ability": "You start knowing a good player & their character. If the Demon kills them, you die too.", + "flavor": "Take a jacket if you go outside, dearie. And your thermos. And your scarf. I have a weak heart, you know. Whatever would I do if you caught cold...or worse?" + }, + { + "id": "librarian", + "name": "Librarian", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "Show the Outsider character token. Point to both the *OUTSIDER* and *WRONG* players.", + "otherNightReminder": "", + "reminders": [ + "Outsider", + "Wrong" + ], + "setup": false, + "ability": "You start knowing that 1 of 2 players is a particular Outsider. (Or that zero are in play.)", + "flavor": "Certainly madam, under normal circumstances, you may borrow the Codex Malificarium from the library vaults. However, you do not seem to be a member." + }, + { + "id": "shugenja", + "name": "Shugenja", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "Point clockwise or anticlockwise around the circle.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You start knowing if your closest evil player is clockwise or anti-clockwise. If equidistant, this info is arbitrary.", + "flavor": "これは夢。それも夢。すべて夢です。" + }, + { + "id": "pixie", + "name": "Pixie", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Show the Townsfolk character token marked *MAD*.", + "otherNightReminder": "", + "reminders": [ + "Mad", + "Has Ability" + ], + "setup": false, + "ability": "You start knowing 1 in-play Townsfolk. If you were mad that you were this character, you gain their ability when they die.", + "flavor": "Round and round the garden, go. Little girls run to and fro. Little boys climb up the tree. Which of these should Pixie be? Ladies smile and go to town. Lords with axe chop forest down. What’s yours is mine. What’s mine, divine. Silly little Pixie, me." + }, + { + "id": "bountyhunter", + "name": "Bounty Hunter", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Point to the *KNOWN* player.", + "otherNightReminder": "If the *KNOWN* player died today or tonight, point to a new *KNOWN* player.", + "reminders": [ + "Know" + ], + "setup": true, + "ability": "You start knowing 1 evil player. If the player you know dies, you learn another evil player tonight. [1 Townsfolk is evil]", + "flavor": "Alone, I walk these streets, paved with the sick stench of corruption. Its thickness worms its way into my nostrils, unbidden, burning with revulsion. And anticipation. The illness of this wretched place grows each night. And I... I am the cure." + }, + { + "id": "empath", + "name": "Empath", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "Give a finger signal.", + "otherNightReminder": "Give a finger signal.", + "reminders": [], + "setup": false, + "ability": "Each night, you learn how many of your 2 alive neighbors are evil.", + "flavor": "My skin prickles. Something is not right here. I can feel it." + }, + { + "id": "highpriestess", + "name": "High Priestess", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "Point to a player.", + "otherNightReminder": "Point to a player.", + "reminders": [], + "setup": false, + "ability": "Each night, learn which player the Storyteller believes you should talk to most.", + "flavor": "There is life behind the personality that uses personalities as masks. There are times when life puts off the mask and deep answers to deep." + }, + { + "id": "sailor", + "name": "Sailor", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "The Sailor chooses a living player. :reminder:", + "otherNightReminder": "The Sailor chooses a living player. :reminder:", + "reminders": [ + "Drunk" + ], + "setup": false, + "ability": "Each night, choose an alive player: either you or they are drunk until dusk. You can't die.", + "flavor": "I'll drink any one of yer under the table! You! The chatterbox! Reckon you can take me? No? Howza 'bout you, Grandma? You ever tried Old McKillys Extra Spiced Rum before? Guaranteed to put hairs on yer chest! Step aboard, aye!" + }, + { + "id": "balloonist", + "name": "Balloonist", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Show any player. :reminder:", + "otherNightReminder": "Show a player with a different character type to previously. :reminder:", + "reminders": [ + "Know" + ], + "setup": true, + "ability": "Each night, you learn a player of a different character type than last night. [+0 or +1 Outsider]", + "flavor": "More heat! Higher! Higher! Più alto! Ahhh... it is so beautiful from up here, don't you agree? Can you see the children fishing by the river, under the willow? Can you see the glint of the sun on the circus tent-poles? What's this? An old man, alone, passed out in the vineyard? Less heat! Lower! Lower! Vai più in basso!" + }, + { + "id": "general", + "name": "General", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Give a thumb signal.", + "otherNightReminder": "Give a thumb signal.", + "reminders": [], + "setup": false, + "ability": "Each night, you learn which alignment the Storyteller believes is winning: good, evil, or neither.", + "flavor": "I don’t have time for quotes." + }, + { + "id": "preacher", + "name": "Preacher", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "The Preacher chooses a player. :reminder: If they chose a Minion: Put the Preacher to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token and the Preacher token.", + "otherNightReminder": "The Preacher chooses a player. :reminder: If they chose a Minion: Put the Preacher to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token and the Preacher token.", + "reminders": [ + "No Ability", + "No Ability", + "No Ability" + ], + "setup": false, + "ability": "Each night, choose a player: a Minion, if chosen, learns this. All chosen Minions have no ability.", + "flavor": "It is better to be rich and healthy than poor and sick." + }, + { + "id": "chambermaid", + "name": "Chambermaid", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "The Chambermaid chooses 2 living players. Give a finger signal.", + "otherNightReminder": "The Chambermaid chooses 2 living players. Give a finger signal.", + "reminders": [], + "setup": false, + "ability": "Each night, choose 2 alive players (not yourself): you learn how many woke tonight due to their ability.", + "flavor": "I aint seen nothin' untoward, Milady. Begging your pardon, but if I did see somethin', it certainly weren't the master o' the house sneaking into the professor's laboratory 'round eleven o'clock and mixing up fancy potions, just like you said, Miss." + }, + { + "id": "villageidiot", + "name": "Village Idiot", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "Choose a Village Idiot to be drunk. Wake the Village Idiots one at a time, they choose a player, show either good or evil thumbs according to the alignment of that player.", + "otherNightReminder": "Wake the Village Idiots one at a time, they choose a player, show either good or evil thumbs according to the alignment of that player.", + "reminders": [ + "Drunk" + ], + "setup": true, + "ability": "Each night, choose a player: you learn their alignment. [+0 to +2 Village Idiots. 1 of the extras is drunk]", + "special": [ + { + "type": "selection", + "name": "bag-duplicate" + } + ], + "flavor": "Roses are blue, and violets are red, Please reverse what I just said." + }, + { + "id": "snakecharmer", + "name": "Snake Charmer", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "The Snake Charmer chooses a player. If they chose the Demon: Show the *YOU ARE* & Demon tokens. Give a thumbs down. Swap the Snake Charmer & Demon tokens. Put the old Snake Charmer to sleep. Wake the old Demon. Show the *YOU ARE* and Snake Charmer tokens & give a thumbs up. :reminder:", + "otherNightReminder": "The Snake Charmer chooses a player. If they chose the Demon: Show the *YOU ARE* & Demon tokens. Give a thumbs down. Swap the Snake Charmer & Demon tokens. Put the old Snake Charmer to sleep. Wake the old Demon. Show the *YOU ARE* and Snake Charmer tokens & give a thumbs up. :reminder:", + "reminders": [ + "Poisoned" + ], + "setup": false, + "ability": "Each night, choose an alive player: a chosen Demon swaps characters & alignments with you & is then poisoned.", + "flavor": "Effendi... I am but a humble man, but my pipe is golden and a single tune will tame the wildest djinn, Inshallah. They say that greed hangs more men than rope. But not I, Effendi... not I." + }, + { + "id": "mathematician", + "name": "Mathematician", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "Give a finger signal.", + "otherNightReminder": "Give a finger signal.", + "reminders": [ + "Abnormal", + "Abnormal", + "Abnormal", + "Abnormal", + "Abnormal" + ], + "setup": false, + "ability": "Each night, you learn how many players’ abilities worked abnormally (since dawn) due to another character's ability.", + "flavor": "Any consistent formal system x, within which a certain amount of elementary arithmetic can be carried out is incomplete; that is, there are statements of the language of x which can neither be proved nor disproved in x. Ergo, you are drunk." + }, + { + "id": "king", + "name": "King", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Wake the Demon. Show the *THIS PLAYER IS* token and the King token, then point to the King.", + "otherNightReminder": "If the dead equal or outnumber the living, show the character token of an alive player.", + "reminders": [], + "setup": false, + "ability": "Each night, if the dead equal or outnumber the living, you learn 1 alive character. The Demon knows you are the King.", + "flavor": "Betwixt the unknown strains of mortal strife / And morbid night, sweet with mystery and woe / Lies unfettered joys of fate’s long and colored life / Who’s garden blooms with each painted Face to Show." + }, + { + "id": "dreamer", + "name": "Dreamer", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "The Dreamer points to a player. Show 1 good & 1 evil character token, 1 of which is their character.", + "otherNightReminder": "The Dreamer points to a player. Show 1 good & 1 evil character token, 1 of which is their character.", + "reminders": [], + "setup": false, + "ability": "Each night, choose a player (not yourself or Travellers): you learn 1 good & 1 evil character, 1 of which is correct.", + "flavor": "I remember the Clockmaker. The sky was red and it was raining fractal triangles. There was a smell of violets and a bubbling sound. A woman with glowing eyes and a scraggly beard was hissing at the sky. Then, I awoke." + }, + { + "id": "fortuneteller", + "name": "Fortune Teller", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "The Fortune Teller chooses 2 players. Nod if either is the Demon (or the *RED HERRING*).", + "otherNightReminder": "The Fortune Teller chooses 2 players. Nod if either is the Demon (or the *RED HERRING*).", + "reminders": [ + "Red Herring" + ], + "setup": false, + "ability": "Each night, choose 2 players: you learn if either is a Demon. There is a good player that registers as a Demon to you.", + "flavor": "I sense great evil in your soul! But... that could just be your perfume. I am allergic to Elderberry." + }, + { + "id": "cultleader", + "name": "Cult Leader", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "The Cult Leader might change alignment. If so, show the *YOU ARE* info token and a thumbs up or down for their new alignment.", + "otherNightReminder": "The Cult Leader might change alignment. If so, show the *YOU ARE* info token and a thumbs up or down for their new alignment.", + "reminders": [], + "setup": false, + "ability": "Each night, you become the alignment of an alive neighbor. If all good players choose to join your cult, your team wins.", + "flavor": "Thinking themselves wise, they became fools." + }, + { + "id": "flowergirl", + "name": "Flowergirl", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "Either nod or shake your head.", + "reminders": [ + "Demon Voted", + "Demon Not Voted" + ], + "setup": false, + "ability": "Each night*, you learn if a Demon voted today.", + "flavor": "Yesterday's violets have withered and died, but today my poppies bloom." + }, + { + "id": "towncrier", + "name": "Town Crier", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "Either nod or shake your head.", + "reminders": [ + "Minions Not Nominated", + "Minion Nominated" + ], + "setup": false, + "ability": "Each night*, you learn if a Minion nominated today.", + "flavor": "Hear ye! Hear ye! Witchcraft in the labyrinth! Genius savant reveals all! Town in danger! Hear Ye!" + }, + { + "id": "oracle", + "name": "Oracle", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "Give a finger signal.", + "reminders": [], + "setup": false, + "ability": "Each night*, you learn how many dead players are evil.", + "flavor": "Only the chosen may gaze beyond the veil. The dead are restless, and they point in silence toward the icy north." + }, + { + "id": "undertaker", + "name": "Undertaker", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If a player was executed today, show their character token.", + "reminders": [ + "Died Today" + ], + "setup": false, + "ability": "Each night*, you learn which character died by execution today.", + "flavor": "Hmmm....what have we here? The left boot is worn down to the heel, with flint shavings under the tongue. This is the garb of a Military man." + }, + { + "id": "innkeeper", + "name": "Innkeeper", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Innkeeper chooses 2 players. :reminder: :reminder: :reminder:", + "reminders": [ + "Safe", + "Safe", + "Drunk" + ], + "setup": false, + "ability": "Each night*, choose 2 players: they can't die tonight, but 1 is drunk until dusk.", + "flavor": "Come inside, fair traveller, and rest your weary bones. Drink and be merry, for the legions of the Dark One shall not harass thee tonight." + }, + { + "id": "monk", + "name": "Monk", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Monk chooses a player. :reminder:", + "reminders": [ + "Safe" + ], + "setup": false, + "ability": "Each night*, choose a player (not yourself): they are safe from the Demon tonight.", + "flavor": "'Tis an ill and deathly wind that blows tonight. Come, my brother, take shelter in the abbey while the storm rages. By my word, or by my life, you will be safe." + }, + { + "id": "gambler", + "name": "Gambler", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Gambler chooses a player & a character. :reminder:", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "Each night*, choose a player & guess their character: if you guess wrong, you die.", + "flavor": "Heads, I win. Tails, you lose." + }, + { + "id": "acrobat", + "name": "Acrobat", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Acrobat chooses a player. :reminder:", + "reminders": [ + "Dead", + "Chosen" + ], + "setup": false, + "ability": "Each night*, choose a player: if they are or become drunk or poisoned tonight, you die.", + "flavor": "Welcome, one and all, to the greatest show on earth." + }, + { + "id": "exorcist", + "name": "Exorcist", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Exorcist chooses a player. :reminder: Put the Exorcist to sleep. If the Exorcist chose the Demon: Wake the Demon. Show the *THIS CHARACTER SELECTED YOU* & Exorcist tokens. Point to the Exorcist.", + "reminders": [ + "Chosen" + ], + "setup": false, + "ability": "Each night*, choose a player (different to last night): the Demon, if chosen, learns who you are then doesn't wake tonight.", + "flavor": "We cast you out, every unclean spirit, every satanic power, every onslaught of the infernal adversary, every legion, every diabolical group and sect, in the name and by the power of Our Lord Jesus Christ. We command you, begone and fly far from the Church of God, from the souls made by God in His image and redeemed by the precious blood of the divine Lamb." + }, + { + "id": "lycanthrope", + "name": "Lycanthrope", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Lycanthrope chooses a player. :reminder:", + "reminders": [ + "Faux Paw", + "Dead" + ], + "setup": false, + "ability": "Each night*, choose an alive player. If good, they die & the Demon doesn’t kill tonight. One good player registers as evil.", + "flavor": "Beneath the thin veneer of civilisation lies a howling madness." + }, + { + "id": "gossip", + "name": "Gossip", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If the Gossip is due to kill a player, they die. :reminder:", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "Each day, you may make a public statement. Tonight, if it was true, a player dies.", + "flavor": "Blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah. Blah." + }, + { + "id": "savant", + "name": "Savant", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each day, you may visit the Storyteller to learn 2 things in private: 1 is true & 1 is false.", + "flavor": "Seventy-two matchsticks on the floor... the sun sets early but the moon is unchanged... a torn piece of cloth... evil in the manor house... three by three... the one we trusted is not what he seems... green light means magnesium... residue, but the pattern is wrong... Seventy-two matchsticks on the floor..." + }, + { + "id": "alsaahir", + "name": "Alsaahir", + "team": "townsfolk", + "edition": "carousel", + "reminders": [], + "setup": false, + "ability": "Each day, if you publicly guess which players are Minion(s) and which are Demon(s), good wins.", + "flavor": "I am here because of you, and you are here because of me." + }, + { + "id": "engineer", + "name": "Engineer", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "The Engineer might choose Minions or Demons. :reminder: If they do: Put the Engineer to sleep. Wake a target, show them the *YOU ARE* token and their new character token, then put that target to sleep. Repeat for all players that changed characters.", + "otherNightReminder": "The Engineer might choose Minions or Demons. :reminder: If they do: Put the Engineer to sleep. Wake a target, show them the *YOU ARE* token and their new character token, then put that target to sleep. Repeat for all players that changed characters.", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night, choose which Minions or which Demon is in play.", + "flavor": "If it bends, great. If it breaks, well, it probably needed fixing anyway." + }, + { + "id": "nightwatchman", + "name": "Nightwatchman", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "The Nightwatchman might choose a player :reminder: Put the Nightwatchman to sleep. Wake the target and show the *THIS PLAYER IS* and Nightwatchman tokens and point to the Nightwatchman.", + "otherNightReminder": "The Nightwatchman might choose a player :reminder: Put the Nightwatchman to sleep. Wake the target and show the *THIS PLAYER IS* and Nightwatchman tokens and point to the Nightwatchman.", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night, choose a player: they learn you are the Nightwatchman.", + "flavor": "The night is cold and lonely, but I have the moon, the stars, the crisp wind and the soft thud of leather boots on cobbled stone for company. Yonder, candlelight flickers behind a murky window..." + }, + { + "id": "courtier", + "name": "Courtier", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "The Courtier might choose a character. :reminder: :reminder:", + "otherNightReminder": "The Courtier might choose a character. :reminder: :reminder:", + "reminders": [ + "Drunk 3", + "Drunk 2", + "Drunk 1", + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night, choose a character: they are drunk for 3 nights & 3 days.", + "flavor": "I am more afraid of an army of one hundred sheep led by a lion than an army of one hundred lions led by a sheep." + }, + { + "id": "seamstress", + "name": "Seamstress", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "The Seamstress might choose 2 players. Nod or shake your head. :reminder:", + "otherNightReminder": "The Seamstress might choose 2 players. Nod or shake your head. :reminder:", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night, choose 2 players (not yourself): you learn if they are the same alignment.", + "flavor": "Did you hear that stranger in the cashmere coat put the word on our young Belle? And she said yes? Well, that's nothing compared to what Harry and that juggler got up to at the fair! The things I could say if I was a tattletale... my, yes." + }, + { + "id": "philosopher", + "name": "Philosopher", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "The Philosopher might choose a character. If necessary, swap their character token. :reminder:", + "otherNightReminder": "The Philosopher might choose a character. If necessary, swap their character token. :reminder:", + "reminders": [ + "Drunk" + ], + "remindersGlobal": [ + "Is The Philosopher" + ], + "setup": false, + "ability": "Once per game, at night, choose a good character: gain that ability. If this character is in play, they are drunk.", + "special": [ + { + "type": "reveal", + "name": "replace-character" + } + ], + "flavor": "If anything is real, beer is real. Drink, for tomorrow we may die." + }, + { + "id": "huntsman", + "name": "Huntsman", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "The Huntsman might choose a player. :reminder: If that player was the Damsel: Put the Huntsman to sleep. Wake the Damsel and show them the *YOU ARE* info token and their new character token.", + "otherNightReminder": "The Huntsman might choose a player. :reminder: If that player was the Damsel: Put the Huntsman to sleep. Wake the Damsel and show them the *YOU ARE* info token and their new character token.", + "reminders": [ + "No Ability" + ], + "setup": true, + "ability": "Once per game, at night, choose a living player: the Damsel, if chosen, becomes a not-in-play Townsfolk. [+the Damsel]", + "flavor": "My cabin is warm and sturdy. My axe by the door, my boots drying by the fire, and elk stew a-simmering… Hark! A scream echoes through the valley! The rain and the mud and the cold, cold wind mask the scent of the wolves, but I know the path and my pace is steady. I am coming." + }, + { + "id": "professor", + "name": "Professor", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "The Professor might choose a dead player. :reminder: :reminder:", + "reminders": [ + "Alive", + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night*, choose a dead player: if they are a Townsfolk, they are resurrected.", + "flavor": "The process is simple. Attach the hydraulic confabulator to the modified chi matrix amplifier, add 20 CCs of pseudodorafine, keep his Z levels above 20%, and your husband will be fine. Now, all we need is a lightning strike." + }, + { + "id": "artist", + "name": "Artist", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, during the day, privately ask the Storyteller any yes/no question.", + "flavor": "Mon Dieu! C'est lumineux! My work, she is... how you say... Magnifique! Dieu est révélé! Oui." + }, + { + "id": "slayer", + "name": "Slayer", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, during the day, publicly choose a player: if they are the Demon, they die.", + "flavor": "Die." + }, + { + "id": "fisherman", + "name": "Fisherman", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, during the day, visit the Storyteller for some advice to help your team win.", + "flavor": "This was my favourite part of the river... see how the sunlight makes a rainbow from the monastery to the market? This was the best place for big fish. And the older I get, the bigger they were." + }, + { + "id": "princess", + "name": "Princess", + "team": "townsfolk", + "edition": "carousel", + "otherNightReminder": "If the Princess nominated the player who was executed today, the Demon wakes as normal, but no one dies to the Demon's ability.", + "reminders": [ + "Doesn't Kill" + ], + "setup": false, + "ability": "On your 1st day, if you nominated & executed a player, the Demon doesn't kill tonight.", + "flavor": "Our words are hounds, bound by silken threads, dear lords. Let kindness weave them true, lest the reigns unravel and rend our court." + }, + { + "id": "juggler", + "name": "Juggler", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "Give a finger signal.", + "reminders": [ + "Correct", + "Correct", + "Correct", + "Correct", + "Correct" + ], + "setup": false, + "ability": "On your 1st day, publicly guess up to 5 players' characters. That night, you learn how many you got correct.", + "flavor": "For my next trick, as per request, I will need a flower, a bag of beans, a toy snake, a paintbrush, and a motorized gasoline-powered hedge trimming device. I warn you, this trick may be my last. Oh dear." + }, + { + "id": "soldier", + "name": "Soldier", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You are safe from the Demon.", + "flavor": "As David said to Goliath, as Theseus said to the Minotaur, as Arjuna said to Bhagadatta... No." + }, + { + "id": "alchemist", + "name": "Alchemist", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Show the *YOU ARE* token and the character token of a Minion.", + "otherNightReminder": "", + "reminders": [], + "remindersGlobal": [ + "Is The Alchemist" + ], + "setup": false, + "ability": "You have a Minion ability. When using this, the Storyteller may prompt you to choose differently.", + "special": [ + { + "type": "reveal", + "name": "replace-character" + } + ], + "flavor": "Visit the interior of the Earth. By rectification thou shalt find the hidden stone. Above the gold, lieth the red. Kether in Malkuth." + }, + { + "id": "cannibal", + "name": "Cannibal", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Poisoned", + "Lunch" + ], + "setup": false, + "ability": "You have the ability of the recently killed executee. If they are evil, you are poisoned until a good player dies by execution.", + "flavor": "I don’t like clowns. They taste funny." + }, + { + "id": "amnesiac", + "name": "Amnesiac", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Run the Amnesiac's ability, if applicable.", + "otherNightReminder": "Run the Amnesiac's ability, if applicable.", + "reminders": [ + "?", + "?", + "?" + ], + "setup": false, + "ability": "You do not know what your ability is. Each day, privately guess what it is: you learn how accurate you are.", + "flavor": "Wait. What. Who? Oh, ok. Wait. What?" + }, + { + "id": "farmer", + "name": "Farmer", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If the Farmer died tonight, wake an alive good player. Show them the *YOU ARE* info token and a Farmer character token. Replace their previous token with the Farmer token.", + "reminders": [], + "setup": false, + "ability": "When you die at night, an alive good player becomes a Farmer.", + "flavor": "Even the high and mighty need food on the table. Without us, the city starves." + }, + { + "id": "minstrel", + "name": "Minstrel", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Everyone Is Drunk" + ], + "setup": false, + "ability": "When a Minion dies by execution, all other players (except Travellers) are drunk until dusk tomorrow.", + "flavor": "And I shall hear, tho' soft you tread above me... And all my dreams will warm and sweeter be... If you'll not fail to tell me that you love me... I simply sleep in peace until you come to me." + }, + { + "id": "ravenkeeper", + "name": "Ravenkeeper", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If the Ravenkeeper died tonight, the Ravenkeeper chooses a player. Show that player's character token.", + "reminders": [], + "setup": false, + "ability": "If you die at night, you are woken to choose a player: you learn their character.", + "flavor": "My birds will avenge me! Fly! Fly, my sweet and dutiful pets! To the manor and to the river! To the alleys and to the salons! Fly!" + }, + { + "id": "sage", + "name": "Sage", + "edition": "snv", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If the Demon killed the Sage, wake the Sage and point to 2 players, 1 of which is the Demon.", + "reminders": [], + "setup": false, + "ability": "If the Demon kills you, you learn that it is 1 of 2 players.", + "flavor": "These mountainous tomes guard the secret, I am sure of it! Twixt word and word, it lies in wait. More candles, boy! More ink! These notes may look arcane, but the infernal puzzle is revealing itself." + }, + { + "id": "choirboy", + "name": "Choirboy", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "If the Demon killed the King, point to the Demon player.", + "reminders": [], + "setup": true, + "ability": "If the Demon kills the King, you learn which player is the Demon. [+the King]", + "flavor": "I saw it, I did. I was in the pews, tidying the hymn books, when a dreadful tune started from the pipe organ. The organist had a long cloak, and long fingers on the keys. And a hat that looked… just like… yours." + }, + { + "id": "banshee", + "name": "Banshee", + "team": "townsfolk", + "edition": "carousel", + "firstNightReminder": "", + "otherNightReminder": "If the Banshee was killed by the Demon tonight, announce to all players that the Banshee has died. :reminder:", + "reminders": [ + "Has Ability" + ], + "setup": false, + "ability": "If the Demon kills you, all players learn this. From now on, you may nominate twice per day and vote twice per nomination.", + "flavor": "Gorm do shúile, dearg do ghruaig, ní bheidh sé i bhfad, is a mbeidh tú san uaigh." + }, + { + "id": "tealady", + "name": "Tea Lady", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Cannot Die", + "Cannot Die" + ], + "setup": false, + "ability": "If both your alive neighbors are good, they can't die.", + "flavor": "If you are cold, tea will warm you. If you are too heated, tea will cool you. If you are depressed, tea will cheer you. If you are excited, tea will calm you." + }, + { + "id": "mayor", + "name": "Mayor", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If only 3 players live & no execution occurs, your team wins. If you die at night, another player might die instead.", + "flavor": "We must put our differences aside, and cease this senseless killing. We are all taxpayers after all. Well, most of us." + }, + { + "id": "fool", + "name": "Fool", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "The 1st time you die, you don't.", + "flavor": "...and the King said 'What?! I've never even owned a pair of rubber pantaloons, let alone a custard cannon!' Ho-ho! Jolly day!" + }, + { + "id": "virgin", + "name": "Virgin", + "edition": "tb", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "The 1st time you are nominated, if the nominator is a Townsfolk, they are executed immediately.", + "flavor": "I am pure. Let those who are without sin cast themselves down and suffer in my stead. My reputation shall not be stained with your venomous accusations." + }, + { + "id": "magician", + "name": "Magician", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Include the Magician in the Minion and Demon Info steps.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "The Demon thinks you are a Minion. Minions think you are a Demon.", + "flavor": "1... 2... Abra... 3... 4... Cadabra... *poof!* And, as you can see, ladies and gentlemen, Captain Farnsworth’s bag of gold has disappeared! Gone! Without a trace! Thank you, and goodnight!" + }, + { + "id": "poppygrower", + "name": "Poppy Grower", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "Do not do the Minion Info and Demon Info steps. Wake the Demon, show the *THESE CHARACTERS ARE NOT IN PLAY* info token and any three good character tokens that are not in play.", + "otherNightReminder": "If the Poppy Grower died today or tonight, wake the Minions, show the *THIS IS THE DEMON* info token and point to the Demon. Put them to sleep. Wake the Demon, show the *THESE ARE YOUR MINIONS* info token and point to the Minions. Put the Demon to sleep.", + "reminders": [ + "Evil Wakes" + ], + "setup": false, + "ability": "Minions & Demons do not know each other. If you die, they learn who each other are that night.", + "flavor": "In the hidden groves of the deep forest, the black poppy dwells. To see its revelry is to be enchanted. To smell its thick aroma is to be lost forever, a slave to the gods of light and dark." + }, + { + "id": "pacifist", + "name": "Pacifist", + "edition": "bmr", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Executed good players might not die.", + "flavor": "Distrust all in whom the impulse to punish is powerful." + }, + { + "id": "atheist", + "name": "Atheist", + "edition": "carousel", + "team": "townsfolk", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": true, + "ability": "The Storyteller can break the game rules, and if executed, good wins, even if you are dead. [No evil characters]", + "special": [ + { + "type": "selection", + "name": "bag-duplicate" + } + ], + "flavor": "Let us disperse with unnecessary conjecture and silly paranoia. There is a perfectly rational explanation for everything. Yes, a teacup may indeed be orbiting the planet, too small to see, but I shall drink my tea from the very real china in my very real hands." + }, + { + "id": "hermit", + "name": "Hermit", + "team": "outsider", + "edition": "carousel", + "reminders": [ + "1", + "2", + "3" + ], + "setup": true, + "ability": "You have all Outsider abilities. [-0 or -1 Outsider]", + "flavor": "In the lost and forgotten places of the earth, the soul’s light beckons." + }, + { + "id": "butler", + "name": "Butler", + "edition": "tb", + "team": "outsider", + "firstNightReminder": "The Butler chooses a player. :reminder:", + "otherNightReminder": "The Butler chooses a player. :reminder:", + "reminders": [ + "Master" + ], + "setup": false, + "ability": "Each night, choose a player (not yourself): tomorrow, you may only vote if they are voting too.", + "flavor": "Yes, sir... No, sir... Certainly, sir." + }, + { + "id": "goon", + "name": "Goon", + "edition": "bmr", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Drunk" + ], + "setup": false, + "ability": "Each night, the 1st player to choose you with their ability is drunk until dusk. You become their alignment.", + "flavor": "Yes boss. I explained fings real good to dat geezer. He don't want me explain it again. Nah boss, I don't need no doctor - it's only a knife wound. Be right come mornin'" + }, + { + "id": "ogre", + "name": "Ogre", + "team": "outsider", + "edition": "carousel", + "firstNightReminder": "The Ogre points to a player. :reminder:", + "reminders": [ + "Friend" + ], + "setup": false, + "ability": "On your 1st night, choose a player (not yourself): you become their alignment (you don't know which) even if drunk or poisoned.", + "flavor": "" + }, + { + "id": "lunatic", + "name": "Lunatic", + "edition": "bmr", + "team": "outsider", + "firstNightReminder": "If there are 7 or more players, wake the Lunatic: Show the *THESE ARE YOUR MINIONS* token. Point to any players. Show the *THESE CHARACTERS ARE NOT IN PLAY* token. Show 3 good character tokens. Put the Lunatic to sleep. Wake the Demon. Show the *YOU ARE* info token and the Demon token. Show the *THIS PLAYER IS* info token and the Lunatic token, then point to the Lunatic.", + "otherNightReminder": "Do whatever needs to be done to simulate the Demon acting. Put the Lunatic to sleep. Wake the Demon. Show the Lunatic token & point to them, then their target(s).", + "reminders": [ + "Chosen", + "Chosen", + "Chosen" + ], + "setup": false, + "ability": "You think you are a Demon, but you are not. The Demon knows who you are & who you choose at night.", + "flavor": "I am the night... I think." + }, + { + "id": "drunk", + "name": "Drunk", + "edition": "tb", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "remindersGlobal": [ + "Is The Drunk" + ], + "setup": true, + "ability": "You do not know you are the Drunk. You think you are a Townsfolk character, but you are not.", + "special": [ + { + "type": "selection", + "name": "bag-disabled" + }, + { + "type": "reveal", + "name": "replace-character" + } + ], + "flavor": "I’m only a *hic* social drinker, my dear. Admittedly, I am a heavy *burp* socializer." + }, + { + "id": "tinker", + "name": "Tinker", + "edition": "bmr", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "The Tinker might die. :reminder:", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "You might die at any time.", + "flavor": "I think I see the problem. Luckily, I have an idea! This catapult will shoot twice as far with just a minor adjustment..." + }, + { + "id": "recluse", + "name": "Recluse", + "edition": "tb", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You might register as evil & as a Minion or Demon, even if dead.", + "flavor": "Garn git ya darn grub ya mitts ofma lorn yasee. Grr. Natsy pikkins yonder southwise ye begittin afta ya! Git! Me harvy no so widda licks and demmons no be fightin' hadsup ne'er ma kin. Git, assay!" + }, + { + "id": "golem", + "name": "Golem", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "May Not Nominate" + ], + "setup": false, + "ability": "You may only nominate once per game. When you do, if the nominee is not the Demon, they die.", + "flavor": "Golem help? Golem smash! Golem help." + }, + { + "id": "sweetheart", + "name": "Sweetheart", + "edition": "snv", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "If the Sweetheart died, a player became drunk immediately. If you haven't done this yet, do so now. :reminder:", + "reminders": [ + "Drunk" + ], + "setup": false, + "ability": "When you die, 1 player is drunk from now on.", + "flavor": "I will never forget her. Never." + }, + { + "id": "plaguedoctor", + "name": "Plague Doctor", + "team": "outsider", + "edition": "carousel", + "firstNightReminder": "", + "otherNightReminder": "If the Plague Doctor died, the Storyteller gained a Minion ability. If you haven’t done this yet, do so now.", + "reminders": [ + "Storyteller Ability" + ], + "setup": false, + "ability": "When you die, the Storyteller gains a Minion ability.", + "flavor": "Pleauze shtay shtill. Thinks nid tiime for hillink. Myrhh-myrhh." + }, + { + "id": "klutz", + "name": "Klutz", + "edition": "snv", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "When you learn that you died, publicly choose 1 alive player: if they are evil, your team loses.", + "flavor": "Oops." + }, + { + "id": "moonchild", + "name": "Moonchild", + "edition": "bmr", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "If the Moonchild is due to kill a good player, they die. :reminder:", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "When you learn that you died, publicly choose 1 alive player. Tonight, if it was a good player, they die.", + "flavor": "Scorpio looks sideways at the lovers, and you have a choice to make. With silver cross my palm, and your fate shall be revealed. With steel cross my throat, and by the stars you shall regret it." + }, + { + "id": "saint", + "name": "Saint", + "edition": "tb", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If you die by execution, your team loses.", + "flavor": "Wisdom begets peace. Patience begets wisdom. Fear not, for the time shall come when fear too shall pass. Let us pray, and may the unity of our vision make saints of us all." + }, + { + "id": "barber", + "name": "Barber", + "edition": "snv", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "If the Barber died today or tonight, show the Demon the *THIS CHARACTER SELECTED YOU* & Barber tokens. If the Demon chose 2 players, wake one at a time. Show the *YOU ARE* token & their new character token.", + "reminders": [ + "Haircuts Tonight" + ], + "setup": false, + "ability": "If you died today or tonight, the Demon may choose 2 players (not another Demon) to swap characters.", + "flavor": "Did you know that barbery and surgery were once the same profession? No? Well, now you do." + }, + { + "id": "hatter", + "name": "Hatter", + "team": "outsider", + "edition": "carousel", + "firstNightReminder": "", + "otherNightReminder": "If the Hatter died today or tonight, wake Minions and Demons, allow them to choose new characters.", + "reminders": [ + "Tea Party Tonight" + ], + "setup": false, + "ability": "If you died today or tonight, the Minion & Demon players may choose new Minion & Demon characters to be.", + "flavor": "One Hat. Too Hat. Three Hat. Tea Hat. Fore Hat. Thrive Hat. Six Hat. Sticks Hat." + }, + { + "id": "mutant", + "name": "Mutant", + "edition": "snv", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If you are “mad” about being an Outsider, you might be executed.", + "flavor": "I am not a freak! I am a human being! Have mercy!" + }, + { + "id": "politician", + "name": "Politician", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If you were the player most responsible for your team losing, you change alignment & win, even if dead.", + "flavor": "I'm glad you asked that question. Truly, I am. But I think the REAL question here is..." + }, + { + "id": "zealot", + "name": "Zealot", + "team": "outsider", + "edition": "carousel", + "reminders": [], + "setup": false, + "ability": "If there are 5 or more players alive, you must vote for every nomination.", + "flavor": "I enjoy talking to you. Your mind appeals to me. It resembles my own mind except that you happen to be insane." + }, + { + "id": "damsel", + "name": "Damsel", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "If the Damsel was chosen by the Huntsman, show them the *YOU ARE* info token and their new character token.", + "otherNightReminder": "If the Damsel was chosen by the Huntsman, show them the *YOU ARE* info token and their new character token.", + "reminders": [ + "Guess Used" + ], + "setup": false, + "ability": "All Minions know a Damsel is in play. If a Minion publicly guesses you (once), your team loses.", + "flavor": "Don't touch the hair, honey." + }, + { + "id": "snitch", + "name": "Snitch", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "Wake each Minion. Show the *THESE CHARACTERS ARE NOT IN PLAY* token and three not-in-play character tokens. Put each Minion to sleep.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each Minion gets 3 bluffs.", + "flavor": "It was John." + }, + { + "id": "heretic", + "name": "Heretic", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Whoever wins, loses & whoever loses, wins, even if you are dead.", + "flavor": "After the hail has smashed the roof and splintered the glass of the Cathedral windows, it melts again into the earth, like a dying lamb in the desert sun. Such is the parable of the madman." + }, + { + "id": "puzzlemaster", + "name": "Puzzlemaster", + "edition": "carousel", + "team": "outsider", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Drunk", + "Guess Used" + ], + "setup": false, + "ability": "1 player is drunk, even if you die. If you guess (once) who it is, learn the Demon player, but guess wrong & get false info.", + "flavor": "When one begins to think that some thing is merely some other thing, one is usually on the brink of an error. Patience, patience. Don’t confuse just and should with is and isn’t." + }, + { + "id": "mezepheles", + "name": "Mezepheles", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "Show the written word.", + "otherNightReminder": "If a player is marked with the *TURNS EVIL* reminder, wake them. Show the *YOU ARE* info token and a thumbs down. The Mezepheles loses their ability. :reminder:", + "reminders": [ + "Turns Evil", + "No Ability" + ], + "setup": false, + "ability": "You start knowing a secret word. The 1st good player to say this word becomes evil that night.", + "flavor": "That which issues from the heart alone, will bend the hearts of others to your own." + }, + { + "id": "godfather", + "name": "Godfather", + "edition": "bmr", + "team": "minion", + "firstNightReminder": "Show the character tokens of all in-play Outsiders.", + "otherNightReminder": "If an Outsider died today, the Godfather chooses a player. :reminder:", + "reminders": [ + "Died Today", + "Dead" + ], + "setup": true, + "ability": "You start knowing which Outsiders are in play. If 1 died today, choose a player tonight: they die. [-1 or +1 Outsider]", + "flavor": "Normally, it's just business. But when you insult my daughter, you insult me. And when you insult me, you insult my family. You really should be more careful - it would be a shame if you had an unfortunate accident." + }, + { + "id": "poisoner", + "name": "Poisoner", + "edition": "tb", + "team": "minion", + "firstNightReminder": "The Poisoner chooses a player. :reminder:", + "otherNightReminder": "The Poisoner chooses a player. :reminder:", + "reminders": [ + "Poisoned" + ], + "setup": false, + "ability": "Each night, choose a player: they are poisoned tonight and tomorrow day.", + "flavor": "Add compound Alpha to compound Beta... NOT TOO MUCH!" + }, + { + "id": "devilsadvocate", + "name": "Devil's Advocate", + "edition": "bmr", + "team": "minion", + "firstNightReminder": "The Devil's Advocate chooses a living player. :reminder:", + "otherNightReminder": "The Devil's Advocate chooses a living player. :reminder:", + "reminders": [ + "Survives Execution" + ], + "setup": false, + "ability": "Each night, choose a living player (different to last night): if executed tomorrow, they don't die.", + "flavor": "My client, should the objection be overruled, pleads innocent by virtue of the prosecution's non-observance of statute 27.B - incorrect or misleading conjugation of a verb. The fact that nine of the jury died last night is simply prima facie, which is, as Wills vs Thule set precedent for, further reason to acquit." + }, + { + "id": "spy", + "name": "Spy", + "edition": "tb", + "team": "minion", + "firstNightReminder": "Show the Grimoire for as long as the Spy needs.", + "otherNightReminder": "Show the Grimoire for as long as the Spy needs.", + "reminders": [], + "setup": false, + "special": [ + { + "type": "signal", + "name": "grimoire", + "time": "night" + } + ], + "ability": "Each night, you see the Grimoire. You might register as good & as a Townsfolk or Outsider, even if dead.", + "flavor": "Any brewmaster worth their liquor, knows no concoction pours trouble quicker, than one where spies seem double." + }, + { + "id": "harpy", + "name": "Harpy", + "team": "minion", + "edition": "carousel", + "firstNightReminder": "The Harpy chooses two players. :reminder: :reminder: Put the Harpy to sleep. Wake the 1st target. Show the *THIS CHARACTER SELECTED YOU* token, the Harpy token, then point to the 2nd target.", + "otherNightReminder": "The Harpy chooses two players. :reminder: :reminder: Put the Harpy to sleep. Wake the 1st target. Show the *THIS CHARACTER SELECTED YOU* token, the Harpy token, then point to the 2nd target.", + "reminders": [ + "Mad", + "2nd" + ], + "setup": false, + "ability": "Each night, choose 2 players: tomorrow, the 1st player is mad that the 2nd is evil, or one or both might die.", + "flavor": "So fair a day I never did see, nor so fowl a presence hanging over me." + }, + { + "id": "witch", + "name": "Witch", + "edition": "snv", + "team": "minion", + "firstNightReminder": "The Witch chooses a player. :reminder:", + "otherNightReminder": "The Witch chooses a player. :reminder:", + "reminders": [ + "Cursed" + ], + "setup": false, + "ability": "Each night, choose a player: if they nominate tomorrow, they die. If just 3 players live, you lose this ability.", + "flavor": "Three drops of goat's blood. A lock of hair, torn in anger. The name is spoken, the shadow cast. Walk left foot first down that brambled path, and don't look back." + }, + { + "id": "cerenovus", + "name": "Cerenovus", + "edition": "snv", + "team": "minion", + "firstNightReminder": "The Cerenovus chooses a player & a character. :reminder: Put the Cerenovus to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token, the Cerenovus token, then the madness-character token.", + "otherNightReminder": "The Cerenovus chooses a player & a character. :reminder: Put the Cerenovus to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token, the Cerenovus token, then the madness-character token.", + "reminders": [ + "Mad" + ], + "setup": false, + "ability": "Each night, choose a player & a good character: they are “mad” they are this character tomorrow, or might be executed.", + "flavor": "Reality is merely an opinion. Specifically, my opinion." + }, + { + "id": "fearmonger", + "name": "Fearmonger", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "The Fearmonger chooses a player. :reminder: Declare that \"the Fearmonger has chosen a player.\"", + "otherNightReminder": "The Fearmonger chooses a player. :reminder: If the player wasn't already marked with the *FEAR* reminder, declare that \"the Fearmonger has chosen a player.\"", + "reminders": [ + "Fear" + ], + "setup": false, + "ability": "Each night, choose a player: if you nominate & execute them, their team loses. All players know if you choose a new player.", + "flavor": "Beware of gazing long into the Abyss, lest the Abyss also gaze into you." + }, + { + "id": "pithag", + "name": "Pit-Hag", + "edition": "snv", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "The Pit-Hag chooses a player & a character. If they chose a character that is not in play: Put the Pit-Hag to sleep. Wake the target. Show the *YOU ARE* token & their new character token.", + "reminders": [], + "setup": false, + "ability": "Each night*, choose a player & a character they become (if not in play). If a Demon is made, deaths tonight are arbitrary.", + "flavor": "Round about the cauldron go; In the poison'd entrails throw; Toad, that under cold stone; Days and nights has thirty-one; Sweated venom sleeping got; Boil thou first in the charmed pot." + }, + { + "id": "psychopath", + "name": "Psychopath", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each day, before nominations, you may publicly choose a player: they die. If executed, you only die if you lose roshambo.", + "flavor": "Surprise!" + }, + { + "id": "assassin", + "name": "Assassin", + "edition": "bmr", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "The Assassin might choose a player. :reminder: :reminder:", + "reminders": [ + "Dead", + "No Ability" + ], + "setup": false, + "ability": "Once per game, at night*, choose a player: they die, even if for some reason they could not.", + "flavor": "..." + }, + { + "id": "wizard", + "name": "Wizard", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "Run the Wizard's ability, if applicable.", + "otherNightReminder": "Run the Wizard's ability, if applicable.", + "reminders": [ + "?", + "?" + ], + "setup": false, + "ability": "Once per game, choose to make a wish. If granted, it might have a price & leave a clue as to its nature.", + "flavor": "Every man and every woman is a star. Love is the law, love under will." + }, + { + "id": "widow", + "name": "Widow", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "Show the Grimoire for as long as the Widow needs. The Widow chooses a player. :reminder:", + "otherNightReminder": "", + "reminders": [ + "Poisoned", + "Know" + ], + "remindersGlobal": [], + "setup": false, + "special": [ + { + "name": "grimoire", + "type": "signal", + "time": "night" + } + ], + "ability": "On your 1st night, look at the Grimoire & choose a player: they are poisoned. 1 good player knows a Widow is in play.", + "flavor": "More wine? Château d’Ergot ’07 is a very special vintage. My yes, very special indeed." + }, + { + "id": "xaan", + "name": "Xaan", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "Mark the Xaan with the *NIGHT 1* reminder. If X is 1, mark the Xaan with the *X* reminder token. :reminder: :reminder:", + "otherNightReminder": "Change the Xaan reminder token to the relevant night. If it is night X, mark the Xaan with the *X* reminder token. :reminder: :reminder:", + "reminders": [ + "Night 1", + "Night 2", + "Night 3", + "X" + ], + "setup": true, + "ability": "On night X, all Townsfolk are poisoned until dusk. [X Outsiders]", + "flavor": "Down they fall. One by one. By two, by three, by five." + }, + { + "id": "marionette", + "name": "Marionette", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "Wake the Demon. Point to the player marked *IS THE MARIONETTE* and show the *THIS PLAYER IS* token and the Marionette character token.", + "otherNightReminder": "", + "reminders": [], + "remindersGlobal": [ + "Is The Marionette" + ], + "setup": true, + "ability": "You think you are a good character, but you are not. The Demon knows who you are. [You neighbor the Demon]", + "special": [ + { + "type": "selection", + "name": "bag-disabled" + }, + { + "type": "reveal", + "name": "replace-character" + } + ], + "flavor": "Words, words. They're all we have to go on." + }, + { + "id": "wraith", + "name": "Wraith", + "team": "minion", + "edition": "carousel", + "firstNightReminder": "Wake the Wraith whenever other evil players wake.", + "otherNightReminder": "Wake the Wraith whenever other evil players wake.", + "reminders": [], + "setup": false, + "ability": "You may choose to open your eyes at night. You wake when other evil players do.", + "flavor": "Ra'āb ina pān ṣilli ša dāri. Rigim qallu ina šūri, šītu ša šunātīka iredde, u napšutka idlul ina pān maṣṣartī dāriti.", + "special": [ + { + "type": "player", + "name": "open-eyes", + "time": "night" + } + ] + }, + { + "id": "summoner", + "name": "Summoner", + "team": "minion", + "edition": "carousel", + "firstNightReminder": "Show the *THESE CHARACTERS ARE NOT IN PLAY* token. Show 3 not-in-play good character tokens.", + "otherNightReminder": "Change the Summoner reminder token to the relevant night. If it is night 3, the Summoner chooses a player and a Demon. Put the Summoner to sleep. Wake the chosen player. Show the *YOU ARE* token, a thumbs down and the chosen Demon token.", + "reminders": [ + "Night 1", + "Night 2", + "Night 3" + ], + "setup": true, + "ability": "You get 3 bluffs. On the 3rd night, choose a player: they become an evil Demon of your choice. [No Demon]", + "flavor": "Hail the guardians of the north; by my intellect, thou art cut. Hail the guardians of the east; by my will, thou art dominated. Hail the guardians of the south; by that which lies beyond, the mystery is revealed. Hail the guardians of the west; a shield in the darkness" + }, + { + "id": "eviltwin", + "name": "Evil Twin", + "edition": "snv", + "team": "minion", + "firstNightReminder": "Wake both twins. Allow eye contact. Show the good twin's character token to the Evil Twin & vice versa.", + "otherNightReminder": "", + "reminders": [ + "Twin" + ], + "setup": false, + "ability": "You & an opposing player know each other. If the good player is executed, evil wins. Good can't win if you both live.", + "flavor": "I'm not Sara! I'm Clara! SHE is Sara! Sara is the evil one! Not me!" + }, + { + "id": "goblin", + "name": "Goblin", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Claimed" + ], + "setup": false, + "ability": "If you publicly claim to be the Goblin when nominated & are executed that day, your team wins.", + "flavor": "You don’t want to insult the goblins. You really, really don’t. On a completely different note… can I have another piece of cake?" + }, + { + "id": "boomdandy", + "name": "Boomdandy", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If you are executed, all but 3 players die. After a 10 to 1 countdown, the player with the most players pointing at them, dies.", + "special": [ + { + "type": "ability", + "name": "pointing", + "time": "day" + } + ], + "flavor": "Tick... Tick... Tick... TOCK." + }, + { + "id": "mastermind", + "name": "Mastermind", + "edition": "bmr", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If the Demon dies by execution (ending the game), play for 1 more day. If a player is then executed, their team loses.", + "flavor": "The tentacles of that monster are nailed to the doors of the church. Mothers and children are dancing in the street. Excellent. Everything is proceeding exactly as I have planned." + }, + { + "id": "scarletwoman", + "name": "Scarlet Woman", + "edition": "tb", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "If the Scarlet Woman became the Demon today, show them the *YOU ARE* token, then the Demon token.", + "reminders": [ + "Is The Demon" + ], + "setup": false, + "ability": "If there are 5 or more players alive & the Demon dies, you become the Demon. (Travellers don't count.)", + "flavor": "You have shown me the secrets of the Council of the Purple Flame. We have lain together in fire and in lust and in beastly commune, and I am forever your servant. But tonight, my dear, I am your master." + }, + { + "id": "vizier", + "name": "Vizier", + "team": "minion", + "edition": "carousel", + "firstNightReminder": "Announce the Vizier player to the group.", + "reminders": [], + "setup": false, + "ability": "All players know you are the Vizier. You cannot die during the day. If good voted, you may choose to execute immediately.", + "flavor": "An excellent decision, as always, sire. Such a petty crime as bumping into the Bishop indeed deserves your ‘justice’ and ‘mercy’. Take a stroll in the gardens. Visit the gallery and peruse the sculptures of Von Strauf. Relax, sire. Leave everything… to me." + }, + { + "id": "organgrinder", + "name": "Organ Grinder", + "edition": "carousel", + "team": "minion", + "firstNightReminder": "The Organ Grinder either nods their head yes to be drunk, or shakes their head no to be sober. :reminder:", + "otherNightReminder": "The Organ Grinder either nods their head yes to be drunk, or shakes their head no to be sober. :reminder:", + "reminders": [ + "About To Die", + "Drunk" + ], + "setup": false, + "ability": "All players keep their eyes closed when voting and the vote tally is secret. Each night, choose if you are drunk until dusk.", + "special": [ + { + "type": "vote", + "name": "hidden" + } + ], + "flavor": "Round and round the handles go. The more you dance the less you know." + }, + { + "id": "boffin", + "name": "Boffin", + "team": "minion", + "edition": "carousel", + "firstNightReminder": "Wake the Boffin and the Demon. Show the not-in-play good character token. Put the Boffin and the Demon to sleep.", + "reminders": [], + "setup": false, + "ability": "The Demon (even if drunk or poisoned) has a not-in-play good character’s ability. You both know which.", + "flavor": "Stellar hydrogen, vast, inert; carbon, oxygen, neon gases, all ruined. Molecular chaos, entropy, yields new cosmic phenomena, rebirth from atomic chaos, dense matter collapsing. All in a teeny little bottle." + }, + { + "id": "baron", + "name": "Baron", + "edition": "tb", + "team": "minion", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": true, + "ability": "There are extra Outsiders in play. [+2 Outsiders]", + "flavor": "This town has gone to the dogs, what? Cheap foreign labor... that's the ticket. Stuff them in the mine, I say. A bit of hard work never hurt anyone, and a clip'o'the ears to any brigand who says otherwise. It's all about the bottom line, what?" + }, + { + "id": "yaggababble", + "name": "Yaggababble", + "team": "demon", + "edition": "carousel", + "firstNightReminder": "Choose a secret phrase. Wake the Yaggababble and let them know their secret phrase.", + "otherNightReminder": "For each time the Yaggababble said the phrase today, you may choose a player. They die. :reminder:", + "reminders": [ + "Dead", + "Dead", + "Dead" + ], + "setup": false, + "ability": "You start knowing a secret phrase. For each time you said it publicly today, a player might die.", + "flavor": "Murders inside the Rue Morgue? Фальшивые новости! Hounds on the Baskerville moor? Фальшивые новости! Death while sailing the Nile? Фальшивые новости!" + }, + { + "id": "pukka", + "name": "Pukka", + "edition": "bmr", + "team": "demon", + "firstNightReminder": "The Pukka chooses a player. :reminder:", + "otherNightReminder": "The Pukka chooses a player. :reminder: The previously poisoned player dies then becomes healthy. :reminder:", + "reminders": [ + "Poisoned", + "Poisoned", + "Dead" + ], + "setup": false, + "ability": "Each night, choose a player: they are poisoned. The previously poisoned player dies then becomes healthy.", + "flavor": "You truly have been kind welcoming me into your beautiful home. I am so sorry I accidentally scratched you. A little thing. No matter. But please, take this golden toothpick as a humble token of my regret." + }, + { + "id": "lilmonsta", + "name": "Lil' Monsta", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "Wake all Minions, allow them to choose a babysitter. :reminder:", + "otherNightReminder": "Wake all Minions, allow them to choose a babysitter. :reminder: A player might die. :reminder:", + "reminders": [], + "remindersGlobal": [ + "Is The Demon", + "Dead" + ], + "setup": true, + "ability": "Each night, Minions choose who babysits Lil' Monsta & \"is the Demon\". Each night*, a player might die. [+1 Minion]", + "special": [ + { + "type": "ability", + "name": "pointing", + "time": "night", + "global": "minion" + }, + { + "type": "selection", + "name": "bag-disabled" + } + ], + "flavor": "Step 1: Be cute. Step 2: World domination. Step 3: Bweakfast." + }, + { + "id": "nodashii", + "name": "No Dashii", + "edition": "snv", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The No Dashii chooses a player. :reminder:", + "reminders": [ + "Dead", + "Poisoned", + "Poisoned" + ], + "setup": false, + "ability": "Each night*, choose a player: they die. Your 2 Townsfolk neighbors are poisoned.", + "flavor": "By the sins of Arnoch, I feel thy laden stench. By the curs-ed sun and her foul legion of tiny grinning gods, I corrupt thee. By the blessed night and the hidden depths of the horrid and unholy sea, I end thy squalid life upon this plane." + }, + { + "id": "imp", + "name": "Imp", + "edition": "tb", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Imp chooses a player. :reminder: If the Imp chose themselves: Replace 1 alive Minion token with a spare Imp token. Put the old Imp to sleep. Wake the new Imp. Show the *YOU ARE* token, then show the Imp token.", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "Each night*, choose a player: they die. If you kill yourself this way, a Minion becomes the Imp.", + "flavor": "We must keep our wits sharp and our sword sharper. Evil walks among us, and will stop at nothing to destroy us good, simple folk, bringing our fine town to ruin. Trust no-one. But, if you must trust someone, trust me." + }, + { + "id": "shabaloth", + "name": "Shabaloth", + "edition": "bmr", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "A previously chosen player might be resurrected. :reminder: The Shabaloth chooses 2 players. :reminder: :reminder:", + "reminders": [ + "Dead", + "Dead", + "Alive" + ], + "setup": false, + "ability": "Each night*, choose 2 players: they die. A dead player you chose last night might be regurgitated.", + "flavor": "Blarg f'taag nm mataan! No sho gumtha m'sik na yuuu. Fluuuuuuuuurg h-sikkkh." + }, + { + "id": "ojo", + "name": "Ojo", + "team": "demon", + "edition": "carousel", + "firstNightReminder": "", + "otherNightReminder": "The Ojo chooses a character. :reminder: ", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "Each night*, choose a character: they die. If they are not in play, the Storyteller chooses who dies.", + "flavor": "Like a bonfire on a moonless night… I see you, mortal." + }, + { + "id": "kazali", + "name": "Kazali", + "team": "demon", + "edition": "carousel", + "firstNightReminder": "Wake the Kazali, allow them to choose Minions.", + "otherNightReminder": "The Kazali chooses a player. :reminder: ", + "reminders": [ + "Dead" + ], + "setup": true, + "ability": "Each night*, choose a player: they die. [You choose which players are which Minions. -? to +? Outsiders]", + "flavor": "Gon(z)a7les6. Take cau8tun. The mech4an4ion is iNvert10d. E99ors insy6tum. Reco{7}fig." + }, + { + "id": "po", + "name": "Po", + "edition": "bmr", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Po may choose a player OR chooses 3 players if they chose no-one last night. :reminder: or :reminder: :reminder: :reminder:", + "reminders": [ + "Dead", + "Dead", + "Dead", + "3 Attacks" + ], + "setup": false, + "ability": "Each night*, you may choose a player: they die. If your last choice was no-one, choose 3 players tonight.", + "flavor": "Would you like a flower? I'm so lonely." + }, + { + "id": "zombuul", + "name": "Zombuul", + "edition": "bmr", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "If no one died today, the Zombuul chooses a player. :reminder:", + "reminders": [ + "Died Today", + "Dead" + ], + "setup": false, + "ability": "Each night*, if no-one died today, choose a player: they die. The 1st time you die, you live but register as dead.", + "flavor": "I do not. Understand. Your ways. Fellow human. Show me. The dirt. Where the holy. Lay. Sleeping. I too. Must sleep. Soon." + }, + { + "id": "vigormortis", + "name": "Vigormortis", + "edition": "snv", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Vigormortis chooses a player. :reminder: If that player is a Minion, poison a neighboring Townsfolk. :reminder: :reminder:", + "reminders": [ + "Dead", + "Has Ability", + "Has Ability", + "Has Ability", + "Poisoned", + "Poisoned", + "Poisoned" + ], + "setup": true, + "ability": "Each night*, choose a player: they die. Minions you kill keep their ability & poison 1 Townsfolk neighbor. [-1 Outsider]", + "flavor": "All doors are one door. All keys are one key. All cups are one cup, but whosoever drinketh of the water that I give shall never thirst, but the water shall be in him a well springing up into everlasting life." + }, + { + "id": "vortox", + "name": "Vortox", + "edition": "snv", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Vortox chooses a player. :reminder:", + "reminders": [ + "Dead" + ], + "setup": false, + "ability": "Each night*, choose a player: they die. Townsfolk abilities yield false info. Each day, if no-one is executed, evil wins.", + "flavor": "Black is White. Right is Wrong. Left is Right. Up is Long. Down is Sight. Short is Blind. Follow me. Answers find." + }, + { + "id": "legion", + "name": "Legion", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "A player might die. :reminder:", + "reminders": [ + "Dead", + "About To Die" + ], + "setup": true, + "ability": "Each night*, a player might die. Executions fail if only evil voted. You register as a Minion too. [Most players are Legion]", + "special": [ + { + "type": "selection", + "name": "bag-duplicate" + } + ], + "flavor": "We are the chill wind on a winter’s day. We are the shadow in the moonless night. We are the poison in your tea and the whisper in your ear. We are everywhere." + }, + { + "id": "fanggu", + "name": "Fang Gu", + "edition": "snv", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Fang Gu chooses a player. :reminder: If they chose an Outsider (once only): Replace the Outsider token with the spare Fang Gu token. Put the Fang Gu to sleep. Wake the target. Show the *YOU ARE* and Fang Gu tokens & give a thumbs-down. :reminder:", + "reminders": [ + "Dead", + "Once" + ], + "setup": true, + "ability": "Each night*, choose a player: they die. The 1st Outsider this kills becomes an evil Fang Gu & you die instead. [+1 Outsider]", + "flavor": "Your walls and your weapons are but smoke in dreams." + }, + { + "id": "lordoftyphon", + "name": "Lord of Typhon", + "team": "demon", + "edition": "carousel", + "firstNightReminder": "Replace neighbors of the Lord of Typhon with Minions, wake them, tell them their new alignment and character, then do minion info.", + "otherNightReminder": "The Lord of Typhon chooses a player. :reminder:", + "reminders": [ + "Dead" + ], + "setup": true, + "ability": "Each night*, choose a player: they die. [Evil characters are in a line. You are in the middle. +1 Minion. -? to +? Outsiders]", + "flavor": "In the shadowed and forgotten corners of the cosmos, where the stars whisper secrets to the void, lies a truth so profound that the merest glimpse of it unravels the sanity of mortal minds." + }, + { + "id": "lleech", + "name": "Lleech", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "The Lleech chooses a player. :reminder:", + "otherNightReminder": "The Lleech chooses a player. :reminder:", + "reminders": [ + "Dead", + "Poisoned" + ], + "setup": false, + "ability": "Each night*, choose a player: they die. You start by choosing a player: they are poisoned. You die if & only if they are dead.", + "flavor": "Tasty, tasty, tasty, tasty, tasty, tasty, tasty, tasty brai- I mean pie! Yes. Tasty pie. That’s what I meant to say." + }, + { + "id": "alhadikhia", + "name": "Al-Hadikhia", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "The Al-Hadikhia chooses three players. :reminder: :reminder: :reminder: Wake the player marked *1* and say \"the Al-Hadikhia has chosen\", then the player's name, then \"Do you choose to live?\" They either nod or shake their head. Put them to sleep and add or remove shrouds accordingly. Repeat for players marked *2* and *3*. If all three players are now alive, add a shroud to all three.", + "reminders": [ + "1", + "2", + "3" + ], + "setup": false, + "ability": "Each night*, you may choose 3 players (all players learn who): each silently chooses to live or die, but if all live, all die.", + "flavor": "Alsukut min dhahab." + }, + { + "id": "riot", + "name": "Riot", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Day 1", + "Day 2", + "Day 3" + ], + "setup": false, + "ability": "On day 3, Minions become Riot & nominees die but nominate an alive player immediately. This must happen.", + "flavor": "Larga vida a la revolución! Mi revolucion!" + }, + { + "id": "leviathan", + "name": "Leviathan", + "edition": "carousel", + "team": "demon", + "firstNightReminder": "Declare that \"The Leviathan is in play.\" Mark the Leviathan with the *DAY 1* reminder. :reminder:", + "otherNightReminder": "Optionally, declare that \"The Leviathan is in play.\" Replace the reminder token. :reminder:", + "reminders": [ + "Day 1", + "Day 2", + "Day 3", + "Day 4", + "Day 5", + "Good Player Executed" + ], + "setup": false, + "ability": "If more than 1 good player is executed, evil wins. All players know you are in play. After day 5, evil wins.", + "flavor": "To the last, I grapple with thee. From Hell’s heart, I stab at thee. For hate’s sake, I spit my last breath at thee." + }, + { + "id": "thief", + "name": "Thief", + "edition": "tb", + "team": "traveller", + "firstNightReminder": "The Thief chooses a player. :reminder:", + "otherNightReminder": "The Thief chooses a player. :reminder:", + "reminders": [ + "Negative Vote" + ], + "setup": false, + "ability": "Each night, choose a player (not yourself): their vote counts negatively tomorrow.", + "special": [ + { + "type": "vote", + "name": "multiplier", + "value": -1 + } + ], + "flavor": "I ain't done nuffink. I weren't even in dat alley last night! It weren't me what stole Mayor Bruno's briefcase wiv all dem fancy dockoments innit. Besides, it was too 'eavy to carry far." + }, + { + "id": "bureaucrat", + "name": "Bureaucrat", + "edition": "tb", + "team": "traveller", + "firstNightReminder": "The Bureaucrat chooses a player. :reminder:", + "otherNightReminder": "The Bureaucrat chooses a player. :reminder:", + "reminders": [ + "3 Votes" + ], + "setup": false, + "ability": "Each night, choose a player (not yourself): their vote counts as 3 votes tomorrow.", + "special": [ + { + "type": "vote", + "name": "multiplier", + "value": 3 + } + ], + "flavor": "Sign here please. And here. And here. Aaaaaaaaand here. This should all be sorted and tallied by the end of the day, assuming everyone's signatures are legible. We haven't had a mix-up in the paperwork for ages. Yesterday noon, if memory serves..." + }, + { + "id": "barista", + "name": "Barista", + "edition": "snv", + "team": "traveller", + "firstNightReminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", + "otherNightReminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", + "reminders": [ + "Sober & Healthy", + "Acts Twice", + "?", + "?" + ], + "setup": false, + "ability": "Each night, until dusk, 1) a player becomes sober, healthy & gets true info, or 2) their ability works twice. They learn which.", + "flavor": "A cup of coffee with no cream, Monsieur? I’m terribly sorry, but we’re fresh out of cream — how about with no milk?" + }, + { + "id": "harlot", + "name": "Harlot", + "edition": "snv", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "The Harlot points at any living player. Then, put the Harlot to sleep. Wake the chosen player, show them the *THIS CHARACTER SELECTED YOU* token, then the Harlot token. That player either nods their head yes or shakes their head no. If they nodded their head yes, wake the Harlot and show them the chosen player's character token. Then, you may decide that both players die.", + "reminders": [ + "Dead", + "Dead" + ], + "setup": false, + "ability": "Each night*, choose a living player: if they agree, you learn their character, but you both might die.", + "flavor": "Enchanté, Sailor. You look like you need someone to really listen to your troubles. I'm a good listener. Very, very good." + }, + { + "id": "butcher", + "name": "Butcher", + "edition": "snv", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each day, after the 1st execution, you may nominate again.", + "flavor": "It tastes like chicken. More please." + }, + { + "id": "gunslinger", + "name": "Gunslinger", + "edition": "tb", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each day, after the 1st vote has been tallied, you may choose a player that voted: they die.", + "flavor": "It's time someone took matters into their own hands. That someone... is me." + }, + { + "id": "matron", + "name": "Matron", + "edition": "bmr", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Each day, you may choose up to 3 sets of 2 players to swap seats. Players may not leave their seats to talk in private.", + "flavor": "Miss Featherbottom, be quiet. Master Rutherford, a teacup needs just the four fingers, please. I know you are a father of nine, but age, or lack there-of as the case may be, is never an excuse for poor manners." + }, + { + "id": "gangster", + "name": "Gangster", + "edition": "carousel", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Once per day, you may choose to kill an alive neighbor, if your other alive neighbor agrees.", + "flavor": "I like your shoes. It would be such a shame if you had a little accident, and they got ruined. Now that you mention it, I like your cufflinks too." + }, + { + "id": "bonecollector", + "name": "Bone Collector", + "edition": "snv", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "The Bone Collector either shakes their head no or points at any dead player. If they pointed at any dead player, put the Bone Collector's 'Has Ability' reminder by the chosen player's character token. (They may need to be woken tonight to use it.)", + "reminders": [ + "No Ability", + "Has Ability" + ], + "setup": false, + "ability": "Once per game, at night*, choose a dead player: they regain their ability until dusk.", + "flavor": "I collect many things. Hair. Teeth. Clothes. Fragments of poems. The dreams of lost lovers. My secret arts are not for you to know but my fee is a mere pittance. Bring me the blood of a noblewoman who died of heartbreak under a full moon, and you shall have your answers." + }, + { + "id": "judge", + "name": "Judge", + "edition": "bmr", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "ability": "Once per game, if another player nominated, you may choose to force the current execution to pass or fail.", + "flavor": "I find the defendant guilty of the crimes of murder, fraud, arson, larceny, impersonating an officer of the law, practicing medicine without a license, slander, regicide, and littering." + }, + { + "id": "apprentice", + "name": "Apprentice", + "edition": "bmr", + "team": "traveller", + "firstNightReminder": "Show the Apprentice the *YOU ARE* card, then a Townsfolk or Minion token. In the Grimoire, replace the Apprentice token with that character token, and put the Apprentice's *IS THE APPRENTICE* reminder by that character token.", + "otherNightReminder": "", + "reminders": [ + "Is The Apprentice" + ], + "setup": false, + "ability": "On your 1st night, you gain a Townsfolk ability (if good), or a Minion ability (if evil).", + "special": [ + { + "type": "signal", + "name": "grimoire", + "time": "night" + } + ], + "flavor": "For years have I traveled, studying the ways of The Craft. Which craft, you ask? Simply that of the simple folk. Nothing to worry about. Not yet." + }, + { + "id": "beggar", + "name": "Beggar", + "edition": "tb", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "You must use a vote token to vote. If a dead player gives you theirs, you learn their alignment. You are sober & healthy.", + "flavor": "Alms for the poor, good Sir? Spare a coin, Madam? Thank you. God bless! You're a right kind soul and no mistake! I'll have some swanky nosh tonight, I will!" + }, + { + "id": "deviant", + "name": "Deviant", + "edition": "snv", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If you were funny today, you cannot die by exile.", + "flavor": "Twas the lady's quip, forsooth." + }, + { + "id": "scapegoat", + "name": "Scapegoat", + "edition": "tb", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "If a player of your alignment is executed, you might be executed instead.", + "flavor": "Good evening! Thank you for inviting me to the ball. I'm not from around here, but you sure seem like a friendly bunch, by golly. I'm sure we'll get along just dandy. What's all that rope for?" + }, + { + "id": "gnome", + "name": "Gnome", + "edition": "carousel", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Amigo" + ], + "setup": false, + "ability": "All players start knowing a player of your alignment. You may choose to kill anyone who nominates them.", + "flavor": "Four the score or seven beers, no shows are goes for me and my. A prank to crack the cranks and planks o’ the floor foundation length, so incontravertabubbilly mini. The large essays down streams of joyse, no greater than is scene, not inherdt. Ha-urrumph." + }, + { + "id": "bishop", + "name": "Bishop", + "edition": "bmr", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Nominate Good", + "Nominate Evil" + ], + "setup": false, + "ability": "Only the Storyteller can nominate. At least 1 opposing player must be nominated each day.", + "flavor": "In nomine Patris, et Filii, et Spiritus Sancti… Nos mos Dei. Deus vult de nobis." + }, + { + "id": "voudon", + "name": "Voudon", + "edition": "bmr", + "team": "traveller", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "ability": "Only you & the dead can vote. They don't need a vote token to do so. A 50% majority isn't required.", + "flavor": "Bien venu. Sit down. Breathe deep. Enter the land of the dead. See with their eyes. Speak with their voice. Yon sel lang se janm ase." + }, + { + "id": "angel", + "firstNightReminder": "Announce which players are protected by the Angel. Add the *PROTECTED* token to the relevant players.", + "otherNightReminder": "", + "reminders": [ + "Protected", + "Protected", + "Something Bad" + ], + "setup": false, + "name": "Angel", + "team": "fabled", + "edition": "fabled", + "ability": "Something bad might happen to whoever is most responsible for the death of a new player.", + "flavor": "Let those who are without sin dare to raise their hand to my chosen, for I shall strike such fools down with the fury and righteousness of a thousand storms." + }, + { + "id": "buddhist", + "firstNightReminder": "Declare which players are affected by the Buddhist.", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Buddhist", + "team": "fabled", + "edition": "fabled", + "ability": "For the first 2 minutes of each day, veteran players may not talk.", + "flavor": "You throw thorns. Falling in my silence, they become flowers." + }, + { + "id": "deusexfiasco", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Whoopsie" + ], + "setup": true, + "name": "Deus ex Fiasco", + "team": "fabled", + "edition": "fabled", + "ability": "At least once per game, the Storyteller will make a mistake, correct it, and publicly admit to it.", + "flavor": "It's not a bug, it's a feature. It's not an error, it's a tweak. It's not broken, it's quirky." + }, + { + "id": "djinn", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Djinn", + "team": "fabled", + "edition": "fabled", + "ability": "Use the Djinn's special rule. All players know what it is.", + "flavor": "نحن لسنا هنا. انت لست حقيقي. كل شيء هو وهم. أسئلتك هي جبل نار في يوم صافٍ." + }, + { + "id": "doomsayer", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Doomsayer", + "team": "fabled", + "edition": "fabled", + "ability": "If 4 or more players live, each living player may publicly choose (once per game) that a player of their own alignment dies.", + "flavor": "And on the Seventh Day, there shall be a great flood and a pestilence upon the People of the Village of the Ravens! The dead shall rise and the living shall repent! O Woe! O Unholy day! Only by great sacrifice shall they prevail! So sayeth the Sages of Nostros and so sayeth I." + }, + { + "id": "duchess", + "firstNightReminder": "", + "otherNightReminder": "Wake each player marked *VISITOR* or *FALSE INFO* one at a time. Show them the Duchess token, then fingers (1, 2, 3) equaling the number of evil players marked “Visitor” or, if you are waking the player marked “False Info,” show them any number of fingers except the number of evil players marked “Visitor.”", + "reminders": [ + "Visitor", + "Visitor", + "False Info" + ], + "setup": false, + "name": "Duchess", + "team": "fabled", + "edition": "fabled", + "ability": "Each day, 3 players may choose to visit you. At night*, each visitor learns how many visitors are evil, but 1 gets false info.", + "flavor": "We shall entertain between the hours of 6 and 7 precisely. Tea at 6:15. Scones at 6:45. Do not be late. Formal wear applies, as always." + }, + { + "id": "ferryman", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Ferryman", + "team": "fabled", + "edition": "fabled", + "ability": "On the final day, all dead players regain their vote token.", + "special": [ + { + "name": "ghost-votes", + "type": "ability", + "time": "day" + } + ], + "flavor": "When righteous dreams come, they have the weight of truth." + }, + { + "id": "fibbin", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No Ability" + ], + "setup": false, + "name": "Fibbin", + "team": "fabled", + "edition": "fabled", + "ability": "Once per game, 1 good player might get incorrect information.", + "flavor": "Tee-hee-hee. Tee. Hee. Hee." + }, + { + "id": "fiddler", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Fiddler", + "team": "fabled", + "edition": "fabled", + "ability": "Once per game, the Demon secretly chooses an opposing player: all players choose which of these 2 players win.", + "special": [ + { + "name": "pointing", + "type": "ability", + "time": "day" + } + ], + "flavor": "I'll wager mi lyef ye cannae best me in a fiddle contest, ye boss-eyed snook! We'll go out on the lash, get the pub jammers an' have a right craic. I'll be layin' ma boots into ya come mornin' ye rumbly muppet." + }, + { + "id": "hellslibrarian", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Something Bad" + ], + "setup": false, + "name": "Hell's Librarian", + "team": "fabled", + "edition": "fabled", + "ability": "Something bad might happen to whoever talks when the Storyteller has asked for silence.", + "flavor": "Shhhhhh. Please be quiet. It is best not to disturb the Librarian. I've heard it has a temper." + }, + { + "id": "revolutionary", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "Register Falsely?", + "Aligned", + "Aligned" + ], + "setup": false, + "name": "Revolutionary", + "team": "fabled", + "edition": "fabled", + "ability": "2 neighboring players are known to be the same alignment. Once per game, 1 of them registers falsely.", + "flavor": "United we feigned. Divided, we stalled." + }, + { + "id": "sentinel", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": true, + "name": "Sentinel", + "team": "fabled", + "edition": "fabled", + "ability": "There might be 1 extra or 1 fewer Outsider in play.", + "flavor": "Name, please. Papers, please. Weapons, please." + }, + { + "id": "spiritofivory", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [ + "No More Evil" + ], + "setup": false, + "name": "Spirit of Ivory", + "team": "fabled", + "edition": "fabled", + "ability": "There can't be more than 1 extra evil player.", + "flavor": "The Wasteland calls. Bones rise to flesh, then fall to dust. The great spirit grows. The great spirit watches. The great spirit guides. The human listens, or the human is no more." + }, + { + "id": "toymaker", + "firstNightReminder": "Resolve the Minion Info and Demon Info steps even though there are fewer than 7 players.", + "otherNightReminder": "If it is a night when a Demon attack could end the game, and the Demon is marked *FINAL NIGHT: NO ATTACK*, then the Demon does not act tonight. (Do not wake them.)", + "reminders": [ + "Final Night: No Attack" + ], + "setup": false, + "name": "Toymaker", + "team": "fabled", + "edition": "fabled", + "ability": "The Demon may choose not to attack & must do this at least once per game. Evil players get normal starting info.", + "flavor": "It buzzes! It walks down stairs! It keeps you warm at night! It tastes like sugar! The kiddies love it! Introducing... the brand new... Warm'o-buzzy-wuzzy-walk'a'bot-thingy-contraption! Fun for all ages!" + }, + { + "id": "bootlegger", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Bootlegger", + "team": "loric", + "edition": "loric", + "ability": "This script has homebrew characters or rules.", + "flavor": "When I was born, I was named ‘Homebrewy McHomebrewface’, like my father before me. A respectable name, for a dignified age." + }, + { + "id": "bigwig", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Big Wig", + "team": "loric", + "edition": "loric", + "ability": "Each nominee chooses a player: until voting, only they may speak & they are mad the nominee is good or they might die.", + "flavor": "Vanity asks ‘Is it popular?’ Cowardice asks ‘Is it safe?’ Conscience asks ‘Is it right?’ Who among us will ask: ‘Is it true?’" + }, + { + "id": "gardener", + "firstNightReminder": "", + "otherNightReminder": "", + "reminders": [], + "setup": false, + "name": "Gardener", + "team": "loric", + "edition": "loric", + "ability": "The Storyteller assigns 1 or more players' characters.", + "special": [ + { + "name": "distribute-roles", + "type": "ability", + "time": "pregame" + } + ], + "flavor": "When sophistry becomes stupidity and hypocrisy cruelty, retreat to the garden. The final love of old men is flowers and stones." + }, + { + "id": "stormcatcher", + "firstNightReminder": "At the start of the night, announce which character is stormcaught. If that character is in play, mark that player as *STORMCAUGHT*. :reminder: Wake each evil player and show them the character token, then the marked player. If not in play, wake each evil player, show them the *THESE CHARACTERS ARE NOT IN PLAY* token and the relevant character token.", + "otherNightReminder": "", + "reminders": [ + "Stormcaught" + ], + "setup": false, + "name": "Storm Catcher", + "team": "loric", + "edition": "loric", + "ability": "Name a good character. If in play, they can only die by execution, but evil players learn which player it is.", + "flavor": "At dawn, the temple’s long shadow creeps to the fountain. At dusk, the obelisk blocks the red glare, cooling warm water under the archway. All lines converge here. A storm is coming, and this, this pebbled and lush and holy place between the apple trees, is the eye." + }, + { + "id": "tor", + "firstNightReminder": "", + "otherNightReminder": "If a player died tonight, show the *YOU ARE* info token, their character token, & a thumbs up or thumbs down.", + "reminders": [], + "setup": true, + "name": "Tor", + "team": "loric", + "edition": "loric", + "ability": "Players don't know their character or alignment. They learn them when they die.", + "flavor": "With thunder as my voice and lightning as my blade, I, the eternal guardian, feast upon the fools who dare approach the forbidden gate. Behold, my sacred goal! To purge the beetle from the belly of the rocky earth, to ensnare it in a net of stars, on the hilltop where heaven meets earth." + } +] \ No newline at end of file diff --git a/src/botc_tokens/helpers/role.py b/src/botc_tokens/helpers/role.py index 64650e6..b7732ad 100644 --- a/src/botc_tokens/helpers/role.py +++ b/src/botc_tokens/helpers/role.py @@ -7,6 +7,7 @@ class Role: """A role in a script. Attributes: + id: The id of the role. name: The name of the role. ability: The description of the role. type: The type of the role (Townsfolk, Outsider, Minion, Demon, or Traveller). @@ -16,6 +17,7 @@ class Role: icon: The filename of the icon. home_script: The name of the script in which the role is found. """ + id: str name: str ability: str = None type: str = None diff --git a/src/botc_tokens/helpers/text_tools.py b/src/botc_tokens/helpers/text_tools.py index 473a5d6..913d905 100644 --- a/src/botc_tokens/helpers/text_tools.py +++ b/src/botc_tokens/helpers/text_tools.py @@ -173,3 +173,12 @@ def format_filename(in_string): in_string = in_string.replace(' ', '_').replace('/', '-').replace('\\', '-').replace(':', '-').replace('?', 'Q') file_name = ''.join(char for char in in_string if char in valid_chars) return file_name + +def replace_font_in_text(in_string): + """Take a string and return a font family conform text, so that text can be rendered. + + Args: + in_string: The string to convert to a font family conform text. + + """ + return in_string.replace('ő', 'ô').replace('ű', 'û').replace('Ő', 'Ô').replace('Ű', 'Û') \ No newline at end of file diff --git a/src/botc_tokens/helpers/wiki_soup.py b/src/botc_tokens/helpers/wiki_soup.py index 68da5a0..a8ab448 100644 --- a/src/botc_tokens/helpers/wiki_soup.py +++ b/src/botc_tokens/helpers/wiki_soup.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup +from .text_tools import replace_font_in_text from .. import data_dir @@ -31,6 +32,25 @@ def load_from_web(self): night_from_web = urlopen("https://script.bloodontheclocktower.com/data/nightsheet.json").read().decode('utf-8') self.night_data = json.loads(night_from_web) + def load_from_bloodstar(self, bloodstar_url): + """Load the role data from the bloodstar site.""" + roles_from_bloodstar = json.loads(urlopen(bloodstar_url).read().decode('utf-8')) + meta_data = next((item for item in roles_from_bloodstar if item.get('id') == '_meta'), None) + version_name = None + if meta_data: + version_name = meta_data.get('name') + else: + print(f"[yellow]Warning:[/] Could not find metadata for the script") + # Filter and convert the roles + roles_with_version = [] + for role in roles_from_bloodstar: + if role.get('id') != '_meta': + role['version'] = version_name + role['name'] = replace_font_in_text(role['name']) + role['ability'] = replace_font_in_text(role['ability']) + roles_with_version.append(role) + self.role_data = roles_with_version + def _get_wiki_soup(self, role_name): """Take a role name and return a BeautifulSoup object for the role's wiki page.""" # Check if we have already seen this role @@ -63,7 +83,7 @@ def get_ability_text(self, role_name): ability = ability.replace("\"", "'") return ability - def get_reminders(self, role_name): + def get_reminders(self, role_name, skip_reminders=False): """Take a role name and grab the reminders.""" # First check if we have the role in the override reminders file if role_name in self.reminder_overrides: @@ -74,6 +94,8 @@ def get_reminders(self, role_name): return self.reminders[role_name] # If we don't have the role, go get it from the wiki + if skip_reminders: + return [] reminders = set() soup = self._get_wiki_soup(role_name) reminder_title = soup.find(id="How_to_Run") diff --git a/tests/commands/test_update.py b/tests/commands/test_update.py index 2b8c4d1..91a812b 100644 --- a/tests/commands/test_update.py +++ b/tests/commands/test_update.py @@ -55,6 +55,19 @@ def check_expected_json(input_file_path): j = json.load(f) assert j == expected_role_json.get(input_file_path.name) +def check_expected_folder(input_folder_path): + """Ensure each folder exists and, if it is the same amount of json file, and png file is in it.""" + assert input_folder_path.is_dir() + json_cnt = 0 + png_cnt = 0 + for item in input_folder_path.iterdir(): + if item.is_file(): + if item.suffix == '.json': + json_cnt += 1 + elif item.suffix == '.png': + png_cnt += 1 + assert json_cnt == png_cnt + def test_update_command(tmp_path): """Test the update command in its normal configuration.""" @@ -63,11 +76,58 @@ def test_update_command(tmp_path): # Verify that it worked expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.png"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), + ] + check_output_folder(output_path, expected_files=expected_files, check_func=check_expected_json) + +def test_update_command_built_in(tmp_path): + """Test the update command with built-in roles.""" + output_path = tmp_path / "roles" + _run_cmd(["--output", str(output_path), "--use-built-in"]) + + # Verify that it worked + expected_folders = [ + str(Path("bmr") / "demon"), + str(Path("bmr") / "minion"), + str(Path("bmr") / "outsider"), + str(Path("bmr") / "townsfolk"), + str(Path("bmr") / "traveller"), + str(Path("tb") / "demon"), + str(Path("tb") / "minion"), + str(Path("tb") / "outsider"), + str(Path("tb") / "townsfolk"), + str(Path("tb") / "traveller"), + str(Path("snv") / "demon"), + str(Path("snv") / "minion"), + str(Path("snv") / "outsider"), + str(Path("snv") / "townsfolk"), + str(Path("snv") / "traveller"), + str(Path("carousel") / "demon"), + str(Path("fabled") / "fabled"), + str(Path("loric") / "loric") + ] + for folder in expected_folders: + check_expected_folder(output_path / folder) + +def test_update_command_bloodstar_url(tmp_path): + """Test the update command with bloodstar url roles.""" + reminders_file = tmp_path / "reminders.json" + with open(reminders_file, "w") as f: + json.dump({"Second": ["SECOND REMINDER"]}, f) + output_path = tmp_path / "roles" + _run_cmd(["--output", str(output_path), "--bloodstar-url", "https://bloodstar.xyz/p/user/script/script.json?1", "--reminders", str(reminders_file)]) + + # Verify that it worked + expected_files = [ + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), ] check_output_folder(output_path, expected_files=expected_files, check_func=check_expected_json) @@ -76,20 +136,20 @@ def test_update_existing_folder(tmp_path): """Test when a file in the output folder already exists.""" output_path = tmp_path / "roles" output_path.mkdir() - first_file = output_path / "54 - Unreal Experimental" / "townsfolk" / "First.json" + first_file = output_path / "54 - Unreal Experimental" / "townsfolk" / "first.json" first_file.parent.mkdir(parents=True, exist_ok=True) with open(first_file, "w") as f: - json.dump(expected_role_json.get("First.json"), f) + json.dump(expected_role_json.get("first.json"), f) _run_cmd(["--output", str(output_path)]) # Verify that it worked expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.png"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), ] check_output_folder(output_path, expected_files=expected_files, check_func=check_expected_json) @@ -98,7 +158,7 @@ def test_update_bad_json(tmp_path, capsys): """Test when a file in the output folder exists, but isn't in the format we expect.""" output_path = tmp_path / "roles" output_path.mkdir() - first_file = output_path / "54 - Unreal Experimental" / "townsfolk" / "First.json" + first_file = output_path / "54 - Unreal Experimental" / "townsfolk" / "first.json" first_file.parent.mkdir(parents=True, exist_ok=True) with open(first_file, "w") as f: f.write("This is not json") @@ -106,11 +166,11 @@ def test_update_bad_json(tmp_path, capsys): # Verify that we got the files we expected expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), - str(Path("99 - Ignored") / "outsider" / "Third.png"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), + str(Path("99 - Ignored") / "outsider" / "third.png"), ] check_output_folder(output_path, expected_files=expected_files) @@ -130,8 +190,8 @@ def test_update_script_filter(tmp_path): # Verify that it worked expected_files = [ - str(Path("99 - Ignored") / "outsider" / "Third.json"), - str(Path("99 - Ignored") / "outsider" / "Third.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), + str(Path("99 - Ignored") / "outsider" / "third.png"), ] check_output_folder(output_path, expected_files=expected_files) @@ -152,18 +212,18 @@ def test_update_icon_already_exists(tmp_path): """Test when the icon already exists.""" output_path = tmp_path / "roles" output_path.mkdir() - icon_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "First.png" + icon_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "first.png" icon_path.parent.mkdir(parents=True, exist_ok=True) icon_path.touch() _run_cmd(["--output", str(output_path)]) # Verify that it worked expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.png"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), ] check_output_folder(output_path, expected_files=expected_files) @@ -178,17 +238,17 @@ def test_update_custom_reminders_file(tmp_path): # Verify that it worked expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.png"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), ] check_output_folder(output_path, expected_files=expected_files) - with open(output_path / "54 - Unreal Experimental" / "townsfolk" / "First.json", "r") as f: + with open(output_path / "54 - Unreal Experimental" / "townsfolk" / "first.json", "r") as f: j = json.load(f) assert j["reminders"] == ["Custom reminder"] - with open(output_path / "54 - Unreal Experimental" / "demon" / "Second.json", "r") as f: + with open(output_path / "54 - Unreal Experimental" / "demon" / "second.json", "r") as f: j = json.load(f) assert j["reminders"] == ["SECOND REMINDER"] @@ -197,23 +257,23 @@ def test_update_existing_icon_and_json(tmp_path): """Test when the icon and json file already exist.""" output_path = tmp_path / "roles" output_path.mkdir() - icon_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "First.png" + icon_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "first.png" icon_path.parent.mkdir(parents=True, exist_ok=True) icon_path.touch() - json_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "First.json" + json_path = output_path / "54 - Unreal Experimental" / "townsfolk" / "first.json" json_path.parent.mkdir(parents=True, exist_ok=True) with open(json_path, "w") as f: - json.dump(expected_role_json.get("First.json"), f) + json.dump(expected_role_json.get("first.json"), f) _run_cmd(["--output", str(output_path)]) # Verify that it worked expected_files = [ - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.json"), - str(Path("54 - Unreal Experimental") / "townsfolk" / "First.png"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.json"), - str(Path("54 - Unreal Experimental") / "demon" / "Second.png"), - str(Path("99 - Ignored") / "outsider" / "Third.json"), - str(Path("99 - Ignored") / "outsider" / "Third.png"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.json"), + str(Path("54 - Unreal Experimental") / "townsfolk" / "first.png"), + str(Path("54 - Unreal Experimental") / "demon" / "second.json"), + str(Path("54 - Unreal Experimental") / "demon" / "second.png"), + str(Path("99 - Ignored") / "outsider" / "third.json"), + str(Path("99 - Ignored") / "outsider" / "third.png"), ] check_output_folder(output_path, expected_files=expected_files) @@ -223,7 +283,7 @@ def test_web_error_getting_icon(tmp_path, capsys): output_path = tmp_path / "roles" wiki = MagicMock() wiki.get_big_icon_url.return_value = "First.png" - found_role = Role(name="First") + found_role = Role(id="first", name="First") with patch("botc_tokens.commands.update.urlopen") as urlopen_mock: image_read_mock = MagicMock() fp = StringIO() # This is necessary to avoid an issue when deconstructing urllib.error.HTTPError diff --git a/tests/helpers/test_role.py b/tests/helpers/test_role.py index 9689a45..a406369 100644 --- a/tests/helpers/test_role.py +++ b/tests/helpers/test_role.py @@ -4,5 +4,5 @@ def test_role_str(): """Make sure the __str__ method works.""" - role = Role(name="Villager", ability="You are a boring villager.") + role = Role(id="villager", name="Villager", ability="You are a boring villager.") assert str(role) == "Villager: You are a boring villager." diff --git a/tests/helpers/test_wiki_soup.py b/tests/helpers/test_wiki_soup.py index f834c5b..e26bf88 100644 --- a/tests/helpers/test_wiki_soup.py +++ b/tests/helpers/test_wiki_soup.py @@ -34,7 +34,7 @@ def test_wiki_soup_creation(): wiki_soup.load_from_web() assert wiki_soup assert wiki_soup.role_data[0]["name"] == "First" - assert wiki_soup.night_data["firstNight"] == ["DUSK", "First"] + assert wiki_soup.night_data["firstNight"] == ["DUSK", "first"] def test_wiki_soup_get_ability_text(): diff --git a/tests/testhelpers.py b/tests/testhelpers.py index 4a04f9f..c7735ca 100644 --- a/tests/testhelpers.py +++ b/tests/testhelpers.py @@ -50,11 +50,39 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): assert (expected_text in output.out) or (expected_text in output.err) +webmock_bloodstar = [ + # Bloodstar url sample response + b"""[ + { + "id": "_meta", + "name": "Test" + }, + { + "id": "first", + "image": "https://www.bloodstar.xyz/first.png", + "reminders": [ + "No ability" + ], + "name": "First", + "team": "townsfolk", + "ability": "First ability" + }, + { + "id": "second", + "image": "https://www.bloodstar.xyz/second.png", + "name": "Second", + "team": "demon", + "ability": "Second ability", + "otherNightReminder": "reminder text" + } +]""" +] + webmock_list = [ # The first call is for the role data b"""[ { - "id": "First", + "id": "first", "name": "First", "roleType": "townsfolk", "print": "unused", @@ -63,7 +91,7 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): "isDisabled": false }, { - "id": "Second", + "id": "second", "name": "Second", "roleType": "demon", "print": "unused", @@ -72,7 +100,7 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): "isDisabled": false }, { - "id": "Third", + "id": "third", "name": "Third", "roleType": "outsider", "print": "unused", @@ -85,12 +113,12 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): b"""{ "firstNight": [ "DUSK", - "First" + "first" ], "otherNight": [ "DUSK", - "First", - "Second" + "first", + "second" ] }""", # The third call is for the first role's wiki page @@ -123,29 +151,32 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): expected_role_json = { - "First.json": { + "first.json": { + 'id': 'first', 'ability': 'First ability description', 'affects_setup': False, 'first_night': True, 'home_script': '54 - Unreal Experimental', - 'icon': 'First.png', + 'icon': 'first.png', 'name': 'First', 'other_nights': True, 'reminders': [], 'type': 'townsfolk' }, - "Second.json": { + "second.json": { + 'id': 'second', 'ability': 'Second ability description [Affects Setup]', 'affects_setup': True, 'first_night': False, 'home_script': '54 - Unreal Experimental', - 'icon': 'Second.png', + 'icon': 'second.png', 'name': 'Second', 'other_nights': True, 'reminders': ["SECOND REMINDER"], 'type': 'demon' }, - "Third.json": { + "third.json": { + "id": "third", "name": "Third", "ability": "", "type": "outsider", From e9d4432d4cc43f9823c607545dd7fb5c81d061fc Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 14 Nov 2025 21:40:51 +0100 Subject: [PATCH 2/7] Resolved test case to enable wiki images for built-in --- src/botc_tokens/commands/update.py | 2 +- tests/commands/test_update.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 885caa7..1da6a37 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -128,7 +128,7 @@ def run(): found_role = \ process_role(role, role_file, wiki, step_progress, step_task, role_output_path, - len(args.bloodstar_url) > 0 or args.use_built_in, args.use_playtest or args.use_built_in) + len(args.bloodstar_url) > 0 or args.use_built_in, args.use_playtest) if found_role is not None: # Check if the role is in our forced_setup list diff --git a/tests/commands/test_update.py b/tests/commands/test_update.py index 91a812b..7a490d7 100644 --- a/tests/commands/test_update.py +++ b/tests/commands/test_update.py @@ -87,7 +87,7 @@ def test_update_command(tmp_path): def test_update_command_built_in(tmp_path): """Test the update command with built-in roles.""" output_path = tmp_path / "roles" - _run_cmd(["--output", str(output_path), "--use-built-in"]) + _run_cmd(["--output", str(output_path), "--use-built-in", "--use-playtest"]) # Verify that it worked expected_folders = [ From 7c56c4e7d44a5f2b4bc7cb03a5a06fe7c2f35854 Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 14 Nov 2025 21:59:27 +0100 Subject: [PATCH 3/7] Updated README --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5e3db3..af6e86b 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,21 @@ will only download assets that are not already present in the local folder. We h resulting entries, especially the reminder token section. While the utility does its best to guess, it isn't perfect. If you already know the reminder tokens, you can create a JSON file for the utility to use. See the [example repository](https://github.com/Tsubashi/botc_tokens-Examples) for an example of the format. + +**Please note:** recent changes in the script tool crashed wiki +download, so we recommend using the `--use-built-in` option in order generate base game roles as of date 2025-11. +```bash +botc_tokens update --use-built-in --output-dir /path/to/inputs --reminders /path/to/reminders.json +``` + +### Updating from bloodstar url +If you have an already existing bloodstar script and you want to create tokens for your in-person games, you can use +the `update` command with `--bloodstar-url` option to download your characters in the tool's format. +The tool tries to convert the tokens as per our best knowledge. Please keep in mind, that the results are best +when the bloodstar script is filled with setup, first/other nights reminder text and reminder tokens. +Visit [bloodstar](https://bloodstar.xyz) to start your editing there. ```bash -botc_tokens update --output-dir /path/to/inputs --reminders /path/to/reminders.json +botc_tokens update --output-dir /path/to/inputs --bloodstar-url https://www.bloodstar.xyz/p/TheBeardedDan/Krampus/script.json?9712244e ``` ### Creating tokens From b3fecdd33dd0e76f30df42921876ae7d0ca6e26f Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 21 Nov 2025 00:42:46 +0100 Subject: [PATCH 4/7] Implemented review comments --- README.md | 15 +- src/botc_tokens/commands/update.py | 59 +- src/botc_tokens/data/known_roles.json | 2550 ------------------------- src/botc_tokens/helpers/text_tools.py | 9 - src/botc_tokens/helpers/wiki_soup.py | 21 +- tests/commands/test_update.py | 80 +- tests/testhelpers.py | 60 +- 7 files changed, 92 insertions(+), 2702 deletions(-) delete mode 100755 src/botc_tokens/data/known_roles.json diff --git a/README.md b/README.md index af6e86b..e5e3db3 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,8 @@ will only download assets that are not already present in the local folder. We h resulting entries, especially the reminder token section. While the utility does its best to guess, it isn't perfect. If you already know the reminder tokens, you can create a JSON file for the utility to use. See the [example repository](https://github.com/Tsubashi/botc_tokens-Examples) for an example of the format. - -**Please note:** recent changes in the script tool crashed wiki -download, so we recommend using the `--use-built-in` option in order generate base game roles as of date 2025-11. -```bash -botc_tokens update --use-built-in --output-dir /path/to/inputs --reminders /path/to/reminders.json -``` - -### Updating from bloodstar url -If you have an already existing bloodstar script and you want to create tokens for your in-person games, you can use -the `update` command with `--bloodstar-url` option to download your characters in the tool's format. -The tool tries to convert the tokens as per our best knowledge. Please keep in mind, that the results are best -when the bloodstar script is filled with setup, first/other nights reminder text and reminder tokens. -Visit [bloodstar](https://bloodstar.xyz) to start your editing there. ```bash -botc_tokens update --output-dir /path/to/inputs --bloodstar-url https://www.bloodstar.xyz/p/TheBeardedDan/Krampus/script.json?9712244e +botc_tokens update --output-dir /path/to/inputs --reminders /path/to/reminders.json ``` ### Creating tokens diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 1da6a37..00c789e 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -33,16 +33,12 @@ def _parse_args(): help="Directory in which to write the json and icon files (Default: 'inputs')") parser.add_argument('--script-filter', type=str, default='', help="Filter for scripts to pull (Example: 'Experimental')") - parser.add_argument('-b', '--bloodstar-url', type=str, default='', - help="Link to bloodstar hosted script json") parser.add_argument('--reminders', type=str, help="JSON file to override reminder guesses from the wiki.") parser.add_argument('-c', '--custom-list', type=str, default=None, - help="JSON file with a custom list of roles to update.") + help="Local or web hosted JSON file with a custom list of roles to update.") parser.add_argument('--use-playtest', action='store_true', help="Use the playtest icon source instead of the wiki.") - parser.add_argument('--use-built-in', action='store_true', - help="Use the built-in role list as of date 2025-11-13.") args = parser.parse_args(sys.argv[2:]) return args @@ -86,7 +82,7 @@ def run(): role_task = overall_progress.add_task("Updating role data...", total=None) step_task = step_progress.add_task("Grabbing role data") - wiki = prep_wiki(args.script_filter, args.bloodstar_url, args.custom_list, args.use_built_in) + wiki = prep_wiki(args.script_filter, args.custom_list) if wiki is None: return 1 @@ -127,8 +123,7 @@ def run(): role_file = role_output_path / f"{format_filename(role['id'])}.json" found_role = \ - process_role(role, role_file, wiki, step_progress, step_task, role_output_path, - len(args.bloodstar_url) > 0 or args.use_built_in, args.use_playtest) + process_role(role, role_file, wiki, step_progress, step_task, role_output_path, args.use_playtest) if found_role is not None: # Check if the role is in our forced_setup list @@ -145,42 +140,39 @@ def run(): step_progress.stop_task(step_task) -def prep_wiki(script_filter, bloodstar_url, custom_list=None, use_built_in=False): +def prep_wiki(script_filter, custom_list=None): """Prepare the wiki object, loading the data from the web or a custom list. Args: script_filter (str): The filter to use when downloading the data. - bloodstar_url (str): The URL of the bloodstar site. custom_list (str): The path to a custom list of roles to use. - use_built_in (bool): Whether to load roles from built in file. """ # Gather the requested role data wiki = WikiSoup(script_filter) - if use_built_in: - wiki.role_data = json.load(open(data_dir / "known_roles.json", encoding="utf-8")) - elif bloodstar_url.startswith("https://www.bloodstar.xyz"): - wiki.load_from_bloodstar(bloodstar_url) - elif custom_list: - custom_list_path = Path(custom_list) - if not custom_list_path.exists(): - print(f"[red]Error:[/] Could not find '{custom_list}'") - return None - with open(custom_list_path, "r") as f: - custom_list = json.load(f) - try: - validate(custom_list, json.load(open(data_dir / "role_schema.json"))) - except ValidationError as e: - print(f"[yellow]Warning:[/] The custom json specified does not fit the copy of the TPI schema that I " - f"have. Specifically: {e}" - f"\n\nI will continue, but [yellow]be warned that it might not work[/].") - wiki.role_data = custom_list + if custom_list: + if custom_list.startswith("https://"): + wiki.load_from_web_json(custom_list) + else: + custom_list_path = Path(custom_list) + if not custom_list_path.exists(): + print(f"[red]Error:[/] Could not find '{custom_list}'") + return None + with open(custom_list_path, "r") as f: + custom_list = json.load(f) + try: + validate(custom_list, json.load(open(data_dir / "role_schema.json"))) + except ValidationError as e: + print(f"[yellow]Warning:[/] The custom json specified does not fit the copy of the TPI schema that I " + f"have. Specifically: {e}" + f"\n\nI will continue, but [yellow]be warned that it might not work[/].") + wiki.role_data = custom_list else: # Download the official lists from the script tool wiki.load_from_web() return wiki -def process_role(role, file, wiki, step_progress, step_task, role_output_path, skip_reminders=False, use_playtest=False): +def process_role(role, file, wiki, step_progress, step_task, role_output_path, use_playtest=False): """Process a role, grabbing the relevant data and returning a Role object. Args: @@ -217,7 +209,7 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, s if role.get("remindersGlobal"): found_role.reminders.extend(role.get("remindersGlobal")) if not found_role.reminders: - found_role.reminders = get_role_reminders(name, wiki, skip_reminders) + found_role.reminders = get_role_reminders(name, wiki) # Determine night actions if role.get("firstNight") or has_value(role.get("firstNightReminder")): @@ -336,16 +328,15 @@ def get_role_ability(name, wiki): return "" -def get_role_reminders(name, wiki, skip_reminders=False): +def get_role_reminders(name, wiki): """Get the reminders for a role, using the wiki if needed. Args: name (str): The name of the role to search. wiki (WikiSoup): The wiki soup object. - skip_reminders (bool): Whether to skip the reminder search in case of built-in and bloodstar homebrew characters. """ try: - return wiki.get_reminders(name, skip_reminders) + return wiki.get_reminders(name) except RuntimeError: print(f"[red]Error:[/] No reminder info found for {name}") return [] diff --git a/src/botc_tokens/data/known_roles.json b/src/botc_tokens/data/known_roles.json deleted file mode 100755 index 3c87bc3..0000000 --- a/src/botc_tokens/data/known_roles.json +++ /dev/null @@ -1,2550 +0,0 @@ -[ - { - "id": "steward", - "name": "Steward", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "Point to the good player marked *KNOW*.", - "reminders": [ - "Know" - ], - "setup": false, - "ability": "You start knowing 1 good player.", - "flavor": "How DARE you accuse Her Ladyship of wrongdoing? I’ve known her my entire life! All nine years!" - }, - { - "id": "knight", - "name": "Knight", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "Point to the two non-Demon players marked *KNOW*.", - "reminders": [ - "Know", - "Know" - ], - "setup": false, - "ability": "You start knowing 2 players that are not the Demon.", - "flavor": "When a man lies, he murders some part of the world." - }, - { - "id": "chef", - "name": "Chef", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "Give a finger signal.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You start knowing how many pairs of evil players there are.", - "flavor": "This evening's reservations seem odd. Never before has Mrs. Mayweather kept company with that scamp from Hudson Lane. Yet, tonight, they have a table for two. Strange." - }, - { - "id": "noble", - "name": "Noble", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Point to all three players marked *KNOW*.", - "otherNightReminder": "", - "reminders": [ - "Know", - "Know", - "Know" - ], - "setup": false, - "ability": "You start knowing 3 players, 1 and only 1 of which is evil.", - "flavor": "Sarcasm is indeed the lowest form of wit. But speaking in response to your criticism, Sir, it is, nevertheless, a form of wit." - }, - { - "id": "investigator", - "name": "Investigator", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "Show the Minion character token. Point to both the *MINION* and *WRONG* players.", - "otherNightReminder": "", - "reminders": [ - "Minion", - "Wrong" - ], - "setup": false, - "ability": "You start knowing that 1 of 2 players is a particular Minion.", - "flavor": "It is a fine night for a stroll, wouldn't you say, Mister Morozov? Or should I say... BARON Morozov?" - }, - { - "id": "washerwoman", - "name": "Washerwoman", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "Show the Townsfolk character token. Point to both the *TOWNSFOLK* and *WRONG* players.", - "otherNightReminder": "", - "reminders": [ - "Townsfolk", - "Wrong" - ], - "setup": false, - "ability": "You start knowing that 1 of 2 players is a particular Townsfolk.", - "flavor": "Bloodstains on a dinner jacket? No, this is cooking sherry. How careless." - }, - { - "id": "clockmaker", - "name": "Clockmaker", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "Give a finger signal.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You start knowing how many steps from the Demon to its nearest Minion.", - "flavor": "Do not disturb me. The tick must continue, for the circle is a symbol of life and contains all things - all answers - in its divine machinery. I must work." - }, - { - "id": "grandmother", - "name": "Grandmother", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "Point to the grandchild player & show their character token.", - "otherNightReminder": "If the grandchild was killed by the Demon, the Grandmother dies too. :reminder:", - "reminders": [ - "Grandchild", - "Dead" - ], - "setup": false, - "ability": "You start knowing a good player & their character. If the Demon kills them, you die too.", - "flavor": "Take a jacket if you go outside, dearie. And your thermos. And your scarf. I have a weak heart, you know. Whatever would I do if you caught cold...or worse?" - }, - { - "id": "librarian", - "name": "Librarian", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "Show the Outsider character token. Point to both the *OUTSIDER* and *WRONG* players.", - "otherNightReminder": "", - "reminders": [ - "Outsider", - "Wrong" - ], - "setup": false, - "ability": "You start knowing that 1 of 2 players is a particular Outsider. (Or that zero are in play.)", - "flavor": "Certainly madam, under normal circumstances, you may borrow the Codex Malificarium from the library vaults. However, you do not seem to be a member." - }, - { - "id": "shugenja", - "name": "Shugenja", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "Point clockwise or anticlockwise around the circle.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You start knowing if your closest evil player is clockwise or anti-clockwise. If equidistant, this info is arbitrary.", - "flavor": "これは夢。それも夢。すべて夢です。" - }, - { - "id": "pixie", - "name": "Pixie", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Show the Townsfolk character token marked *MAD*.", - "otherNightReminder": "", - "reminders": [ - "Mad", - "Has Ability" - ], - "setup": false, - "ability": "You start knowing 1 in-play Townsfolk. If you were mad that you were this character, you gain their ability when they die.", - "flavor": "Round and round the garden, go. Little girls run to and fro. Little boys climb up the tree. Which of these should Pixie be? Ladies smile and go to town. Lords with axe chop forest down. What’s yours is mine. What’s mine, divine. Silly little Pixie, me." - }, - { - "id": "bountyhunter", - "name": "Bounty Hunter", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Point to the *KNOWN* player.", - "otherNightReminder": "If the *KNOWN* player died today or tonight, point to a new *KNOWN* player.", - "reminders": [ - "Know" - ], - "setup": true, - "ability": "You start knowing 1 evil player. If the player you know dies, you learn another evil player tonight. [1 Townsfolk is evil]", - "flavor": "Alone, I walk these streets, paved with the sick stench of corruption. Its thickness worms its way into my nostrils, unbidden, burning with revulsion. And anticipation. The illness of this wretched place grows each night. And I... I am the cure." - }, - { - "id": "empath", - "name": "Empath", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "Give a finger signal.", - "otherNightReminder": "Give a finger signal.", - "reminders": [], - "setup": false, - "ability": "Each night, you learn how many of your 2 alive neighbors are evil.", - "flavor": "My skin prickles. Something is not right here. I can feel it." - }, - { - "id": "highpriestess", - "name": "High Priestess", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "Point to a player.", - "otherNightReminder": "Point to a player.", - "reminders": [], - "setup": false, - "ability": "Each night, learn which player the Storyteller believes you should talk to most.", - "flavor": "There is life behind the personality that uses personalities as masks. There are times when life puts off the mask and deep answers to deep." - }, - { - "id": "sailor", - "name": "Sailor", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "The Sailor chooses a living player. :reminder:", - "otherNightReminder": "The Sailor chooses a living player. :reminder:", - "reminders": [ - "Drunk" - ], - "setup": false, - "ability": "Each night, choose an alive player: either you or they are drunk until dusk. You can't die.", - "flavor": "I'll drink any one of yer under the table! You! The chatterbox! Reckon you can take me? No? Howza 'bout you, Grandma? You ever tried Old McKillys Extra Spiced Rum before? Guaranteed to put hairs on yer chest! Step aboard, aye!" - }, - { - "id": "balloonist", - "name": "Balloonist", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Show any player. :reminder:", - "otherNightReminder": "Show a player with a different character type to previously. :reminder:", - "reminders": [ - "Know" - ], - "setup": true, - "ability": "Each night, you learn a player of a different character type than last night. [+0 or +1 Outsider]", - "flavor": "More heat! Higher! Higher! Più alto! Ahhh... it is so beautiful from up here, don't you agree? Can you see the children fishing by the river, under the willow? Can you see the glint of the sun on the circus tent-poles? What's this? An old man, alone, passed out in the vineyard? Less heat! Lower! Lower! Vai più in basso!" - }, - { - "id": "general", - "name": "General", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Give a thumb signal.", - "otherNightReminder": "Give a thumb signal.", - "reminders": [], - "setup": false, - "ability": "Each night, you learn which alignment the Storyteller believes is winning: good, evil, or neither.", - "flavor": "I don’t have time for quotes." - }, - { - "id": "preacher", - "name": "Preacher", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "The Preacher chooses a player. :reminder: If they chose a Minion: Put the Preacher to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token and the Preacher token.", - "otherNightReminder": "The Preacher chooses a player. :reminder: If they chose a Minion: Put the Preacher to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token and the Preacher token.", - "reminders": [ - "No Ability", - "No Ability", - "No Ability" - ], - "setup": false, - "ability": "Each night, choose a player: a Minion, if chosen, learns this. All chosen Minions have no ability.", - "flavor": "It is better to be rich and healthy than poor and sick." - }, - { - "id": "chambermaid", - "name": "Chambermaid", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "The Chambermaid chooses 2 living players. Give a finger signal.", - "otherNightReminder": "The Chambermaid chooses 2 living players. Give a finger signal.", - "reminders": [], - "setup": false, - "ability": "Each night, choose 2 alive players (not yourself): you learn how many woke tonight due to their ability.", - "flavor": "I aint seen nothin' untoward, Milady. Begging your pardon, but if I did see somethin', it certainly weren't the master o' the house sneaking into the professor's laboratory 'round eleven o'clock and mixing up fancy potions, just like you said, Miss." - }, - { - "id": "villageidiot", - "name": "Village Idiot", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "Choose a Village Idiot to be drunk. Wake the Village Idiots one at a time, they choose a player, show either good or evil thumbs according to the alignment of that player.", - "otherNightReminder": "Wake the Village Idiots one at a time, they choose a player, show either good or evil thumbs according to the alignment of that player.", - "reminders": [ - "Drunk" - ], - "setup": true, - "ability": "Each night, choose a player: you learn their alignment. [+0 to +2 Village Idiots. 1 of the extras is drunk]", - "special": [ - { - "type": "selection", - "name": "bag-duplicate" - } - ], - "flavor": "Roses are blue, and violets are red, Please reverse what I just said." - }, - { - "id": "snakecharmer", - "name": "Snake Charmer", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "The Snake Charmer chooses a player. If they chose the Demon: Show the *YOU ARE* & Demon tokens. Give a thumbs down. Swap the Snake Charmer & Demon tokens. Put the old Snake Charmer to sleep. Wake the old Demon. Show the *YOU ARE* and Snake Charmer tokens & give a thumbs up. :reminder:", - "otherNightReminder": "The Snake Charmer chooses a player. If they chose the Demon: Show the *YOU ARE* & Demon tokens. Give a thumbs down. Swap the Snake Charmer & Demon tokens. Put the old Snake Charmer to sleep. Wake the old Demon. Show the *YOU ARE* and Snake Charmer tokens & give a thumbs up. :reminder:", - "reminders": [ - "Poisoned" - ], - "setup": false, - "ability": "Each night, choose an alive player: a chosen Demon swaps characters & alignments with you & is then poisoned.", - "flavor": "Effendi... I am but a humble man, but my pipe is golden and a single tune will tame the wildest djinn, Inshallah. They say that greed hangs more men than rope. But not I, Effendi... not I." - }, - { - "id": "mathematician", - "name": "Mathematician", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "Give a finger signal.", - "otherNightReminder": "Give a finger signal.", - "reminders": [ - "Abnormal", - "Abnormal", - "Abnormal", - "Abnormal", - "Abnormal" - ], - "setup": false, - "ability": "Each night, you learn how many players’ abilities worked abnormally (since dawn) due to another character's ability.", - "flavor": "Any consistent formal system x, within which a certain amount of elementary arithmetic can be carried out is incomplete; that is, there are statements of the language of x which can neither be proved nor disproved in x. Ergo, you are drunk." - }, - { - "id": "king", - "name": "King", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Wake the Demon. Show the *THIS PLAYER IS* token and the King token, then point to the King.", - "otherNightReminder": "If the dead equal or outnumber the living, show the character token of an alive player.", - "reminders": [], - "setup": false, - "ability": "Each night, if the dead equal or outnumber the living, you learn 1 alive character. The Demon knows you are the King.", - "flavor": "Betwixt the unknown strains of mortal strife / And morbid night, sweet with mystery and woe / Lies unfettered joys of fate’s long and colored life / Who’s garden blooms with each painted Face to Show." - }, - { - "id": "dreamer", - "name": "Dreamer", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "The Dreamer points to a player. Show 1 good & 1 evil character token, 1 of which is their character.", - "otherNightReminder": "The Dreamer points to a player. Show 1 good & 1 evil character token, 1 of which is their character.", - "reminders": [], - "setup": false, - "ability": "Each night, choose a player (not yourself or Travellers): you learn 1 good & 1 evil character, 1 of which is correct.", - "flavor": "I remember the Clockmaker. The sky was red and it was raining fractal triangles. There was a smell of violets and a bubbling sound. A woman with glowing eyes and a scraggly beard was hissing at the sky. Then, I awoke." - }, - { - "id": "fortuneteller", - "name": "Fortune Teller", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "The Fortune Teller chooses 2 players. Nod if either is the Demon (or the *RED HERRING*).", - "otherNightReminder": "The Fortune Teller chooses 2 players. Nod if either is the Demon (or the *RED HERRING*).", - "reminders": [ - "Red Herring" - ], - "setup": false, - "ability": "Each night, choose 2 players: you learn if either is a Demon. There is a good player that registers as a Demon to you.", - "flavor": "I sense great evil in your soul! But... that could just be your perfume. I am allergic to Elderberry." - }, - { - "id": "cultleader", - "name": "Cult Leader", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "The Cult Leader might change alignment. If so, show the *YOU ARE* info token and a thumbs up or down for their new alignment.", - "otherNightReminder": "The Cult Leader might change alignment. If so, show the *YOU ARE* info token and a thumbs up or down for their new alignment.", - "reminders": [], - "setup": false, - "ability": "Each night, you become the alignment of an alive neighbor. If all good players choose to join your cult, your team wins.", - "flavor": "Thinking themselves wise, they became fools." - }, - { - "id": "flowergirl", - "name": "Flowergirl", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "Either nod or shake your head.", - "reminders": [ - "Demon Voted", - "Demon Not Voted" - ], - "setup": false, - "ability": "Each night*, you learn if a Demon voted today.", - "flavor": "Yesterday's violets have withered and died, but today my poppies bloom." - }, - { - "id": "towncrier", - "name": "Town Crier", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "Either nod or shake your head.", - "reminders": [ - "Minions Not Nominated", - "Minion Nominated" - ], - "setup": false, - "ability": "Each night*, you learn if a Minion nominated today.", - "flavor": "Hear ye! Hear ye! Witchcraft in the labyrinth! Genius savant reveals all! Town in danger! Hear Ye!" - }, - { - "id": "oracle", - "name": "Oracle", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "Give a finger signal.", - "reminders": [], - "setup": false, - "ability": "Each night*, you learn how many dead players are evil.", - "flavor": "Only the chosen may gaze beyond the veil. The dead are restless, and they point in silence toward the icy north." - }, - { - "id": "undertaker", - "name": "Undertaker", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If a player was executed today, show their character token.", - "reminders": [ - "Died Today" - ], - "setup": false, - "ability": "Each night*, you learn which character died by execution today.", - "flavor": "Hmmm....what have we here? The left boot is worn down to the heel, with flint shavings under the tongue. This is the garb of a Military man." - }, - { - "id": "innkeeper", - "name": "Innkeeper", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Innkeeper chooses 2 players. :reminder: :reminder: :reminder:", - "reminders": [ - "Safe", - "Safe", - "Drunk" - ], - "setup": false, - "ability": "Each night*, choose 2 players: they can't die tonight, but 1 is drunk until dusk.", - "flavor": "Come inside, fair traveller, and rest your weary bones. Drink and be merry, for the legions of the Dark One shall not harass thee tonight." - }, - { - "id": "monk", - "name": "Monk", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Monk chooses a player. :reminder:", - "reminders": [ - "Safe" - ], - "setup": false, - "ability": "Each night*, choose a player (not yourself): they are safe from the Demon tonight.", - "flavor": "'Tis an ill and deathly wind that blows tonight. Come, my brother, take shelter in the abbey while the storm rages. By my word, or by my life, you will be safe." - }, - { - "id": "gambler", - "name": "Gambler", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Gambler chooses a player & a character. :reminder:", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "Each night*, choose a player & guess their character: if you guess wrong, you die.", - "flavor": "Heads, I win. Tails, you lose." - }, - { - "id": "acrobat", - "name": "Acrobat", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Acrobat chooses a player. :reminder:", - "reminders": [ - "Dead", - "Chosen" - ], - "setup": false, - "ability": "Each night*, choose a player: if they are or become drunk or poisoned tonight, you die.", - "flavor": "Welcome, one and all, to the greatest show on earth." - }, - { - "id": "exorcist", - "name": "Exorcist", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Exorcist chooses a player. :reminder: Put the Exorcist to sleep. If the Exorcist chose the Demon: Wake the Demon. Show the *THIS CHARACTER SELECTED YOU* & Exorcist tokens. Point to the Exorcist.", - "reminders": [ - "Chosen" - ], - "setup": false, - "ability": "Each night*, choose a player (different to last night): the Demon, if chosen, learns who you are then doesn't wake tonight.", - "flavor": "We cast you out, every unclean spirit, every satanic power, every onslaught of the infernal adversary, every legion, every diabolical group and sect, in the name and by the power of Our Lord Jesus Christ. We command you, begone and fly far from the Church of God, from the souls made by God in His image and redeemed by the precious blood of the divine Lamb." - }, - { - "id": "lycanthrope", - "name": "Lycanthrope", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Lycanthrope chooses a player. :reminder:", - "reminders": [ - "Faux Paw", - "Dead" - ], - "setup": false, - "ability": "Each night*, choose an alive player. If good, they die & the Demon doesn’t kill tonight. One good player registers as evil.", - "flavor": "Beneath the thin veneer of civilisation lies a howling madness." - }, - { - "id": "gossip", - "name": "Gossip", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If the Gossip is due to kill a player, they die. :reminder:", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "Each day, you may make a public statement. Tonight, if it was true, a player dies.", - "flavor": "Blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah. Blah." - }, - { - "id": "savant", - "name": "Savant", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each day, you may visit the Storyteller to learn 2 things in private: 1 is true & 1 is false.", - "flavor": "Seventy-two matchsticks on the floor... the sun sets early but the moon is unchanged... a torn piece of cloth... evil in the manor house... three by three... the one we trusted is not what he seems... green light means magnesium... residue, but the pattern is wrong... Seventy-two matchsticks on the floor..." - }, - { - "id": "alsaahir", - "name": "Alsaahir", - "team": "townsfolk", - "edition": "carousel", - "reminders": [], - "setup": false, - "ability": "Each day, if you publicly guess which players are Minion(s) and which are Demon(s), good wins.", - "flavor": "I am here because of you, and you are here because of me." - }, - { - "id": "engineer", - "name": "Engineer", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "The Engineer might choose Minions or Demons. :reminder: If they do: Put the Engineer to sleep. Wake a target, show them the *YOU ARE* token and their new character token, then put that target to sleep. Repeat for all players that changed characters.", - "otherNightReminder": "The Engineer might choose Minions or Demons. :reminder: If they do: Put the Engineer to sleep. Wake a target, show them the *YOU ARE* token and their new character token, then put that target to sleep. Repeat for all players that changed characters.", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night, choose which Minions or which Demon is in play.", - "flavor": "If it bends, great. If it breaks, well, it probably needed fixing anyway." - }, - { - "id": "nightwatchman", - "name": "Nightwatchman", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "The Nightwatchman might choose a player :reminder: Put the Nightwatchman to sleep. Wake the target and show the *THIS PLAYER IS* and Nightwatchman tokens and point to the Nightwatchman.", - "otherNightReminder": "The Nightwatchman might choose a player :reminder: Put the Nightwatchman to sleep. Wake the target and show the *THIS PLAYER IS* and Nightwatchman tokens and point to the Nightwatchman.", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night, choose a player: they learn you are the Nightwatchman.", - "flavor": "The night is cold and lonely, but I have the moon, the stars, the crisp wind and the soft thud of leather boots on cobbled stone for company. Yonder, candlelight flickers behind a murky window..." - }, - { - "id": "courtier", - "name": "Courtier", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "The Courtier might choose a character. :reminder: :reminder:", - "otherNightReminder": "The Courtier might choose a character. :reminder: :reminder:", - "reminders": [ - "Drunk 3", - "Drunk 2", - "Drunk 1", - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night, choose a character: they are drunk for 3 nights & 3 days.", - "flavor": "I am more afraid of an army of one hundred sheep led by a lion than an army of one hundred lions led by a sheep." - }, - { - "id": "seamstress", - "name": "Seamstress", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "The Seamstress might choose 2 players. Nod or shake your head. :reminder:", - "otherNightReminder": "The Seamstress might choose 2 players. Nod or shake your head. :reminder:", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night, choose 2 players (not yourself): you learn if they are the same alignment.", - "flavor": "Did you hear that stranger in the cashmere coat put the word on our young Belle? And she said yes? Well, that's nothing compared to what Harry and that juggler got up to at the fair! The things I could say if I was a tattletale... my, yes." - }, - { - "id": "philosopher", - "name": "Philosopher", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "The Philosopher might choose a character. If necessary, swap their character token. :reminder:", - "otherNightReminder": "The Philosopher might choose a character. If necessary, swap their character token. :reminder:", - "reminders": [ - "Drunk" - ], - "remindersGlobal": [ - "Is The Philosopher" - ], - "setup": false, - "ability": "Once per game, at night, choose a good character: gain that ability. If this character is in play, they are drunk.", - "special": [ - { - "type": "reveal", - "name": "replace-character" - } - ], - "flavor": "If anything is real, beer is real. Drink, for tomorrow we may die." - }, - { - "id": "huntsman", - "name": "Huntsman", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "The Huntsman might choose a player. :reminder: If that player was the Damsel: Put the Huntsman to sleep. Wake the Damsel and show them the *YOU ARE* info token and their new character token.", - "otherNightReminder": "The Huntsman might choose a player. :reminder: If that player was the Damsel: Put the Huntsman to sleep. Wake the Damsel and show them the *YOU ARE* info token and their new character token.", - "reminders": [ - "No Ability" - ], - "setup": true, - "ability": "Once per game, at night, choose a living player: the Damsel, if chosen, becomes a not-in-play Townsfolk. [+the Damsel]", - "flavor": "My cabin is warm and sturdy. My axe by the door, my boots drying by the fire, and elk stew a-simmering… Hark! A scream echoes through the valley! The rain and the mud and the cold, cold wind mask the scent of the wolves, but I know the path and my pace is steady. I am coming." - }, - { - "id": "professor", - "name": "Professor", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "The Professor might choose a dead player. :reminder: :reminder:", - "reminders": [ - "Alive", - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night*, choose a dead player: if they are a Townsfolk, they are resurrected.", - "flavor": "The process is simple. Attach the hydraulic confabulator to the modified chi matrix amplifier, add 20 CCs of pseudodorafine, keep his Z levels above 20%, and your husband will be fine. Now, all we need is a lightning strike." - }, - { - "id": "artist", - "name": "Artist", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, during the day, privately ask the Storyteller any yes/no question.", - "flavor": "Mon Dieu! C'est lumineux! My work, she is... how you say... Magnifique! Dieu est révélé! Oui." - }, - { - "id": "slayer", - "name": "Slayer", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, during the day, publicly choose a player: if they are the Demon, they die.", - "flavor": "Die." - }, - { - "id": "fisherman", - "name": "Fisherman", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, during the day, visit the Storyteller for some advice to help your team win.", - "flavor": "This was my favourite part of the river... see how the sunlight makes a rainbow from the monastery to the market? This was the best place for big fish. And the older I get, the bigger they were." - }, - { - "id": "princess", - "name": "Princess", - "team": "townsfolk", - "edition": "carousel", - "otherNightReminder": "If the Princess nominated the player who was executed today, the Demon wakes as normal, but no one dies to the Demon's ability.", - "reminders": [ - "Doesn't Kill" - ], - "setup": false, - "ability": "On your 1st day, if you nominated & executed a player, the Demon doesn't kill tonight.", - "flavor": "Our words are hounds, bound by silken threads, dear lords. Let kindness weave them true, lest the reigns unravel and rend our court." - }, - { - "id": "juggler", - "name": "Juggler", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "Give a finger signal.", - "reminders": [ - "Correct", - "Correct", - "Correct", - "Correct", - "Correct" - ], - "setup": false, - "ability": "On your 1st day, publicly guess up to 5 players' characters. That night, you learn how many you got correct.", - "flavor": "For my next trick, as per request, I will need a flower, a bag of beans, a toy snake, a paintbrush, and a motorized gasoline-powered hedge trimming device. I warn you, this trick may be my last. Oh dear." - }, - { - "id": "soldier", - "name": "Soldier", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You are safe from the Demon.", - "flavor": "As David said to Goliath, as Theseus said to the Minotaur, as Arjuna said to Bhagadatta... No." - }, - { - "id": "alchemist", - "name": "Alchemist", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Show the *YOU ARE* token and the character token of a Minion.", - "otherNightReminder": "", - "reminders": [], - "remindersGlobal": [ - "Is The Alchemist" - ], - "setup": false, - "ability": "You have a Minion ability. When using this, the Storyteller may prompt you to choose differently.", - "special": [ - { - "type": "reveal", - "name": "replace-character" - } - ], - "flavor": "Visit the interior of the Earth. By rectification thou shalt find the hidden stone. Above the gold, lieth the red. Kether in Malkuth." - }, - { - "id": "cannibal", - "name": "Cannibal", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Poisoned", - "Lunch" - ], - "setup": false, - "ability": "You have the ability of the recently killed executee. If they are evil, you are poisoned until a good player dies by execution.", - "flavor": "I don’t like clowns. They taste funny." - }, - { - "id": "amnesiac", - "name": "Amnesiac", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Run the Amnesiac's ability, if applicable.", - "otherNightReminder": "Run the Amnesiac's ability, if applicable.", - "reminders": [ - "?", - "?", - "?" - ], - "setup": false, - "ability": "You do not know what your ability is. Each day, privately guess what it is: you learn how accurate you are.", - "flavor": "Wait. What. Who? Oh, ok. Wait. What?" - }, - { - "id": "farmer", - "name": "Farmer", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If the Farmer died tonight, wake an alive good player. Show them the *YOU ARE* info token and a Farmer character token. Replace their previous token with the Farmer token.", - "reminders": [], - "setup": false, - "ability": "When you die at night, an alive good player becomes a Farmer.", - "flavor": "Even the high and mighty need food on the table. Without us, the city starves." - }, - { - "id": "minstrel", - "name": "Minstrel", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Everyone Is Drunk" - ], - "setup": false, - "ability": "When a Minion dies by execution, all other players (except Travellers) are drunk until dusk tomorrow.", - "flavor": "And I shall hear, tho' soft you tread above me... And all my dreams will warm and sweeter be... If you'll not fail to tell me that you love me... I simply sleep in peace until you come to me." - }, - { - "id": "ravenkeeper", - "name": "Ravenkeeper", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If the Ravenkeeper died tonight, the Ravenkeeper chooses a player. Show that player's character token.", - "reminders": [], - "setup": false, - "ability": "If you die at night, you are woken to choose a player: you learn their character.", - "flavor": "My birds will avenge me! Fly! Fly, my sweet and dutiful pets! To the manor and to the river! To the alleys and to the salons! Fly!" - }, - { - "id": "sage", - "name": "Sage", - "edition": "snv", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If the Demon killed the Sage, wake the Sage and point to 2 players, 1 of which is the Demon.", - "reminders": [], - "setup": false, - "ability": "If the Demon kills you, you learn that it is 1 of 2 players.", - "flavor": "These mountainous tomes guard the secret, I am sure of it! Twixt word and word, it lies in wait. More candles, boy! More ink! These notes may look arcane, but the infernal puzzle is revealing itself." - }, - { - "id": "choirboy", - "name": "Choirboy", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "If the Demon killed the King, point to the Demon player.", - "reminders": [], - "setup": true, - "ability": "If the Demon kills the King, you learn which player is the Demon. [+the King]", - "flavor": "I saw it, I did. I was in the pews, tidying the hymn books, when a dreadful tune started from the pipe organ. The organist had a long cloak, and long fingers on the keys. And a hat that looked… just like… yours." - }, - { - "id": "banshee", - "name": "Banshee", - "team": "townsfolk", - "edition": "carousel", - "firstNightReminder": "", - "otherNightReminder": "If the Banshee was killed by the Demon tonight, announce to all players that the Banshee has died. :reminder:", - "reminders": [ - "Has Ability" - ], - "setup": false, - "ability": "If the Demon kills you, all players learn this. From now on, you may nominate twice per day and vote twice per nomination.", - "flavor": "Gorm do shúile, dearg do ghruaig, ní bheidh sé i bhfad, is a mbeidh tú san uaigh." - }, - { - "id": "tealady", - "name": "Tea Lady", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Cannot Die", - "Cannot Die" - ], - "setup": false, - "ability": "If both your alive neighbors are good, they can't die.", - "flavor": "If you are cold, tea will warm you. If you are too heated, tea will cool you. If you are depressed, tea will cheer you. If you are excited, tea will calm you." - }, - { - "id": "mayor", - "name": "Mayor", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If only 3 players live & no execution occurs, your team wins. If you die at night, another player might die instead.", - "flavor": "We must put our differences aside, and cease this senseless killing. We are all taxpayers after all. Well, most of us." - }, - { - "id": "fool", - "name": "Fool", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "The 1st time you die, you don't.", - "flavor": "...and the King said 'What?! I've never even owned a pair of rubber pantaloons, let alone a custard cannon!' Ho-ho! Jolly day!" - }, - { - "id": "virgin", - "name": "Virgin", - "edition": "tb", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "The 1st time you are nominated, if the nominator is a Townsfolk, they are executed immediately.", - "flavor": "I am pure. Let those who are without sin cast themselves down and suffer in my stead. My reputation shall not be stained with your venomous accusations." - }, - { - "id": "magician", - "name": "Magician", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Include the Magician in the Minion and Demon Info steps.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "The Demon thinks you are a Minion. Minions think you are a Demon.", - "flavor": "1... 2... Abra... 3... 4... Cadabra... *poof!* And, as you can see, ladies and gentlemen, Captain Farnsworth’s bag of gold has disappeared! Gone! Without a trace! Thank you, and goodnight!" - }, - { - "id": "poppygrower", - "name": "Poppy Grower", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "Do not do the Minion Info and Demon Info steps. Wake the Demon, show the *THESE CHARACTERS ARE NOT IN PLAY* info token and any three good character tokens that are not in play.", - "otherNightReminder": "If the Poppy Grower died today or tonight, wake the Minions, show the *THIS IS THE DEMON* info token and point to the Demon. Put them to sleep. Wake the Demon, show the *THESE ARE YOUR MINIONS* info token and point to the Minions. Put the Demon to sleep.", - "reminders": [ - "Evil Wakes" - ], - "setup": false, - "ability": "Minions & Demons do not know each other. If you die, they learn who each other are that night.", - "flavor": "In the hidden groves of the deep forest, the black poppy dwells. To see its revelry is to be enchanted. To smell its thick aroma is to be lost forever, a slave to the gods of light and dark." - }, - { - "id": "pacifist", - "name": "Pacifist", - "edition": "bmr", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Executed good players might not die.", - "flavor": "Distrust all in whom the impulse to punish is powerful." - }, - { - "id": "atheist", - "name": "Atheist", - "edition": "carousel", - "team": "townsfolk", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": true, - "ability": "The Storyteller can break the game rules, and if executed, good wins, even if you are dead. [No evil characters]", - "special": [ - { - "type": "selection", - "name": "bag-duplicate" - } - ], - "flavor": "Let us disperse with unnecessary conjecture and silly paranoia. There is a perfectly rational explanation for everything. Yes, a teacup may indeed be orbiting the planet, too small to see, but I shall drink my tea from the very real china in my very real hands." - }, - { - "id": "hermit", - "name": "Hermit", - "team": "outsider", - "edition": "carousel", - "reminders": [ - "1", - "2", - "3" - ], - "setup": true, - "ability": "You have all Outsider abilities. [-0 or -1 Outsider]", - "flavor": "In the lost and forgotten places of the earth, the soul’s light beckons." - }, - { - "id": "butler", - "name": "Butler", - "edition": "tb", - "team": "outsider", - "firstNightReminder": "The Butler chooses a player. :reminder:", - "otherNightReminder": "The Butler chooses a player. :reminder:", - "reminders": [ - "Master" - ], - "setup": false, - "ability": "Each night, choose a player (not yourself): tomorrow, you may only vote if they are voting too.", - "flavor": "Yes, sir... No, sir... Certainly, sir." - }, - { - "id": "goon", - "name": "Goon", - "edition": "bmr", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Drunk" - ], - "setup": false, - "ability": "Each night, the 1st player to choose you with their ability is drunk until dusk. You become their alignment.", - "flavor": "Yes boss. I explained fings real good to dat geezer. He don't want me explain it again. Nah boss, I don't need no doctor - it's only a knife wound. Be right come mornin'" - }, - { - "id": "ogre", - "name": "Ogre", - "team": "outsider", - "edition": "carousel", - "firstNightReminder": "The Ogre points to a player. :reminder:", - "reminders": [ - "Friend" - ], - "setup": false, - "ability": "On your 1st night, choose a player (not yourself): you become their alignment (you don't know which) even if drunk or poisoned.", - "flavor": "" - }, - { - "id": "lunatic", - "name": "Lunatic", - "edition": "bmr", - "team": "outsider", - "firstNightReminder": "If there are 7 or more players, wake the Lunatic: Show the *THESE ARE YOUR MINIONS* token. Point to any players. Show the *THESE CHARACTERS ARE NOT IN PLAY* token. Show 3 good character tokens. Put the Lunatic to sleep. Wake the Demon. Show the *YOU ARE* info token and the Demon token. Show the *THIS PLAYER IS* info token and the Lunatic token, then point to the Lunatic.", - "otherNightReminder": "Do whatever needs to be done to simulate the Demon acting. Put the Lunatic to sleep. Wake the Demon. Show the Lunatic token & point to them, then their target(s).", - "reminders": [ - "Chosen", - "Chosen", - "Chosen" - ], - "setup": false, - "ability": "You think you are a Demon, but you are not. The Demon knows who you are & who you choose at night.", - "flavor": "I am the night... I think." - }, - { - "id": "drunk", - "name": "Drunk", - "edition": "tb", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "remindersGlobal": [ - "Is The Drunk" - ], - "setup": true, - "ability": "You do not know you are the Drunk. You think you are a Townsfolk character, but you are not.", - "special": [ - { - "type": "selection", - "name": "bag-disabled" - }, - { - "type": "reveal", - "name": "replace-character" - } - ], - "flavor": "I’m only a *hic* social drinker, my dear. Admittedly, I am a heavy *burp* socializer." - }, - { - "id": "tinker", - "name": "Tinker", - "edition": "bmr", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "The Tinker might die. :reminder:", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "You might die at any time.", - "flavor": "I think I see the problem. Luckily, I have an idea! This catapult will shoot twice as far with just a minor adjustment..." - }, - { - "id": "recluse", - "name": "Recluse", - "edition": "tb", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You might register as evil & as a Minion or Demon, even if dead.", - "flavor": "Garn git ya darn grub ya mitts ofma lorn yasee. Grr. Natsy pikkins yonder southwise ye begittin afta ya! Git! Me harvy no so widda licks and demmons no be fightin' hadsup ne'er ma kin. Git, assay!" - }, - { - "id": "golem", - "name": "Golem", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "May Not Nominate" - ], - "setup": false, - "ability": "You may only nominate once per game. When you do, if the nominee is not the Demon, they die.", - "flavor": "Golem help? Golem smash! Golem help." - }, - { - "id": "sweetheart", - "name": "Sweetheart", - "edition": "snv", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "If the Sweetheart died, a player became drunk immediately. If you haven't done this yet, do so now. :reminder:", - "reminders": [ - "Drunk" - ], - "setup": false, - "ability": "When you die, 1 player is drunk from now on.", - "flavor": "I will never forget her. Never." - }, - { - "id": "plaguedoctor", - "name": "Plague Doctor", - "team": "outsider", - "edition": "carousel", - "firstNightReminder": "", - "otherNightReminder": "If the Plague Doctor died, the Storyteller gained a Minion ability. If you haven’t done this yet, do so now.", - "reminders": [ - "Storyteller Ability" - ], - "setup": false, - "ability": "When you die, the Storyteller gains a Minion ability.", - "flavor": "Pleauze shtay shtill. Thinks nid tiime for hillink. Myrhh-myrhh." - }, - { - "id": "klutz", - "name": "Klutz", - "edition": "snv", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "When you learn that you died, publicly choose 1 alive player: if they are evil, your team loses.", - "flavor": "Oops." - }, - { - "id": "moonchild", - "name": "Moonchild", - "edition": "bmr", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "If the Moonchild is due to kill a good player, they die. :reminder:", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "When you learn that you died, publicly choose 1 alive player. Tonight, if it was a good player, they die.", - "flavor": "Scorpio looks sideways at the lovers, and you have a choice to make. With silver cross my palm, and your fate shall be revealed. With steel cross my throat, and by the stars you shall regret it." - }, - { - "id": "saint", - "name": "Saint", - "edition": "tb", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If you die by execution, your team loses.", - "flavor": "Wisdom begets peace. Patience begets wisdom. Fear not, for the time shall come when fear too shall pass. Let us pray, and may the unity of our vision make saints of us all." - }, - { - "id": "barber", - "name": "Barber", - "edition": "snv", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "If the Barber died today or tonight, show the Demon the *THIS CHARACTER SELECTED YOU* & Barber tokens. If the Demon chose 2 players, wake one at a time. Show the *YOU ARE* token & their new character token.", - "reminders": [ - "Haircuts Tonight" - ], - "setup": false, - "ability": "If you died today or tonight, the Demon may choose 2 players (not another Demon) to swap characters.", - "flavor": "Did you know that barbery and surgery were once the same profession? No? Well, now you do." - }, - { - "id": "hatter", - "name": "Hatter", - "team": "outsider", - "edition": "carousel", - "firstNightReminder": "", - "otherNightReminder": "If the Hatter died today or tonight, wake Minions and Demons, allow them to choose new characters.", - "reminders": [ - "Tea Party Tonight" - ], - "setup": false, - "ability": "If you died today or tonight, the Minion & Demon players may choose new Minion & Demon characters to be.", - "flavor": "One Hat. Too Hat. Three Hat. Tea Hat. Fore Hat. Thrive Hat. Six Hat. Sticks Hat." - }, - { - "id": "mutant", - "name": "Mutant", - "edition": "snv", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If you are “mad” about being an Outsider, you might be executed.", - "flavor": "I am not a freak! I am a human being! Have mercy!" - }, - { - "id": "politician", - "name": "Politician", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If you were the player most responsible for your team losing, you change alignment & win, even if dead.", - "flavor": "I'm glad you asked that question. Truly, I am. But I think the REAL question here is..." - }, - { - "id": "zealot", - "name": "Zealot", - "team": "outsider", - "edition": "carousel", - "reminders": [], - "setup": false, - "ability": "If there are 5 or more players alive, you must vote for every nomination.", - "flavor": "I enjoy talking to you. Your mind appeals to me. It resembles my own mind except that you happen to be insane." - }, - { - "id": "damsel", - "name": "Damsel", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "If the Damsel was chosen by the Huntsman, show them the *YOU ARE* info token and their new character token.", - "otherNightReminder": "If the Damsel was chosen by the Huntsman, show them the *YOU ARE* info token and their new character token.", - "reminders": [ - "Guess Used" - ], - "setup": false, - "ability": "All Minions know a Damsel is in play. If a Minion publicly guesses you (once), your team loses.", - "flavor": "Don't touch the hair, honey." - }, - { - "id": "snitch", - "name": "Snitch", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "Wake each Minion. Show the *THESE CHARACTERS ARE NOT IN PLAY* token and three not-in-play character tokens. Put each Minion to sleep.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each Minion gets 3 bluffs.", - "flavor": "It was John." - }, - { - "id": "heretic", - "name": "Heretic", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Whoever wins, loses & whoever loses, wins, even if you are dead.", - "flavor": "After the hail has smashed the roof and splintered the glass of the Cathedral windows, it melts again into the earth, like a dying lamb in the desert sun. Such is the parable of the madman." - }, - { - "id": "puzzlemaster", - "name": "Puzzlemaster", - "edition": "carousel", - "team": "outsider", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Drunk", - "Guess Used" - ], - "setup": false, - "ability": "1 player is drunk, even if you die. If you guess (once) who it is, learn the Demon player, but guess wrong & get false info.", - "flavor": "When one begins to think that some thing is merely some other thing, one is usually on the brink of an error. Patience, patience. Don’t confuse just and should with is and isn’t." - }, - { - "id": "mezepheles", - "name": "Mezepheles", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "Show the written word.", - "otherNightReminder": "If a player is marked with the *TURNS EVIL* reminder, wake them. Show the *YOU ARE* info token and a thumbs down. The Mezepheles loses their ability. :reminder:", - "reminders": [ - "Turns Evil", - "No Ability" - ], - "setup": false, - "ability": "You start knowing a secret word. The 1st good player to say this word becomes evil that night.", - "flavor": "That which issues from the heart alone, will bend the hearts of others to your own." - }, - { - "id": "godfather", - "name": "Godfather", - "edition": "bmr", - "team": "minion", - "firstNightReminder": "Show the character tokens of all in-play Outsiders.", - "otherNightReminder": "If an Outsider died today, the Godfather chooses a player. :reminder:", - "reminders": [ - "Died Today", - "Dead" - ], - "setup": true, - "ability": "You start knowing which Outsiders are in play. If 1 died today, choose a player tonight: they die. [-1 or +1 Outsider]", - "flavor": "Normally, it's just business. But when you insult my daughter, you insult me. And when you insult me, you insult my family. You really should be more careful - it would be a shame if you had an unfortunate accident." - }, - { - "id": "poisoner", - "name": "Poisoner", - "edition": "tb", - "team": "minion", - "firstNightReminder": "The Poisoner chooses a player. :reminder:", - "otherNightReminder": "The Poisoner chooses a player. :reminder:", - "reminders": [ - "Poisoned" - ], - "setup": false, - "ability": "Each night, choose a player: they are poisoned tonight and tomorrow day.", - "flavor": "Add compound Alpha to compound Beta... NOT TOO MUCH!" - }, - { - "id": "devilsadvocate", - "name": "Devil's Advocate", - "edition": "bmr", - "team": "minion", - "firstNightReminder": "The Devil's Advocate chooses a living player. :reminder:", - "otherNightReminder": "The Devil's Advocate chooses a living player. :reminder:", - "reminders": [ - "Survives Execution" - ], - "setup": false, - "ability": "Each night, choose a living player (different to last night): if executed tomorrow, they don't die.", - "flavor": "My client, should the objection be overruled, pleads innocent by virtue of the prosecution's non-observance of statute 27.B - incorrect or misleading conjugation of a verb. The fact that nine of the jury died last night is simply prima facie, which is, as Wills vs Thule set precedent for, further reason to acquit." - }, - { - "id": "spy", - "name": "Spy", - "edition": "tb", - "team": "minion", - "firstNightReminder": "Show the Grimoire for as long as the Spy needs.", - "otherNightReminder": "Show the Grimoire for as long as the Spy needs.", - "reminders": [], - "setup": false, - "special": [ - { - "type": "signal", - "name": "grimoire", - "time": "night" - } - ], - "ability": "Each night, you see the Grimoire. You might register as good & as a Townsfolk or Outsider, even if dead.", - "flavor": "Any brewmaster worth their liquor, knows no concoction pours trouble quicker, than one where spies seem double." - }, - { - "id": "harpy", - "name": "Harpy", - "team": "minion", - "edition": "carousel", - "firstNightReminder": "The Harpy chooses two players. :reminder: :reminder: Put the Harpy to sleep. Wake the 1st target. Show the *THIS CHARACTER SELECTED YOU* token, the Harpy token, then point to the 2nd target.", - "otherNightReminder": "The Harpy chooses two players. :reminder: :reminder: Put the Harpy to sleep. Wake the 1st target. Show the *THIS CHARACTER SELECTED YOU* token, the Harpy token, then point to the 2nd target.", - "reminders": [ - "Mad", - "2nd" - ], - "setup": false, - "ability": "Each night, choose 2 players: tomorrow, the 1st player is mad that the 2nd is evil, or one or both might die.", - "flavor": "So fair a day I never did see, nor so fowl a presence hanging over me." - }, - { - "id": "witch", - "name": "Witch", - "edition": "snv", - "team": "minion", - "firstNightReminder": "The Witch chooses a player. :reminder:", - "otherNightReminder": "The Witch chooses a player. :reminder:", - "reminders": [ - "Cursed" - ], - "setup": false, - "ability": "Each night, choose a player: if they nominate tomorrow, they die. If just 3 players live, you lose this ability.", - "flavor": "Three drops of goat's blood. A lock of hair, torn in anger. The name is spoken, the shadow cast. Walk left foot first down that brambled path, and don't look back." - }, - { - "id": "cerenovus", - "name": "Cerenovus", - "edition": "snv", - "team": "minion", - "firstNightReminder": "The Cerenovus chooses a player & a character. :reminder: Put the Cerenovus to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token, the Cerenovus token, then the madness-character token.", - "otherNightReminder": "The Cerenovus chooses a player & a character. :reminder: Put the Cerenovus to sleep. Wake the target. Show the *THIS CHARACTER SELECTED YOU* token, the Cerenovus token, then the madness-character token.", - "reminders": [ - "Mad" - ], - "setup": false, - "ability": "Each night, choose a player & a good character: they are “mad” they are this character tomorrow, or might be executed.", - "flavor": "Reality is merely an opinion. Specifically, my opinion." - }, - { - "id": "fearmonger", - "name": "Fearmonger", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "The Fearmonger chooses a player. :reminder: Declare that \"the Fearmonger has chosen a player.\"", - "otherNightReminder": "The Fearmonger chooses a player. :reminder: If the player wasn't already marked with the *FEAR* reminder, declare that \"the Fearmonger has chosen a player.\"", - "reminders": [ - "Fear" - ], - "setup": false, - "ability": "Each night, choose a player: if you nominate & execute them, their team loses. All players know if you choose a new player.", - "flavor": "Beware of gazing long into the Abyss, lest the Abyss also gaze into you." - }, - { - "id": "pithag", - "name": "Pit-Hag", - "edition": "snv", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "The Pit-Hag chooses a player & a character. If they chose a character that is not in play: Put the Pit-Hag to sleep. Wake the target. Show the *YOU ARE* token & their new character token.", - "reminders": [], - "setup": false, - "ability": "Each night*, choose a player & a character they become (if not in play). If a Demon is made, deaths tonight are arbitrary.", - "flavor": "Round about the cauldron go; In the poison'd entrails throw; Toad, that under cold stone; Days and nights has thirty-one; Sweated venom sleeping got; Boil thou first in the charmed pot." - }, - { - "id": "psychopath", - "name": "Psychopath", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each day, before nominations, you may publicly choose a player: they die. If executed, you only die if you lose roshambo.", - "flavor": "Surprise!" - }, - { - "id": "assassin", - "name": "Assassin", - "edition": "bmr", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "The Assassin might choose a player. :reminder: :reminder:", - "reminders": [ - "Dead", - "No Ability" - ], - "setup": false, - "ability": "Once per game, at night*, choose a player: they die, even if for some reason they could not.", - "flavor": "..." - }, - { - "id": "wizard", - "name": "Wizard", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "Run the Wizard's ability, if applicable.", - "otherNightReminder": "Run the Wizard's ability, if applicable.", - "reminders": [ - "?", - "?" - ], - "setup": false, - "ability": "Once per game, choose to make a wish. If granted, it might have a price & leave a clue as to its nature.", - "flavor": "Every man and every woman is a star. Love is the law, love under will." - }, - { - "id": "widow", - "name": "Widow", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "Show the Grimoire for as long as the Widow needs. The Widow chooses a player. :reminder:", - "otherNightReminder": "", - "reminders": [ - "Poisoned", - "Know" - ], - "remindersGlobal": [], - "setup": false, - "special": [ - { - "name": "grimoire", - "type": "signal", - "time": "night" - } - ], - "ability": "On your 1st night, look at the Grimoire & choose a player: they are poisoned. 1 good player knows a Widow is in play.", - "flavor": "More wine? Château d’Ergot ’07 is a very special vintage. My yes, very special indeed." - }, - { - "id": "xaan", - "name": "Xaan", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "Mark the Xaan with the *NIGHT 1* reminder. If X is 1, mark the Xaan with the *X* reminder token. :reminder: :reminder:", - "otherNightReminder": "Change the Xaan reminder token to the relevant night. If it is night X, mark the Xaan with the *X* reminder token. :reminder: :reminder:", - "reminders": [ - "Night 1", - "Night 2", - "Night 3", - "X" - ], - "setup": true, - "ability": "On night X, all Townsfolk are poisoned until dusk. [X Outsiders]", - "flavor": "Down they fall. One by one. By two, by three, by five." - }, - { - "id": "marionette", - "name": "Marionette", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "Wake the Demon. Point to the player marked *IS THE MARIONETTE* and show the *THIS PLAYER IS* token and the Marionette character token.", - "otherNightReminder": "", - "reminders": [], - "remindersGlobal": [ - "Is The Marionette" - ], - "setup": true, - "ability": "You think you are a good character, but you are not. The Demon knows who you are. [You neighbor the Demon]", - "special": [ - { - "type": "selection", - "name": "bag-disabled" - }, - { - "type": "reveal", - "name": "replace-character" - } - ], - "flavor": "Words, words. They're all we have to go on." - }, - { - "id": "wraith", - "name": "Wraith", - "team": "minion", - "edition": "carousel", - "firstNightReminder": "Wake the Wraith whenever other evil players wake.", - "otherNightReminder": "Wake the Wraith whenever other evil players wake.", - "reminders": [], - "setup": false, - "ability": "You may choose to open your eyes at night. You wake when other evil players do.", - "flavor": "Ra'āb ina pān ṣilli ša dāri. Rigim qallu ina šūri, šītu ša šunātīka iredde, u napšutka idlul ina pān maṣṣartī dāriti.", - "special": [ - { - "type": "player", - "name": "open-eyes", - "time": "night" - } - ] - }, - { - "id": "summoner", - "name": "Summoner", - "team": "minion", - "edition": "carousel", - "firstNightReminder": "Show the *THESE CHARACTERS ARE NOT IN PLAY* token. Show 3 not-in-play good character tokens.", - "otherNightReminder": "Change the Summoner reminder token to the relevant night. If it is night 3, the Summoner chooses a player and a Demon. Put the Summoner to sleep. Wake the chosen player. Show the *YOU ARE* token, a thumbs down and the chosen Demon token.", - "reminders": [ - "Night 1", - "Night 2", - "Night 3" - ], - "setup": true, - "ability": "You get 3 bluffs. On the 3rd night, choose a player: they become an evil Demon of your choice. [No Demon]", - "flavor": "Hail the guardians of the north; by my intellect, thou art cut. Hail the guardians of the east; by my will, thou art dominated. Hail the guardians of the south; by that which lies beyond, the mystery is revealed. Hail the guardians of the west; a shield in the darkness" - }, - { - "id": "eviltwin", - "name": "Evil Twin", - "edition": "snv", - "team": "minion", - "firstNightReminder": "Wake both twins. Allow eye contact. Show the good twin's character token to the Evil Twin & vice versa.", - "otherNightReminder": "", - "reminders": [ - "Twin" - ], - "setup": false, - "ability": "You & an opposing player know each other. If the good player is executed, evil wins. Good can't win if you both live.", - "flavor": "I'm not Sara! I'm Clara! SHE is Sara! Sara is the evil one! Not me!" - }, - { - "id": "goblin", - "name": "Goblin", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Claimed" - ], - "setup": false, - "ability": "If you publicly claim to be the Goblin when nominated & are executed that day, your team wins.", - "flavor": "You don’t want to insult the goblins. You really, really don’t. On a completely different note… can I have another piece of cake?" - }, - { - "id": "boomdandy", - "name": "Boomdandy", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If you are executed, all but 3 players die. After a 10 to 1 countdown, the player with the most players pointing at them, dies.", - "special": [ - { - "type": "ability", - "name": "pointing", - "time": "day" - } - ], - "flavor": "Tick... Tick... Tick... TOCK." - }, - { - "id": "mastermind", - "name": "Mastermind", - "edition": "bmr", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If the Demon dies by execution (ending the game), play for 1 more day. If a player is then executed, their team loses.", - "flavor": "The tentacles of that monster are nailed to the doors of the church. Mothers and children are dancing in the street. Excellent. Everything is proceeding exactly as I have planned." - }, - { - "id": "scarletwoman", - "name": "Scarlet Woman", - "edition": "tb", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "If the Scarlet Woman became the Demon today, show them the *YOU ARE* token, then the Demon token.", - "reminders": [ - "Is The Demon" - ], - "setup": false, - "ability": "If there are 5 or more players alive & the Demon dies, you become the Demon. (Travellers don't count.)", - "flavor": "You have shown me the secrets of the Council of the Purple Flame. We have lain together in fire and in lust and in beastly commune, and I am forever your servant. But tonight, my dear, I am your master." - }, - { - "id": "vizier", - "name": "Vizier", - "team": "minion", - "edition": "carousel", - "firstNightReminder": "Announce the Vizier player to the group.", - "reminders": [], - "setup": false, - "ability": "All players know you are the Vizier. You cannot die during the day. If good voted, you may choose to execute immediately.", - "flavor": "An excellent decision, as always, sire. Such a petty crime as bumping into the Bishop indeed deserves your ‘justice’ and ‘mercy’. Take a stroll in the gardens. Visit the gallery and peruse the sculptures of Von Strauf. Relax, sire. Leave everything… to me." - }, - { - "id": "organgrinder", - "name": "Organ Grinder", - "edition": "carousel", - "team": "minion", - "firstNightReminder": "The Organ Grinder either nods their head yes to be drunk, or shakes their head no to be sober. :reminder:", - "otherNightReminder": "The Organ Grinder either nods their head yes to be drunk, or shakes their head no to be sober. :reminder:", - "reminders": [ - "About To Die", - "Drunk" - ], - "setup": false, - "ability": "All players keep their eyes closed when voting and the vote tally is secret. Each night, choose if you are drunk until dusk.", - "special": [ - { - "type": "vote", - "name": "hidden" - } - ], - "flavor": "Round and round the handles go. The more you dance the less you know." - }, - { - "id": "boffin", - "name": "Boffin", - "team": "minion", - "edition": "carousel", - "firstNightReminder": "Wake the Boffin and the Demon. Show the not-in-play good character token. Put the Boffin and the Demon to sleep.", - "reminders": [], - "setup": false, - "ability": "The Demon (even if drunk or poisoned) has a not-in-play good character’s ability. You both know which.", - "flavor": "Stellar hydrogen, vast, inert; carbon, oxygen, neon gases, all ruined. Molecular chaos, entropy, yields new cosmic phenomena, rebirth from atomic chaos, dense matter collapsing. All in a teeny little bottle." - }, - { - "id": "baron", - "name": "Baron", - "edition": "tb", - "team": "minion", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": true, - "ability": "There are extra Outsiders in play. [+2 Outsiders]", - "flavor": "This town has gone to the dogs, what? Cheap foreign labor... that's the ticket. Stuff them in the mine, I say. A bit of hard work never hurt anyone, and a clip'o'the ears to any brigand who says otherwise. It's all about the bottom line, what?" - }, - { - "id": "yaggababble", - "name": "Yaggababble", - "team": "demon", - "edition": "carousel", - "firstNightReminder": "Choose a secret phrase. Wake the Yaggababble and let them know their secret phrase.", - "otherNightReminder": "For each time the Yaggababble said the phrase today, you may choose a player. They die. :reminder:", - "reminders": [ - "Dead", - "Dead", - "Dead" - ], - "setup": false, - "ability": "You start knowing a secret phrase. For each time you said it publicly today, a player might die.", - "flavor": "Murders inside the Rue Morgue? Фальшивые новости! Hounds on the Baskerville moor? Фальшивые новости! Death while sailing the Nile? Фальшивые новости!" - }, - { - "id": "pukka", - "name": "Pukka", - "edition": "bmr", - "team": "demon", - "firstNightReminder": "The Pukka chooses a player. :reminder:", - "otherNightReminder": "The Pukka chooses a player. :reminder: The previously poisoned player dies then becomes healthy. :reminder:", - "reminders": [ - "Poisoned", - "Poisoned", - "Dead" - ], - "setup": false, - "ability": "Each night, choose a player: they are poisoned. The previously poisoned player dies then becomes healthy.", - "flavor": "You truly have been kind welcoming me into your beautiful home. I am so sorry I accidentally scratched you. A little thing. No matter. But please, take this golden toothpick as a humble token of my regret." - }, - { - "id": "lilmonsta", - "name": "Lil' Monsta", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "Wake all Minions, allow them to choose a babysitter. :reminder:", - "otherNightReminder": "Wake all Minions, allow them to choose a babysitter. :reminder: A player might die. :reminder:", - "reminders": [], - "remindersGlobal": [ - "Is The Demon", - "Dead" - ], - "setup": true, - "ability": "Each night, Minions choose who babysits Lil' Monsta & \"is the Demon\". Each night*, a player might die. [+1 Minion]", - "special": [ - { - "type": "ability", - "name": "pointing", - "time": "night", - "global": "minion" - }, - { - "type": "selection", - "name": "bag-disabled" - } - ], - "flavor": "Step 1: Be cute. Step 2: World domination. Step 3: Bweakfast." - }, - { - "id": "nodashii", - "name": "No Dashii", - "edition": "snv", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The No Dashii chooses a player. :reminder:", - "reminders": [ - "Dead", - "Poisoned", - "Poisoned" - ], - "setup": false, - "ability": "Each night*, choose a player: they die. Your 2 Townsfolk neighbors are poisoned.", - "flavor": "By the sins of Arnoch, I feel thy laden stench. By the curs-ed sun and her foul legion of tiny grinning gods, I corrupt thee. By the blessed night and the hidden depths of the horrid and unholy sea, I end thy squalid life upon this plane." - }, - { - "id": "imp", - "name": "Imp", - "edition": "tb", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Imp chooses a player. :reminder: If the Imp chose themselves: Replace 1 alive Minion token with a spare Imp token. Put the old Imp to sleep. Wake the new Imp. Show the *YOU ARE* token, then show the Imp token.", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "Each night*, choose a player: they die. If you kill yourself this way, a Minion becomes the Imp.", - "flavor": "We must keep our wits sharp and our sword sharper. Evil walks among us, and will stop at nothing to destroy us good, simple folk, bringing our fine town to ruin. Trust no-one. But, if you must trust someone, trust me." - }, - { - "id": "shabaloth", - "name": "Shabaloth", - "edition": "bmr", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "A previously chosen player might be resurrected. :reminder: The Shabaloth chooses 2 players. :reminder: :reminder:", - "reminders": [ - "Dead", - "Dead", - "Alive" - ], - "setup": false, - "ability": "Each night*, choose 2 players: they die. A dead player you chose last night might be regurgitated.", - "flavor": "Blarg f'taag nm mataan! No sho gumtha m'sik na yuuu. Fluuuuuuuuurg h-sikkkh." - }, - { - "id": "ojo", - "name": "Ojo", - "team": "demon", - "edition": "carousel", - "firstNightReminder": "", - "otherNightReminder": "The Ojo chooses a character. :reminder: ", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "Each night*, choose a character: they die. If they are not in play, the Storyteller chooses who dies.", - "flavor": "Like a bonfire on a moonless night… I see you, mortal." - }, - { - "id": "kazali", - "name": "Kazali", - "team": "demon", - "edition": "carousel", - "firstNightReminder": "Wake the Kazali, allow them to choose Minions.", - "otherNightReminder": "The Kazali chooses a player. :reminder: ", - "reminders": [ - "Dead" - ], - "setup": true, - "ability": "Each night*, choose a player: they die. [You choose which players are which Minions. -? to +? Outsiders]", - "flavor": "Gon(z)a7les6. Take cau8tun. The mech4an4ion is iNvert10d. E99ors insy6tum. Reco{7}fig." - }, - { - "id": "po", - "name": "Po", - "edition": "bmr", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Po may choose a player OR chooses 3 players if they chose no-one last night. :reminder: or :reminder: :reminder: :reminder:", - "reminders": [ - "Dead", - "Dead", - "Dead", - "3 Attacks" - ], - "setup": false, - "ability": "Each night*, you may choose a player: they die. If your last choice was no-one, choose 3 players tonight.", - "flavor": "Would you like a flower? I'm so lonely." - }, - { - "id": "zombuul", - "name": "Zombuul", - "edition": "bmr", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "If no one died today, the Zombuul chooses a player. :reminder:", - "reminders": [ - "Died Today", - "Dead" - ], - "setup": false, - "ability": "Each night*, if no-one died today, choose a player: they die. The 1st time you die, you live but register as dead.", - "flavor": "I do not. Understand. Your ways. Fellow human. Show me. The dirt. Where the holy. Lay. Sleeping. I too. Must sleep. Soon." - }, - { - "id": "vigormortis", - "name": "Vigormortis", - "edition": "snv", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Vigormortis chooses a player. :reminder: If that player is a Minion, poison a neighboring Townsfolk. :reminder: :reminder:", - "reminders": [ - "Dead", - "Has Ability", - "Has Ability", - "Has Ability", - "Poisoned", - "Poisoned", - "Poisoned" - ], - "setup": true, - "ability": "Each night*, choose a player: they die. Minions you kill keep their ability & poison 1 Townsfolk neighbor. [-1 Outsider]", - "flavor": "All doors are one door. All keys are one key. All cups are one cup, but whosoever drinketh of the water that I give shall never thirst, but the water shall be in him a well springing up into everlasting life." - }, - { - "id": "vortox", - "name": "Vortox", - "edition": "snv", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Vortox chooses a player. :reminder:", - "reminders": [ - "Dead" - ], - "setup": false, - "ability": "Each night*, choose a player: they die. Townsfolk abilities yield false info. Each day, if no-one is executed, evil wins.", - "flavor": "Black is White. Right is Wrong. Left is Right. Up is Long. Down is Sight. Short is Blind. Follow me. Answers find." - }, - { - "id": "legion", - "name": "Legion", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "A player might die. :reminder:", - "reminders": [ - "Dead", - "About To Die" - ], - "setup": true, - "ability": "Each night*, a player might die. Executions fail if only evil voted. You register as a Minion too. [Most players are Legion]", - "special": [ - { - "type": "selection", - "name": "bag-duplicate" - } - ], - "flavor": "We are the chill wind on a winter’s day. We are the shadow in the moonless night. We are the poison in your tea and the whisper in your ear. We are everywhere." - }, - { - "id": "fanggu", - "name": "Fang Gu", - "edition": "snv", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Fang Gu chooses a player. :reminder: If they chose an Outsider (once only): Replace the Outsider token with the spare Fang Gu token. Put the Fang Gu to sleep. Wake the target. Show the *YOU ARE* and Fang Gu tokens & give a thumbs-down. :reminder:", - "reminders": [ - "Dead", - "Once" - ], - "setup": true, - "ability": "Each night*, choose a player: they die. The 1st Outsider this kills becomes an evil Fang Gu & you die instead. [+1 Outsider]", - "flavor": "Your walls and your weapons are but smoke in dreams." - }, - { - "id": "lordoftyphon", - "name": "Lord of Typhon", - "team": "demon", - "edition": "carousel", - "firstNightReminder": "Replace neighbors of the Lord of Typhon with Minions, wake them, tell them their new alignment and character, then do minion info.", - "otherNightReminder": "The Lord of Typhon chooses a player. :reminder:", - "reminders": [ - "Dead" - ], - "setup": true, - "ability": "Each night*, choose a player: they die. [Evil characters are in a line. You are in the middle. +1 Minion. -? to +? Outsiders]", - "flavor": "In the shadowed and forgotten corners of the cosmos, where the stars whisper secrets to the void, lies a truth so profound that the merest glimpse of it unravels the sanity of mortal minds." - }, - { - "id": "lleech", - "name": "Lleech", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "The Lleech chooses a player. :reminder:", - "otherNightReminder": "The Lleech chooses a player. :reminder:", - "reminders": [ - "Dead", - "Poisoned" - ], - "setup": false, - "ability": "Each night*, choose a player: they die. You start by choosing a player: they are poisoned. You die if & only if they are dead.", - "flavor": "Tasty, tasty, tasty, tasty, tasty, tasty, tasty, tasty brai- I mean pie! Yes. Tasty pie. That’s what I meant to say." - }, - { - "id": "alhadikhia", - "name": "Al-Hadikhia", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "The Al-Hadikhia chooses three players. :reminder: :reminder: :reminder: Wake the player marked *1* and say \"the Al-Hadikhia has chosen\", then the player's name, then \"Do you choose to live?\" They either nod or shake their head. Put them to sleep and add or remove shrouds accordingly. Repeat for players marked *2* and *3*. If all three players are now alive, add a shroud to all three.", - "reminders": [ - "1", - "2", - "3" - ], - "setup": false, - "ability": "Each night*, you may choose 3 players (all players learn who): each silently chooses to live or die, but if all live, all die.", - "flavor": "Alsukut min dhahab." - }, - { - "id": "riot", - "name": "Riot", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Day 1", - "Day 2", - "Day 3" - ], - "setup": false, - "ability": "On day 3, Minions become Riot & nominees die but nominate an alive player immediately. This must happen.", - "flavor": "Larga vida a la revolución! Mi revolucion!" - }, - { - "id": "leviathan", - "name": "Leviathan", - "edition": "carousel", - "team": "demon", - "firstNightReminder": "Declare that \"The Leviathan is in play.\" Mark the Leviathan with the *DAY 1* reminder. :reminder:", - "otherNightReminder": "Optionally, declare that \"The Leviathan is in play.\" Replace the reminder token. :reminder:", - "reminders": [ - "Day 1", - "Day 2", - "Day 3", - "Day 4", - "Day 5", - "Good Player Executed" - ], - "setup": false, - "ability": "If more than 1 good player is executed, evil wins. All players know you are in play. After day 5, evil wins.", - "flavor": "To the last, I grapple with thee. From Hell’s heart, I stab at thee. For hate’s sake, I spit my last breath at thee." - }, - { - "id": "thief", - "name": "Thief", - "edition": "tb", - "team": "traveller", - "firstNightReminder": "The Thief chooses a player. :reminder:", - "otherNightReminder": "The Thief chooses a player. :reminder:", - "reminders": [ - "Negative Vote" - ], - "setup": false, - "ability": "Each night, choose a player (not yourself): their vote counts negatively tomorrow.", - "special": [ - { - "type": "vote", - "name": "multiplier", - "value": -1 - } - ], - "flavor": "I ain't done nuffink. I weren't even in dat alley last night! It weren't me what stole Mayor Bruno's briefcase wiv all dem fancy dockoments innit. Besides, it was too 'eavy to carry far." - }, - { - "id": "bureaucrat", - "name": "Bureaucrat", - "edition": "tb", - "team": "traveller", - "firstNightReminder": "The Bureaucrat chooses a player. :reminder:", - "otherNightReminder": "The Bureaucrat chooses a player. :reminder:", - "reminders": [ - "3 Votes" - ], - "setup": false, - "ability": "Each night, choose a player (not yourself): their vote counts as 3 votes tomorrow.", - "special": [ - { - "type": "vote", - "name": "multiplier", - "value": 3 - } - ], - "flavor": "Sign here please. And here. And here. Aaaaaaaaand here. This should all be sorted and tallied by the end of the day, assuming everyone's signatures are legible. We haven't had a mix-up in the paperwork for ages. Yesterday noon, if memory serves..." - }, - { - "id": "barista", - "name": "Barista", - "edition": "snv", - "team": "traveller", - "firstNightReminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", - "otherNightReminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", - "reminders": [ - "Sober & Healthy", - "Acts Twice", - "?", - "?" - ], - "setup": false, - "ability": "Each night, until dusk, 1) a player becomes sober, healthy & gets true info, or 2) their ability works twice. They learn which.", - "flavor": "A cup of coffee with no cream, Monsieur? I’m terribly sorry, but we’re fresh out of cream — how about with no milk?" - }, - { - "id": "harlot", - "name": "Harlot", - "edition": "snv", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "The Harlot points at any living player. Then, put the Harlot to sleep. Wake the chosen player, show them the *THIS CHARACTER SELECTED YOU* token, then the Harlot token. That player either nods their head yes or shakes their head no. If they nodded their head yes, wake the Harlot and show them the chosen player's character token. Then, you may decide that both players die.", - "reminders": [ - "Dead", - "Dead" - ], - "setup": false, - "ability": "Each night*, choose a living player: if they agree, you learn their character, but you both might die.", - "flavor": "Enchanté, Sailor. You look like you need someone to really listen to your troubles. I'm a good listener. Very, very good." - }, - { - "id": "butcher", - "name": "Butcher", - "edition": "snv", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each day, after the 1st execution, you may nominate again.", - "flavor": "It tastes like chicken. More please." - }, - { - "id": "gunslinger", - "name": "Gunslinger", - "edition": "tb", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each day, after the 1st vote has been tallied, you may choose a player that voted: they die.", - "flavor": "It's time someone took matters into their own hands. That someone... is me." - }, - { - "id": "matron", - "name": "Matron", - "edition": "bmr", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Each day, you may choose up to 3 sets of 2 players to swap seats. Players may not leave their seats to talk in private.", - "flavor": "Miss Featherbottom, be quiet. Master Rutherford, a teacup needs just the four fingers, please. I know you are a father of nine, but age, or lack there-of as the case may be, is never an excuse for poor manners." - }, - { - "id": "gangster", - "name": "Gangster", - "edition": "carousel", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Once per day, you may choose to kill an alive neighbor, if your other alive neighbor agrees.", - "flavor": "I like your shoes. It would be such a shame if you had a little accident, and they got ruined. Now that you mention it, I like your cufflinks too." - }, - { - "id": "bonecollector", - "name": "Bone Collector", - "edition": "snv", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "The Bone Collector either shakes their head no or points at any dead player. If they pointed at any dead player, put the Bone Collector's 'Has Ability' reminder by the chosen player's character token. (They may need to be woken tonight to use it.)", - "reminders": [ - "No Ability", - "Has Ability" - ], - "setup": false, - "ability": "Once per game, at night*, choose a dead player: they regain their ability until dusk.", - "flavor": "I collect many things. Hair. Teeth. Clothes. Fragments of poems. The dreams of lost lovers. My secret arts are not for you to know but my fee is a mere pittance. Bring me the blood of a noblewoman who died of heartbreak under a full moon, and you shall have your answers." - }, - { - "id": "judge", - "name": "Judge", - "edition": "bmr", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "ability": "Once per game, if another player nominated, you may choose to force the current execution to pass or fail.", - "flavor": "I find the defendant guilty of the crimes of murder, fraud, arson, larceny, impersonating an officer of the law, practicing medicine without a license, slander, regicide, and littering." - }, - { - "id": "apprentice", - "name": "Apprentice", - "edition": "bmr", - "team": "traveller", - "firstNightReminder": "Show the Apprentice the *YOU ARE* card, then a Townsfolk or Minion token. In the Grimoire, replace the Apprentice token with that character token, and put the Apprentice's *IS THE APPRENTICE* reminder by that character token.", - "otherNightReminder": "", - "reminders": [ - "Is The Apprentice" - ], - "setup": false, - "ability": "On your 1st night, you gain a Townsfolk ability (if good), or a Minion ability (if evil).", - "special": [ - { - "type": "signal", - "name": "grimoire", - "time": "night" - } - ], - "flavor": "For years have I traveled, studying the ways of The Craft. Which craft, you ask? Simply that of the simple folk. Nothing to worry about. Not yet." - }, - { - "id": "beggar", - "name": "Beggar", - "edition": "tb", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "You must use a vote token to vote. If a dead player gives you theirs, you learn their alignment. You are sober & healthy.", - "flavor": "Alms for the poor, good Sir? Spare a coin, Madam? Thank you. God bless! You're a right kind soul and no mistake! I'll have some swanky nosh tonight, I will!" - }, - { - "id": "deviant", - "name": "Deviant", - "edition": "snv", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If you were funny today, you cannot die by exile.", - "flavor": "Twas the lady's quip, forsooth." - }, - { - "id": "scapegoat", - "name": "Scapegoat", - "edition": "tb", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "If a player of your alignment is executed, you might be executed instead.", - "flavor": "Good evening! Thank you for inviting me to the ball. I'm not from around here, but you sure seem like a friendly bunch, by golly. I'm sure we'll get along just dandy. What's all that rope for?" - }, - { - "id": "gnome", - "name": "Gnome", - "edition": "carousel", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Amigo" - ], - "setup": false, - "ability": "All players start knowing a player of your alignment. You may choose to kill anyone who nominates them.", - "flavor": "Four the score or seven beers, no shows are goes for me and my. A prank to crack the cranks and planks o’ the floor foundation length, so incontravertabubbilly mini. The large essays down streams of joyse, no greater than is scene, not inherdt. Ha-urrumph." - }, - { - "id": "bishop", - "name": "Bishop", - "edition": "bmr", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Nominate Good", - "Nominate Evil" - ], - "setup": false, - "ability": "Only the Storyteller can nominate. At least 1 opposing player must be nominated each day.", - "flavor": "In nomine Patris, et Filii, et Spiritus Sancti… Nos mos Dei. Deus vult de nobis." - }, - { - "id": "voudon", - "name": "Voudon", - "edition": "bmr", - "team": "traveller", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "ability": "Only you & the dead can vote. They don't need a vote token to do so. A 50% majority isn't required.", - "flavor": "Bien venu. Sit down. Breathe deep. Enter the land of the dead. See with their eyes. Speak with their voice. Yon sel lang se janm ase." - }, - { - "id": "angel", - "firstNightReminder": "Announce which players are protected by the Angel. Add the *PROTECTED* token to the relevant players.", - "otherNightReminder": "", - "reminders": [ - "Protected", - "Protected", - "Something Bad" - ], - "setup": false, - "name": "Angel", - "team": "fabled", - "edition": "fabled", - "ability": "Something bad might happen to whoever is most responsible for the death of a new player.", - "flavor": "Let those who are without sin dare to raise their hand to my chosen, for I shall strike such fools down with the fury and righteousness of a thousand storms." - }, - { - "id": "buddhist", - "firstNightReminder": "Declare which players are affected by the Buddhist.", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Buddhist", - "team": "fabled", - "edition": "fabled", - "ability": "For the first 2 minutes of each day, veteran players may not talk.", - "flavor": "You throw thorns. Falling in my silence, they become flowers." - }, - { - "id": "deusexfiasco", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Whoopsie" - ], - "setup": true, - "name": "Deus ex Fiasco", - "team": "fabled", - "edition": "fabled", - "ability": "At least once per game, the Storyteller will make a mistake, correct it, and publicly admit to it.", - "flavor": "It's not a bug, it's a feature. It's not an error, it's a tweak. It's not broken, it's quirky." - }, - { - "id": "djinn", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Djinn", - "team": "fabled", - "edition": "fabled", - "ability": "Use the Djinn's special rule. All players know what it is.", - "flavor": "نحن لسنا هنا. انت لست حقيقي. كل شيء هو وهم. أسئلتك هي جبل نار في يوم صافٍ." - }, - { - "id": "doomsayer", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Doomsayer", - "team": "fabled", - "edition": "fabled", - "ability": "If 4 or more players live, each living player may publicly choose (once per game) that a player of their own alignment dies.", - "flavor": "And on the Seventh Day, there shall be a great flood and a pestilence upon the People of the Village of the Ravens! The dead shall rise and the living shall repent! O Woe! O Unholy day! Only by great sacrifice shall they prevail! So sayeth the Sages of Nostros and so sayeth I." - }, - { - "id": "duchess", - "firstNightReminder": "", - "otherNightReminder": "Wake each player marked *VISITOR* or *FALSE INFO* one at a time. Show them the Duchess token, then fingers (1, 2, 3) equaling the number of evil players marked “Visitor” or, if you are waking the player marked “False Info,” show them any number of fingers except the number of evil players marked “Visitor.”", - "reminders": [ - "Visitor", - "Visitor", - "False Info" - ], - "setup": false, - "name": "Duchess", - "team": "fabled", - "edition": "fabled", - "ability": "Each day, 3 players may choose to visit you. At night*, each visitor learns how many visitors are evil, but 1 gets false info.", - "flavor": "We shall entertain between the hours of 6 and 7 precisely. Tea at 6:15. Scones at 6:45. Do not be late. Formal wear applies, as always." - }, - { - "id": "ferryman", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Ferryman", - "team": "fabled", - "edition": "fabled", - "ability": "On the final day, all dead players regain their vote token.", - "special": [ - { - "name": "ghost-votes", - "type": "ability", - "time": "day" - } - ], - "flavor": "When righteous dreams come, they have the weight of truth." - }, - { - "id": "fibbin", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No Ability" - ], - "setup": false, - "name": "Fibbin", - "team": "fabled", - "edition": "fabled", - "ability": "Once per game, 1 good player might get incorrect information.", - "flavor": "Tee-hee-hee. Tee. Hee. Hee." - }, - { - "id": "fiddler", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Fiddler", - "team": "fabled", - "edition": "fabled", - "ability": "Once per game, the Demon secretly chooses an opposing player: all players choose which of these 2 players win.", - "special": [ - { - "name": "pointing", - "type": "ability", - "time": "day" - } - ], - "flavor": "I'll wager mi lyef ye cannae best me in a fiddle contest, ye boss-eyed snook! We'll go out on the lash, get the pub jammers an' have a right craic. I'll be layin' ma boots into ya come mornin' ye rumbly muppet." - }, - { - "id": "hellslibrarian", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Something Bad" - ], - "setup": false, - "name": "Hell's Librarian", - "team": "fabled", - "edition": "fabled", - "ability": "Something bad might happen to whoever talks when the Storyteller has asked for silence.", - "flavor": "Shhhhhh. Please be quiet. It is best not to disturb the Librarian. I've heard it has a temper." - }, - { - "id": "revolutionary", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "Register Falsely?", - "Aligned", - "Aligned" - ], - "setup": false, - "name": "Revolutionary", - "team": "fabled", - "edition": "fabled", - "ability": "2 neighboring players are known to be the same alignment. Once per game, 1 of them registers falsely.", - "flavor": "United we feigned. Divided, we stalled." - }, - { - "id": "sentinel", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": true, - "name": "Sentinel", - "team": "fabled", - "edition": "fabled", - "ability": "There might be 1 extra or 1 fewer Outsider in play.", - "flavor": "Name, please. Papers, please. Weapons, please." - }, - { - "id": "spiritofivory", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [ - "No More Evil" - ], - "setup": false, - "name": "Spirit of Ivory", - "team": "fabled", - "edition": "fabled", - "ability": "There can't be more than 1 extra evil player.", - "flavor": "The Wasteland calls. Bones rise to flesh, then fall to dust. The great spirit grows. The great spirit watches. The great spirit guides. The human listens, or the human is no more." - }, - { - "id": "toymaker", - "firstNightReminder": "Resolve the Minion Info and Demon Info steps even though there are fewer than 7 players.", - "otherNightReminder": "If it is a night when a Demon attack could end the game, and the Demon is marked *FINAL NIGHT: NO ATTACK*, then the Demon does not act tonight. (Do not wake them.)", - "reminders": [ - "Final Night: No Attack" - ], - "setup": false, - "name": "Toymaker", - "team": "fabled", - "edition": "fabled", - "ability": "The Demon may choose not to attack & must do this at least once per game. Evil players get normal starting info.", - "flavor": "It buzzes! It walks down stairs! It keeps you warm at night! It tastes like sugar! The kiddies love it! Introducing... the brand new... Warm'o-buzzy-wuzzy-walk'a'bot-thingy-contraption! Fun for all ages!" - }, - { - "id": "bootlegger", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Bootlegger", - "team": "loric", - "edition": "loric", - "ability": "This script has homebrew characters or rules.", - "flavor": "When I was born, I was named ‘Homebrewy McHomebrewface’, like my father before me. A respectable name, for a dignified age." - }, - { - "id": "bigwig", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Big Wig", - "team": "loric", - "edition": "loric", - "ability": "Each nominee chooses a player: until voting, only they may speak & they are mad the nominee is good or they might die.", - "flavor": "Vanity asks ‘Is it popular?’ Cowardice asks ‘Is it safe?’ Conscience asks ‘Is it right?’ Who among us will ask: ‘Is it true?’" - }, - { - "id": "gardener", - "firstNightReminder": "", - "otherNightReminder": "", - "reminders": [], - "setup": false, - "name": "Gardener", - "team": "loric", - "edition": "loric", - "ability": "The Storyteller assigns 1 or more players' characters.", - "special": [ - { - "name": "distribute-roles", - "type": "ability", - "time": "pregame" - } - ], - "flavor": "When sophistry becomes stupidity and hypocrisy cruelty, retreat to the garden. The final love of old men is flowers and stones." - }, - { - "id": "stormcatcher", - "firstNightReminder": "At the start of the night, announce which character is stormcaught. If that character is in play, mark that player as *STORMCAUGHT*. :reminder: Wake each evil player and show them the character token, then the marked player. If not in play, wake each evil player, show them the *THESE CHARACTERS ARE NOT IN PLAY* token and the relevant character token.", - "otherNightReminder": "", - "reminders": [ - "Stormcaught" - ], - "setup": false, - "name": "Storm Catcher", - "team": "loric", - "edition": "loric", - "ability": "Name a good character. If in play, they can only die by execution, but evil players learn which player it is.", - "flavor": "At dawn, the temple’s long shadow creeps to the fountain. At dusk, the obelisk blocks the red glare, cooling warm water under the archway. All lines converge here. A storm is coming, and this, this pebbled and lush and holy place between the apple trees, is the eye." - }, - { - "id": "tor", - "firstNightReminder": "", - "otherNightReminder": "If a player died tonight, show the *YOU ARE* info token, their character token, & a thumbs up or thumbs down.", - "reminders": [], - "setup": true, - "name": "Tor", - "team": "loric", - "edition": "loric", - "ability": "Players don't know their character or alignment. They learn them when they die.", - "flavor": "With thunder as my voice and lightning as my blade, I, the eternal guardian, feast upon the fools who dare approach the forbidden gate. Behold, my sacred goal! To purge the beetle from the belly of the rocky earth, to ensnare it in a net of stars, on the hilltop where heaven meets earth." - } -] \ No newline at end of file diff --git a/src/botc_tokens/helpers/text_tools.py b/src/botc_tokens/helpers/text_tools.py index 913d905..473a5d6 100644 --- a/src/botc_tokens/helpers/text_tools.py +++ b/src/botc_tokens/helpers/text_tools.py @@ -173,12 +173,3 @@ def format_filename(in_string): in_string = in_string.replace(' ', '_').replace('/', '-').replace('\\', '-').replace(':', '-').replace('?', 'Q') file_name = ''.join(char for char in in_string if char in valid_chars) return file_name - -def replace_font_in_text(in_string): - """Take a string and return a font family conform text, so that text can be rendered. - - Args: - in_string: The string to convert to a font family conform text. - - """ - return in_string.replace('ő', 'ô').replace('ű', 'û').replace('Ő', 'Ô').replace('Ű', 'Û') \ No newline at end of file diff --git a/src/botc_tokens/helpers/wiki_soup.py b/src/botc_tokens/helpers/wiki_soup.py index a8ab448..5ac6188 100644 --- a/src/botc_tokens/helpers/wiki_soup.py +++ b/src/botc_tokens/helpers/wiki_soup.py @@ -5,7 +5,6 @@ from bs4 import BeautifulSoup -from .text_tools import replace_font_in_text from .. import data_dir @@ -32,10 +31,10 @@ def load_from_web(self): night_from_web = urlopen("https://script.bloodontheclocktower.com/data/nightsheet.json").read().decode('utf-8') self.night_data = json.loads(night_from_web) - def load_from_bloodstar(self, bloodstar_url): - """Load the role data from the bloodstar site.""" - roles_from_bloodstar = json.loads(urlopen(bloodstar_url).read().decode('utf-8')) - meta_data = next((item for item in roles_from_bloodstar if item.get('id') == '_meta'), None) + def load_from_web_json(self, custom_url): + """Load the role data from the provided web hosted JSON file.""" + roles_from_web_json = json.loads(urlopen(custom_url).read().decode('utf-8')) + meta_data = next((item for item in roles_from_web_json if item.get('id') == '_meta'), None) version_name = None if meta_data: version_name = meta_data.get('name') @@ -43,11 +42,11 @@ def load_from_bloodstar(self, bloodstar_url): print(f"[yellow]Warning:[/] Could not find metadata for the script") # Filter and convert the roles roles_with_version = [] - for role in roles_from_bloodstar: + for role in roles_from_web_json: if role.get('id') != '_meta': - role['version'] = version_name - role['name'] = replace_font_in_text(role['name']) - role['ability'] = replace_font_in_text(role['ability']) + role['version'] = version_name if role.get('version') is None else role['version'] + role['name'] = role['name'] + role['ability'] = role.get('ability') roles_with_version.append(role) self.role_data = roles_with_version @@ -83,7 +82,7 @@ def get_ability_text(self, role_name): ability = ability.replace("\"", "'") return ability - def get_reminders(self, role_name, skip_reminders=False): + def get_reminders(self, role_name): """Take a role name and grab the reminders.""" # First check if we have the role in the override reminders file if role_name in self.reminder_overrides: @@ -94,8 +93,6 @@ def get_reminders(self, role_name, skip_reminders=False): return self.reminders[role_name] # If we don't have the role, go get it from the wiki - if skip_reminders: - return [] reminders = set() soup = self._get_wiki_soup(role_name) reminder_title = soup.find(id="How_to_Run") diff --git a/tests/commands/test_update.py b/tests/commands/test_update.py index 7a490d7..f48d916 100644 --- a/tests/commands/test_update.py +++ b/tests/commands/test_update.py @@ -7,27 +7,41 @@ from unittest.mock import MagicMock, patch from urllib.error import HTTPError -# Third Party -from testhelpers import check_output_folder, expected_role_json, webmock_list - # Application Specific from botc_tokens.commands import update from botc_tokens.helpers.role import Role +# Third Party +from testhelpers import check_output_folder, expected_role_json, webmock_list + + +def web_response(url): + web_read_mock = MagicMock() + if url.find('roles.json') != -1: + web_read_mock.read.return_value = webmock_list[0] + elif url.find('nightsheet.json') != -1: + web_read_mock.read.return_value = webmock_list[1] + elif url.find('/First') != -1: + web_read_mock.read.return_value = webmock_list[2] + elif url.find('/Second') != -1: + web_read_mock.read.return_value = webmock_list[3] + elif url.find('/Third') != -1: + web_read_mock.read.return_value = webmock_list[4] + elif url.find('bloodstar') != -1: + web_read_mock.read.return_value = webmock_list[5] + return web_read_mock @contextmanager def web_mock(): """Mock out actual web access.""" # First create the return data we would expect from the web, in the order we expect it. - wiki_read_mock = MagicMock() - wiki_read_mock.read.side_effect = webmock_list image_read_mock = MagicMock() image_read_mock.read.return_value = (Path(__file__).parent.parent / "data" / "icons" / "1.png").read_bytes() # Now mock out all the web calls to instead return the data we created with patch("botc_tokens.helpers.wiki_soup.urlopen") as wiki_soup_urlopen_mock: - wiki_soup_urlopen_mock.return_value.__enter__.return_value.read = wiki_read_mock - wiki_soup_urlopen_mock.return_value = wiki_read_mock + wiki_soup_urlopen_mock.return_value.__enter__.reuturn_value.read = web_response + wiki_soup_urlopen_mock.side_effect = web_response # Make sure to patch it in the update command as well, since we don't want to actually download the images with patch("botc_tokens.commands.update.urlopen") as update_urlopen_mock: update_urlopen_mock.return_value.__enter__.return_value.read = image_read_mock @@ -55,19 +69,6 @@ def check_expected_json(input_file_path): j = json.load(f) assert j == expected_role_json.get(input_file_path.name) -def check_expected_folder(input_folder_path): - """Ensure each folder exists and, if it is the same amount of json file, and png file is in it.""" - assert input_folder_path.is_dir() - json_cnt = 0 - png_cnt = 0 - for item in input_folder_path.iterdir(): - if item.is_file(): - if item.suffix == '.json': - json_cnt += 1 - elif item.suffix == '.png': - png_cnt += 1 - assert json_cnt == png_cnt - def test_update_command(tmp_path): """Test the update command in its normal configuration.""" @@ -84,42 +85,15 @@ def test_update_command(tmp_path): ] check_output_folder(output_path, expected_files=expected_files, check_func=check_expected_json) -def test_update_command_built_in(tmp_path): - """Test the update command with built-in roles.""" - output_path = tmp_path / "roles" - _run_cmd(["--output", str(output_path), "--use-built-in", "--use-playtest"]) - - # Verify that it worked - expected_folders = [ - str(Path("bmr") / "demon"), - str(Path("bmr") / "minion"), - str(Path("bmr") / "outsider"), - str(Path("bmr") / "townsfolk"), - str(Path("bmr") / "traveller"), - str(Path("tb") / "demon"), - str(Path("tb") / "minion"), - str(Path("tb") / "outsider"), - str(Path("tb") / "townsfolk"), - str(Path("tb") / "traveller"), - str(Path("snv") / "demon"), - str(Path("snv") / "minion"), - str(Path("snv") / "outsider"), - str(Path("snv") / "townsfolk"), - str(Path("snv") / "traveller"), - str(Path("carousel") / "demon"), - str(Path("fabled") / "fabled"), - str(Path("loric") / "loric") - ] - for folder in expected_folders: - check_expected_folder(output_path / folder) -def test_update_command_bloodstar_url(tmp_path): - """Test the update command with bloodstar url roles.""" +def test_update_command_custom_json_url(tmp_path): + """Test the update command with web hosted custom json url roles.""" reminders_file = tmp_path / "reminders.json" with open(reminders_file, "w") as f: json.dump({"Second": ["SECOND REMINDER"]}, f) output_path = tmp_path / "roles" - _run_cmd(["--output", str(output_path), "--bloodstar-url", "https://bloodstar.xyz/p/user/script/script.json?1", "--reminders", str(reminders_file)]) + _run_cmd(["--output", str(output_path), "-c", "https://bloodstar.xyz/p/user/script/script.json?1", "--reminders", + str(reminders_file)]) # Verify that it worked expected_files = [ @@ -170,7 +144,6 @@ def test_update_bad_json(tmp_path, capsys): str(Path("54 - Unreal Experimental") / "demon" / "second.json"), str(Path("54 - Unreal Experimental") / "demon" / "second.png"), str(Path("99 - Ignored") / "outsider" / "third.json"), - str(Path("99 - Ignored") / "outsider" / "third.png"), ] check_output_folder(output_path, expected_files=expected_files) @@ -191,7 +164,6 @@ def test_update_script_filter(tmp_path): # Verify that it worked expected_files = [ str(Path("99 - Ignored") / "outsider" / "third.json"), - str(Path("99 - Ignored") / "outsider" / "third.png"), ] check_output_folder(output_path, expected_files=expected_files) @@ -264,7 +236,7 @@ def test_update_existing_icon_and_json(tmp_path): json_path.parent.mkdir(parents=True, exist_ok=True) with open(json_path, "w") as f: json.dump(expected_role_json.get("first.json"), f) - _run_cmd(["--output", str(output_path)]) + _run_cmd(["--output", str(output_path), "--use-playtest"]) # Verify that it worked expected_files = [ diff --git a/tests/testhelpers.py b/tests/testhelpers.py index c7735ca..c6f09b8 100644 --- a/tests/testhelpers.py +++ b/tests/testhelpers.py @@ -12,6 +12,7 @@ def check_output_folder(output_path, expected_files, check_func=None): :param check_func: Function for checking each file in expected_files. Defaults to checking that the file exists. """ + # Default check function, in case one isn't passed in. def default_check(input_file_path): """Ensure each file exists and is a file.""" @@ -50,34 +51,6 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): assert (expected_text in output.out) or (expected_text in output.err) -webmock_bloodstar = [ - # Bloodstar url sample response - b"""[ - { - "id": "_meta", - "name": "Test" - }, - { - "id": "first", - "image": "https://www.bloodstar.xyz/first.png", - "reminders": [ - "No ability" - ], - "name": "First", - "team": "townsfolk", - "ability": "First ability" - }, - { - "id": "second", - "image": "https://www.bloodstar.xyz/second.png", - "name": "Second", - "team": "demon", - "ability": "Second ability", - "otherNightReminder": "reminder text" - } -]""" -] - webmock_list = [ # The first call is for the role data b"""[ @@ -147,9 +120,38 @@ def expect_exit_with_output(capsys, expected_text, expected_code=1): """, + b"""[ + { + "id": "_meta", + "name": "54 - Unreal Experimental" + }, + { + "id": "first", + "image": "https://www.bloodstar.xyz/first.png", + "name": "First", + "team": "townsfolk", + "firstNightReminder": "first night wakes", + "otherNightReminder": "other nights wakes" + }, + { + "id": "second", + "image": "https://www.bloodstar.xyz/second.png", + "name": "Second", + "team": "demon", + "otherNightReminder": "reminder text" + }, + { + "id": "third", + "name": "Third", + "roleType": "outsider", + "print": "unused", + "icon": "unused", + "version": "99 - Ignored", + "isDisabled": false + } + ]""", ] - expected_role_json = { "first.json": { 'id': 'first', From f67c9fd23049315199159eec9c402027a5aa014e Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 12 Dec 2025 06:52:49 +0000 Subject: [PATCH 5/7] Added review comment fixes --- src/botc_tokens/commands/create.py | 4 ++-- src/botc_tokens/commands/update.py | 16 +++++++++------- src/botc_tokens/helpers/wiki_soup.py | 2 -- tests/commands/test_update.py | 5 +++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/botc_tokens/commands/create.py b/src/botc_tokens/commands/create.py index 5959224..1d204af 100644 --- a/src/botc_tokens/commands/create.py +++ b/src/botc_tokens/commands/create.py @@ -136,8 +136,8 @@ def load_components(component_package): print(f"\n[red]Error:[/][bold] Could not load component: {str(e)}[/]") return None except BadZipFile: - print(f"\n[red]Error:[/][bold] Could not load components from '{component_package}' it does not appear to be a \n" - "valid components package.[/]") + print(f"\n[red]Error:[/][bold] Could not load components from '{component_package}' it does not appear" + " to be a \nvalid components package.[/]") return None except FileNotFoundError as e: print(f"\n[red]Error:[/][bold] Unable to load components from '{component_package}': {str(e)}") diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 00c789e..10bb202 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -159,13 +159,13 @@ def prep_wiki(script_filter, custom_list=None): return None with open(custom_list_path, "r") as f: custom_list = json.load(f) - try: - validate(custom_list, json.load(open(data_dir / "role_schema.json"))) - except ValidationError as e: - print(f"[yellow]Warning:[/] The custom json specified does not fit the copy of the TPI schema that I " - f"have. Specifically: {e}" - f"\n\nI will continue, but [yellow]be warned that it might not work[/].") wiki.role_data = custom_list + try: + validate(wiki.role_data, json.load(open(data_dir / "role_schema.json"))) + except ValidationError as e: + print(f"[yellow]Warning:[/] The custom json specified does not fit the copy of the TPI schema that I " + f"have. Specifically: {e}" + f"\n\nI will continue, but [yellow]be warned that it might not work[/].") else: # Download the official lists from the script tool wiki.load_from_web() @@ -186,7 +186,7 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u """ name = role['name'] role_id = role['id'] - found_role = Role(id= role_id, name=name) + found_role = Role(id=role_id, name=name) # Check if we have a json file for the role if file.exists(): @@ -243,11 +243,13 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u return found_role + def has_value(value): if isinstance(value, str): return len(value) > 0 return False + def get_role_icon(found_role, role, role_output_path, wiki, use_playtest=False): """Get the icon for a role, using the wiki if needed. diff --git a/src/botc_tokens/helpers/wiki_soup.py b/src/botc_tokens/helpers/wiki_soup.py index 5ac6188..1ba2cf3 100644 --- a/src/botc_tokens/helpers/wiki_soup.py +++ b/src/botc_tokens/helpers/wiki_soup.py @@ -38,8 +38,6 @@ def load_from_web_json(self, custom_url): version_name = None if meta_data: version_name = meta_data.get('name') - else: - print(f"[yellow]Warning:[/] Could not find metadata for the script") # Filter and convert the roles roles_with_version = [] for role in roles_from_web_json: diff --git a/tests/commands/test_update.py b/tests/commands/test_update.py index f48d916..8b267c8 100644 --- a/tests/commands/test_update.py +++ b/tests/commands/test_update.py @@ -7,11 +7,12 @@ from unittest.mock import MagicMock, patch from urllib.error import HTTPError +# Third Party +from testhelpers import check_output_folder, expected_role_json, webmock_list + # Application Specific from botc_tokens.commands import update from botc_tokens.helpers.role import Role -# Third Party -from testhelpers import check_output_folder, expected_role_json, webmock_list def web_response(url): From 88aa139549494429e283ffba5eac8511fbde9338 Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Fri, 12 Dec 2025 06:55:03 +0000 Subject: [PATCH 6/7] Fix import order --- src/botc_tokens/commands/update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 10bb202..5cd5e8d 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -3,13 +3,13 @@ """Download story from the requested url.""" # Standard library import argparse -import json -import sys -import urllib.parse from dataclasses import asdict +import json from pathlib import Path +import sys from time import sleep from urllib.error import HTTPError +import urllib.parse from urllib.request import Request, urlopen # Third-party libraries From 0db47fe7d527677869497462ec88e10197c950f9 Mon Sep 17 00:00:00 2001 From: Artur Mano Marschal Date: Thu, 18 Dec 2025 10:45:59 +0100 Subject: [PATCH 7/7] Fix lint issues --- src/botc_tokens/commands/update.py | 5 +++++ tests/commands/test_update.py | 5 +++++ tests/testhelpers.py | 1 - 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/botc_tokens/commands/update.py b/src/botc_tokens/commands/update.py index 5cd5e8d..b915da4 100644 --- a/src/botc_tokens/commands/update.py +++ b/src/botc_tokens/commands/update.py @@ -245,6 +245,11 @@ def process_role(role, file, wiki, step_progress, step_task, role_output_path, u def has_value(value): + """Checks if the provided value is a non-empty string. + + Args: + value (str): The value to be checked. + """ if isinstance(value, str): return len(value) > 0 return False diff --git a/tests/commands/test_update.py b/tests/commands/test_update.py index 8b267c8..d56e2be 100644 --- a/tests/commands/test_update.py +++ b/tests/commands/test_update.py @@ -16,6 +16,11 @@ def web_response(url): + """Routes mock response based on URL. + + Args: + url (str): The URL on which the call has been made. + """ web_read_mock = MagicMock() if url.find('roles.json') != -1: web_read_mock.read.return_value = webmock_list[0] diff --git a/tests/testhelpers.py b/tests/testhelpers.py index c6f09b8..afb6053 100644 --- a/tests/testhelpers.py +++ b/tests/testhelpers.py @@ -12,7 +12,6 @@ def check_output_folder(output_path, expected_files, check_func=None): :param check_func: Function for checking each file in expected_files. Defaults to checking that the file exists. """ - # Default check function, in case one isn't passed in. def default_check(input_file_path): """Ensure each file exists and is a file."""