From 7574117d9a5498ac49b0950520f32d58ec57556b Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 19 Aug 2020 15:57:02 -0400 Subject: [PATCH] WIP ConquestGame and per guild saving --- conquest/conquest.py | 79 ++++++++--- conquest/regioner.py | 306 ++++++++++++++++++++++++------------------- 2 files changed, 234 insertions(+), 151 deletions(-) diff --git a/conquest/conquest.py b/conquest/conquest.py index a8758dc..c642a85 100644 --- a/conquest/conquest.py +++ b/conquest/conquest.py @@ -13,7 +13,7 @@ from redbot.core.bot import Red from redbot.core.data_manager import bundled_data_path, cog_data_path from redbot.core.utils.predicates import MessagePredicate -from conquest.regioner import ConquestMap, composite_regions +from conquest.regioner import ConquestGame, ConquestMap, MapMaker, composite_regions class Conquest(commands.Cog): @@ -25,6 +25,9 @@ class Conquest(commands.Cog): default_maps_json = {"maps": []} + # Usage: self.config.games.get_raw("game_name", "is_custom") + default_games = {"map_name": None, "is_custom": False} + def __init__(self, bot: Red): super().__init__() self.bot = bot @@ -32,8 +35,8 @@ class Conquest(commands.Cog): self, identifier=67_111_110_113_117_101_115_116, force_registration=True ) - default_guild = {} - default_global = {"current_map": None, "is_custom": False} + default_guild = {"current_game": None} + default_global = {"games": {}} self.config.register_guild(**default_guild) self.config.register_global(**default_global) @@ -51,14 +54,13 @@ class Conquest(commands.Cog): self.asset_path: Optional[pathlib.Path] = None - self.is_custom = False - self.current_map = None - self.map_data = None + self.current_maps = {} # key, value = guild.id, game_name + self.map_data = {} # key, value = guild.id, ConquestGame self.ext = "PNG" self.ext_format = "PNG" - self.mm: Union[ConquestMap, None] = None + self.mm: Union[MapMaker, None] = None async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" @@ -75,22 +77,34 @@ class Conquest(commands.Cog): Initial loading of data from bundled_data_path and config """ self.asset_path = bundled_data_path(self) / "assets" - self.current_map = await self.config.current_map() + for guild in self.bot.guilds: + game_name = await self.config.guild(guild).current_game() + if game_name is not None: + await self.load_guild_data(guild, game_name) self.is_custom = await self.config.is_custom() if self.current_map: await self.current_map_load() + async def load_guild_data(self, guild: discord.Guild, game_name: str): + game_name = await self.config.guild(guild).current_game() + if game_name is not None: + map_data = self.config.games.get_raw(game_name) + + self.current_maps[guild.id] = ConquestGame() + async def current_map_load(self): map_path = self._path_if_custom() - map_data_path = map_path / self.current_map / "data.json" - try: - with map_data_path.open() as mapdata: - self.map_data: dict = json.load(mapdata) - except FileNotFoundError as e: - print(e) - await self.config.current_map.set(None) - return + self.map_data = ConquestMap(map_path / self.current_map) + await self.map_data.load_data() + # map_data_path = map_path / self.current_map / "data.json" + # try: + # with map_data_path.open() as mapdata: + # self.map_data: dict = json.load(mapdata) + # except FileNotFoundError as e: + # print(e) + # await self.config.current_map.set(None) + # return async def _get_current_map_path(self): return self.current_map_folder / self.current_map @@ -205,7 +219,7 @@ class Conquest(commands.Cog): return if not self.mm: - self.mm = ConquestMap(self.custom_map_path) + self.mm = MapMaker(self.custom_map_path) self.mm.custom = True if map_path: @@ -229,6 +243,10 @@ class Conquest(commands.Cog): # Wait what? return + if mm_img.mode == "P": + # Maybe convert to L to prevent RGB? + mm_img = mm_img.convert() # No P mode, convert it + result = await self.mm.init_directory(map_name, target_save, mm_img) if not result: @@ -274,7 +292,7 @@ class Conquest(commands.Cog): await ctx.maybe_send_embed(f"Map {map_name} not found in {self.custom_map_path}") return - self.mm = ConquestMap(map_path) + self.mm = MapMaker(map_path) await self.mm.load_data() await ctx.tick() @@ -311,6 +329,31 @@ class Conquest(commands.Cog): await ctx.maybe_send_embed(f"{len(regions)} masks generated into {masks_dir}") + @_mapmaker_masks.command(name="delete") + async def _mapmaker_masks_delete(self, ctx: commands.Context, mask_list: Greedy[int]): + """ + Delete the listed masks from the map + """ + if not mask_list: + await ctx.send_help() + return + + if not self.mm: + await ctx.maybe_send_embed("No map currently being worked on") + return + + masks_dir = self.mm.masks_path() + if not masks_dir.exists() or not masks_dir.is_dir(): + await ctx.maybe_send_embed("There are no masks") + return + + async with ctx.typing(): + result = await self.mm.delete_masks(mask_list) + if result: + await ctx.maybe_send_embed(f"Delete masks: {mask_list}") + else: + await ctx.maybe_send_embed(f"Failed to delete masks") + @_mapmaker_masks.command(name="combine") async def _mapmaker_masks_combine( self, ctx: commands.Context, mask_list: Greedy[int], recommended=False diff --git a/conquest/regioner.py b/conquest/regioner.py index f3c805b..22f1f94 100644 --- a/conquest/regioner.py +++ b/conquest/regioner.py @@ -107,6 +107,27 @@ def floodfill(image, xy, value, border=None, thresh=0) -> set: return filled_pixels +def create_number_mask(regions, filepath, filename): + base_img_path = filepath / filename + if not base_img_path.exists(): + return False + + base_img: Image.Image = Image.open(base_img_path).convert("L") + + number_img = Image.new("L", base_img.size, 255) + fnt = ImageFont.load_default() + d = ImageDraw.Draw(number_img) + for region_num, region in regions.items(): + text = getattr(region, "name", str(region_num)) + + w1, h1 = region.center + w2, h2 = fnt.getsize(text) + + d.text((w1 - (w2 / 2), h1 - (h2 / 2)), text, font=fnt, fill=0) + number_img.save(filepath / f"numbers.png", "PNG") + return True + + class ConquestMap: def __init__(self, path: pathlib.Path): self.path = path @@ -116,33 +137,6 @@ class ConquestMap: self.region_max = None self.regions = {} - async def change_name(self, new_name: str, new_path: pathlib.Path): - if new_path.exists() and new_path.is_dir(): - # This is an overwrite operation - # await ctx.maybe_send_embed(f"{map_name} already exists, okay to overwrite?") - # - # pred = MessagePredicate.yes_or_no(ctx) - # try: - # await self.bot.wait_for("message", check=pred, timeout=30) - # except TimeoutError: - # await ctx.maybe_send_embed("Response timed out, cancelling save") - # return - # if not pred.result: - # return - return False, "Overwrite currently not supported" - - # This is a new name - new_path.mkdir() - - shutil.copytree(self.path, new_path) - - self.name = new_name - self.path = new_path - - await self.save_data() - - return True - def masks_path(self): return self.path / "masks" @@ -158,29 +152,6 @@ class ConquestMap: def numbered_path(self): return self.path / "numbered.png" - async def init_directory(self, name: str, path: pathlib.Path, image: Image.Image): - if not path.exists() or not path.is_dir(): - path.mkdir() - - self.name = name - self.path = path - - await self.save_data() - - image.save(self.blank_path(), "PNG") - - return True - - async def save_data(self): - to_save = { - "name": self.name, - "custom": self.custom, - "region_max": self.region_max, - "regions": {num: r.get_json() for num, r in self.regions.items()}, - } - with self.data_path().open("w+") as dp: - json.dump(to_save, dp, sort_keys=True, indent=4) - async def load_data(self): with self.data_path().open() as dp: data = json.load(dp) @@ -196,72 +167,23 @@ class ConquestMap: async def save_region(self, region): if not self.custom: return False - pass - - async def generate_masks(self): - regioner = Regioner(filename="blank.png", filepath=self.path) - loop = asyncio.get_running_loop() - regions = await loop.run_in_executor(None, regioner.execute) - - if not regions: - return regions - - self.regions = regions - self.region_max = len(regions) + 1 - - await self.save_data() - return regions + pass # TODO: region data saving async def create_number_mask(self): - regioner = Regioner(filename="blank.png", filepath=self.path) - loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, regioner.create_number_mask, self.regions) - - async def combine_masks(self, mask_list: List[int]): loop = asyncio.get_running_loop() - lowest, eliminated = await loop.run_in_executor(None, self._img_combine_masks, mask_list) - - if not lowest: - return lowest - - try: - elim_regions = [self.regions[n] for n in eliminated] - lowest_region = self.regions[lowest] - except KeyError: - return False - - # points = [self.mm["regions"][f"{n}"]["center"] for n in mask_list] - # - # points = [(r.center, r.weight) for r in elim_regions] - - weighted_points = [r.center for r in elim_regions for _ in range(r.weight)] + [ - lowest_region.center for _ in range(lowest_region.weight) - ] - - lowest_region.center = get_center(weighted_points) - - for key in eliminated: - self.regions.pop(key) - # self.mm["regions"].pop(f"{key}") - - if self.region_max in eliminated: # Max region has changed - self.region_max = max(self.regions.keys()) - - await self.create_number_mask() - - await self.save_data() - - return lowest + return await loop.run_in_executor( + None, create_number_mask, self.regions, self.path, "blank.png" + ) def _img_combine_masks(self, mask_list: List[int]): if not mask_list: - return False, None + return False, None, None if not self.blank_path().exists(): - return False, None + return False, None, None if not self.masks_path().exists(): - return False, None + return False, None, None base_img: Image.Image = Image.open(self.blank_path()) mask = Image.new("1", base_img.size, 1) @@ -278,8 +200,7 @@ class ConquestMap: mask2 = Image.open(self.masks_path() / f"{mask_num}.png").convert("1") mask = ImageChops.logical_and(mask, mask2) - mask.save(self.masks_path() / f"{lowest_num}.png", "PNG") - return lowest_num, eliminated_masks + return lowest_num, eliminated_masks, mask async def get_sample(self): files = [self.blank_path()] @@ -344,6 +265,140 @@ class ConquestMap: return current_numbered_img +class ConquestGame(ConquestMap): + def __init__(self, path: pathlib.Path): + super().__init__(path) + self + + async def start_game(self): + pass + + +class MapMaker(ConquestMap): + async def change_name(self, new_name: str, new_path: pathlib.Path): + if new_path.exists() and new_path.is_dir(): + # This is an overwrite operation + # await ctx.maybe_send_embed(f"{map_name} already exists, okay to overwrite?") + # + # pred = MessagePredicate.yes_or_no(ctx) + # try: + # await self.bot.wait_for("message", check=pred, timeout=30) + # except TimeoutError: + # await ctx.maybe_send_embed("Response timed out, cancelling save") + # return + # if not pred.result: + # return + return False, "Overwrite currently not supported" + + # This is a new name + new_path.mkdir() + + shutil.copytree(self.path, new_path) + + self.custom = True # If this wasn't a custom map, it is now + + self.name = new_name + self.path = new_path + + await self.save_data() + + return True + + async def generate_masks(self): + regioner = Regioner(filename="blank.png", filepath=self.path) + loop = asyncio.get_running_loop() + regions = await loop.run_in_executor(None, regioner.execute) + + if not regions: + return regions + + self.regions = regions + self.region_max = len(regions) + 1 + + await self.save_data() + return regions + + async def combine_masks(self, mask_list: List[int]): + loop = asyncio.get_running_loop() + lowest, eliminated, mask = await loop.run_in_executor( + None, self._img_combine_masks, mask_list + ) + + if not lowest: + return lowest + + try: + elim_regions = [self.regions[n] for n in eliminated] + lowest_region = self.regions[lowest] + except KeyError: + return False + + mask.save(self.masks_path() / f"{lowest}.png", "PNG") + + # points = [self.mm["regions"][f"{n}"]["center"] for n in mask_list] + # + # points = [(r.center, r.weight) for r in elim_regions] + + weighted_points = [r.center for r in elim_regions for _ in range(r.weight)] + [ + lowest_region.center for _ in range(lowest_region.weight) + ] + + lowest_region.center = get_center(weighted_points) + + for key in eliminated: + self.regions.pop(key) + # self.mm["regions"].pop(f"{key}") + + if self.region_max in eliminated: # Max region has changed + self.region_max = max(self.regions.keys()) + + await self.create_number_mask() + + await self.save_data() + + return lowest + + async def delete_masks(self, mask_list): + try: + for key in mask_list: + self.regions.pop(key) + # self.mm["regions"].pop(f"{key}") + except KeyError: + return False + + if self.region_max in mask_list: # Max region has changed + self.region_max = max(self.regions.keys()) + + await self.create_number_mask() + + await self.save_data() + + return mask_list + + async def save_data(self): + to_save = { + "name": self.name, + "custom": self.custom, + "region_max": self.region_max, + "regions": {num: r.get_json() for num, r in self.regions.items()}, + } + with self.data_path().open("w+") as dp: + json.dump(to_save, dp, sort_keys=True, indent=4) + + async def init_directory(self, name: str, path: pathlib.Path, image: Image.Image): + if not path.exists() or not path.is_dir(): + path.mkdir() + + self.name = name + self.path = path + + await self.save_data() + + image.save(self.blank_path(), "PNG") + + return True + + class Region: def __init__(self, center, weight, **kwargs): self.center = center @@ -356,12 +411,15 @@ class Region: class Regioner: def __init__( - self, filepath: pathlib.Path, filename: str, wall_color="black", region_color="white" + self, filepath: pathlib.Path, filename: str, region_color=None, wall_color="black" ): self.filepath = filepath self.filename = filename self.wall_color = ImageColor.getcolor(wall_color, "L") - self.region_color = ImageColor.getcolor(region_color, "L") + if region_color is None: + self.region_color = None + else: + self.region_color = ImageColor.getcolor(region_color, "L") def execute(self): """ @@ -395,8 +453,10 @@ class Regioner: for x1 in range(base_img.width): if (x1, y1) in already_processed: continue - if base_img.getpixel((x1, y1)) == self.region_color: - filled = floodfill(base_img, (x1, y1), black, self.wall_color) + if ( + self.region_color is None and base_img.getpixel((x1, y1)) != self.wall_color + ) or base_img.getpixel((x1, y1)) == self.region_color: + filled = floodfill(base_img, (x1, y1), self.wall_color, self.wall_color) if filled: # Pixels were updated, make them into a mask mask = Image.new("L", base_img.size, 255) for x2, y2 in filled: @@ -412,25 +472,5 @@ class Regioner: # TODO: save mask_centers - self.create_number_mask(regions) + create_number_mask(regions, self.filepath, self.filename) return regions - - def create_number_mask(self, regions): - base_img_path = self.filepath / self.filename - if not base_img_path.exists(): - return False - - base_img: Image.Image = Image.open(base_img_path).convert("L") - - number_img = Image.new("L", base_img.size, 255) - fnt = ImageFont.load_default() - d = ImageDraw.Draw(number_img) - for region_num, region in regions.items(): - text = getattr(region, "name", str(region_num)) - - w1, h1 = region.center - w2, h2 = fnt.getsize(text) - - d.text((w1 - (w2 / 2), h1 - (h2 / 2)), text, font=fnt, fill=0) - number_img.save(self.filepath / f"numbers.png", "PNG") - return True