-
Notifications
You must be signed in to change notification settings - Fork 0
/
unit.py
301 lines (240 loc) · 9.31 KB
/
unit.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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
import math
import dice
import model_profile
import attacks
class NullModelException(Exception):
def __repr__(self):
"Null model passed - you must specify a model profile to add to a unit."
class Unit:
def __init__(self, name, type):
self.name = name
self.models = []
self.leader = None
self.type = type
self.alive = False
# method to determine the leader for the unit
def chooseLeader(self):
"""Read the model list and assigned the one with the highest Ld to the leader attribute"""
best = None
for record in self.models:
if best:
if record['profile'].leadership > best['profile'].leadership:
best = record
else:
best = record
self.leader = best
def addModel(self, model, count):
# check that a model was passed
if (model == None):
raise NullModelException
# mark this unit as alive
self.alive = True
record = {'profile': model, 'count': count, 'wounds': 0}
self.models.append(record)
# choose the new leader
self.chooseLeader()
# expire the wargear and rule lists
self.wargear = None
self.rules = None
def destroy(self):
"""Handle the event of a unit being wiped out"""
self.alive = False
# method to remove a model record
def removeModel(self, model):
"""Remove a provided model record.
Calls the destroy handler or leader chooser as appropriate.
"""
# remove the model
self.models.remove(model)
# call death method if this unit is destroyed
if len(self.models) == 0:
self.destroy()
else:
# otherwise, find the new leader
self.chooseLeader()
def count_members(self):
"""Count the total members of the unit"""
count = 0
for record in self.models:
count += record['count']
return count
def print_feature_list(self, feature_list_name):
string = ""
feature_list = None
if feature_list_name == "wargear":
feature_list = self.wargear
elif feature_list_name == "rules":
feature_list = self.rules
else:
raise Exception(
"Invalid feature list (" + feature_list_name + "). must be 'wargear' or 'rules'");
# if the lists have been expired, recreate them, and recurse
if feature_list == None:
self.compose_feature_lists()
return self.print_feature_list(feature_list_name);
for item in feature_list:
string += " " + item
if len(feature_list[item]) < len(self.models):
string += "("
subsequent = False
for profile in feature_list[item]:
if subsequent:
string += ", "
else:
subsequent = True
string += profile.name
string += " only)"
string += "\n"
return string
def compose_feature_lists(self):
wargear = {}
rules = {}
for record in self.models:
model_gear = record['profile'].wargear
for item in model_gear:
if item.name in wargear.keys():
wargear[item.name].append(record['profile'])
else:
wargear[item.name] = [record['profile']]
model_rules = record['profile'].rules
for rule in model_rules:
if rule.name in rules.keys():
rules[rule.name].append(record['profile'])
else:
rules[rule.name] = [record['profile']]
self.wargear = wargear
self.rules = rules
def print_summary(self):
string = ""
# unit name
string += self.name + "\n"
statlines = {}
offset = 0;
for record in self.models:
model = record['profile']
statlines[model.name] = model.get_statline()
# determine longest profile name
if len(model.name) > offset:
offset = len(model.name)
# print statlines
string += (" " * (offset + 1)) + model_profile.statline_header + "\n"
for profile in statlines:
string += profile
# align the statlines
string += " " * (offset - len(profile))
string += " " + statlines[profile] + "\n"
# print type
string += "Unit Type: " + self.type + "\n"
# print composition
string += "Unit Composition:\n"
for record in self.models:
string += " " + str(record['count']) + " " + record['profile'].name + "\n"
# print wargear
string += "Wargear:\n"
if not hasattr(self, 'wargear'):
self.compose_feature_lists();
string += self.print_feature_list("wargear")
# print rules
string += "Special Rules:\n"
if not hasattr(self, 'rules'):
self.compose_feature_lists();
string += self.print_feature_list("rules")
return string
def get_majority_toughness(self):
majority = None
for record in self.models:
if not majority:
majority = record
else:
if record['count'] > majority['count']:
majority = record
elif record['count'] == majority['count']:
if record['profile'].toughness > majority['profile'].toughness:
majority = record
return majority['profile'].toughness
def take_wounds(self, attack):
""" Take saves on inflicted wounds, apply wounds, and remove casualties
Arguments:
attack: a WoundingAttack object describing the attack
Returns:
the total number of unsaved wounds suffered by the unit
"""
unsaved = 0
remaining_wounds = attack.wounds['passes']
while remaining_wounds and self.alive:
# determine the number of wounds we can roll for now
model = self.models[0]
wounds_to_take = remaining_wounds
wounds_characteristic = model['profile'].wounds
max_wounds = model['count'] * wounds_characteristic - model['wounds'];
if wounds_to_take > max_wounds:
wounds_to_take = max_wounds
remaining_wounds -= wounds_to_take
save = model['profile'].best_save(attack)
# roll the dice
roll = dice.d6(wounds_to_take, save).roll()
# inflict casualties
casualties = roll['fails']
unsaved += casualties
# handle wounded models first
if model['wounds']:
to_go = wounds_characteristic - model['wounds']
if casualties >= to_go:
model['wounds'] = 0
model['count'] -= 1
else :
model['wounds'] += to_go
casualties -= to_go
# handle further casualties
if casualties:
model['count'] -= math.floor(casualties / model['profile'].wounds)
# add leftover wounds
model['wounds'] = casualties % model['profile'].wounds
# remove eradicated models
if model['count'] < 1:
self.removeModel(model)
return unsaved
# generates a dict of attacks versus a target unit
def attack(self, target, weapons, range):
tests = {}
# initialize the dict
for weapon in weapons:
tests[weapon] = []
for record in self.models:
profile = record['profile']
this_attack = None
i = 0
# find the highest-priority weapon for this model
while (this_attack == None and i < len(weapons)):
weapon = weapons[i]
for held_weapon in profile.weapons:
if held_weapon.name == weapon:
# determine hit roll
hit_roll = 7 - profile.ballistic_skill
# determine wound roll
wound_roll = attacks.roll_to_wound(
held_weapon.strength,
target.get_majority_toughness());
# build the attack object
this_attack = attacks.WoundingAttack(
held_weapon.shots(range) * record['count'],
hit_roll,
wound_roll,
held_weapon)
tests[weapon].append(this_attack)
i += 1
return tests
def stat_wounds(self, attack):
''' calculate casualty statistics of this attack '''
remaining_wounds = attack.stat()['wounds']
model = self.models[0]
save = model['profile'].best_save(attack)
return remaining_wounds - dice.d6(remaining_wounds, save).stat()
def clone(self):
'''
clone this unit profile
'''
new = Unit(self.name)
for model in self.models:
new.addModel(model['profile'], model['count'])
return new