Skip to content

Commit 7002b29

Browse files
Working on getting the core Pacman elements in.
1 parent d5c5f45 commit 7002b29

File tree

10 files changed

+275
-17
lines changed

10 files changed

+275
-17
lines changed

pacai/agents/dummy.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pacai.core.action
2+
import pacai.core.agent
3+
import pacai.core.gamestate
4+
5+
class DummyAgent(pacai.core.agent.Agent):
6+
"""
7+
An agent that only takes the STOP action.
8+
At first this may seem useless, but dummy agents can serve several purposes.
9+
Like being a stand-in for a future agent, fallback for a failing agent, or a placeholder when running a replay.
10+
"""
11+
12+
def get_action(self, state: pacai.core.gamestate.GameState) -> pacai.core.action.Action:
13+
return pacai.core.action.STOP
14+
15+
def game_start(self, agent_index: int, suggested_seed: int, initial_state: pacai.core.gamestate.GameState) -> None:
16+
pass
17+
18+
def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
19+
pass

pacai/agents/random.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import random
2+
3+
import pacai.core.action
4+
import pacai.core.agent
5+
import pacai.core.gamestate
6+
7+
class RandomAgent(pacai.core.agent.Agent):
8+
""" An agent that just takes random (legal) action. """
9+
10+
def __init__(self, *args, **kwargs) -> None:
11+
super().__init__(*args, *kwargs)
12+
13+
self._rng: random.Random | None = None
14+
15+
def get_action(self, state: pacai.core.gamestate.GameState) -> pacai.core.action.Action:
16+
""" Choose a random action. """
17+
18+
if (self._rng is None):
19+
raise ValueError("Cannot get an action before starting the game.")
20+
21+
legal_actions = state.get_legal_actions()
22+
return self._rng.choice(legal_actions)
23+
24+
def game_start(self, agent_index: int, suggested_seed: int, initial_state: pacai.core.gamestate.GameState) -> None:
25+
""" Initialize the agent's random number generator. """
26+
27+
self._rng = random.Random(suggested_seed)
28+
29+
def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
30+
""" Do nothing. """
31+
32+
pass

pacai/core/agent.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,14 @@ def __init__(self, args: AgentArguments) -> None:
4141
For example, an agent with a move delay of 50 will move twice as often as an agent with a move delay of 100.
4242
"""
4343

44-
# TEST
4544
@abc.abstractmethod
4645
def get_action(self, state: pacai.core.gamestate.GameState) -> pacai.core.action.Action:
4746
pass
4847

49-
# TEST
5048
@abc.abstractmethod
5149
def game_start(self, agent_index: int, suggested_seed: int, initial_state: pacai.core.gamestate.GameState) -> None:
5250
pass
5351

54-
# TEST
5552
@abc.abstractmethod
5653
def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
5754
pass

pacai/core/board.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import re
3+
import typing
34

5+
import pacai.core.action
46
import pacai.util.file
57
import pacai.util.json
68
import pacai.util.reflection
@@ -18,11 +20,43 @@
1820
class Marker(str):
1921
"""
2022
A marker represents something that can appear on a board.
21-
These are similar to a game piece of token in a traditional board game (like the top hat or dog in Monolopy).
23+
These are similar to a game piece of token in a traditional board game (like the top hat or dog in Monopoly).
2224
"""
2325

2426
pass
2527

28+
class Position(typing.NamedTuple):
29+
"""
30+
A 2-dimension location.
31+
The first value represent row/y/height,
32+
and the second value represents col/x/width.
33+
"""
34+
35+
row: int
36+
""" The row / y / height of this position. """
37+
38+
col: int
39+
""" The col / x / width of this position. """
40+
41+
def to_index(self, width: int) -> int:
42+
""" Convert this position into a 1-dimension index. """
43+
return (self.row * width) + self.col
44+
45+
@staticmethod
46+
def from_index(index: int, width: int) -> 'Position':
47+
""" Convert a 1-dimension index into a 2-dimension position. """
48+
row = index // width
49+
col = index % width
50+
51+
return Position(row, col)
52+
53+
def add(self, other: 'Position') -> 'Position':
54+
"""
55+
Add another position (offset) to this one and return the result.
56+
"""
57+
58+
return Position(self.row + other.row, self.col + other.col)
59+
2660
class Board:
2761
"""
2862
A board represents the static (non-agent) components of a game.
@@ -44,9 +78,19 @@ def __init__(self,
4478
extra_markers: list[str] = [],
4579
strip: bool = True,
4680
**kwargs) -> None:
81+
self._marker_empty: Marker = Marker(marker_empty)
82+
"""
83+
The marker used for empty locations.
84+
"""
85+
86+
self._marker_wall: Marker = Marker(marker_wall)
87+
"""
88+
The marker used for wall locations.
89+
"""
90+
4791
self._markers: dict[str, Marker] = {
48-
marker_empty: Marker(marker_empty),
49-
marker_wall: Marker(marker_wall),
92+
marker_empty: self._marker_empty,
93+
marker_wall: self._marker_wall,
5094
}
5195
""" Map the text for a marker to the actual marker. """
5296

