# ╔══════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ |H|e|x|a|H|o|s|t| ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ ║ # ║ © 2026 HexaHost — All Rights Reserved ║ # ║ ║ # ║ discord ── https://discord.gg/hexahost ║ # ║ github ── https://github.com/theoneandonlymace ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════╝ import asyncio import difflib import os import pathlib import random from typing import Union, Optional from io import BytesIO import discord from discord.ext import commands from PIL import Image, ImageFilter, ImageOps from .utils import * class CountryGuesser: """ CountryGuesser Game """ embed: discord.Embed accepted_length: Optional[int] country: str def __init__( self, *, is_flags: bool = False, light_mode: bool = False, hard_mode: bool = False, hints: int = 1, ) -> None: self.embed_color: Optional[DiscordColor] = None self.hints = hints self.is_flags = is_flags self.hard_mode = hard_mode if self.is_flags: self.light_mode = False else: self.light_mode = light_mode folder = "assets/country-flags" if self.is_flags else "assets/country-data" self._countries_path = pathlib.Path(__file__).parent / folder self.all_countries = os.listdir(self._countries_path) self.responses_count = 0 @executor() def invert_image(self, image_path: Union[BytesIO, os.PathLike, str]) -> BytesIO: with Image.open(image_path) as img: img = img.convert("RGBA") r, g, b, a = img.split() rgb = Image.merge("RGB", (r, g, b)) rgb = ImageOps.invert(rgb) rgb = rgb.split() img = Image.merge("RGBA", rgb + (a,)) buf = BytesIO() img.save(buf, "PNG") buf.seek(0) return buf @executor() def blur_image(self, image_path: Union[BytesIO, os.PathLike, str]) -> BytesIO: with Image.open(image_path) as img: img = img.convert("RGBA") img = img.filter(ImageFilter.GaussianBlur(10)) buf = BytesIO() img.save(buf, "PNG") buf.seek(0) return buf async def get_country(self) -> discord.File: country_file = random.choice(self.all_countries) self.country = country_file.strip()[:-4].lower() file = os.path.join(self._countries_path, country_file) if self.hard_mode: file = await self.blur_image(file) if self.light_mode: file = await self.invert_image(file) return discord.File(file, "country.png") def get_blanks(self) -> str: return " ".join("_" if char != " " else " " for char in self.country) def get_hint(self) -> str: blanks = ["_" if char != " " else " " for char in self.country] times = round(len(blanks) / 3) for _ in range(times): idx = random.choice(range(len(self.country))) blanks[idx] = self.country[idx] return " ".join(blanks) def get_accuracy(self, guess: str) -> int: return round(difflib.SequenceMatcher(None, guess, self.country).ratio() * 100) def get_embed(self) -> discord.Embed: embed = discord.Embed( title="Guess that country!", description=f"```fix\n{self.get_blanks()}\n```", color=discord.Color.random(), ) embed.add_field( name="\u200b", value=f"```yml\nblurred: {str(self.hard_mode).lower()}\nflag-mode: {str(self.is_flags).lower()}\n```", inline=False, ) embed.set_image(url="attachment://country.png") return embed async def wait_for_response( self, ctx: commands.Context[commands.Bot], *, options: tuple[str, ...] = (), length: Optional[int] = None, ) -> Optional[tuple[discord.Message, str]]: def check(m: discord.Message) -> bool: if length: return ( m.channel == ctx.channel and m.author != ctx.bot.user and len(m.content) == length ) else: return m.channel == ctx.channel and m.author != ctx.bot.user message: discord.Message = await ctx.bot.wait_for( "message", timeout=self.timeout, check=check ) content = message.content.strip().lower() if options: if not content in options: return return message, content async def start( self, ctx: commands.Context[commands.Bot], *, timeout: Optional[float] = 100.0, embed_color: DiscordColor = discord.Color.random(), ) -> discord.Message: file = await self.get_country() self.timeout = timeout self.embed_color = discord.Color.random() self.embed = self.get_embed() self.embed.set_footer(text="send your guess within 100 seconds into the chat now!") self.message = await ctx.send(embed=self.embed, file=file) self.accepted_length = None start_time = asyncio.get_event_loop().time() while not ctx.bot.is_closed() and asyncio.get_event_loop().time() - start_time < self.timeout: try: msg, response = await self.wait_for_response(ctx) except asyncio.TimeoutError: break self.responses_count += 1 if response == self.country: elapsed_time = round(asyncio.get_event_loop().time() - start_time, 2) await msg.reply( f"That is correct! The country was `{self.country.title()}`" ) return await self.end_game(ctx, msg.author, elapsed_time) else: acc = self.get_accuracy(response) if self.responses_count % 10 == 0 and self.hints: hint = self.get_hint() await ctx.send(f"Hint: `{hint}`") await msg.reply( f"That was incorrect! but you are `{acc}%` of the way there!", mention_author=False, ) # Check if the time has exceeded the timeout #if asyncio.get_event_loop().time() - start_time > timeout: #return await self.end_game(ctx) # Call end_game when timeout occurs return await self.end_game(ctx) async def end_game( self, ctx: commands.Context[commands.Bot], winner: Optional[discord.User] = None, time_taken: Optional[float] = None, manual_end: bool = False ) -> discord.Message: embed = discord.Embed(title="Game Over", color=self.embed_color) if winner and time_taken: embed.add_field( name="Winner", value=f"{winner.mention} ({winner.name})", inline=False, ) embed.add_field(name="Time Taken", value=f"{time_taken} seconds", inline=False) elif manual_end: embed.description = "The game was manually ended." else: embed.description = f"Time's up! No one guessed the country. The correct answer was `{self.country.title()}`." return await ctx.send(embed=embed) async def end_game_manually(self, ctx: commands.Context): await self.end_game(ctx, manual_end=True)