Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #99

Merged
merged 17 commits into from
Oct 25, 2024
Merged

Dev #99

Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pz:
gym:
poetry run python3 -m examples.gymnasium_example

remote:
poetry run python3 -m examples.client_example
# Create new replay and run it
make n_replay:
poetry run python3 -m examples.record_replay_example
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,19 @@ An observation for one agent is a dictionary `{"observation": observation, "acti
The `observation` is a `Dict`. Values are either `numpy` matrices with shape `(N,M)`, or simple `int` constants:
| Key | Shape | Description |
| -------------------- | --------- | ---------------------------------------------------------------------------- |
| `army` | `(N,M)` | Number of units in a cell regardless of the owner |
| `general` | `(N,M)` | Mask indicating cells containing a general |
| `city` | `(N,M)` | Mask indicating cells containing a city |
| `visible_cells` | `(N,M)` | Mask indicating cells that are visible to the agent |
| `owned_cells` | `(N,M)` | Mask indicating cells owned by the agent |
| `opponent_cells` | `(N,M)` | Mask indicating cells owned by the opponent |
| `neutral_cells` | `(N,M)` | Mask indicating cells that are not owned by any agent |
| `structure` | `(N,M)` | Mask indicating whether cells contain cities or mountains, even out of FoV |
| `armies` | `(N,M)` | Number of units in a visible cell regardless of the owner |
| `generals` | `(N,M)` | Mask indicating visible cells containing a general |
| `cities` | `(N,M)` | Mask indicating visible cells containing a city |
| `mountains` | `(N,M)` | Mask indicating visible cells containing mountains |
| `neutral_cells` | `(N,M)` | Mask indicating visible cells that are not owned by any agent |
| `owned_cells` | `(N,M)` | Mask indicating visible cells owned by the agent |
| `opponent_cells` | `(N,M)` | Mask indicating visible cells owned by the opponent |
| `fog_cells` | `(N,M)` | Mask indicating fog cells that are not mountains or cities |
| `structures_in_fog` | `(N,M)` | Mask showing cells containing either cities or mountains in fog |
| `owned_land_count` | — | Number of cells the agent owns |
| `owned_army_count` | — | Total number of units owned by the agent |
| `opponent_land_count`| — | Number of cells owned by the opponent |
| `opponent_army_count`| — | Total number of units owned by the opponent |
| `is_winner` | — | Indicates whether the agent won |
| `timestep` | — | Current timestep of the game |

The `action_mask` is a 3D array with shape `(N, M, 4)`, where each element corresponds to whether a move is valid from cell
Expand Down
12 changes: 12 additions & 0 deletions examples/client_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from generals.remote import autopilot

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--user_id", type=str, default="user_id9l")
parser.add_argument("--lobby_id", type=str, default="elj2")
parser.add_argument("--agent_id", type=str, default="Expander") # agent_id should be "registered" in AgentFactory

if __name__ == "__main__":
args = parser.parse_args()
autopilot(args.agent_id, args.user_id, args.lobby_id)
4 changes: 2 additions & 2 deletions generals/agents/agent_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ def make_agent(agent_type: str, **kwargs) -> Agent:
"""
Creates an agent of the specified type.
"""
if agent_type == "random":
if agent_type == "Random":
return RandomAgent(**kwargs)
elif agent_type == "expander":
elif agent_type == "Expander":
return ExpanderAgent(**kwargs)
else:
raise ValueError(f"Unknown agent type: {agent_type}")
6 changes: 4 additions & 2 deletions generals/agents/expander_agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import numpy as np

from generals.core.config import Action, Direction, Observation
from generals.core.config import Direction
from generals.core.game import Action
from generals.core.observation import Observation

from .agent import Agent

Expand All @@ -26,7 +28,7 @@ def act(self, observation: Observation) -> Action:
"split": 0,
}

army = observation["army"]
army = observation["armies"]
opponent = observation["opponent_cells"]
neutral = observation["neutral_cells"]

Expand Down
3 changes: 2 additions & 1 deletion generals/agents/random_agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np

from generals.core.game import Action, Observation
from generals.core.game import Action
from generals.core.observation import Observation

from .agent import Agent

Expand Down
80 changes: 57 additions & 23 deletions generals/core/channels.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
from scipy.ndimage import maximum_filter # type: ignore

from .config import MOUNTAIN, PASSABLE

Expand All @@ -7,20 +8,21 @@

class Channels:
"""
army - army size in each cell
general - general mask (1 if general is in cell, 0 otherwise)
mountain - mountain mask (1 if cell is mountain, 0 otherwise)
city - city mask (1 if cell is city, 0 otherwise)
armies - army size in each cell
generals - general mask (1 if general is in cell, 0 otherwise)
mountains - mountain mask (1 if cell is mountain, 0 otherwise)
cities - city mask (1 if cell is city, 0 otherwise)
passable - passable mask (1 if cell is passable, 0 otherwise)
ownership_i - ownership mask for player i (1 if player i owns cell, 0 otherwise)
ownership_neutral - ownership mask for neutral cells that are passable (1 if cell is neutral, 0 otherwise)
ownership_neutral - ownership mask for neutral cells that are
passable (1 if cell is neutral, 0 otherwise)
"""

def __init__(self, grid: np.ndarray, _agents: list[str]):
self._army: np.ndarray = np.where(np.isin(grid, valid_generals), 1, 0).astype(int)
self._general: np.ndarray = np.where(np.isin(grid, valid_generals), 1, 0).astype(bool)
self._mountain: np.ndarray = np.where(grid == MOUNTAIN, 1, 0).astype(bool)
self._city: np.ndarray = np.where(np.char.isdigit(grid), 1, 0).astype(bool)
self._armies: np.ndarray = np.where(np.isin(grid, valid_generals), 1, 0).astype(int)
self._generals: np.ndarray = np.where(np.isin(grid, valid_generals), 1, 0).astype(bool)
self._mountains: np.ndarray = np.where(grid == MOUNTAIN, 1, 0).astype(bool)
self._cities: np.ndarray = np.where(np.char.isdigit(grid), 1, 0).astype(bool)
self._passable: np.ndarray = (grid != MOUNTAIN).astype(bool)

self._ownership: dict[str, np.ndarray] = {
Expand All @@ -31,39 +33,71 @@ def __init__(self, grid: np.ndarray, _agents: list[str]):

# City costs are 40 + digit in the cell
city_costs = np.where(np.char.isdigit(grid), grid, "0").astype(int)
self.army += 40 * self.city + city_costs
self.armies += 40 * self.cities + city_costs

def get_visibility(self, agent_id: str) -> np.ndarray:
channel = self._ownership[agent_id]
return maximum_filter(channel, size=3)

@staticmethod
def channel_to_indices(channel: np.ndarray) -> np.ndarray:
"""
Returns a list of indices of cells with non-zero values from specified a channel.
"""
return np.argwhere(channel != 0)

@property
def ownership(self) -> dict[str, np.ndarray]:
return self._ownership

@ownership.setter
def ownership(self, value):
self._ownership = value

@property
def army(self) -> np.ndarray:
return self._army
def armies(self) -> np.ndarray:
return self._armies

@army.setter
def army(self, value):
self._army = value
@armies.setter
def armies(self, value):
self._armies = value

@property
def general(self) -> np.ndarray:
return self._general
def generals(self) -> np.ndarray:
return self._generals

@generals.setter
def generals(self, value):
self._generals = value

@property
def mountain(self) -> np.ndarray:
return self._mountain
def mountains(self) -> np.ndarray:
return self._mountains

@mountains.setter
def mountains(self, value):
self._mountains = value

@property
def city(self) -> np.ndarray:
return self._city
def cities(self) -> np.ndarray:
return self._cities

@cities.setter
def cities(self, value):
self._cities = value

@property
def passable(self) -> np.ndarray:
return self._passable

@passable.setter
def passable(self, value):
self._passable = value

@property
def ownership_neutral(self) -> np.ndarray:
return self._ownership["neutral"]

def _set_passable(self, value):
self._passable = value
@ownership_neutral.setter
def ownership_neutral(self, value):
self._ownership["neutral"] = value
18 changes: 4 additions & 14 deletions generals/core/config.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
from collections.abc import Callable
from enum import Enum, IntEnum, StrEnum
from importlib.resources import files
from typing import Any, Literal, TypeAlias

import gymnasium as gym
import numpy as np

# Type aliases
Observation: TypeAlias = dict[str, np.ndarray | dict[str, gym.Space]]
Action: TypeAlias = dict[str, int | np.ndarray]
Info: TypeAlias = dict[str, Any]

Reward: TypeAlias = float
RewardFn: TypeAlias = Callable[[Observation, Action, bool, Info], Reward]
AgentID: TypeAlias = str
from typing import Literal

# Game Literals
PASSABLE: Literal["."] = "."
Expand All @@ -34,6 +21,9 @@ class Direction(Enum):
RIGHT = (0, 1)


DIRECTIONS = [Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT]


class Path(StrEnum):
GENERAL_PATH = str(files("generals.assets.images") / "crownie.png")
CITY_PATH = str(files("generals.assets.images") / "citie.png")
Expand Down
Loading
Loading