diff --git a/README.md b/README.md index 54c3944..c914634 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,42 @@ 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 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 +- Remote SSH sessions +- Automated testing and analysis + ## Types of Living Entities - Chicken - Pig @@ -19,6 +55,10 @@ 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 ------------ | ------------- space | pause/unpause @@ -41,6 +81,24 @@ f11 | toggle fullscreen mode r | restart q | quit +### 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. ## Support 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/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/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/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 new file mode 100644 index 0000000..d2b7621 --- /dev/null +++ b/src/textSimulationRunner.py @@ -0,0 +1,369 @@ +import time +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 +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 +from simulation.simulationController import SimulationController +from lib.pyenvlib.entity import Entity +from entity.livingEntity import LivingEntity + +# @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 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.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 + 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.""" + curses.wrapper(self._run_with_curses) + + 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 + 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: + while self.running: + # Handle keyboard input (non-blocking) + self._handle_input() + + # Update simulation through controller + self.controller.update() + + # 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(): + 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: + time.sleep((self.config.maxTickSpeed - self.config.tickSpeed) / self.config.maxTickSpeed) + else: + time.sleep(0.01) # Small delay to prevent CPU spinning + + except KeyboardInterrupt: + self.simulation.cleanup() + 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.""" + 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) + + # Create controller to manage gameplay actions + self.controller = SimulationController(self.simulation, self.config) + + self.simulation.generateInitialEntities() + self.simulation.placeInitialEntitiesInEnvironment() + + 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() + + # 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'): + self.controller.spawnPig() + elif key == ord('k'): + self.controller.spawnCow() + elif key == ord('w'): + self.controller.spawnWolf() + elif key == ord('f'): + self.controller.spawnFox() + elif key == ord('b'): + self.controller.spawnRabbit() + elif key == ord('l'): + self.controller.toggleTickSpeedLimit() + elif key == ord(']'): + self.controller.increaseTickSpeed() + elif key == ord('['): + self.controller.decreaseTickSpeed() + + except: + pass # Ignore input errors + + def _draw_screen(self): + """Draw the environment visualization and stats.""" + try: + # 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 + 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.controller.isDebug() 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) + + # Get top entity + topEntityId = list(location.getEntities().keys())[-1] + topEntity = location.getEntities()[topEntityId] + + # 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: + 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)}", + ] + + 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]) + + if self.controller.isDebug(): + # 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.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) + 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 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()