summaryrefslogtreecommitdiff
path: root/robots/state.py
blob: 37d25543f0197f31b0c1ef80bffc25a214569e2f (about) (plain)
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)