-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathevent.py
266 lines (217 loc) · 8.81 KB
/
event.py
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
"""
Represents unit fights for Micro Wars.
GNU General Public License v3.0: See the LICENSE file.
"""
import copy
import json
from typing import Dict, List, Tuple
from AoE2ScenarioParser.aoe2_scenario import AoE2Scenario
from AoE2ScenarioParser.pieces.structs.unit import UnitStruct
import util
import util_techs
import util_units
# Default filepath from which to load fight data.
DEFAULT_FILE = 'events.json'
# The maximum number of fights, including the tiebreaker.
FIGHT_LIMIT = 36
# The number of tiles in a fight square.
TILE_WIDTH = 20
# The fight grid consists of FIGHT_GRID_LENGTH x FIGHT_GRID_LENGTH squares.
FIGHT_GRID_LENGTH = 6
# The maximum number of points a fight is worth.
MAX_POINTS = 100
def get_start_tile(index: int) -> Tuple[int, int]:
"""
Returns the integer (x, y) tile coordinates of the starting tile
for the fight number index.
"""
y, x = divmod(index, FIGHT_GRID_LENGTH)
return x * TILE_WIDTH, y * TILE_WIDTH
class Minigame:
"""An instance represents a minigame."""
def __init__(self, n: str, techs: List[str]):
"""Initializes a new Minigame with name n."""
self._name = n
self._techs = sorted(techs)
@property
def name(self) -> str:
"""Returns this Minigame's name."""
return self._name
def tech_names(self):
"""Yields the technologies researched by this Minigame."""
yield from self._techs
def __str__(self):
return self.name()
class FightData:
"""An instance represents a fight in the middle of the map."""
def __init__(self, techs: List[str], points: Dict[str, int]):
"""
Initializes a new FightData object.
Arguments:
techs: The names of the technologies researched at the start of
the fight.
points: A map from unit name to the number of points killing that
unit is worth.
Raises:
ValueError:
* An element of techs is not a valid technology name.
* A key in points is not a valid unit name.
* A value in points is nonpositive.
"""
self.techs = sorted(techs)
self.points = points
for tech_name in self.techs:
if not util_techs.is_tech(tech_name):
raise ValueError(f'{tech_name} is not a valid tech name.')
for unit_name, point_value in self.points.items():
if not util_units.is_unit(unit_name):
raise ValueError(f'{unit_name} is not a valid unit name.')
if point_value < 0:
msg = f'{unit_name}: {point_value} must be nonnegative.'
raise ValueError(msg)
def __str__(self):
return json.dumps({'techs': self.techs, 'points': self.points})
@staticmethod
def from_json(s: str):
"""Returns a FightData object that is represented by json string s."""
loaded = json.loads(s)
return FightData(loaded['techs'], loaded['points'])
class Fight:
"""
Represents a fight between players with two groups of units.
The individual units are positioned in the place where they are teleported
for the fight to begin.
"""
def __init__(self, fight_data: FightData,
p1_units: List[UnitStruct], p2_units: List[UnitStruct]):
"""
Initializes a new fight with the fight data and unit lists.
Raise a ValueError if a player has no units, if the total
possible point values exceed the max limit, or if there is
no point value for some unit in the fight.
"""
self.techs = fight_data.techs
self.points = fight_data.points
self.p1_units = p1_units
self.p2_units = p2_units
if not self.p1_units:
raise ValueError('Player 1 has no units.')
if not self.p2_units:
raise ValueError('Player 2 has no units.')
self._p2_bonus = MAX_POINTS
num_points = 0
for unit in self.p1_units:
name = util_units.get_name(unit)
if name not in self.points:
raise ValueError(f'There is no point value for {name}.')
pts = self.points[name]
self._p2_bonus -= pts
if self._p2_bonus < 0:
msg = f"Player 1's units exceed the point limit {MAX_POINTS}."
raise ValueError(msg)
self._p1_bonus = MAX_POINTS
for unit in self.p2_units:
name = util_units.get_name(unit)
if name not in self.points:
raise ValueError(f'There is no point value for {name}.')
pts = self.points[name]
self._p1_bonus -= pts
if self._p1_bonus < 0:
msg = f"Player 2's units exceed the point limit {MAX_POINTS}."
raise ValueError(msg)
def objectives_description(self) -> str:
"""
Returns the string used to display the objectives for this fight.
The objectives include each unit name and the number of points
the unit is worth, organized from highest to lowest point value.
"""
return '\n'.join(
f'* {util.pretty_print_name(name)}: {points}'
for name, points in sorted(self.points.items(), key=lambda t: -t[1])
)
@property
def p1_bonus(self):
"""
Returns the number of bonus points Player 1 earns for winning
this Fight.
"""
return self._p1_bonus
@property
def p2_bonus(self):
"""
Returns the number of bonus points Player 2 earns for winning
this Fight.
"""
return self._p2_bonus
# TODO annotate type of event_data and return type with sum of fight or minigame
def make_fights(units_scn: AoE2Scenario, event_data,
center: Tuple[int, int], offset: int):
"""
Raises a ValueError if there is an invalid fight.
center is the tile around which the fight is centered.
offset is the number of tiles away from the center to move the units.
The kth fight is loaded from the kth tile in the units_scn.
"""
p1_units_all = util_units.get_units_array(units_scn, 1)
p2_units_all = util_units.get_units_array(units_scn, 2)
# num_fights is the index from which to load the next fight
fight_index = 0
events = []
techs = set()
for event in event_data:
if isinstance(event, Minigame):
events.append(event)
continue
assert isinstance(event, FightData)
fd = event
x1, y1 = get_start_tile(fight_index)
x2, y2 = x1 + TILE_WIDTH, y1 + TILE_WIDTH
p1_units = util_units.units_in_area(p1_units_all, x1, y1, x2, y2)
if not p1_units:
raise ValueError(f'Fight at tile {fight_index} has no units.')
p2_units = util_units.units_in_area(p2_units_all, x1, y1, x2, y2)
if not p2_units:
# Symmetrical fight where only 1 player has units.
# Creates a single, mirrored fight.
p2_units = [copy.deepcopy(unit) for unit in p1_units]
util_units.center_units(p1_units, center, offset)
util_units.center_units_flip(p2_units, center, offset)
events.append(Fight(fd, p1_units, p2_units))
else:
# Asymmetrical fight where p1 and p2 both have units.
# Creates two rounds, with players switching units between fights.
p1_units2 = [copy.deepcopy(unit) for unit in p2_units]
p2_units2 = [copy.deepcopy(unit) for unit in p1_units]
util_units.center_units(p1_units, center, offset)
util_units.center_units(p2_units, center, -offset)
events.append(Fight(fd, p1_units, p2_units))
util_units.center_units_flip(p1_units2, center, -offset)
util_units.center_units_flip(p2_units2, center, offset)
events.append(Fight(fd, p1_units2, p2_units2))
for tech in fd.techs:
if tech in techs:
raise ValueError(f'Tech {tech} is researched multiple times.')
techs.add(tech)
fight_index += 1
return events
# TODO annotate the sum type of fight data and minigame name
def load_fight_data(filepath: str = DEFAULT_FILE):
"""
Parses the fight json at filepath and returns a list of the
fight information from that file.
Raises a ValueError if there are too many fights.
"""
with open(filepath) as json_file:
loaded = json.loads(json_file.read())
event_data = []
num_fights = 0
for event in loaded:
if isinstance(event, str):
event_data.append(Minigame(event, []))
else:
num_fights += 1
if num_fights > FIGHT_LIMIT:
msg = f'{num_fights} fights exceeds the limit {FIGHT_LIMIT}.'
raise ValueError(msg)
event_data.append(FightData(event['techs'], event['points']))
return event_data