@@ -104,6 +148,44 @@ def _process_text(self, board_text: str, strip: bool = True) -> tuple[int, int,
104148

105149
return height, width, locations
106150

151+
def _get_index(self, position):
152+
"""
153+
Get the internal 1-d index for this position.
154+
Will raise if this position is not valid.
155+
"""
156+
157+
index = position.to_index(self.width)
158+
if ((index < 0) or (index >= len(self._locations))):
159+
raise ValueError("Invalid position: %s.", str(position))
160+
161+
return index
162+
163+
def is_wall(self, position):
164+
return (self._locations[self._get_index(position)] == self._marker_wall)
165+
166+
def get_neighbors(self, position: Position) -> list[tuple[pacai.core.action.Action, Position]]:
167+
"""
168+
Get positions that are directly touching (via cardinal directions) the given position
169+
without being inside a wall,
170+
and the action it would take to get there.
171+
"""
172+
173+
neighbors = []
174+
for (action, offset) in CARDINAL_OFFSETS:
175+
neighbor = position.add(offset)
176+
177+
if ((neighbor.row < 0) or (neighbor.col < 0)):
178+
continue
179+
180+
if ((neighbor.row >= self.height) or (neighbor.col >= self.width)):
181+
continue
182+
183+
if (self.is_wall(neighbor)):
184+
continue
185+
186+
neighbors.append((action, neighbor))
187+
188+
return neighbors
107189

108190
def load_path(path: str) -> Board:
109191
""" Load a board from a file. """
@@ -138,3 +220,10 @@ def load_string(text: str) -> Board:
138220

139221
board_class = options.get('class', DEFAULT_BOARD_CLASS)
140222
return pacai.util.reflection.new_object(board_class, board_text, **options)
223+
224+
CARDINAL_OFFSETS: list[tuple[pacai.core.action.Action, Position]] = [
225+
(pacai.core.action.NORTH, Position(-1, 0)),
226+
(pacai.core.action.EAST, Position(0, 1)),
227+
(pacai.core.action.WEST, Position(0, -1)),
228+
(pacai.core.action.SOUTH, Position(1, 0)),
229+
]

pacai/core/game.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Game(abc.ABC):
4242
def __init__(self,
4343
agent_args: list[pacai.core.agent.AgentArguments],
4444
isolation_level: pacai.core.isolation.Level = pacai.core.isolation.Level.NONE,
45+
max_moves: int = -1,
4546
seed: int | None = None,
4647
) -> None:
4748
"""
@@ -55,12 +56,22 @@ def __init__(self,
5556
seed = random.randint(0, 2**64)
5657

5758
self._seed: int = seed
59+
""" The random seed for this game's RNG. """
5860

5961
self._agent_args: list[pacai.core.agent.AgentArguments] = agent_args
62+
""" The required information for creating the agents for this game. """
63+
6064
if (len(self._agent_args) == 0):
6165
raise ValueError("No agents provided.")
6266

6367
self._isolation_level: pacai.core.isolation.Level = isolation_level
68+
""" The isolation level to use for this game. """
69+
70+
self._max_moves: int = max_moves
71+
"""
72+
The total number of moves (between all agents) allowed for this game.
73+
If -1, unlimited moves are allowed.
74+
"""
6475

6576
def _setup(self) -> None:
6677
""" Prepare for a game. """
@@ -69,7 +80,7 @@ def _setup(self) -> None:
6980
pass
7081

7182
@abc.abstractmethod
72-
def get_initial_state(self, rng: random.Random) -> pacai.core.gamestate.GameState:
83+
def get_initial_state(self, rng: random.Random, board: pacai.core.board.Board) -> pacai.core.gamestate.GameState:
7384
""" Create the initial state for this game. """
7485

7586
pass
@@ -89,7 +100,9 @@ def check_end(self, state: pacai.core.gamestate.GameState) -> bool:
89100

90101
pass
91102

92-
def run(self) -> GameResult:
103+
# TODO(eriq): Validate that the board works for this game (e.g., number of agent positions).
104+
105+
def run(self, board: pacai.core.board.Board) -> GameResult:
93106
"""
94107
The main "game loop" for all games.
95108
One round of the loop will:
@@ -119,17 +132,17 @@ def run(self) -> GameResult:
119132
result = GameResult(result_id, self._seed, self._agent_args)
120133

121134
# Create the initial game state.
122-
state = self.get_initial_state(rng)
135+
state = self.get_initial_state(rng, board)
123136

124137
# Notify agents about the start of the game.
125138
isolator.game_start(rng, state)
126139

127-
turn_count = 0
128-
while (True):
140+
move_count = 0
141+
while ((self._max_moves < 0) or (move_count < self._max_moves)):
129142
# Choose the next agent to move.
130143
agent_index = self._get_next_agent_index(tickets)
131144

132-
logging.debug("Turn %d, agent %d, state: '%s'.", turn_count, agent_index, state)
145+
logging.debug("Turn %d, agent %d, state: '%s'.", move_count, agent_index, state)
133146

134147
# Get the next action from the agent.
135148
action_record = isolator.get_action(agent_index, state)
@@ -148,10 +161,12 @@ def run(self) -> GameResult:
148161
# Issue the agent a new ticket.
149162
tickets[agent_index] = tickets[agent_index].next(self._agent_args[agent_index].move_delay)
150163

151-
# Increment the turn count.
152-
turn_count += 1
164+
# Increment the move count.
165+
move_count += 1
153166

154-
# TEST
167+
# Check if this game ended naturally or in a timeout.
168+
if (not state.game_over):
169+
state.timeout = True
155170

156171
# Notify agents about the end of this game.
157172
isolator.game_complete(state)

pacai/core/gamestate.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22

3+
import pacai.core.action
34
import pacai.core.board
45

56
class GameState(abc.ABC):
@@ -11,5 +12,28 @@ class GameState(abc.ABC):
1112
(since this class has been optimized for performance).
1213
"""
1314

14-
def __init__(self, board: pacai.core.board.Board) -> None:
15-
self._board: pacai.core.board.Board = board
15+
def __init__(self,
16+
board: pacai.core.board.Board,
17+
agent_index: int = -1,
18+
game_over: bool = False,
19+
timeout: bool = False) -> None:
20+
self.agent_index: int = agent_index
21+
"""
22+
The index of the agent with the current move.
23+
-1 indicates that the agent to move has not been selected yet.
24+
"""
25+
26+
self.board: pacai.core.board.Board = board
27+
""" The current board. """
28+
29+
self.game_over: bool = game_over
30+
""" Indicates that this state represents a complete game. """
31+
32+
self.timeout: bool = timeout
33+
""" Indicates that the game ended in a timeout. """
34+
35+
@abc.abstractmethod
36+
def get_legal_actions(self) -> list[pacai.core.action.Action]:
37+
""" Get the moves that the current agent is allowed to make. """
38+
39+
pass

pacai/pacman/__init__.py

Whitespace-only changes.

pacai/pacman/board.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,33 @@ def __init__(self, board_text: str, extra_markers: list[str] = [], **kwargs) ->
2525
]
2626

