Skip to content

Commit

Permalink
Merge pull request #70 from strakam/restructure-files-update-readme
Browse files Browse the repository at this point in the history
Restructure files update readme
  • Loading branch information
strakam authored Oct 2, 2024
2 parents f26eb44 + 8e93898 commit d7f9924
Show file tree
Hide file tree
Showing 26 changed files with 94 additions and 82 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ at:
python3 tests/sb3_check.py

test_performance:
python3 -m tests.parallel_api_test
python3 -m tests.parallel_api_check

pytest:
pytest
Expand Down
49 changes: 28 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
[Installation](#-installation)[Getting Started](#-getting-started)[Customization](#-custom-maps)[Environment](#-environment)
</div>

[Generals.io](https://generals.io/) is a real-time strategy game where players compete to conquer their opponents' generals on a 2D grid. While the goal is simple — capture the enemy general — the gameplay involves a lot of depth. Players need to employ strategic planning, deception, and manage both micro and macro mechanics throughout the game. The combination of these elements makes the game highly engaging and complex.
Generals-RL is a real-time strategy environment where players compete to conquer their opponents' generals on a 2D grid.
While the goal is simple — capture the enemy general — the gameplay involves a lot of depth.
Players need to employ strategic planning, deception, and manage both micro and macro mechanics throughout the game.
The combination of these elements makes the game highly engaging and complex.

This repository aims to make bot development more accessible, especially for Machine Learning based agents.

Expand All @@ -23,6 +26,10 @@ Highlights:
* 🔧 Easy customization of environments
* 🔬 Analysis tools such as replays

> [!NOTE]
> This repository is based on the [generals.io](https://generals.io) game.
> Check it out, its a lot of fun !
## 📦 Installation
Stable release version is available through pip:
```bash
Expand All @@ -38,7 +45,7 @@ pip install -e .
## Usage example (🤸 Gymnasium)

```python
from generals.env import gym_generals
from generals import gym_generals
from generals.agents import RandomAgent, ExpanderAgent

# Initialize agents
Expand Down Expand Up @@ -73,44 +80,44 @@ Creating your first agent is very simple.
> [!TIP]
> Check out `Makefile` and run some examples to get a feel for the game 🤗.
## 🎨 Custom maps
Maps are handled via `Mapper` class. You can instantiate the class with desired map properties, and it will generate
maps with these properties for each run.
## 🎨 Custom grids
Grids are generated via `GridFactory`. You can instantiate the class with desired grid properties, and it will generate
grid with these properties for each run.
```python
from generals.env import pz_generals
from generals.map import Mapper
from generals import pz_generals
from generals import GridFactory

mapper = Mapper(
grid_factory = GridFactory(
grid_dims=(10, 10), # Dimensions of the grid (height, width)
mountain_density=0.2, # Probability of a mountain in a cell
city_density=0.05, # Probability of a city in a cell
general_positions=[(0,3),(5,7)], # Positions of generals (i, j)
)

# Create environment
env = pz_generals(mapper=mapper, ...)
env = pz_generals(grid_factory=grid_factory, ...)
```
You can also specify map manually, as a string via `options` dict:
You can also specify grids manually, as a string via `options` dict:
```python
from generals.env import pz_generals
from generals.map import Mapper
from generals import pz_generals
from generals import GridFactory

mapper = Mapper()
env = pz_generals(mapper=mapper, ...)
grid_factory = GridFactory()
env = pz_generals(grid_factory=grid_factory, ...)

map = """
grid = """
.3.#
#..A
#..#
.#.B
"""

options = {'map' : map}
options = {"grid": grid}

# Pass the new map to the environment (for the next game)
# Pass the new grid to the environment (for the next game)
env.reset(options=options)
```
Maps are encoded using these symbols:
Grids are encoded using these symbols:
- `.` for cells where you can move your army
- `#` for mountains (terrain that can not be passed)
- `A,B` are positions of generals
Expand All @@ -120,7 +127,7 @@ Maps are encoded using these symbols:
We can store replays and then analyze them. `Replay` class handles replay related functionality.
### Storing a replay
```python
from generals.env import pz_generals
from generals import pz_generals

options = {"replay": "my_replay"}
env = pz_generals(...)
Expand All @@ -130,7 +137,7 @@ env.reset(options=options) # The next game will be encoded in my_replay.pkl
### Loading a replay

```python
from generals.replay import Replay
from generals import Replay

# Initialize Replay instance
replay = Replay.load("my_replay")
Expand Down Expand Up @@ -189,6 +196,6 @@ def custom_reward_fn(observation, action, done, info):
# Give agent a reward based on the number of cells they own
return observation["observation"]["owned_land_count"]
env = generals_v0(reward_fn=custom_reward_fn)
env = pz_generals(reward_fn=custom_reward_fn)
observations, info = env.reset()
```
8 changes: 4 additions & 4 deletions examples/complete_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from generals.env import pz_generals
from generals import pz_generals
from generals.agents import RandomAgent, ExpanderAgent
from generals.grid import GridFactory
from generals import GridFactory

# Initialize agents - their names are then called for actions
randomer = RandomAgent("Random1", color=(255, 125, 0))
Expand All @@ -18,7 +18,7 @@
general_positions=[(0, 0), (3, 3)],
)

# Custom map that will override mapper's map for next game
# Custom map that will override GridFactory for this game
map = """
A..#
.#3#
Expand All @@ -30,7 +30,7 @@
env = pz_generals(gf, agents, render_mode=None) # Disable rendering

options = {
"map": map,
"grid": map,
"replay_file": "replay",
}

Expand Down
2 changes: 1 addition & 1 deletion examples/gymnasium_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from generals.env import gym_generals
from generals import gym_generals
from generals.agents import RandomAgent, ExpanderAgent

# Initialize agents
Expand Down
2 changes: 1 addition & 1 deletion examples/pettingzoo_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from generals.env import pz_generals
from generals import pz_generals
from generals.agents import ExpanderAgent, RandomAgent

# Initialize agents
Expand Down
2 changes: 1 addition & 1 deletion examples/replay_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from generals.replay import Replay
from generals import Replay

replay = Replay.load("replay.pkl")
replay.play()
8 changes: 4 additions & 4 deletions generals/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .core.grid import GridFactory, Grid
from .envs.env import pz_generals, gym_generals
from .core.replay import Replay


__all__ = ['generals', 'generals_v0', 'game', 'game_config']

__version__ = '0.0.1'
__author__ = 'Matej Straka'
__all__ = ['GridFactory', 'Grid', 'Replay', pz_generals, gym_generals]
2 changes: 1 addition & 1 deletion generals/agents/expander_agent.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np
from .agent import Agent

from generals.config import DIRECTIONS
from generals.core.config import DIRECTIONS


class ExpanderAgent(Agent):
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion generals/channels.py → generals/core/channels.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np

from generals.config import MOUNTAIN, PASSABLE
from .config import MOUNTAIN, PASSABLE

valid_generals = ["A", "B"] # Generals are represented by A and B

Expand Down
1 change: 0 additions & 1 deletion generals/config.py → generals/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
##################
# Game constants #
##################
INCREMENT_RATE: int = 50 # every 50 ticks, number of units increases
GAME_SPEED: float = 8 # by default, every 8 ticks, actions are processed

########################
Expand Down
22 changes: 12 additions & 10 deletions generals/game.py → generals/core/game.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import warnings
from typing import Any
from typing_extensions import TypeAlias

from .channels import Channels
from generals.grid import Grid
import numpy as np
import gymnasium as gym
from typing_extensions import TypeAlias

from generals.config import DIRECTIONS, PASSABLE, MOUNTAIN, INCREMENT_RATE
from .channels import Channels
from .grid import Grid
from .config import DIRECTIONS

from scipy.ndimage import maximum_filter

Observation: TypeAlias = dict[str, gym.Space | dict[str, gym.Space]]
Action: TypeAlias = gym.Space
Info: TypeAlias = dict[str, Any]

increment_rate = 50


class Game:
def __init__(self, grid: Grid, agents: list[str]):
Expand Down Expand Up @@ -138,7 +140,9 @@ def visibility_channel(self, ownership_channel: np.ndarray) -> np.ndarray:
"""
return maximum_filter(ownership_channel, size=3)

def step(self, actions: dict[str, Action]) -> tuple[dict[str, Observation], dict[str, dict]]:
def step(
self, actions: dict[str, Action]
) -> tuple[dict[str, Observation], dict[str, dict]]:
"""
Perform one step of the game
Expand Down Expand Up @@ -242,18 +246,16 @@ def _global_game_update(self) -> None:

owners = self.agents

# every TICK_RATE steps, increase army size in each cell
if self.time % INCREMENT_RATE == 0:
# every `increment_rate` steps, increase army size in each cell
if self.time % increment_rate == 0:
for owner in owners:
self.channels.army += self.channels.ownership[owner]

# Increment armies on general and city cells, but only if they are owned by player
if self.time % 2 == 0 and self.time > 0:
update_mask = self.channels.general + self.channels.city
for owner in owners:
self.channels.army += (
update_mask * self.channels.ownership[owner]
)
self.channels.army += update_mask * self.channels.ownership[owner]

def is_done(self) -> bool:
"""
Expand Down
2 changes: 1 addition & 1 deletion generals/grid.py → generals/core/grid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
from generals.config import PASSABLE, MOUNTAIN
from .config import PASSABLE, MOUNTAIN


class Grid:
Expand Down
4 changes: 2 additions & 2 deletions generals/replay.py → generals/core/replay.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pickle
import time

from generals.grid import Grid
from generals.core.grid import Grid
from generals.core.game import Game
from generals.gui import GUI
from generals.game import Game
from copy import deepcopy


Expand Down
Empty file added generals/envs/__init__.py
Empty file.
9 changes: 5 additions & 4 deletions generals/env.py → generals/envs/env.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from .agents import Agent
from .integrations.gymnasium_integration import Gym_Generals, RewardFn
from .integrations.pettingzoo_integration import PZ_Generals
from generals.agents import Agent
from .gymnasium_integration import Gym_Generals, RewardFn
from .pettingzoo_integration import PZ_Generals

from generals import GridFactory

from .grid import GridFactory


def pz_generals(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
import functools
from copy import deepcopy

from ..agents import Agent
from ..game import Game, Action, Observation
from ..grid import GridFactory
from ..gui import GUI
from ..replay import Replay
from generals.agents import Agent
from generals.core.game import Game, Action, Observation, Info
from generals.core.grid import GridFactory
from generals.gui import GUI
from generals.core.replay import Replay

# Type aliases
from generals.game import Info
Reward: TypeAlias = float
RewardFn: TypeAlias = Callable[[dict[str, Observation], Action, bool, Info], Reward]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@

from pettingzoo.utils.env import AgentID

from ..game import Game, Action, Observation
from ..grid import GridFactory
from ..agents import Agent
from ..gui import GUI
from ..replay import Replay
from generals.core.game import Game, Action, Observation, Info
from generals.core.grid import GridFactory
from generals.agents import Agent
from generals.gui import GUI
from generals.core.replay import Replay


# Type aliases
from generals.game import Info

Reward: TypeAlias = float
RewardFn: TypeAlias = Callable[[dict[str, Observation], Action, bool, Info], Reward]

Expand Down Expand Up @@ -63,9 +61,9 @@ def render(self, fps=6) -> None:
if self.render_mode == "human":
self.gui.tick(fps=fps)

def reset(self, seed: int | None = None, options: dict | None = None) -> tuple[
dict[AgentID, Observation], dict[AgentID, dict]
]:
def reset(
self, seed: int | None = None, options: dict | None = None
) -> tuple[dict[AgentID, Observation], dict[AgentID, dict]]:
if options is None:
options = {}
self.agents = deepcopy(self.possible_agents)
Expand Down Expand Up @@ -94,8 +92,14 @@ def reset(self, seed: int | None = None, options: dict | None = None) -> tuple[
infos = {agent: {} for agent in self.agents}
return observations, infos

def step(self, actions: dict[AgentID, Action]) -> tuple[
dict[AgentID, Observation], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, Info]
def step(
self, actions: dict[AgentID, Action]
) -> tuple[
dict[AgentID, Observation],
dict[AgentID, float],
dict[AgentID, bool],
dict[AgentID, bool],
dict[AgentID, Info],
]:
observations, infos = self.game.step(actions)

Expand Down
2 changes: 1 addition & 1 deletion generals/gui/gui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any

from ..game import Game
from generals.core.game import Game
from .properties import Properties
from .event_handler import EventHandler
from .rendering import Renderer
Expand Down
4 changes: 2 additions & 2 deletions generals/gui/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from pygame.time import Clock

from generals import config as c
from generals.game import Game
from generals.core import config as c
from generals.core.game import Game


@dataclass
Expand Down
Loading

0 comments on commit d7f9924

Please sign in to comment.