1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
from collections import defaultdict, Counter
from robots.constants import City, DIRECTIONS
from robots.utils import ceil_div#, immutable
class GameState:
"""The state of a game at a point in time.
Instances can be serialized for inter-process communication.
"""
def __init__(self, board):
self.cities = board
"""
A 2D list of the city types (robots.contants.City).
Index using row-major format (state.cities[y][x]).
"""
self.allegiances = {}
"""
Dictionary mapping (x, y) positions to players.
Cities without allegience are not present in the dictionary.
"""
self.robots_by_player = {}
"""
Dictionary mapping each player to a list of their robots.
Each robot is represented by (x, y, energy).
"""
def __hash__(self):
return id(self)
# return hash(immutable(self._authorative_state))
def __eq__(self, other):
if isinstance(other, GameState):
return self._authorative_state == other._authorative_state
def to_json(self):
return (
self.cities,
{
'%d,%d' % position: player
for position, player in self.allegiances.items()
},
self.robots_by_player
)
@classmethod
def from_json(cls, obj):
cities, allegiances, robots_by_player = obj
state = cls(cities)
for position, player in allegiances.items():
position = tuple(map(int, position.split(',')))
state.allegiances[position] = player
state.robots_by_player = robots_by_player
return state
@property
def _authorative_state(self):
return (self.cities, self.allegiances, self.robots_by_player)
@property
def players(self):
"""List of players."""
return self.robots_by_player.keys()
@property
def robots(self):
"""
Grid of the game board, showing the 0 or 1 robot in each city.
Dictionary mapping (x, y) to either [] or [(player, energy)].
"""
result = defaultdict(list)
for player, robots in self.robots_by_player.items():
for x, y, energy in robots:
result[x, y].append((player, energy))
return result
@property
def allegiances_by_player(self):
"""
Dictionary mapping each player to a list of cities they control.
"""
result = defaultdict(list)
for (x, y), player in self.allegiances.items():
result[player].append((x, y))
return result
@property
def board(self):
"""
A 2D list with all available information about each city.
Each city is represented with a dictionary:
>>> {
... 'city': robots.constants.City,
... 'allegiance': player or None,
... 'robots': [robot] or [],
... }
"""
# TODO: remove this once I've figured out caching.
self_robots = self.robots
result = []
for y, row in enumerate(self.cities):
result_row = []
for x, city in enumerate(row):
allegiance = self.allegiances.get((x, y))
robots = self_robots[x, y]
result_row.append({
'city': city,
'allegiance': allegiance,
'robots': robots,
})
result.append(result_row)
return result
@property
def width(self):
"""Width of the map."""
return len(self.cities[0])
@property
def height(self):
"""Height of the map."""
return len(self.cities)
@property
def factories(self):
"""
Set of the (x, y) positions of all factories on the map.
"""
return {
(x, y)
for y, row in enumerate(self.cities)
for x, city in enumerate(row)
if city == City.FACTORY
}
@property
def factories_by_player(self):
"""
Dictionary mapping each player to a list of factory (x, y) positions
they control.
The None key represents unowned factories.
"""
result = defaultdict(list)
for p in self.factories:
player = self.allegiances.get(p)
result[player].append(p)
return result
@property
def n_alive_players(self):
"""How many players are still alive."""
return sum(
1
for player, robots in self.robots_by_player.items()
if robots
)
@property
def n_allegiable_cities(self):
"""How many cities are capable of pledging allegiance."""
return sum(
1
for row in self.cities
for city in row
if city in City.allegiable
)
@property
def n_allegiances_by_player(self):
"""How many cities have pledged allegiance to each player."""
return Counter(self.allegiances.values())
@property
def n_cities_to_win(self):
"""How many cities you need to pledge allegiance to you to win."""
return ceil_div(self.n_allegiable_cities, self.n_alive_players)
def expected_position(self, x, y, action):
"""
Return the expected position for a robot at (x, y) if it performs the
given action.
Of course, combat (and possibly other factors in the future) may
influence whether it actually ends up at the returned position.
"""
if action not in DIRECTIONS:
return (x, y)
dx, dy = DIRECTIONS[action]
nx = (x + dx) % self.width
ny = (y + dy) % self.height
if self.cities[ny][nx] not in City.traversable:
return (x, y)
return (nx, ny)
|