2727
super().__init__(board_text, extra_markers = extra_markers, **kwargs)
28+
29+
# TODO(eriq): Verify that the ghost indexes match.
30+
31+
self._agents: list[int | None] = [None]
32+
"""
33+
Keep explicit track of each agent on this board.
34+
The agents are indexed by their same index in the game.
35+
Pacman is always 0 and the ghosts are indexed by the order they appear.
36+
37+
A None position means that the agent is not currently on the board.
38+
39+
Note that this quick lookup requires additional maintenance when agents move.
40+
"""
41+
42+
for index in range(len(self._locations)):
43+
marker = self._locations[index]
44+
if (marker not in {MARKER_PACMAN, MARKER_GHOST}):
45+
continue
46+
47+
if (marker == MARKER_PACMAN):
48+
self._agents[0] = index
49+
else:
50+
self._agents.append(index)
51+
52+
def get_agent_position(self, agent_index: int) -> pacai.core.board.Position | None:
53+
index = self._agents[agent_index]
54+
if (index is None):
55+
return None
56+
57+
return pacai.core.board.Position.from_index(index, self.width)

pacai/pacman/game.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import abc
2+
import random
3+
4+
import pacai.core.agent
5+
import pacai.core.board
6+
import pacai.core.game
7+
import pacai.core.gamestate
8+
import pacai.pacman.gamestate
9+
10+
class Game(pacai.core.game.Game):
11+
"""
12+
A game following the standard rules of PacMan.
13+
"""
14+
15+
def get_initial_state(self, rng: random.Random, board: pacai.core.board.Board) -> pacai.core.gamestate.GameState:
16+
return pacai.pacman.gamestate.GameState(board)
17+
18+
@abc.abstractmethod
19+
def process_action(self, state: pacai.core.gamestate.GameState, action_record: pacai.core.agent.ActionRecord) -> pacai.core.gamestate.GameState:
20+
# TEST
21+
return state
22+
23+
@abc.abstractmethod
24+
def check_end(self, state: pacai.core.gamestate.GameState) -> bool:
25+
# TEST
26+
return True

0 commit comments

Comments
 (0)