# This file is part of Senator Sperma # Licensed under the GPL-3.0 License # Copyright (C) 2026 Salmonik @ salmonik@tuta.io # https://codeberg.org/salmonik/senator-sperma import asyncio import logging import math import os import re import discord from discord.ext import commands logger = logging.getLogger(__name__) _plt_module = None _plot_available = False def _ensure_plot(): global _plt_module, _plot_available if _plt_module is not None: return _plot_available try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as _plt_module _plot_available = True except ImportError: _plot_available = False logger.warning("matplotlib not available, plot command will be disabled") return _plot_available def parse_expression(expr: str) -> str: expr = expr.replace("^", "**") expr = re.sub(r"(\d)([x])", r"\1*\2", expr) expr = re.sub(r"([x])(\d)", r"\1*\2", expr) expr = re.sub(r"(\d)([y])", r"\1*\2", expr) expr = re.sub(r"([y])(\d)", r"\1*\2", expr) return expr def is_implicit(expr: str) -> bool: return "=" in expr and "y" in expr and "x" in expr def parse_implicit(expr: str) -> tuple[str, str]: lhs, rhs = expr.split("=", 1) lhs = parse_expression(lhs.strip()) rhs = parse_expression(rhs.strip()) return lhs, rhs class Plot(commands.Cog): def __init__(self, bot): self.bot = bot def _generate_plot(self, func_expr: str, output_path: str): global _plt_module if not _ensure_plot(): raise RuntimeError("matplotlib not available") plt = _plt_module if is_implicit(func_expr): self._generate_implicit_plot(func_expr, output_path) return parsed_expr = parse_expression(func_expr) x = [i * 0.1 for i in range(-100, 101)] y = [] for xi in x: try: result = eval( parsed_expr, { "x": xi, "math": math, "sin": math.sin, "cos": math.cos, "tan": math.tan, "sqrt": math.sqrt, "log": math.log, "exp": math.exp, "pi": math.pi, "e": math.e, }, ) if math.isfinite(result): y.append(result) else: y.append(float("nan")) except Exception: y.append(float("nan")) valid_y = [yi for yi in y if not math.isnan(yi)] if not valid_y: raise ValueError("Brak danych do wykreślenia dla podanej funkcji") fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(x, y, linewidth=2, color="#00ff00") ax.set_title(f"y = {func_expr}", fontsize=14) ax.set_xlabel("x", fontsize=12) ax.set_ylabel("y", fontsize=12) ax.grid(True, alpha=0.3) ax.axhline(y=0, color="white", linewidth=0.5) ax.axvline(x=0, color="white", linewidth=0.5) ax.set_facecolor("#1a1a1a") fig.patch.set_facecolor("#1a1a1a") plt.savefig(output_path, dpi=100, bbox_inches="tight") plt.close() def _generate_implicit_plot(self, func_expr: str, output_path: str): global _plt_module plt = _plt_module lhs, rhs = parse_implicit(func_expr) x_range = 20 y_range = 20 x_vals = [] y_vals = [] for xi in range(-x_range * 10, x_range * 10 + 1): x = xi / 10.0 for yi in range(-y_range * 10, y_range * 10 + 1): y = yi / 10.0 try: left_val = eval(lhs, {"x": x, "y": y, "math": math, "sqrt": math.sqrt, "pi": math.pi, "e": math.e}) right_val = eval(rhs, {"x": x, "y": y, "math": math, "sqrt": math.sqrt, "pi": math.pi, "e": math.e}) diff = abs(left_val - right_val) if math.isfinite(diff) and diff < 0.3: x_vals.append(x) y_vals.append(y) except Exception: pass if not x_vals or not y_vals: raise ValueError("Brak danych do wykreślenia dla podanej funkcji") fig, ax = plt.subplots(figsize=(10, 10)) ax.scatter(x_vals, y_vals, s=1, color="#00ff00", alpha=0.5) ax.set_title(f"{func_expr}", fontsize=14) ax.set_xlabel("x", fontsize=12) ax.set_ylabel("y", fontsize=12) ax.grid(True, alpha=0.3) ax.axhline(y=0, color="white", linewidth=0.5) ax.axvline(x=0, color="white", linewidth=0.5) ax.set_xlim(-x_range, x_range) ax.set_ylim(-y_range, y_range) ax.set_aspect("equal") ax.set_facecolor("#1a1a1a") fig.patch.set_facecolor("#1a1a1a") plt.savefig(output_path, dpi=100, bbox_inches="tight") plt.close() @commands.slash_command(name="funkcja", description="Generuje wykres funkcji matematycznej") async def plot(self, ctx, funkcja: str = discord.Option(str, "Funkcja np. x^3, sin(x), x^2 + 2*x - 1", required=True)): if not _ensure_plot(): await ctx.respond("Generowanie wykresów nie jest dostępne (brak matplotlib)", ephemeral=True) return await ctx.defer() from cogs.utils.temp import get_temp_dir temp_path = os.path.join(get_temp_dir(), f"plot_{os.getpid()}_{asyncio.get_event_loop().time()}.png") try: loop = asyncio.get_event_loop() await loop.run_in_executor(None, self._generate_plot, funkcja, temp_path) await ctx.respond(file=discord.File(temp_path, filename="plot.png")) logger.info(f"Generated plot for function: {funkcja}") except Exception as e: logger.error(f"Plot generation error: {e}") await ctx.respond(f"Błąd generowania wykresu: {str(e)}", ephemeral=True) finally: try: if os.path.exists(temp_path): os.unlink(temp_path) except Exception as e: logger.error(f"Error cleaning up temp file: {e}") def setup(bot): bot.add_cog(Plot(bot))