From 035ef9004621ad4be69278146c7dc32023eb1dbc Mon Sep 17 00:00:00 2001 From: TaylorBoyd Date: Tue, 28 Apr 2026 20:51:47 -0400 Subject: [PATCH 1/4] image saving added --- .gitignore | 176 +------------------------------------ Buttons/Turn.py | 3 +- helpers/DrawHelper.py | 14 ++- helpers/GamestateHelper.py | 11 ++- 4 files changed, 24 insertions(+), 180 deletions(-) diff --git a/.gitignore b/.gitignore index c49ba88..f514b74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,174 +1,2 @@ -#asyncEclipse -config.json -lib/ -scripts/ -include/ -pyvenv.cfg -launch.json -.idea -test.py -ActiveGames/ - - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Created by venv; see https://docs.python.org/3/library/venv.html +* diff --git a/Buttons/Turn.py b/Buttons/Turn.py index 0f46077..4a5a8ac 100644 --- a/Buttons/Turn.py +++ b/Buttons/Turn.py @@ -281,6 +281,7 @@ async def runUpkeep(game: GamestateHelper, interaction: discord.Interaction): asyncio.create_task(thread.edit(archived=True)) await game.upkeep(interaction) drawing = DrawHelper(game.gamestate) + if game.gamestate["roundNum"] < 9: await interaction.channel.send(f"Tech Available At Start Of Round {game.gamestate['roundNum']}", file=await asyncio.to_thread(drawing.show_available_techs)) @@ -301,7 +302,7 @@ async def runUpkeep(game: GamestateHelper, interaction: discord.Interaction): view.add_item(Button(label="Declare Winner", style=discord.ButtonStyle.blurple, custom_id="declareWinner")) await interaction.channel.send("It seems like the game should be ended, " "hit this button to reveal the winner.", view=view) - asyncio.create_task(game.showUpdate(f"Start of round {str(game.gamestate['roundNum'])}", interaction)) + asyncio.create_task(game.showUpdate(f"Start of round {str(game.gamestate['roundNum'])}", interaction, finalSave=True)) @staticmethod async def showReputation(game: GamestateHelper, interaction: discord.Interaction, player): diff --git a/helpers/DrawHelper.py b/helpers/DrawHelper.py index b5b5388..f87b2b9 100644 --- a/helpers/DrawHelper.py +++ b/helpers/DrawHelper.py @@ -1652,7 +1652,7 @@ def get_public_points(self, player, showPrivateRegardless:bool): points -= 2 return points - def show_game(self): + def show_game(self, finalSave=False): def load_tile_coordinates(): configs = Properties() with open("data/tileImageCoordinates.properties", "rb") as f: @@ -1740,6 +1740,7 @@ def create_player_area(): width = max([context2.size[0], context3.size[0] + context4.size[0] + 150, cropped_context.size[0], context5.size[0]]) height = (cropped_context.size[1] + context2.size[1] + + max(context3.size[1], context4.size[1]) + 90) final_context = Image.new("RGBA", (width, height), (0, 0, 0, 255)) centering = int((width - cropped_context.size[0])/2) @@ -1755,6 +1756,17 @@ def create_player_area(): context4.size[1]))) bytes_io = BytesIO() final_context.save(bytes_io, format="WEBP") + if finalSave == True: + if "roundNum" in self.gamestate: + rnd = self.gamestate["roundNum"] + else: + rnd = 1 + output_dir = f'gamehistory/{self.gamestate["game_id"]}' + filename = f'{self.gamestate["game_id"]}-{rnd-1}.png' + if not os.path.exists(output_dir): + os.makedirs(output_dir) + full_path = os.path.join(output_dir, filename) + final_context.save(full_path) bytes_io.seek(0) return discord.File(bytes_io, filename="map_image.webp") diff --git a/helpers/GamestateHelper.py b/helpers/GamestateHelper.py index fbe517a..3807997 100644 --- a/helpers/GamestateHelper.py +++ b/helpers/GamestateHelper.py @@ -1392,9 +1392,12 @@ def get_owned_tiles(self, player): tiles.append(tile) return tiles - async def showGame(self, thread, message): + async def showGame(self, thread, message, finalSave = False): drawing = DrawHelper(self.gamestate) - map_result = await asyncio.to_thread(drawing.show_game) + if finalSave: + map_result = drawing.show_game(finalSave) + else: + map_result = await asyncio.to_thread(drawing.show_game) view = View() view.add_item(Button(label="Show Game", style=discord.ButtonStyle.blurple, custom_id="showGame")) view.add_item(Button(label="Show Reputation", style=discord.ButtonStyle.gray, custom_id="showReputation")) @@ -1406,12 +1409,12 @@ async def showGame(self, thread, message): view.add_item(button) await thread.send(view=view) - async def showUpdate(self, message: str, interaction: discord.Interaction): + async def showUpdate(self, message: str, interaction: discord.Interaction, finalSave = False): if "-" in interaction.channel.name: thread_name = interaction.channel.name.split("-")[0] + "-bot-map-updates" thread = discord.utils.get(interaction.channel.threads, name=thread_name) if thread is not None: - asyncio.create_task(self.showGame(thread, message)) + asyncio.create_task(self.showGame(thread, message, finalSave)) def getPlayerFromHSLocation(self, location): if "sector" not in self.gamestate["board"].get(location, []): From d479f8ad8ce3148393c3fcb3dbb615869afc3a62 Mon Sep 17 00:00:00 2001 From: TaylorBoyd Date: Tue, 28 Apr 2026 21:59:54 -0400 Subject: [PATCH 2/4] Update .gitignore --- .gitignore | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f514b74..5274dfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,175 @@ -# Created by venv; see https://docs.python.org/3/library/venv.html -* +#asyncEclipse +config.json +lib/ +scripts/ +include/ +pyvenv.cfg +launch.json +.idea +test.py +ActiveGames/ +GameHistory/ + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ From 26dedfd806889229513c1734535e201fb678152b Mon Sep 17 00:00:00 2001 From: TaylorBoyd Date: Wed, 29 Apr 2026 17:38:44 -0400 Subject: [PATCH 3/4] save json files at end of round --- Buttons/Turn.py | 3 ++- helpers/GamestateHelper.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Buttons/Turn.py b/Buttons/Turn.py index 4a5a8ac..b8533dc 100644 --- a/Buttons/Turn.py +++ b/Buttons/Turn.py @@ -281,7 +281,7 @@ async def runUpkeep(game: GamestateHelper, interaction: discord.Interaction): asyncio.create_task(thread.edit(archived=True)) await game.upkeep(interaction) drawing = DrawHelper(game.gamestate) - + game.saveEndRound() if game.gamestate["roundNum"] < 9: await interaction.channel.send(f"Tech Available At Start Of Round {game.gamestate['roundNum']}", file=await asyncio.to_thread(drawing.show_available_techs)) @@ -304,6 +304,7 @@ async def runUpkeep(game: GamestateHelper, interaction: discord.Interaction): "hit this button to reveal the winner.", view=view) asyncio.create_task(game.showUpdate(f"Start of round {str(game.gamestate['roundNum'])}", interaction, finalSave=True)) + @staticmethod async def showReputation(game: GamestateHelper, interaction: discord.Interaction, player): msg = f"{interaction.user.mention} Your reputation tiles hold the following values: " diff --git a/helpers/GamestateHelper.py b/helpers/GamestateHelper.py index 3807997..79ec1fb 100644 --- a/helpers/GamestateHelper.py +++ b/helpers/GamestateHelper.py @@ -189,6 +189,13 @@ def get_gamestate(self): self.file = f gamestate = json.load(f) return gamestate + + def saveEndRound(self): + output_dir = f'GameHistory/{self.game_id}' + if not os.path.exists(output_dir): + os.makedirs(output_dir) + with open(f'GameHistory/{self.game_id}/{self.game_id}-{self.gamestate["roundNum"]-1}.json', "w") as outfile: + json.dump(self.gamestate, outfile) def is_file_open(self,file_path): try: From 17319f35aa4a2c2a128ece880f2cd474aef9739a Mon Sep 17 00:00:00 2001 From: TaylorBoyd Date: Wed, 29 Apr 2026 19:00:35 -0400 Subject: [PATCH 4/4] image resizing and zip sending --- helpers/DrawHelper.py | 5 +++-- helpers/GamestateHelper.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/helpers/DrawHelper.py b/helpers/DrawHelper.py index f87b2b9..3f91776 100644 --- a/helpers/DrawHelper.py +++ b/helpers/DrawHelper.py @@ -1762,11 +1762,12 @@ def create_player_area(): else: rnd = 1 output_dir = f'gamehistory/{self.gamestate["game_id"]}' - filename = f'{self.gamestate["game_id"]}-{rnd-1}.png' + filename = f'{self.gamestate["game_id"]}-{rnd-1}.webp' if not os.path.exists(output_dir): os.makedirs(output_dir) full_path = os.path.join(output_dir, filename) - final_context.save(full_path) + resized = final_context.resize((int(width*0.3), int(height*0.3))) + resized.save(full_path, quality=50, compress_level=8) bytes_io.seek(0) return discord.File(bytes_io, filename="map_image.webp") diff --git a/helpers/GamestateHelper.py b/helpers/GamestateHelper.py index 79ec1fb..65e011d 100644 --- a/helpers/GamestateHelper.py +++ b/helpers/GamestateHelper.py @@ -4,6 +4,7 @@ import portalocker import discord import config +import shutil from helpers.DrawHelper import DrawHelper from helpers.EmojiHelper import Emoji from helpers.PlayerHelper import PlayerHelper @@ -98,6 +99,15 @@ async def endGame(self, interaction: discord.Interaction): guild = interaction.guild self.gamestate["gameEnded"] = True self.update() + history_path = f'GameHistory/{self.game_id}' + if os.path.exists(history_path): + shutil.make_archive(history_path, 'zip', history_path) + stats_channel = discord.utils.get(guild.channels, name='ggranger-stats-paradise') + if stats_channel: + zf = discord.File(f'{history_path}.zip') + await stats_channel.send(file=zf) + os.remove(f'{history_path}.zip') + shutil.rmtree(f'{history_path}') category = interaction.channel.category role = discord.utils.get(guild.roles, name=self.game_id) for channel in guild.channels: