From f54c4dfc7f48ed0686ae679c0685687b1bf881f0 Mon Sep 17 00:00:00 2001 From: Luke Murphy <60206183+lukemurphy147@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:38:20 -0400 Subject: [PATCH 1/2] Fixed tie-vote handling --- bot.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 053cac4..3870d56 100644 --- a/bot.py +++ b/bot.py @@ -157,6 +157,25 @@ def retry_mastodon_call(self, func, *args, retries=5, interval=10, **kwargs): time.sleep(interval) return False # Failed to execute + def strip_emoji(result): + match result.lower(): + case "up ⬆️": + return "up" + case "down ⬇️": + return "down" + case "left ⬅️": + return "left" + case "right ➡️": + return "right" + case "🅰": + return "a" + case "🅱": + return "b" + case "start": + return "start" + case "select": + return "select" + def run(self): """ Runs the main gameplay, reads mastodon poll result, takes action, generates new posts @@ -174,13 +193,19 @@ def run(self): poll_status = self.mastodon.status(poll_id) poll_results = poll_status.poll["options"] - max_result = max(poll_results, key=lambda x: x["votes_count"]) - if max_result["votes_count"] == 0: + max_votes = max(map(lambda x: x["votes_count"], poll_results)) + if max_votes == 0: button = self.random_button() top_result = f"Random (no votes, chose {button})" else: - top_result = max_result["title"] - self.take_action(top_result) + top_results = [x for x in poll_results if x["votes_count"] == max_votes] + if len(top_results) > 1: + top_result = random.choice(top_results)["title"] + self.take_action(top_result) + top_result = f"Random (tie vote, chose {self.strip_emoji(top_result)})" + else: + top_result = top_results[0]["title"] + self.take_action(top_result) frames = self.gameboy.loop_until_stopped() result = False From 426e52cda4f882440fc10aec218c0c601b837600 Mon Sep 17 00:00:00 2001 From: Luke Murphy <60206183+lukemurphy147@users.noreply.github.com> Date: Wed, 10 Jul 2024 00:22:21 -0400 Subject: [PATCH 2/2] Made formatting consistent Made button name formatting consistent (except in "test()"). Also, fixed the formatting of "Game Boy", "Mastodon", etc. except where it would break the code. --- bot.py | 184 ++++++++++++++++++++++++--------------------------------- gb.py | 40 ++++++------- 2 files changed, 97 insertions(+), 127 deletions(-) diff --git a/bot.py b/bot.py index 3870d56..f429b96 100644 --- a/bot.py +++ b/bot.py @@ -1,5 +1,5 @@ """ - A bot that interacts with a Mastodon compatible API, plays gameboy games via a Poll + A bot that interacts with a Mastodon compatible API, plays Game Boy games via a poll """ import os @@ -10,7 +10,7 @@ from mastodon import Mastodon from requests.exceptions import RequestException -from gb import Gameboy +from gb import GameBoy script_dir = os.path.dirname(os.path.realpath(__file__)) @@ -19,7 +19,7 @@ class Bot: """ - A Mastodon-API Compatible bot that handles gameboy gameplay through polls + A Mastodon-API compatible bot that handles Game Boy gameplay through polls """ def __init__(self, config_path="config.toml"): @@ -29,61 +29,43 @@ def __init__(self, config_path="config.toml"): with open(config_path, "r", encoding="utf-8") as config_file: self.config = toml.load(config_file) self.mastodon_config = self.config.get("mastodon", {}) - self.gameboy_config = self.config.get("gameboy", {}) + self.game_boy_config = self.config.get("gameboy", {}) self.mastodon = self.login() - print(self.gameboy_config.get("rom")) - rom = os.path.join(script_dir, self.gameboy_config.get("rom")) - self.gameboy = Gameboy(rom, True) + print(self.game_boy_config.get("rom")) + rom = os.path.join(script_dir, self.game_boy_config.get("rom")) + self.game_boy = GameBoy(rom, True) def simulate(self): - """Simulates gameboy actions by pressing random buttons, useful for testing""" + """Simulates Game Boy actions by pressing random buttons, useful for testing""" while True: - # print(self.gameboy.is_running()) - # self.gameboy.random_button() + # print(self.game_boy.is_running()) + # self.game_boy.random_button() buttons = { - "a": self.gameboy.a, - "b": self.gameboy.b, - "start": self.gameboy.start, - "select": self.gameboy.select, - "up": self.gameboy.dpad_up, - "down": self.gameboy.dpad_down, - "right": self.gameboy.dpad_right, - "left": self.gameboy.dpad_left, - "random": self.gameboy.random_button, + "a": self.game_boy.a, + "b": self.game_boy.b, + "start": self.game_boy.start, + "select": self.game_boy.select, + "up": self.game_boy.dpad_up, + "down": self.game_boy.dpad_down, + "right": self.game_boy.dpad_right, + "left": self.game_boy.dpad_left, + "random": self.game_boy.random_button, "tick": "tick", } - # self.gameboy.random_button() + # self.game_boy.random_button() print(buttons) press = input("Button: ") if press == "tick": for _ in range(60): - self.gameboy.pyboy.tick() + self.game_boy.pyboy.tick() else: buttons[press]() - self.gameboy.pyboy.tick() + self.game_boy.pyboy.tick() # time.sleep(1) - def random_button(self): - """Chooses a random button and presses it on the gameboy""" - buttons = { - "a": self.gameboy.a, - "b": self.gameboy.b, - "start": self.gameboy.start, - "select": self.gameboy.select, - "up": self.gameboy.dpad_up, - "down": self.gameboy.dpad_down, - "right": self.gameboy.dpad_right, - "left": self.gameboy.dpad_left, - } - - random_button = random.choice(list(buttons.keys())) - action = buttons[random_button] - action() - return random_button - def login(self): - """Logs into the mastodon server using config credentials""" + """Logs into the Mastodon server using config credentials""" server = self.mastodon_config.get("server") print(f"Logging into {server}") return Mastodon( @@ -128,27 +110,34 @@ def unpin_posts(self, post_id, poll_id): self.mastodon.status_unpin(poll_id) def take_action(self, result): - """Presses button on gameboy based on poll result""" + """Presses button on Game Boy based on poll result""" buttons = { - "up ⬆️": self.gameboy.dpad_up, - "down ⬇️": self.gameboy.dpad_down, - "right ➡️": self.gameboy.dpad_right, - "left ⬅️": self.gameboy.dpad_left, - "🅰": self.gameboy.a, - "🅱": self.gameboy.b, - "start": self.gameboy.start, - "select": self.gameboy.select, + "Up ⬆️": self.game_boy.dpad_up, + "Down ⬇️": self.game_boy.dpad_down, + "Right ➡️": self.game_boy.dpad_right, + "Left ⬅️": self.game_boy.dpad_left, + "🅰": self.game_boy.a, + "🅱": self.game_boy.b, + "Start": self.game_boy.start, + "Select": self.game_boy.select, } print(buttons) # Perform the corresponding action - if result.lower() in buttons: - action = buttons[result.lower()] + if result in buttons: + action = buttons[result] + action() + return result + elif result == "random": + random_button = random.choice(list(buttons.keys())) + action = buttons[random_button] action() + return random_button else: print(f"No action defined for '{result}'.") + return "INVALID BUTTON" def retry_mastodon_call(self, func, *args, retries=5, interval=10, **kwargs): - """Continuously retries mastodon call, useful for servers with timeout issues""" + """Continuously retries Mastodon call, useful for servers with timeout issues""" for _ in range(retries): try: return func(*args, **kwargs) @@ -157,30 +146,11 @@ def retry_mastodon_call(self, func, *args, retries=5, interval=10, **kwargs): time.sleep(interval) return False # Failed to execute - def strip_emoji(result): - match result.lower(): - case "up ⬆️": - return "up" - case "down ⬇️": - return "down" - case "left ⬅️": - return "left" - case "right ➡️": - return "right" - case "🅰": - return "a" - case "🅱": - return "b" - case "start": - return "start" - case "select": - return "select" - def run(self): """ - Runs the main gameplay, reads mastodon poll result, takes action, generates new posts + Runs the main gameplay, reads Mastodon poll result, takes action, generates new posts """ - self.gameboy.load() + self.game_boy.load() post_id, poll_id = self.read_ids() top_result = None @@ -195,27 +165,27 @@ def run(self): poll_results = poll_status.poll["options"] max_votes = max(map(lambda x: x["votes_count"], poll_results)) if max_votes == 0: - button = self.random_button() - top_result = f"Random (no votes, chose {button})" + random_button = self.take_action("random") + top_result = f"Random (no votes, chose {random_button})" else: top_results = [x for x in poll_results if x["votes_count"] == max_votes] if len(top_results) > 1: top_result = random.choice(top_results)["title"] self.take_action(top_result) - top_result = f"Random (tie vote, chose {self.strip_emoji(top_result)})" + top_result = f"Random (tie vote, chose {top_result})" else: top_result = top_results[0]["title"] self.take_action(top_result) - frames = self.gameboy.loop_until_stopped() + frames = self.game_boy.loop_until_stopped() result = False if frames >= 70: - result = self.gameboy.build_gif("gif_images", self.gameboy_config['gif_outline']) + result = self.game_boy.build_gif("gif_images", self.game_boy_config['gif_outline']) else: - self.gameboy.empty_directory(gif_dir) + self.game_boy.empty_directory(gif_dir) - image = self.gameboy.screenshot() - alt_text = 'Screenshot of ' + self.gameboy_config.get('title', 'a Game Boy game.') + image = self.game_boy.screenshot() + alt_text = 'Screenshot of ' + self.game_boy_config.get('title', 'a Game Boy game.') media = self.retry_mastodon_call( self.mastodon.media_post, retries=5, @@ -227,9 +197,9 @@ def run(self): # Probably add a check here if generating a gif is enabled (so we don't # have to generate one every single hour?) try: - previous_frames = self.gameboy.get_recent_frames("screenshots", + previous_frames = self.game_boy.get_recent_frames("screenshots", 25, - self.gameboy_config['gif_outline'] + self.game_boy_config['gif_outline'] ) previous_media = self.retry_mastodon_call( self.mastodon.media_post, @@ -249,7 +219,7 @@ def run(self): interval=10, status=( f"Previous Action: {top_result}\n\n" - "#pokemon #gameboy #nintendo #FediPlaysPokemon" + "#Pokemon #GameBoy #Nintendo #FediPlaysPokemon" ), media_ids=[media_ids], ) @@ -289,7 +259,7 @@ def run(self): retries=5, interval=10, media_file=result, - description="Video of pokemon gold movement", + description="Video of Pokémon Gold movement", ) self.retry_mastodon_call( self.mastodon.status_post, @@ -303,41 +273,41 @@ def run(self): self.save_ids(post["id"], poll["id"]) # Save game state - self.gameboy.save() + self.game_boy.save() def test(self): """Method used for testing""" - self.gameboy.load() - self.gameboy.get_recent_frames("screenshots", 25) - # self.gameboy.build_gif("gif_images") + self.game_boy.load() + self.game_boy.get_recent_frames("screenshots", 25) + # self.game_boy.build_gif("gif_images") while True: inp = input("Action: ") buttons = { - "up": self.gameboy.dpad_up, - "down": self.gameboy.dpad_down, - "right": self.gameboy.dpad_right, - "left": self.gameboy.dpad_left, - "a": self.gameboy.a, - "b": self.gameboy.b, - "start": self.gameboy.start, - "select": self.gameboy.select, + "up": self.game_boy.dpad_up, + "down": self.game_boy.dpad_down, + "right": self.game_boy.dpad_right, + "left": self.game_boy.dpad_left, + "a": self.game_boy.a, + "b": self.game_boy.b, + "start": self.game_boy.start, + "select": self.game_boy.select, } # Perform the corresponding action - if inp.lower() in buttons: - action = buttons[inp.lower()] - # self.gameboy.tick() + if inp in buttons: + action = buttons[inp] + # self.game_boy.tick() action() - frames = self.gameboy.loop_until_stopped() + frames = self.game_boy.loop_until_stopped() if frames > 51: - self.gameboy.build_gif("gif_images") + self.game_boy.build_gif("gif_images") else: - self.gameboy.empty_directory(gif_dir) + self.game_boy.empty_directory(gif_dir) else: print(f"No action defined for '{inp}'.") - self.gameboy.save() - # self.gameboy.build_gif("gif_images") + self.game_boy.save() + # self.game_boy.build_gif("gif_images") # self.take_action(inp) - # self.gameboy.tick(300) + # self.game_boy.tick(300) if __name__ == "__main__": diff --git a/gb.py b/gb.py index f04ff0c..451c192 100644 --- a/gb.py +++ b/gb.py @@ -16,8 +16,8 @@ script_dir = os.path.dirname(os.path.realpath(__file__)) save_loc = os.path.join(script_dir, "save.state") -class Gameboy: - """Provides an easy way to interface with pyboy +class GameBoy: + """Provides an easy way to interface with PyBoy Args: rom (str): A string pointing to a rom file (MUST be GB or GBC, no GBA files) @@ -36,13 +36,13 @@ def is_running(self): return self.running def run(self) -> None: - """Continuously loop while pressing random buttons on the gameboy""" + """Continuously loop while pressing random buttons on the Game Boy""" self.running = True while True: self.random_button() def tick(self, ticks=1, gif=True): - """Advances the gameboy by a specified number of frames. + """Advances the Game Boy by a specified number of frames. Args: ticks (int, optional): The number of frames to advance. Defaults to 1 @@ -55,7 +55,7 @@ def tick(self, ticks=1, gif=True): def compare_frames(self, frame1, frame2): """ - Compares two frames from gameboy screenshot, returns a percentage difference between the two + Compares two frames from Game Boy screenshot, returns a percentage difference between the two """ arr1 = np.array(frame1) arr2 = np.array(frame2) @@ -113,12 +113,12 @@ def build_gif(self, image_path, delete=True, fps=120, output_name="action.mp4", print(len(image_files)) for file in image_files: - gameboy_outline = Image.open( + game_boy_outline = Image.open( os.path.join(script_dir, gif_outline) ).convert("RGB") img = Image.open(os.path.join(gif_dir, file)).convert("RGB") img = img.resize((822, 733)) - combined = gameboy_outline.copy() + combined = game_boy_outline.copy() combined.paste(img, (370, 319)) #370, 319 1192, 1052 combined.save(os.path.join(gif_dir, file)) images.append(os.path.join(gif_dir, file)) @@ -137,11 +137,11 @@ def build_gif(self, image_path, delete=True, fps=120, output_name="action.mp4", return False def stop(self): - """Stops the continuous gameboy loop""" + """Stops the continuous Game Boy loop""" self.running = False def load_rom(self, rom): - """Loads the rom into a pyboy object""" + """Loads the rom into a PyBoy object""" return PyBoy( rom, window_type="SDL2" if self.debug else "headless", @@ -151,13 +151,13 @@ def load_rom(self, rom): ) def dpad_up(self) -> None: - """Presses up on the gameboy""" + """Presses up on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_UP) self.tick(4) self.pyboy.send_input(WindowEvent.RELEASE_ARROW_UP) def dpad_down(self) -> None: - """Presses down on the gameboy""" + """Presses down on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) print("down") self.tick(3) @@ -165,7 +165,7 @@ def dpad_down(self) -> None: # self.tick() def dpad_right(self) -> None: - """Presses right on the gameboy""" + """Presses right on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) print("right") self.tick(3) @@ -173,7 +173,7 @@ def dpad_right(self) -> None: # self.tick() def dpad_left(self) -> None: - """Presses left on the gameboy""" + """Presses left on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_LEFT) print("left") self.tick(3) @@ -181,7 +181,7 @@ def dpad_left(self) -> None: # self.tick() def a(self) -> None: - """Presses a on the gameboy""" + """Presses A on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_A) print("a") self.tick(3) @@ -189,7 +189,7 @@ def a(self) -> None: # self.tick() def b(self) -> None: - """Presses b on the gameboy""" + """Presses B on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_B) print("b") self.tick(3) @@ -197,7 +197,7 @@ def b(self) -> None: # self.tick() def start(self) -> None: - """Presses start on the gameboy""" + """Presses Start on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) print("start") self.tick(3) @@ -205,14 +205,14 @@ def start(self) -> None: # self.tick() def select(self) -> None: - """Presses select on the gameboy""" + """Presses Select on the Game Boy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_SELECT) print("select") self.tick(3) self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_SELECT) def screenshot(self, path="screenshots"): - """Takes a screenshot of gameboy screen and saves it to the path""" + """Takes a screenshot of Game Boy screen and saves it to the path""" screenshot_dir = os.path.join(script_dir, path) # Create screenshots directory if it doesn't exist os.makedirs(screenshot_dir, exist_ok=True) @@ -234,7 +234,7 @@ def screenshot(self, path="screenshots"): return screenshot_path_full def random_button(self): - """Picks a random button and presses it on the gameboy""" + """Picks a random button and presses it on the Game Boy""" button = random.choice( [ self.dpad_up, @@ -266,7 +266,7 @@ def save(self): self.pyboy.save_state(file) def loop_until_stopped(self, threshold=1): - """Simulates the gameboy bot""" + """Simulates the Game Boy bot""" running = True previous_frame = None current_frame = None