diff --git a/bot.py b/bot.py index d2b9dbe..8048c0d 100644 --- a/bot.py +++ b/bot.py @@ -1,14 +1,22 @@ +""" + A bot that interacts with a Mastodon compatible API, plays gameboy games via a Poll +""" + import os import random import time import toml from mastodon import Mastodon +from requests.exceptions import RequestException from gb import Gameboy class Bot: + """ + A Mastodon-API Compatible bot that handles gameboy gameplay through polls + """ def __init__(self, config_path="config.toml"): # If config_path is not provided, use the config.toml file in the same @@ -16,7 +24,7 @@ def __init__(self, config_path="config.toml"): # Get the directory of the current script self.script_dir = os.path.dirname(os.path.realpath(__file__)) config_path = os.path.join(self.script_dir, "config.toml") - with open(config_path, "r") as config_file: + 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", {}) @@ -27,34 +35,35 @@ def __init__(self, config_path="config.toml"): self.gameboy = Gameboy(rom, True) def simulate(self): + """Simulates gameboy actions by pressing random buttons, useful for testing""" while True: # print(self.gameboy.is_running()) - if True: - # self.gameboy.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, - "tick": "tick", - } - # self.gameboy.random_button() - print(buttons) - press = input("Button: ") - if press == "tick": - for i in range(60): - self.gameboy.pyboy.tick() - else: - buttons[press]() + # self.gameboy.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, + "tick": "tick", + } + # self.gameboy.random_button() + print(buttons) + press = input("Button: ") + if press == "tick": + for _ in range(60): self.gameboy.pyboy.tick() - # time.sleep(1) + else: + buttons[press]() + self.gameboy.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, @@ -72,6 +81,7 @@ def random_button(self): return random_button def login(self): + """Logs into the mastodon server using config credentials""" server = self.mastodon_config.get("server") print(f"Logging into {server}") return Mastodon( @@ -79,6 +89,7 @@ def login(self): ) def post_poll(self, status, options, expires_in=60 * 60, reply_id=None): + """Posts a poll to Mastodon compatible server""" poll = self.mastodon.make_poll( options, expires_in=expires_in, hide_totals=False ) @@ -87,18 +98,20 @@ def post_poll(self, status, options, expires_in=60 * 60, reply_id=None): ) def save_ids(self, post_id, poll_id): + """Saves post IDs to a text file""" # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) ids_loc = os.path.join(script_dir, "ids.txt") - with open(ids_loc, "w") as file: + with open(ids_loc, "w", encoding="utf-8") as file: file.write(f"{post_id},{poll_id}") def read_ids(self): + """Reads IDs from the text file""" try: # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) ids_loc = os.path.join(script_dir, "ids.txt") - with open(ids_loc, "r") as file: + with open(ids_loc, "r", encoding="utf-8") as file: content = file.read() if content: post_id, poll_id = content.split(",") @@ -106,15 +119,20 @@ def read_ids(self): except FileNotFoundError: return None, None + return None + def pin_posts(self, post_id, poll_id): + """Pin posts to profile""" self.mastodon.status_pin(poll_id) self.mastodon.status_pin(post_id) def unpin_posts(self, post_id, poll_id): + """Unpin posts from profile""" self.mastodon.status_unpin(post_id) self.mastodon.status_unpin(poll_id) def take_action(self, result): + """Presses button on gameboy based on poll result""" buttons = { "up ⬆️": self.gameboy.dpad_up, "down ⬇️": self.gameboy.dpad_down, @@ -133,16 +151,20 @@ def take_action(self, result): else: print(f"No action defined for '{result}'.") - def retry_mastodon_call(self, func, retries=5, interval=10, *args, **kwargs): + def retry_mastodon_call(self, func, *args, retries=5, interval=10, **kwargs): + """Continuously retries mastodon call, useful for servers with timeout issues""" for _ in range(retries): try: return func(*args, **kwargs) - except Exception as e: - print(f"Failure to execute {e}") + except RequestException as e: + print(f"Failure to execute {func.__name__}: {e}") time.sleep(interval) return False # Failed to execute def run(self): + """ + Runs the main gameplay, reads mastodon poll result, takes action, generates new posts + """ self.gameboy.load() post_id, poll_id = self.read_ids() top_result = None @@ -195,26 +217,18 @@ def run(self): media_ids = [media["id"], previous_media["id"]] except BaseException: media_ids = [media["id"]] - # try: - # media = self.mastodon.media_post(image, description='Screenshot of pokemon gold') - # except: - # time.sleep(45) - # media = self.mastodon.media_post(image, description='Screenshot of pokemon gold') - # time.sleep(50) + post = self.retry_mastodon_call( self.mastodon.status_post, retries=5, interval=10, - status=f"Previous Action: {top_result}\n\n#pokemon #gameboy #nintendo #FediPlaysPokemon", + status=( + f"Previous Action: {top_result}\n\n" + "#pokemon #gameboy #nintendo #FediPlaysPokemon" + ), media_ids=[media_ids], ) - # try: - # post = self.mastodon.status_post(f"Previous Action: {top_result}\n\n#pokemon #gameboy #nintendo", media_ids=[media['id']]) - # except: - # time.sleep(30) - # post = self.mastodon.status_post(f"Previous Action: - # {top_result}\n\n#pokemon #gamebody #nintendo", - # media_ids=[media['id']]) + poll = self.retry_mastodon_call( self.post_poll, retries=5, @@ -233,12 +247,6 @@ def run(self): reply_id=post["id"], ) - # ry: - # poll = self.post_poll("Vote on the next action:", ["Up ⬆️", "Down ⬇️", "Right ➡️ ", "Left ⬅️", "🅰", "🅱", "Start", "Select"], reply_id=post['id']) - # except: - # time.sleep(30) - # poll = self.post_poll("Vote on the next action:", ["Up ⬆️", "Down ⬇️", "Right ➡️ ", "Left ⬅️", "🅰", "🅱", "Start", "Select"], reply_id=post['id']) - self.retry_mastodon_call( self.pin_posts, retries=5, @@ -246,13 +254,7 @@ def run(self): post_id=post["id"], poll_id=poll["id"], ) - # try: - # self.pin_posts(post['id'], poll['id']) - # except: - # time.sleep(30) - # self.pin_posts(post['id'], poll['id']) - # result = self.gameboy.build_gif("gif_images") result = False if result: gif = self.retry_mastodon_call( @@ -277,10 +279,11 @@ def run(self): self.gameboy.save() def test(self): + """Method used for testing""" self.gameboy.load() self.gameboy.get_recent_frames("screenshots", 25) # self.gameboy.build_gif("gif_images") - """while True: + while True: inp = input("Action: ") buttons = { "up": self.gameboy.dpad_up, @@ -290,12 +293,12 @@ def test(self): "a": self.gameboy.a, "b": self.gameboy.b, "start": self.gameboy.start, - "select": self.gameboy.select + "select": self.gameboy.select, } # Perform the corresponding action if inp.lower() in buttons: action = buttons[inp.lower()] - #self.gameboy.tick() + # self.gameboy.tick() action() frames = self.gameboy.loop_until_stopped() if frames > 51: @@ -306,9 +309,9 @@ def test(self): else: print(f"No action defined for '{inp}'.") self.gameboy.save() - #self.gameboy.build_gif("gif_images") - #self.take_action(inp) - #self.gameboy.tick(300)""" + # self.gameboy.build_gif("gif_images") + # self.take_action(inp) + # self.gameboy.tick(300) if __name__ == "__main__": diff --git a/gb.py b/gb.py index 451ed3d..3105fa7 100644 --- a/gb.py +++ b/gb.py @@ -1,12 +1,15 @@ +""" + Convenient class to interface with PyBoy +""" + import os import random import re import shutil -import threading import numpy as np from moviepy.editor import ImageSequenceClip -from PIL import Image, ImageDraw +from PIL import Image from pyboy import PyBoy, WindowEvent @@ -17,6 +20,7 @@ class Gameboy: rom (str): A string pointing to a rom file (MUST be GB or GBC, no GBA files) debug (bool, optional): Enable debug mode. Defaults to false """ + def __init__(self, rom, debug=False): self.debug = debug self.rom = rom @@ -25,29 +29,31 @@ def __init__(self, rom, debug=False): self.pyboy.set_emulation_speed(0) def is_running(self): - """ Returns True if bot is running in constant loop mode, false otherwise """ + """Returns True if bot is running in constant loop mode, false otherwise""" return self.running def run(self) -> None: - """ Continuously loop while pressing random buttons on the gameboy """ + """Continuously loop while pressing random buttons on the gameboy""" 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 gameboy by a specified number of frames. Args: ticks (int, optional): The number of frames to advance. Defaults to 1 gif (bool, optional): Generates screenshots for the gif if True """ - for tick in range(ticks): + for _ in range(ticks): if gif: self.screenshot("gif_images") self.pyboy.tick() def compare_frames(self, frame1, frame2): - """ Compares two frames from gameboy screenshot, returns a percentage difference between the two """ + """ + Compares two frames from gameboy screenshot, returns a percentage difference between the two + """ arr1 = np.array(frame1) arr2 = np.array(frame2) @@ -80,7 +86,7 @@ def get_recent_frames(self, directory, num_frames=100): return os.path.join(script_dir, "test.mp4") def empty_directory(self, directory): - """ Deletes all images in the provided directory """ + """Deletes all images in the provided directory""" image_files = [ i for i in os.listdir(directory) @@ -90,7 +96,7 @@ def empty_directory(self, directory): os.remove(os.path.join(directory, img)) def build_gif(self, image_path, delete=True, fps=120, output_name="action.mp4"): - """ Build a gif from a folder of images """ + """Build a gif from a folder of images""" # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) gif_dir = os.path.join(script_dir, image_path) @@ -115,7 +121,6 @@ def build_gif(self, image_path, delete=True, fps=120, output_name="action.mp4"): if images: save_path = None - duration = int(1000 / fps) frames = images save_path = os.path.join(script_dir, output_name) clip = ImageSequenceClip(frames, fps=fps) @@ -128,11 +133,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 gameboy 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", @@ -142,13 +147,13 @@ def load_rom(self, rom): ) def dpad_up(self) -> None: - """ Presses up on the gameboy """ + """Presses up on the gameboy""" 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 gameboy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) print("down") self.tick(3) @@ -156,7 +161,7 @@ def dpad_down(self) -> None: # self.tick() def dpad_right(self) -> None: - """ Presses right on the gameboy """ + """Presses right on the gameboy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) print("right") self.tick(3) @@ -164,7 +169,7 @@ def dpad_right(self) -> None: # self.tick() def dpad_left(self) -> None: - """ Presses left on the gameboy """ + """Presses left on the gameboy""" self.pyboy.send_input(WindowEvent.PRESS_ARROW_LEFT) print("left") self.tick(3) @@ -172,7 +177,7 @@ def dpad_left(self) -> None: # self.tick() def a(self) -> None: - """ Presses a on the gameboy """ + """Presses a on the gameboy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_A) print("a") self.tick(3) @@ -180,7 +185,7 @@ def a(self) -> None: # self.tick() def b(self) -> None: - """ Presses b on the gameboy """ + """Presses b on the gameboy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_B) print("b") self.tick(3) @@ -188,7 +193,7 @@ def b(self) -> None: # self.tick() def start(self) -> None: - """ Presses start on the gameboy """ + """Presses start on the gameboy""" self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) print("start") self.tick(3) @@ -196,14 +201,14 @@ def start(self) -> None: # self.tick() def select(self) -> None: - """ Presses select on the gameboy """ + """Presses select on the gameboy""" 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 gameboy screen and saves it to the path""" # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) screenshot_dir = os.path.join(script_dir, path) @@ -227,7 +232,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 gameboy""" button = random.choice( [ self.dpad_up, @@ -243,20 +248,21 @@ def random_button(self): button() def load(self): - """ Loads the save state """ + """Loads the save state""" # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) save_loc = os.path.join(script_dir, "save.state") + result = False if os.path.exists(save_loc): with open(save_loc, "rb") as file: self.pyboy.load_state(file) - return True + result = True else: print("Save state does not exist") - return False + return result def save(self): - """ Saves current state to a file """ + """Saves current state to a file""" # Get the directory of the current script script_dir = os.path.dirname(os.path.realpath(__file__)) save_loc = os.path.join(script_dir, "save.state") @@ -265,7 +271,7 @@ def save(self): self.pyboy.save_state(file) def loop_until_stopped(self, threshold=1): - """ Simulates the gameboy bot """ + """Simulates the gameboy bot""" script_dir = os.path.dirname(os.path.realpath(__file__)) running = True previous_frame = None