Skip to content

Commit 1c65834

Browse files
Move work on the core engine.
1 parent 9ffabd7 commit 1c65834

File tree

5 files changed

+221
-58
lines changed

5 files changed

+221
-58
lines changed

pacai/core/agent.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@
33

44
import pacai.core.action
55
import pacai.core.gamestate
6+
import pacai.core.time
67

78
DEFAULT_MOVE_DELAY: int = 100
8-
""" The defaut delay between agent moves. """
9+
""" The default delay between agent moves. """
910

1011
class AgentArguments:
11-
def __init__(self, name: str = '', move_delay: int = DEFAULT_MOVE_DELAY, **kwargs) -> None:
12-
self.name: str = name.strip()
13-
self.move_delay: int = move_delay
14-
self.other_arguments: dict[str, typing.Any] = kwargs
15-
16-
if (len(self.name) == 0):
12+
def __init__(self, name: str = '',
13+
move_delay: int = DEFAULT_MOVE_DELAY,
14+
**kwargs) -> None:
15+
name = name.strip()
16+
if (len(name) == 0):
1717
raise ValueError("Agent name cannot be empty.")
1818

19-
if (self.move_delay <= 0):
19+
if (move_delay <= 0):
2020
raise ValueError("Agent move delay must be > 0.")
2121

22+
self.name: str = name
23+
self.move_delay: int = move_delay
24+
25+
self.other_arguments: dict[str, typing.Any] = kwargs
26+
2227
class Agent(abc.ABC):
2328
""" The base for all agents in the pacai system. """
2429

@@ -43,12 +48,12 @@ def get_action(self, state: pacai.core.gamestate.GameState) -> pacai.core.action
4348

4449
# TEST
4550
@abc.abstractmethod
46-
def game_start(self, agent_index: int, game_state: pacai.core.gamestate.GameState) -> None:
51+
def game_start(self, agent_index: int, suggested_seed: int, initial_state: pacai.core.gamestate.GameState) -> None:
4752
pass
4853

4954
# TEST
5055
@abc.abstractmethod
51-
def game_complete(self, game_state: pacai.core.gamestate.GameState) -> None:
56+
def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
5257
pass
5358

5459
class Ticket(typing.NamedTuple):
@@ -68,6 +73,33 @@ class Ticket(typing.NamedTuple):
6873
num_moves: int
6974
""" The total number of times this agent has moved so far. """
7075

76+
def next(self, move_delay: int) -> 'Ticket':
77+
""" Get the next ticket in the sequence for this agent. """
78+
79+
return Ticket(
80+
next_time = self.next_time + move_delay,
81+
last_time = self.next_time,
82+
num_moves = self.num_moves + 1,
83+
)
84+
85+
class ActionRecord(typing.NamedTuple):
86+
"""
87+
The result of requesting an action from an agent.
88+
Aside from the action, this also includes timing and crashing information.
89+
"""
90+
91+
agent_index: int
92+
""" The index of the agent making this action. """
93+
94+
action: pacai.core.action.Action
95+
""" The action returned by the agent (or pacai.core.action.STOP on a crash). """
96+
97+
duration: pacai.core.time.Duration
98+
""" The duration (in MS) the agent took to compute this action. """
99+
100+
crashed: bool
101+
""" Whether or not the agent crashed (e.g., raised an exception) when computing this action. """
102+
71103
def load(arguments: AgentArguments) -> Agent:
72104
# TEST
73105
raise NotImplementedError()

pacai/core/game.py

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
import abc
2+
import logging
23
import random
34

45
import pacai.core.action
56
import pacai.core.agent
67
import pacai.core.isolation
78
import pacai.core.time
89

9-
class MoveHistoryRecord:
10-
""" A record of a single move made by an agent. """
11-
12-
def __init__(self, index: int, action: pacai.core.action.Action, duration: pacai.core.time.Duration) -> None:
13-
self.index: int = index
14-
""" The index of the agent making this move. """
15-
16-
self.action: pacai.core.action.Action = action
17-
""" The action made by the agent. """
18-
19-
self.duration: pacai.core.time.Duration = duration
20-
""" How long the agent took to compute the this move. """
21-
2210
class GameResult:
2311
""" The result of running a game. """
2412

2513
def __init__(self, id: int, seed: int, agent_args: list[pacai.core.agent.AgentArguments]) -> None:
14+
"""
15+
Create a new game result.
16+
This class is mutable and will be modified as the game progresses.
17+
"""
18+
2619
self.id: int = id
2720
""" The ID of the game. """
2821

