Skip to content
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@ This game allows you to manage a virtual environment containing entities that de

<img src="pics/screenshot4.PNG" alt="screenshot" width="720"/>

## 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
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 17 additions & 2 deletions src/apex.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import pygame
from lib.graphiklib.graphik import Graphik
from screen.mainMenuScreen import MainMenuScreen
Expand Down Expand Up @@ -58,5 +59,19 @@ def __quitApplication(self):
pygame.quit()
quit()

apex = Apex()
apex.run()
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()
106 changes: 41 additions & 65 deletions src/screen/simulationScreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -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()

Expand All @@ -75,32 +76,28 @@ 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

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()
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading