diff --git a/fizicks/util.py b/fizicks/util.py new file mode 100644 index 0000000..866c3e5 --- /dev/null +++ b/fizicks/util.py @@ -0,0 +1,112 @@ +""" +This logging solution uses a decorator combined with a `LogConfig` object to +provide customized logging for methods in a Python class. Here's how it works: + +1. **LogConfig Object**: A `LogConfig` class encapsulates logging instructions, + including custom messages (`before_msg`, `after_msg`) and additional data + (`before_data`, `after_data`) to be logged before and after a method is executed. + +2. **Decorator**: The `log_event` decorator accepts a `CollisionLogger` + instance and a `LogConfig` object. It wraps the target method, executing + the logging instructions specified in the `LogConfig` before and after the + method runs. + +3. **Method Decoration**: Methods are decorated with `@log_event`, passing the + appropriate `LogConfig` object. This setup automatically handles logging + without cluttering the method logic. + +4. **Custom Logging**: When a method is executed, if logging is enabled + (`debug=True`), the decorator logs customized messages and data before and + after the method’s core logic, based on the configurations provided in the + `LogConfig`. +""" + +import logging +from functools import wraps +from typing import Callable + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Create a console handler and set the level +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) + +# Create a formatter and set it for the handler +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) + +# Add the handler to the logger +logger.addHandler(console_handler) + + +class LogConfig: + """ + Configuration for logging events. + """ + + def __init__( + self, + before_msg: str = None, + after_msg: str = None, + before_data: dict = None, + after_data: dict = None, + ): + self.before_msg = before_msg + self.after_msg = after_msg + self.before_data = before_data or {} + self.after_data = after_data or {} + + +def log_event(config: LogConfig, debug: bool = True) -> Callable: + """ + Decorator to log events with the given config. + + Parameters + ---------- + config : LogConfig + The configuration for the logging event. + debug : bool, optional + Whether to log the event in debug mode, by default False. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + debug = kwargs.get("debug", True) + obj = args[1] if len(args) > 1 else None + obj_desc = obj.description() if obj else "" + method_name = func.__name__ + + if debug: + if config.before_msg: + custom_before_msg = config.before_msg.format( + method_name=method_name, object_desc=obj_desc + ) + logger.debug( + custom_before_msg, + {**config.before_data, "Args": args, "Kwargs": kwargs}, + ) + + result = func(*args, **kwargs) + + if debug: + if config.after_msg: + custom_after_msg = config.after_msg.format( + method_name=method_name, object_desc=obj_desc + ) + logger.debug( + custom_after_msg, + { + **config.after_data, + "Result": result, + "Args": args, + "Kwargs": kwargs, + }, + ) + + return result + + return wrapper + + return decorator diff --git a/fizicks/visual.py b/fizicks/visual.py new file mode 100644 index 0000000..6404f75 --- /dev/null +++ b/fizicks/visual.py @@ -0,0 +1,92 @@ +from typing import Any, List + +import pygame + +from fizicks.collision import Collision + + +class VisualDebugger: + """ + A class for visualizing the universe and objects for debugging purposes. + + Uses Pygame to visualize the universe and objects. + + Parameters + ---------- + universe: Universe + The universe to visualize. + objects: List[Matter] + The objects to visualize. + """ + + def __init__(self, universe: "Universe", objects: List["Matter"]): + self.universe = universe + self.objects = objects + + # Pygame initialization + pygame.init() + self.screen = pygame.display.set_mode( + (int(universe.dimensions.x), int(universe.dimensions.y)) + ) + pygame.display.set_caption("Fizicks Visual Debugger") + self.clock = pygame.time.Clock() + + def draw_object(self, obj: "Matter") -> None: + """ + Draws an object on the screen. + + Parameters + ---------- + obj: Matter + The object to draw. + """ + pygame.draw.circle( + self.screen, + obj.color, + (int(obj.position.x), int(obj.position.y)), + int(obj.radius), + ) + + def draw_border(self) -> None: + """ + Draws the border of the universe on the screen. + """ + pygame.draw.rect( + self.screen, + (255, 255, 255), + pygame.Rect( + 0, 0, int(self.universe.dimensions.x), int(self.universe.dimensions.y) + ), + 1, + ) + + def run(self) -> None: + """ + Runs the simulation and visualizes the universe and objects. + """ + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + self.screen.fill((0, 0, 0)) # Clear screen with black + + self.draw_border() + + # Identify collisions + for i, obj in enumerate(self.objects): + for other_obj in self.objects[i + 1 :]: + if obj.collides_with(other_obj): + Collision.resolve(obj, other_obj) + + for obj in self.objects: + obj.update(self.universe) + self.draw_object(obj) + + self.universe.time += 1 + + pygame.display.flip() + self.clock.tick(60) # Cap the frame rate at 60 FPS + + pygame.quit()