@@ -32,9 +25,14 @@ def __init__(self, id: int, seed: int, agent_args: list[pacai.core.agent.AgentAr
3225
self.agent_args: list[pacai.core.agent.AgentArguments] = []
3326
""" The arguments used to construct each agent. """
3427

35-
self.history: list[MoveHistoryRecord] = []
28+
self.history: list[pacai.core.agent.ActionRecord] = []
3629
""" The history of actions taken by each agent in this game. """
3730

31+
def add_action(self, action_record: pacai.core.agent.ActionRecord) -> None:
32+
""" Add an action to the result's game history. """
33+
34+
self.history.append(action_record)
35+
3836
class Game(abc.ABC):
3937
"""
4038
A game that can be run in pacai.
@@ -71,14 +69,23 @@ def _setup(self) -> None:
7169
pass
7270

7371
@abc.abstractmethod
74-
def process_move(self, move: MoveHistoryRecord) -> None:
75-
""" Process the given move and update the game's state. """
72+
def get_initial_state(self, rng: random.Random) -> pacai.core.gamestate.GameState:
73+
""" Create the initial state for this game. """
74+
75+
pass
76+
77+
@abc.abstractmethod
78+
def process_action(self, state: pacai.core.gamestate.GameState, action_record: pacai.core.agent.ActionRecord) -> pacai.core.gamestate.GameState:
79+
""" Process the given move and return an updated game state. """
7680

7781
pass
7882

7983
@abc.abstractmethod
80-
def get_initial_state(self, rng: random.Random) -> pacai.core.gamestate.GameState:
81-
""" Create the initial state for this game. """
84+
def check_end(self, state: pacai.core.gamestate.GameState) -> bool:
85+
"""
86+
Check to see if the game is over.
87+
Return True if the game is now over, False otherwise.
88+
"""
8289

8390
pass
8491

@@ -93,12 +100,14 @@ def run(self) -> GameResult:
93100
and 5) update the display.
94101
"""
95102

103+
logging.debug("Starting a game with seed: %d.", self._seed)
104+
96105
# Create a new random number generator just for this game.
97106
rng = random.Random(self._seed)
98107

99108
# Initialize the agent isolator.
100109
isolator = self._isolation_level.get_isolator()
101-
isolator.game_init(self._agent_args)
110+
isolator.init_agents(self._agent_args)
102111

103112
# Assign initial tickets to all the agents.
104113
tickets = []
@@ -113,24 +122,31 @@ def run(self) -> GameResult:
113122
state = self.get_initial_state(rng)
114123

115124
# Notify agents about the start of the game.
116-
isolator.game_start(state)
125+
isolator.game_start(rng, state)
117126

118127
turn_count = 0
119128
while (True):
129+
# Choose the next agent to move.
120130
agent_index = self._get_next_agent_index(tickets)
121131

132+
logging.debug("Turn %d, agent %d, state: '%s'.", turn_count, agent_index, state)
133+
122134
# Get the next action from the agent.
123-
next_action = isolator.get_action(agent_index, state)
135+
action_record = isolator.get_action(agent_index, state)
124136

125-
# Execute the next action.
137+
# Execute the next action and update the state.
138+
state = self.process_action(state, action_record)
126139

127140
# Update the move history.
128-
129-
# Update the game state.
141+
result.add_action(action_record)
130142

131143
# Check for game ending conditions.
144+
game_over = self.check_end(state)
145+
if (game_over):
146+
break
132147

133148
# Issue the agent a new ticket.
149+
tickets[agent_index] = tickets[agent_index].next(self._agent_args[agent_index].move_delay)
134150

135151
# Increment the turn count.
136152
turn_count += 1

pacai/core/isolation.py

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import abc
22
import enum
3+
import logging
4+
import random
35

46
import pacai.core.action
57
import pacai.core.agent
68
import pacai.core.gamestate
9+
import pacai.core.time
710

8-
class Isolator(abc.ABC):
11+
class AgentIsolator(abc.ABC):
912
"""
1013
An isolator isolates an agent instance from the game being played.
1114
This "isolation" allows the game to hide or protect state from a agent.
@@ -16,16 +19,15 @@ class Isolator(abc.ABC):
1619
"""
1720

1821
@abc.abstractmethod
19-
def game_init(self, agent_args: list[pacai.core.agent.AgentArguments]) -> None:
22+
def init_agents(self, agent_args: list[pacai.core.agent.AgentArguments]) -> None:
2023
"""
21-
Initialize the isolator with the given agent arguments.
22-
Called when a game is just preparing to start.
24+
Initialize the agents this isolator will be responsible for.
2325
"""
2426

2527
pass
2628

2729
@abc.abstractmethod
28-
def game_start(self, initial_state: pacai.core.gamestate.GameState) -> None:
30+
def game_start(self, rng: random.Random, initial_state: pacai.core.gamestate.GameState) -> None:
2931
"""
3032
Pass along the initial game state to each agent and all them the allotted time to start.
3133
"""
@@ -41,9 +43,17 @@ def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
4143
pass
4244

4345
@abc.abstractmethod
44-
def get_action(self, agent_index: int, state: pacai.core.gamestate.GameState) -> pacai.core.action.Action:
46+
def get_action(self, agent_index: int, state: pacai.core.gamestate.GameState) -> pacai.core.agent.ActionRecord:
4547
"""
46-
Get an agent's next action.
48+
Get an agent's next action and how long it took to decide on that action.
49+
"""
50+
51+
pass
52+
53+
@abc.abstractmethod
54+
def close(self) -> None:
55+
"""
56+
Close the isolator and release all owned resources.
4757
"""
4858

4959
pass
@@ -53,26 +63,81 @@ class Level(enum.Enum):
5363
PROCESS = 1
5464
TCP = 2
5565

56-
def get_isolator(self) -> Isolator:
66+
def get_isolator(self, **kwargs) -> AgentIsolator:
5767
""" Get an isolator matching the given level. """
5868

5969
if (self.value == Level.NONE):
60-
return NoneIsolator()
70+
return NoneIsolator(**kwargs)
6171
if (self.value == Level.PROCESS):
62-
return ProcessIsolator()
72+
return ProcessIsolator(**kwargs)
6373
if (self.value == Level.TCP):
64-
return TCPIsolator()
74+
return TCPIsolator(**kwargs)
6575
else:
6676
raise ValueError(f"Unknown isolation level '{self.value}'.")
6777

68-
# TEST
69-
class NoneIsolator(abc.ABC):
70-
pass
78+
class NoneIsolator(AgentIsolator):
79+
"""
80+
An isolator that does not do any isolation between the engine and agents.
81+
All agents will be run in the same thread (and therefore processes space).
82+
This is the simplest and fastest of all isolators, but offers the least control and protection.
83+
Agents cannot be timed out (since they run on the same thread).
84+
Agents can also access any memory or disk that the core engine has access to.
85+
"""
86+
87+
def __init__(self, **kwargs) -> None:
88+
self._agents: list[pacai.core.agent.Agent] | None = None
89+
90+
def init_agents(self, agent_args: list[pacai.core.agent.AgentArguments]) -> None:
91+
self._agents = [pacai.core.agent.load(args) for args in agent_args]
92+
93+
def game_start(self, rng: random.Random, initial_state: pacai.core.gamestate.GameState) -> None:
94+
if (self._agents is None):
95+
raise ValueError("Isolator agents has not been initialized.")
96+
97+
for i in range(len(self._agents)):
98+
suggested_seed = rng.randint(0, 2**64)
99+
self._agents[i].game_start(i, suggested_seed, initial_state)
100+
101+
def game_complete(self, final_state: pacai.core.gamestate.GameState) -> None:
102+
if (self._agents is None):
103+
raise ValueError("Isolator agents has not been initialized.")
104+
105+
for agent in self._agents:
106+
agent.game_complete(final_state)
107+
108+
def get_action(self, agent_index: int, state: pacai.core.gamestate.GameState) -> pacai.core.agent.ActionRecord:
109+
if (self._agents is None):
110+
raise ValueError("Isolator agents has not been initialized.")
111+
112+
agent = self._agents[agent_index]
113+
crashed = False
114+
115+
start_time = pacai.core.time.now()
116+
117+
try:
118+
action = agent.get_action(state)
119+
except Exception as ex:
120+
logging.warning("Agent '%s' (%d) crashed.", agent.name, agent_index, exc_info = ex)
121+
122+
crashed = True
123+
action = pacai.core.action.STOP
124+
125+
end_time = pacai.core.time.now()
126+
127+
return pacai.core.agent.ActionRecord(
128+
agent_index = agent_index,
129+
action = action,
130+
duration = end_time.sub(start_time),
131+
crashed = crashed)
132+
133+
134+
def close(self) -> None:
135+
self._agents = None
71136

72137
# TEST
73-
class ProcessIsolator(abc.ABC):
138+
class ProcessIsolator(AgentIsolator):
74139
pass
75140

76141
# TEST
77-
class TCPIsolator(abc.ABC):
142+
class TCPIsolator(AgentIsolator):
78143
pass

0 commit comments

Comments
 (0)