From 00b4d17c0d527ca0be931793a182544aa3476edf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:12:24 +0000 Subject: [PATCH 1/8] Initial plan From f9eb5d2a792e05575d7682ca1136999b7c26b2ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:23:02 +0000 Subject: [PATCH 2/8] Add text-based UI mode with --text flag Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/apex.py | 19 ++++- src/simulation/simulation.py | 7 +- src/textSimulationRunner.py | 138 +++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/textSimulationRunner.py diff --git a/src/apex.py b/src/apex.py index 37e024b..2bc8d78 100644 --- a/src/apex.py +++ b/src/apex.py @@ -1,3 +1,4 @@ +import argparse import pygame from lib.graphiklib.graphik import Graphik from screen.mainMenuScreen import MainMenuScreen @@ -58,5 +59,19 @@ def __quitApplication(self): pygame.quit() quit() -apex = Apex() -apex.run() \ No newline at end of file +if __name__ == "__main__": + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Apex Ecosystem Simulator') + parser.add_argument('--text', action='store_true', + help='Run simulation in text mode (no GUI)') + args = parser.parse_args() + + if args.text: + # Run in text mode + from textSimulationRunner import TextSimulationRunner + runner = TextSimulationRunner() + runner.run() + else: + # Run in pygame GUI mode (default) + apex = Apex() + apex.run() \ No newline at end of file diff --git a/src/simulation/simulation.py b/src/simulation/simulation.py index 87bf98e..1f416b1 100644 --- a/src/simulation/simulation.py +++ b/src/simulation/simulation.py @@ -27,10 +27,13 @@ # @since July 26th, 2022 class Simulation: # constructors ------------------------------------------------------------ - def __init__(self, name, config, gameDisplay): + def __init__(self, name, config, gameDisplay, soundService=None): self.__config = config self.__gameDisplay = gameDisplay - self.__soundService = SoundService() + if soundService is None: + self.__soundService = SoundService() + else: + self.__soundService = soundService self.environment = Environment(name, self.getConfig().gridSize) diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py new file mode 100644 index 0000000..322cb10 --- /dev/null +++ b/src/textSimulationRunner.py @@ -0,0 +1,138 @@ +import time +import os +import sys +from entity.chicken import Chicken +from entity.cow import Cow +from entity.fox import Fox +from entity.grass import Grass +from entity.pig import Pig +from entity.rabbit import Rabbit +from entity.wolf import Wolf +from simulation.config import Config +from simulation.simulation import Simulation + +# @author Daniel McCoy Stephenson +# @since October 15th, 2024 + +class MockSoundService: + """Mock sound service that doesn't require pygame.""" + def playReproduceSoundEffect(self): + pass + + def playDeathSoundEffect(self): + pass + + +class TextSimulationRunner: + """ + A text-based simulation runner that displays simulation stats to the console + without using pygame graphics. + """ + + def __init__(self, config: Config = None): + self.config = config if config else Config() + self.simulation = None + self.running = True + self.paused = False + self.updateInterval = 1.0 # seconds between display updates + + def run(self): + """Runs the text-based simulation.""" + print("=" * 60) + print("Apex Ecosystem Simulator - Text Mode") + print("=" * 60) + print() + print("Commands:") + print(" Ctrl+C: Quit simulation") + print() + + # Initialize simulation + self.initializeSimulation() + + # Main loop + try: + lastDisplayTime = time.time() + while self.running: + # Update simulation + self.simulation.update() + self.simulation.numTicks += 1 + + # Display stats periodically + currentTime = time.time() + if currentTime - lastDisplayTime >= self.updateInterval: + self.displayStats() + lastDisplayTime = currentTime + + # Check if simulation should end + if self.config.endSimulationUponAllLivingEntitiesDying: + if self.simulation.getNumLivingEntities() == 0: + print("\nAll living entities have died. Simulation ended.") + self.simulation.cleanup() + self.running = False + + # Apply tick speed limit + if self.config.limitTickSpeed: + time.sleep((self.config.maxTickSpeed - self.config.tickSpeed) / self.config.maxTickSpeed) + + except KeyboardInterrupt: + print("\n\nSimulation interrupted by user.") + self.simulation.cleanup() + + def initializeSimulation(self): + """Initializes the simulation with a mock game display.""" + name = "Text-Based Simulation" + + # Create a mock display object for simulation initialization + class MockDisplay: + def get_size(self): + return (self.config.displayWidth, self.config.displayHeight) + + mockDisplay = MockDisplay() + mockDisplay.config = self.config + + # Create simulation with mock sound service + mockSoundService = MockSoundService() + self.simulation = Simulation(name, self.config, mockDisplay, mockSoundService) + + self.simulation.generateInitialEntities() + self.simulation.placeInitialEntitiesInEnvironment() + self.simulation.environment.printInfo() + + print(f"Simulation initialized: {name}") + print(f"Grid size: {self.simulation.environment.getGrid().getColumns()}x{self.simulation.environment.getGrid().getRows()}") + print() + + def displayStats(self): + """Displays current simulation statistics to the console.""" + # Clear screen (works on Unix-like systems) + if os.name != 'nt': + os.system('clear') + else: + os.system('cls') + + print("=" * 60) + print(f"Tick: {self.simulation.numTicks}") + print("=" * 60) + print() + + print(f"Total Entities: {len(self.simulation.entities)}") + print(f"Living Entities: {self.simulation.getNumLivingEntities()}") + print(f"Grass: {self.simulation.getNumberOfEntitiesOfType(Grass)}") + print(f"Excrement: {self.simulation.getNumExcrement()}") + print() + + print("Living Entity Counts:") + print(f" Chickens: {self.simulation.getNumberOfLivingEntitiesOfType(Chicken)}") + print(f" Pigs: {self.simulation.getNumberOfLivingEntitiesOfType(Pig)}") + print(f" Cows: {self.simulation.getNumberOfLivingEntitiesOfType(Cow)}") + print(f" Wolves: {self.simulation.getNumberOfLivingEntitiesOfType(Wolf)}") + print(f" Foxes: {self.simulation.getNumberOfLivingEntitiesOfType(Fox)}") + print(f" Rabbits: {self.simulation.getNumberOfLivingEntitiesOfType(Rabbit)}") + print() + + if self.config.limitTickSpeed: + print(f"Tick Speed: {self.config.tickSpeed}/{self.config.maxTickSpeed}") + + print() + print("Press Ctrl+C to quit") + print("=" * 60) From 7472a1c871fb4d6f89e7aa51051f17311c4d1b9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:25:36 +0000 Subject: [PATCH 3/8] Update README with text-based UI documentation Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 54c3944..ca7349d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,27 @@ This game allows you to manage a virtual environment containing entities that de screenshot +## UI Modes +Apex can be run in two different modes: + +### Pygame GUI Mode (Default) +The standard graphical interface with visual representation of the ecosystem. +```bash +python src/apex.py +``` + +### Text-Based Mode +A lightweight console-based interface that displays simulation statistics without graphics. +```bash +python src/apex.py --text +``` + +The text mode is ideal for: +- Running simulations on headless servers +- Lower resource consumption +- Remote SSH sessions +- Automated testing and analysis + ## Types of Living Entities - Chicken - Pig @@ -19,6 +40,8 @@ If there is no grass, everything collapses. Living entities spawn excrement when their energy needs are met and this turns into grass over time. ## Controls +The following keyboard controls are available in **Pygame GUI Mode**: + Key | Action ------------ | ------------- space | pause/unpause @@ -41,6 +64,8 @@ f11 | toggle fullscreen mode r | restart q | quit +**In Text-Based Mode**, press Ctrl+C to quit the simulation. + At this time, the user can pause/unpause, toggle the tick speed limit, increase/decrease the tick speed, manually spawn living entities, restart the simulation, enter debug mode and quit the application. ## Support From 1bec9e50ccffd6a83ae8d881f015be990684e604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:50:41 +0000 Subject: [PATCH 4/8] Add environment visualization and non-blocking keyboard controls to text mode Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- README.md | 37 +++- src/textSimulationRunner.py | 328 +++++++++++++++++++++++++++++------- 2 files changed, 304 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index ca7349d..c914634 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,26 @@ python src/apex.py ``` ### Text-Based Mode -A lightweight console-based interface that displays simulation statistics without graphics. +A lightweight console-based interface that visualizes the ecosystem using ASCII characters and displays simulation statistics. Features include: +- Real-time environment visualization with colored ASCII characters +- Non-blocking keyboard controls (same as GUI mode) +- Interactive spawning of entities +- Pause/resume, speed control, and debug mode + ```bash python src/apex.py --text ``` +**Legend for Text Mode:** +- `.` = Grass (green) +- `x` = Excrement (yellow) +- `C` = Chicken (yellow) +- `P` = Pig (magenta) +- `K` = Cow (cyan) +- `W` = Wolf (red) +- `F` = Fox (red) +- `R` = Rabbit (white) + The text mode is ideal for: - Running simulations on headless servers - Lower resource consumption @@ -40,6 +55,8 @@ If there is no grass, everything collapses. Living entities spawn excrement when their energy needs are met and this turns into grass over time. ## Controls + +### Pygame GUI Mode The following keyboard controls are available in **Pygame GUI Mode**: Key | Action @@ -64,7 +81,23 @@ f11 | toggle fullscreen mode r | restart q | quit -**In Text-Based Mode**, press Ctrl+C to quit the simulation. +### Text-Based Mode +The following keyboard controls are available in **Text-Based Mode**: + +Key | Action +------------ | ------------- +space | pause/unpause +d | debug mode +c | spawn a chicken +p | spawn a pig +k | spawn a cow +w | spawn a wolf +f | spawn a fox +b | spawn a rabbit +l | toggle tick speed limit +] | increase tick speed (if enabled) +[ | decrease tick speed (if enabled) +q | quit At this time, the user can pause/unpause, toggle the tick speed limit, increase/decrease the tick speed, manually spawn living entities, restart the simulation, enter debug mode and quit the application. diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py index 322cb10..3a768c6 100644 --- a/src/textSimulationRunner.py +++ b/src/textSimulationRunner.py @@ -1,6 +1,7 @@ import time import os import sys +import curses from entity.chicken import Chicken from entity.cow import Cow from entity.fox import Fox @@ -10,6 +11,8 @@ from entity.wolf import Wolf from simulation.config import Config from simulation.simulation import Simulation +from lib.pyenvlib.entity import Entity +from entity.livingEntity import LivingEntity # @author Daniel McCoy Stephenson # @since October 15th, 2024 @@ -25,8 +28,9 @@ def playDeathSoundEffect(self): class TextSimulationRunner: """ - A text-based simulation runner that displays simulation stats to the console - without using pygame graphics. + A text-based simulation runner that visualizes the environment and displays + simulation stats to the console without using pygame graphics. + Supports keyboard commands for interactive control. """ def __init__(self, config: Config = None): @@ -34,50 +38,76 @@ def __init__(self, config: Config = None): self.simulation = None self.running = True self.paused = False - self.updateInterval = 1.0 # seconds between display updates + self.debug = False + self.stdscr = None def run(self): - """Runs the text-based simulation.""" - print("=" * 60) - print("Apex Ecosystem Simulator - Text Mode") - print("=" * 60) - print() - print("Commands:") - print(" Ctrl+C: Quit simulation") - print() + """Runs the text-based simulation with curses for non-blocking input.""" + curses.wrapper(self._run_with_curses) + + def _run_with_curses(self, stdscr): + """Main loop with curses support.""" + self.stdscr = stdscr + + # Initialize curses + curses.curs_set(0) # Hide cursor + stdscr.nodelay(1) # Non-blocking input + stdscr.timeout(0) # Don't wait for input + + # Initialize color pairs if terminal supports colors + if curses.has_colors(): + curses.start_color() + curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # Grass + curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Chicken + curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # Pig + curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK) # Cow + curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK) # Wolf + curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Fox + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # Rabbit + curses.init_pair(8, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Excrement # Initialize simulation self.initializeSimulation() + # Display initial instructions + self._display_instructions() + stdscr.refresh() + time.sleep(2) + # Main loop try: - lastDisplayTime = time.time() while self.running: - # Update simulation - self.simulation.update() - self.simulation.numTicks += 1 + # Handle keyboard input (non-blocking) + self._handle_input() + + # Update simulation if not paused + if not self.paused: + self.simulation.update() + self.simulation.numTicks += 1 - # Display stats periodically - currentTime = time.time() - if currentTime - lastDisplayTime >= self.updateInterval: - self.displayStats() - lastDisplayTime = currentTime + # Draw the environment and stats + self._draw_screen() # Check if simulation should end if self.config.endSimulationUponAllLivingEntitiesDying: if self.simulation.getNumLivingEntities() == 0: - print("\nAll living entities have died. Simulation ended.") + self._show_message("All living entities have died. Simulation ended.") self.simulation.cleanup() self.running = False + time.sleep(2) # Apply tick speed limit if self.config.limitTickSpeed: time.sleep((self.config.maxTickSpeed - self.config.tickSpeed) / self.config.maxTickSpeed) + else: + time.sleep(0.01) # Small delay to prevent CPU spinning except KeyboardInterrupt: - print("\n\nSimulation interrupted by user.") self.simulation.cleanup() - + except Exception as e: + self.simulation.cleanup() + raise e + def initializeSimulation(self): """Initializes the simulation with a mock game display.""" name = "Text-Based Simulation" @@ -96,43 +126,223 @@ def get_size(self): self.simulation.generateInitialEntities() self.simulation.placeInitialEntitiesInEnvironment() - self.simulation.environment.printInfo() + + def _display_instructions(self): + """Display initial instructions.""" + self.stdscr.clear() + height, width = self.stdscr.getmaxyx() + + title = "Apex Ecosystem Simulator - Text Mode" + self.stdscr.addstr(height // 2 - 5, (width - len(title)) // 2, title, curses.A_BOLD) + + instructions = [ + "", + "Controls:", + " SPACE: Pause/Resume", + " d: Toggle debug mode", + " c: Spawn chicken", + " p: Spawn pig", + " k: Spawn cow", + " w: Spawn wolf", + " f: Spawn fox", + " b: Spawn rabbit", + " l: Toggle tick speed limit", + " ]: Increase tick speed", + " [: Decrease tick speed", + " q: Quit", + "", + "Starting simulation..." + ] + + for i, line in enumerate(instructions): + self.stdscr.addstr(height // 2 - 4 + i, (width - 40) // 2, line) + + def _handle_input(self): + """Handle keyboard input (non-blocking).""" + try: + key = self.stdscr.getch() + + if key == ord(' '): + self.paused = not self.paused + elif key == ord('q'): + self.running = False + self.simulation.cleanup() + elif key == ord('d'): + self.debug = not self.debug + elif key == ord('c'): + from entity.chicken import Chicken + chicken = Chicken("player-created-chicken") + self.simulation.environment.addEntity(chicken) + self.simulation.addEntityToTrackedEntities(chicken) + elif key == ord('p'): + from entity.pig import Pig + pig = Pig("player-created-pig") + self.simulation.environment.addEntity(pig) + self.simulation.addEntityToTrackedEntities(pig) + elif key == ord('k'): + from entity.cow import Cow + cow = Cow("player-created-cow") + self.simulation.environment.addEntity(cow) + self.simulation.addEntityToTrackedEntities(cow) + elif key == ord('w'): + from entity.wolf import Wolf + wolf = Wolf("player-created-wolf") + self.simulation.environment.addEntity(wolf) + self.simulation.addEntityToTrackedEntities(wolf) + elif key == ord('f'): + from entity.fox import Fox + fox = Fox("player-created-fox") + self.simulation.environment.addEntity(fox) + self.simulation.addEntityToTrackedEntities(fox) + elif key == ord('b'): + from entity.rabbit import Rabbit + rabbit = Rabbit("player-created-rabbit") + self.simulation.environment.addEntity(rabbit) + self.simulation.addEntityToTrackedEntities(rabbit) + elif key == ord('l'): + self.config.limitTickSpeed = not self.config.limitTickSpeed + elif key == ord(']'): + if self.config.tickSpeed < self.config.maxTickSpeed: + self.config.tickSpeed += 1 + elif key == ord('['): + if self.config.tickSpeed > 1: + self.config.tickSpeed -= 1 + + except: + pass # Ignore input errors + + def _draw_screen(self): + """Draw the environment visualization and stats.""" + try: + self.stdscr.clear() + height, width = self.stdscr.getmaxyx() + + # Calculate grid display area + grid = self.simulation.environment.getGrid() + gridCols = grid.getColumns() + gridRows = grid.getRows() + + # Calculate cell size (use 2 chars wide, 1 char tall for better aspect ratio) + cellWidth = 2 + cellHeight = 1 + + # Calculate available space for grid + statsHeight = 12 if self.debug else 8 + availableHeight = height - statsHeight - 2 + availableWidth = width - 2 + + # Calculate how much of the grid we can display + maxDisplayRows = min(gridRows, availableHeight // cellHeight) + maxDisplayCols = min(gridCols, availableWidth // cellWidth) + + # Draw the environment + startY = 1 + for row in range(maxDisplayRows): + for col in range(maxDisplayCols): + location = grid.getLocationByCoordinates(col, row) + if location != -1: + char, color = self._get_location_char(location) + try: + self.stdscr.addstr(startY + row * cellHeight, 1 + col * cellWidth, char * cellWidth, color) + except: + pass # Ignore if we're at the edge + + # Draw stats below the grid + statsY = startY + maxDisplayRows * cellHeight + 1 + self._draw_stats(statsY, width) + + # Draw status bar at bottom + self._draw_status_bar(height - 1, width) + + self.stdscr.refresh() + except: + pass # Ignore drawing errors + + def _get_location_char(self, location): + """Get the character and color for a location.""" + if location.getNumEntities() == 0: + return ' ', curses.color_pair(0) - print(f"Simulation initialized: {name}") - print(f"Grid size: {self.simulation.environment.getGrid().getColumns()}x{self.simulation.environment.getGrid().getRows()}") - print() + # Get top entity + topEntityId = list(location.getEntities().keys())[-1] + topEntity = location.getEntities()[topEntityId] - def displayStats(self): - """Displays current simulation statistics to the console.""" - # Clear screen (works on Unix-like systems) - if os.name != 'nt': - os.system('clear') + # Determine character and color based on entity type + from entity.grass import Grass + from entity.excrement import Excrement + from entity.chicken import Chicken + from entity.pig import Pig + from entity.cow import Cow + from entity.wolf import Wolf + from entity.fox import Fox + from entity.rabbit import Rabbit + + if isinstance(topEntity, Grass): + return '.', curses.color_pair(1) + elif isinstance(topEntity, Excrement): + return 'x', curses.color_pair(8) + elif isinstance(topEntity, Chicken): + return 'C', curses.color_pair(2) + elif isinstance(topEntity, Pig): + return 'P', curses.color_pair(3) + elif isinstance(topEntity, Cow): + return 'K', curses.color_pair(4) + elif isinstance(topEntity, Wolf): + return 'W', curses.color_pair(5) + elif isinstance(topEntity, Fox): + return 'F', curses.color_pair(6) + elif isinstance(topEntity, Rabbit): + return 'R', curses.color_pair(7) else: - os.system('cls') + return '?', curses.color_pair(0) + + def _draw_stats(self, startY, width): + """Draw simulation statistics.""" + try: + stats = [ + f"Tick: {self.simulation.numTicks} Living: {self.simulation.getNumLivingEntities()} Total: {len(self.simulation.entities)}", + f"Grass: {self.simulation.getNumberOfEntitiesOfType(Grass)} Excrement: {self.simulation.getNumExcrement()}", + f"C:{self.simulation.getNumberOfLivingEntitiesOfType(Chicken)} P:{self.simulation.getNumberOfLivingEntitiesOfType(Pig)} K:{self.simulation.getNumberOfLivingEntitiesOfType(Cow)} W:{self.simulation.getNumberOfLivingEntitiesOfType(Wolf)} F:{self.simulation.getNumberOfLivingEntitiesOfType(Fox)} R:{self.simulation.getNumberOfLivingEntitiesOfType(Rabbit)}", + ] - print("=" * 60) - print(f"Tick: {self.simulation.numTicks}") - print("=" * 60) - print() - - print(f"Total Entities: {len(self.simulation.entities)}") - print(f"Living Entities: {self.simulation.getNumLivingEntities()}") - print(f"Grass: {self.simulation.getNumberOfEntitiesOfType(Grass)}") - print(f"Excrement: {self.simulation.getNumExcrement()}") - print() - - print("Living Entity Counts:") - print(f" Chickens: {self.simulation.getNumberOfLivingEntitiesOfType(Chicken)}") - print(f" Pigs: {self.simulation.getNumberOfLivingEntitiesOfType(Pig)}") - print(f" Cows: {self.simulation.getNumberOfLivingEntitiesOfType(Cow)}") - print(f" Wolves: {self.simulation.getNumberOfLivingEntitiesOfType(Wolf)}") - print(f" Foxes: {self.simulation.getNumberOfLivingEntitiesOfType(Fox)}") - print(f" Rabbits: {self.simulation.getNumberOfLivingEntitiesOfType(Rabbit)}") - print() - - if self.config.limitTickSpeed: - print(f"Tick Speed: {self.config.tickSpeed}/{self.config.maxTickSpeed}") - - print() - print("Press Ctrl+C to quit") - print("=" * 60) + if self.config.limitTickSpeed: + stats.append(f"Tick Speed: {self.config.tickSpeed}/{self.config.maxTickSpeed}") + + for i, stat in enumerate(stats): + if startY + i < curses.LINES - 2: + self.stdscr.addstr(startY + i, 1, stat[:width-2]) + + if self.debug: + # Add more detailed stats in debug mode + debug_stats = [ + "", + f"Grid: {self.simulation.environment.getGrid().getColumns()}x{self.simulation.environment.getGrid().getRows()}", + f"Deaths: {self.simulation.getNumDeaths()}", + ] + for i, stat in enumerate(debug_stats): + if startY + len(stats) + i < curses.LINES - 2: + self.stdscr.addstr(startY + len(stats) + i, 1, stat[:width-2]) + except: + pass + + def _draw_status_bar(self, y, width): + """Draw status bar at bottom.""" + try: + status = "" + if self.paused: + status = "PAUSED - " + status += "q:Quit SPACE:Pause d:Debug [/]:Speed c/p/k/w/f/b:Spawn" + self.stdscr.addstr(y, 0, status[:width], curses.A_REVERSE) + except: + pass + + def _show_message(self, message): + """Show a temporary message on screen.""" + try: + height, width = self.stdscr.getmaxyx() + y = height // 2 + x = (width - len(message)) // 2 + self.stdscr.addstr(y, x, message, curses.A_BOLD) + self.stdscr.refresh() + except: + pass From f2d395cf517cb16e3300371ef64f618ac1eafa24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:07:14 +0000 Subject: [PATCH 5/8] Abstract gameplay logic into SimulationController to decouple UI from simulation Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/screen/simulationScreen.py | 106 ++++++-------- src/simulation/simulationController.py | 132 ++++++++++++++++++ src/textSimulationRunner.py | 74 ++++------ tests/simulation/test_simulationController.py | 97 +++++++++++++ 4 files changed, 298 insertions(+), 111 deletions(-) create mode 100644 src/simulation/simulationController.py create mode 100644 tests/simulation/test_simulationController.py diff --git a/src/screen/simulationScreen.py b/src/screen/simulationScreen.py index c2dce6d..b040427 100644 --- a/src/screen/simulationScreen.py +++ b/src/screen/simulationScreen.py @@ -14,6 +14,7 @@ from screen.screenType import ScreenType from simulation.config import Config from simulation.simulation import Simulation +from simulation.simulationController import SimulationController from ui.textAlertDrawTool import TextAlertDrawTool from ui.textAlertFactory import TextAlertFactory @@ -25,8 +26,7 @@ def __init__(self, graphik: Graphik, config: Config): self.__config = config self.__nextScreen = ScreenType.RESULTS_SCREEN self.__changeScreen = False - self.__paused = False - self.__debug = False + self.__controller = None self.__textAlerts = [] self.__textAlertFactory = TextAlertFactory() self.__textAlertDrawTool = TextAlertDrawTool() @@ -48,17 +48,18 @@ def run(self): elif event.type == pygame.MOUSEBUTTONDOWN and self.__config.localView == False: self.__handleMouseClickEvent(event.pos) - if not self.__paused: - self.simulation.update() - self.__graphik.gameDisplay.fill(self.__config.black) - if self.simulation.getNumLivingEntities() != 0: - if self.__config.localView and self.__selectedEntity != None: - self.__drawAreaAroundSelectedEntity() - else: - self.__drawEnvironment() + # Update simulation through controller + self.__controller.update() + + self.__graphik.gameDisplay.fill(self.__config.black) + if self.simulation.getNumLivingEntities() != 0: + if self.__config.localView and self.__selectedEntity != None: + self.__drawAreaAroundSelectedEntity() + else: + self.__drawEnvironment() - if self.__debug: - self.__displayStats() + if self.__controller.isDebug(): + self.__displayStats() self.__drawTextAlerts() @@ -75,25 +76,19 @@ def run(self): if (self.__config.limitTickSpeed): time.sleep((self.__config.maxTickSpeed - self.__config.tickSpeed)/self.__config.maxTickSpeed) - if not self.__paused: - self.simulation.numTicks += 1 - - if self.__paused: + if self.__controller.isPaused(): x, y = self.__graphik.gameDisplay.get_size() self.__graphik.drawText("PAUSED", x/2, y/2, 50, self.__config.black) - if (self.__config.endSimulationUponAllLivingEntitiesDying): - if self.simulation.getNumLivingEntities() == 0: - time.sleep(1) - self.simulation.cleanup() - if self.__config.randomizeGridSizeUponRestart: - self.__config.randomizeGridSize() - self.__config.randomizeGrassGrowTime() - self.__config.calculateValues() - self.__nextScreen = ScreenType.RESULTS_SCREEN - self.__changeScreen = True - if self.__paused: - self.__paused = False + if self.__controller.shouldEnd(): + time.sleep(1) + self.__controller.quit() + if self.__config.randomizeGridSizeUponRestart: + self.__config.randomizeGridSize() + self.__config.randomizeGrassGrowTime() + self.__config.calculateValues() + self.__nextScreen = ScreenType.RESULTS_SCREEN + self.__changeScreen = True self.__changeScreen = False return self.__nextScreen @@ -101,6 +96,8 @@ def run(self): def initializeSimulation(self): name = "Simulation" self.simulation = Simulation(name, self.__config, self.__graphik.gameDisplay) + # Create controller to manage gameplay actions + self.__controller = SimulationController(self.simulation, self.__config) self.simulation.generateInitialEntities() self.simulation.placeInitialEntitiesInEnvironment() self.simulation.environment.printInfo() @@ -289,58 +286,37 @@ def __addStatToText(self, text, key, value): # Defines the controls of the application. def __handleKeyDownEvent(self, key): + # Use controller for gameplay actions if key == pygame.K_d: - if self.__debug == True: - self.__debug = False - else: - self.__debug = True + self.__controller.toggleDebug() if key == pygame.K_q: - self.simulation.cleanup() - self.simulation.running = False + self.__controller.quit() if key == pygame.K_r: - self.simulation.cleanup() + self.__controller.quit() self.__nextScreen = ScreenType.RESULTS_SCREEN self.__changeScreen = True if key == pygame.K_c: - chicken = Chicken("player-created-chicken") - self.simulation.environment.addEntity(chicken) - self.simulation.addEntityToTrackedEntities(chicken) + self.__controller.spawnChicken() if key == pygame.K_p: - pig = Pig("player-created-pig") - self.simulation.environment.addEntity(pig) - self.simulation.addEntityToTrackedEntities(pig) + self.__controller.spawnPig() if key == pygame.K_k: - cow = Cow("player-created-cow") - self.simulation.environment.addEntity(cow) - self.simulation.addEntityToTrackedEntities(cow) + self.__controller.spawnCow() if key == pygame.K_w: - wolf = Wolf("player-created-wolf") - self.simulation.environment.addEntity(wolf) - self.simulation.addEntityToTrackedEntities(wolf) + self.__controller.spawnWolf() if key == pygame.K_f: - fox = Fox("player-created-fox") - self.simulation.environment.addEntity(fox) - self.simulation.addEntityToTrackedEntities(fox) + self.__controller.spawnFox() if key == pygame.K_b: - rabbit = Rabbit("player-created-rabbit") - self.simulation.environment.addEntity(rabbit) - self.simulation.addEntityToTrackedEntities(rabbit) + self.__controller.spawnRabbit() if key == pygame.K_RIGHTBRACKET: - if self.__config.tickSpeed < self.__config.maxTickSpeed: - self.__config.tickSpeed += 1 + self.__controller.increaseTickSpeed() if key == pygame.K_LEFTBRACKET: - if self.__config.tickSpeed > 1: - self.__config.tickSpeed -= 1 + self.__controller.decreaseTickSpeed() if key == pygame.K_l: - if self.__config.limitTickSpeed: - self.__config.limitTickSpeed = False - else: - self.__config.limitTickSpeed = True + self.__controller.toggleTickSpeedLimit() if key == pygame.K_SPACE or key == pygame.K_ESCAPE: - if self.__paused: - self.__paused = False - else: - self.__paused = True + self.__controller.togglePause() + + # UI-specific controls (not in controller) if key == pygame.K_v: if self.__config.localView: self.__config.localView = False diff --git a/src/simulation/simulationController.py b/src/simulation/simulationController.py new file mode 100644 index 0000000..20aa280 --- /dev/null +++ b/src/simulation/simulationController.py @@ -0,0 +1,132 @@ +from entity.chicken import Chicken +from entity.cow import Cow +from entity.fox import Fox +from entity.pig import Pig +from entity.rabbit import Rabbit +from entity.wolf import Wolf + +# @author Daniel McCoy Stephenson +# @since October 16th, 2024 +class SimulationController: + """ + Controller that abstracts gameplay actions from UI implementation. + This allows both pygame and text UIs to interact with the simulation + in a consistent way without duplicating gameplay logic. + """ + + def __init__(self, simulation, config): + self.simulation = simulation + self.config = config + self.paused = False + self.debug = False + + # State control methods + def togglePause(self): + """Toggle pause state.""" + self.paused = not self.paused + return self.paused + + def setPaused(self, paused): + """Set pause state.""" + self.paused = paused + + def isPaused(self): + """Get pause state.""" + return self.paused + + def toggleDebug(self): + """Toggle debug mode.""" + self.debug = not self.debug + return self.debug + + def isDebug(self): + """Get debug state.""" + return self.debug + + def quit(self): + """Quit the simulation.""" + self.simulation.cleanup() + self.simulation.running = False + + # Speed control methods + def toggleTickSpeedLimit(self): + """Toggle tick speed limit.""" + self.config.limitTickSpeed = not self.config.limitTickSpeed + return self.config.limitTickSpeed + + def increaseTickSpeed(self): + """Increase tick speed if below max.""" + if self.config.tickSpeed < self.config.maxTickSpeed: + self.config.tickSpeed += 1 + return self.config.tickSpeed + + def decreaseTickSpeed(self): + """Decrease tick speed if above min.""" + if self.config.tickSpeed > 1: + self.config.tickSpeed -= 1 + return self.config.tickSpeed + + # Entity spawning methods + def spawnChicken(self): + """Spawn a new chicken entity.""" + chicken = Chicken("player-created-chicken") + self.simulation.environment.addEntity(chicken) + self.simulation.addEntityToTrackedEntities(chicken) + return chicken + + def spawnPig(self): + """Spawn a new pig entity.""" + pig = Pig("player-created-pig") + self.simulation.environment.addEntity(pig) + self.simulation.addEntityToTrackedEntities(pig) + return pig + + def spawnCow(self): + """Spawn a new cow entity.""" + cow = Cow("player-created-cow") + self.simulation.environment.addEntity(cow) + self.simulation.addEntityToTrackedEntities(cow) + return cow + + def spawnWolf(self): + """Spawn a new wolf entity.""" + wolf = Wolf("player-created-wolf") + self.simulation.environment.addEntity(wolf) + self.simulation.addEntityToTrackedEntities(wolf) + return wolf + + def spawnFox(self): + """Spawn a new fox entity.""" + fox = Fox("player-created-fox") + self.simulation.environment.addEntity(fox) + self.simulation.addEntityToTrackedEntities(fox) + return fox + + def spawnRabbit(self): + """Spawn a new rabbit entity.""" + rabbit = Rabbit("player-created-rabbit") + self.simulation.environment.addEntity(rabbit) + self.simulation.addEntityToTrackedEntities(rabbit) + return rabbit + + # Simulation update method + def update(self): + """Update simulation if not paused.""" + if not self.paused: + self.simulation.update() + self.simulation.numTicks += 1 + + # Query methods + def shouldEnd(self): + """Check if simulation should end.""" + if self.config.endSimulationUponAllLivingEntitiesDying: + return self.simulation.getNumLivingEntities() == 0 + return False + + def getSimulation(self): + """Get the simulation instance.""" + return self.simulation + + def getConfig(self): + """Get the config instance.""" + return self.config diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py index 3a768c6..3bb54bf 100644 --- a/src/textSimulationRunner.py +++ b/src/textSimulationRunner.py @@ -11,6 +11,7 @@ from entity.wolf import Wolf from simulation.config import Config from simulation.simulation import Simulation +from simulation.simulationController import SimulationController from lib.pyenvlib.entity import Entity from entity.livingEntity import LivingEntity @@ -31,14 +32,14 @@ class TextSimulationRunner: A text-based simulation runner that visualizes the environment and displays simulation stats to the console without using pygame graphics. Supports keyboard commands for interactive control. + Uses SimulationController to decouple UI from gameplay logic. """ def __init__(self, config: Config = None): self.config = config if config else Config() self.simulation = None + self.controller = None self.running = True - self.paused = False - self.debug = False self.stdscr = None def run(self): @@ -80,21 +81,18 @@ def _run_with_curses(self, stdscr): # Handle keyboard input (non-blocking) self._handle_input() - # Update simulation if not paused - if not self.paused: - self.simulation.update() - self.simulation.numTicks += 1 + # Update simulation through controller + self.controller.update() # Draw the environment and stats self._draw_screen() # Check if simulation should end - if self.config.endSimulationUponAllLivingEntitiesDying: - if self.simulation.getNumLivingEntities() == 0: - self._show_message("All living entities have died. Simulation ended.") - self.simulation.cleanup() - self.running = False - time.sleep(2) + if self.controller.shouldEnd(): + self._show_message("All living entities have died. Simulation ended.") + self.controller.quit() + self.running = False + time.sleep(2) # Apply tick speed limit if self.config.limitTickSpeed: @@ -124,6 +122,9 @@ def get_size(self): mockSoundService = MockSoundService() self.simulation = Simulation(name, self.config, mockDisplay, mockSoundService) + # Create controller to manage gameplay actions + self.controller = SimulationController(self.simulation, self.config) + self.simulation.generateInitialEntities() self.simulation.placeInitialEntitiesInEnvironment() @@ -162,51 +163,32 @@ def _handle_input(self): try: key = self.stdscr.getch() + # Use controller for all gameplay actions if key == ord(' '): - self.paused = not self.paused + self.controller.togglePause() elif key == ord('q'): self.running = False - self.simulation.cleanup() + self.controller.quit() elif key == ord('d'): - self.debug = not self.debug + self.controller.toggleDebug() elif key == ord('c'): - from entity.chicken import Chicken - chicken = Chicken("player-created-chicken") - self.simulation.environment.addEntity(chicken) - self.simulation.addEntityToTrackedEntities(chicken) + self.controller.spawnChicken() elif key == ord('p'): - from entity.pig import Pig - pig = Pig("player-created-pig") - self.simulation.environment.addEntity(pig) - self.simulation.addEntityToTrackedEntities(pig) + self.controller.spawnPig() elif key == ord('k'): - from entity.cow import Cow - cow = Cow("player-created-cow") - self.simulation.environment.addEntity(cow) - self.simulation.addEntityToTrackedEntities(cow) + self.controller.spawnCow() elif key == ord('w'): - from entity.wolf import Wolf - wolf = Wolf("player-created-wolf") - self.simulation.environment.addEntity(wolf) - self.simulation.addEntityToTrackedEntities(wolf) + self.controller.spawnWolf() elif key == ord('f'): - from entity.fox import Fox - fox = Fox("player-created-fox") - self.simulation.environment.addEntity(fox) - self.simulation.addEntityToTrackedEntities(fox) + self.controller.spawnFox() elif key == ord('b'): - from entity.rabbit import Rabbit - rabbit = Rabbit("player-created-rabbit") - self.simulation.environment.addEntity(rabbit) - self.simulation.addEntityToTrackedEntities(rabbit) + self.controller.spawnRabbit() elif key == ord('l'): - self.config.limitTickSpeed = not self.config.limitTickSpeed + self.controller.toggleTickSpeedLimit() elif key == ord(']'): - if self.config.tickSpeed < self.config.maxTickSpeed: - self.config.tickSpeed += 1 + self.controller.increaseTickSpeed() elif key == ord('['): - if self.config.tickSpeed > 1: - self.config.tickSpeed -= 1 + self.controller.decreaseTickSpeed() except: pass # Ignore input errors @@ -312,7 +294,7 @@ def _draw_stats(self, startY, width): if startY + i < curses.LINES - 2: self.stdscr.addstr(startY + i, 1, stat[:width-2]) - if self.debug: + if self.controller.isDebug(): # Add more detailed stats in debug mode debug_stats = [ "", @@ -329,7 +311,7 @@ def _draw_status_bar(self, y, width): """Draw status bar at bottom.""" try: status = "" - if self.paused: + if self.controller.isPaused(): status = "PAUSED - " status += "q:Quit SPACE:Pause d:Debug [/]:Speed c/p/k/w/f/b:Spawn" self.stdscr.addstr(y, 0, status[:width], curses.A_REVERSE) diff --git a/tests/simulation/test_simulationController.py b/tests/simulation/test_simulationController.py new file mode 100644 index 0000000..02715c8 --- /dev/null +++ b/tests/simulation/test_simulationController.py @@ -0,0 +1,97 @@ +import unittest +from simulation.simulationController import SimulationController +from simulation.simulation import Simulation +from simulation.config import Config +from entity.chicken import Chicken +from entity.pig import Pig + +class MockSoundService: + def playReproduceSoundEffect(self): + pass + def playDeathSoundEffect(self): + pass + +class MockDisplay: + def __init__(self, config): + self.config = config + def get_size(self): + return (self.config.displayWidth, self.config.displayHeight) + +class TestSimulationController(unittest.TestCase): + + def setUp(self): + """Set up test fixtures.""" + self.config = Config() + mockDisplay = MockDisplay(self.config) + mockSoundService = MockSoundService() + self.simulation = Simulation("Test", self.config, mockDisplay, mockSoundService) + self.controller = SimulationController(self.simulation, self.config) + + def test_togglePause(self): + """Test pause toggle functionality.""" + self.assertFalse(self.controller.isPaused()) + self.controller.togglePause() + self.assertTrue(self.controller.isPaused()) + self.controller.togglePause() + self.assertFalse(self.controller.isPaused()) + + def test_toggleDebug(self): + """Test debug toggle functionality.""" + self.assertFalse(self.controller.isDebug()) + self.controller.toggleDebug() + self.assertTrue(self.controller.isDebug()) + self.controller.toggleDebug() + self.assertFalse(self.controller.isDebug()) + + def test_tickSpeedControl(self): + """Test tick speed control.""" + initialSpeed = self.config.tickSpeed + self.controller.increaseTickSpeed() + self.assertEqual(self.config.tickSpeed, initialSpeed + 1) + self.controller.decreaseTickSpeed() + self.assertEqual(self.config.tickSpeed, initialSpeed) + + def test_tickSpeedLimit(self): + """Test tick speed limit toggle.""" + initialLimit = self.config.limitTickSpeed + self.controller.toggleTickSpeedLimit() + self.assertEqual(self.config.limitTickSpeed, not initialLimit) + self.controller.toggleTickSpeedLimit() + self.assertEqual(self.config.limitTickSpeed, initialLimit) + + def test_spawnChicken(self): + """Test chicken spawning.""" + initialCount = len(self.simulation.entities) + chicken = self.controller.spawnChicken() + self.assertIsInstance(chicken, Chicken) + self.assertEqual(len(self.simulation.entities), initialCount + 1) + + def test_spawnPig(self): + """Test pig spawning.""" + initialCount = len(self.simulation.entities) + pig = self.controller.spawnPig() + self.assertIsInstance(pig, Pig) + self.assertEqual(len(self.simulation.entities), initialCount + 1) + + def test_update(self): + """Test simulation update through controller.""" + initialTicks = self.simulation.numTicks + self.controller.update() + self.assertEqual(self.simulation.numTicks, initialTicks + 1) + + def test_updateWhenPaused(self): + """Test that simulation doesn't update when paused.""" + self.controller.togglePause() + initialTicks = self.simulation.numTicks + self.controller.update() + # When paused, ticks should not increase + self.assertEqual(self.simulation.numTicks, initialTicks) + + def test_shouldEnd(self): + """Test shouldEnd logic.""" + # With entities, should not end + self.simulation.generateInitialEntities() + self.assertFalse(self.controller.shouldEnd()) + +if __name__ == '__main__': + unittest.main() From 3dc9910e66e81c7b835985f0bb258744d7979794 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:16:01 +0000 Subject: [PATCH 6/8] Fix text visualization bug caused by missing controller.isDebug() call Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/textSimulationRunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py index 3bb54bf..9ddf844 100644 --- a/src/textSimulationRunner.py +++ b/src/textSimulationRunner.py @@ -209,7 +209,7 @@ def _draw_screen(self): cellHeight = 1 # Calculate available space for grid - statsHeight = 12 if self.debug else 8 + statsHeight = 12 if self.controller.isDebug() else 8 availableHeight = height - statsHeight - 2 availableWidth = width - 2 From 9e4311e0ee92393e5e3082d316317d7dc51b3028 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:44:58 +0000 Subject: [PATCH 7/8] Fix excessive screen refresh rate to prevent text jumping Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/textSimulationRunner.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py index 9ddf844..f848a93 100644 --- a/src/textSimulationRunner.py +++ b/src/textSimulationRunner.py @@ -41,6 +41,9 @@ def __init__(self, config: Config = None): self.controller = None self.running = True self.stdscr = None + self.lastDrawTime = 0 + self.drawInterval = 0.1 # Refresh screen at most 10 times per second + self.needsClear = True # Flag to clear screen on first draw or after pause def run(self): """Runs the text-based simulation with curses for non-blocking input.""" @@ -84,8 +87,11 @@ def _run_with_curses(self, stdscr): # Update simulation through controller self.controller.update() - # Draw the environment and stats - self._draw_screen() + # Draw the environment and stats (with rate limiting) + currentTime = time.time() + if currentTime - self.lastDrawTime >= self.drawInterval: + self._draw_screen() + self.lastDrawTime = currentTime # Check if simulation should end if self.controller.shouldEnd(): @@ -166,11 +172,13 @@ def _handle_input(self): # Use controller for all gameplay actions if key == ord(' '): self.controller.togglePause() + self.needsClear = True # Clear screen on pause toggle elif key == ord('q'): self.running = False self.controller.quit() elif key == ord('d'): self.controller.toggleDebug() + self.needsClear = True # Clear screen on debug toggle elif key == ord('c'): self.controller.spawnChicken() elif key == ord('p'): @@ -196,7 +204,11 @@ def _handle_input(self): def _draw_screen(self): """Draw the environment visualization and stats.""" try: - self.stdscr.clear() + # Only clear screen when needed (first draw, after pause, etc) + if self.needsClear: + self.stdscr.clear() + self.needsClear = False + height, width = self.stdscr.getmaxyx() # Calculate grid display area From 9cccb9feb2f45db61f677c42b26d6503fdef96d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:37:37 +0000 Subject: [PATCH 8/8] Capture entity behavior logs and display only latest message Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/textSimulationRunner.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/textSimulationRunner.py b/src/textSimulationRunner.py index f848a93..d2b7621 100644 --- a/src/textSimulationRunner.py +++ b/src/textSimulationRunner.py @@ -2,6 +2,7 @@ import os import sys import curses +from io import StringIO from entity.chicken import Chicken from entity.cow import Cow from entity.fox import Fox @@ -44,6 +45,9 @@ def __init__(self, config: Config = None): self.lastDrawTime = 0 self.drawInterval = 0.1 # Refresh screen at most 10 times per second self.needsClear = True # Flag to clear screen on first draw or after pause + self.lastMessage = "" # Store the last log message + self.messageBuffer = StringIO() # Capture stdout + self.originalStdout = None # Store original stdout def run(self): """Runs the text-based simulation with curses for non-blocking input.""" @@ -53,6 +57,10 @@ def _run_with_curses(self, stdscr): """Main loop with curses support.""" self.stdscr = stdscr + # Redirect stdout to capture print statements + self.originalStdout = sys.stdout + sys.stdout = self + # Initialize curses curses.curs_set(0) # Hide cursor stdscr.nodelay(1) # Non-blocking input @@ -111,6 +119,20 @@ def _run_with_curses(self, stdscr): except Exception as e: self.simulation.cleanup() raise e + finally: + # Restore original stdout + if self.originalStdout: + sys.stdout = self.originalStdout + + def write(self, text): + """Capture stdout writes (called when print() is used).""" + if text and text.strip(): + # Store only the last non-empty message + self.lastMessage = text.strip() + + def flush(self): + """Required for file-like object compatibility.""" + pass def initializeSimulation(self): """Initializes the simulation with a mock game display.""" @@ -302,6 +324,11 @@ def _draw_stats(self, startY, width): if self.config.limitTickSpeed: stats.append(f"Tick Speed: {self.config.tickSpeed}/{self.config.maxTickSpeed}") + # Add a blank line before the last message + if self.lastMessage: + stats.append("") + stats.append(f"Event: {self.lastMessage[:width-10]}") + for i, stat in enumerate(stats): if startY + i < curses.LINES - 2: self.stdscr.addstr(startY + i, 1, stat[:width-2])