1
2 """ This module implements the GameActor class """
3 from random import randint
4 from gameobject import GameObject
5 from gameaction import GameAction
6 from gamecontext import GameContext
7
8
10 """
11 A GameActor (typically a PC or NPC) is an agent that has a
12 context and is capable of initiating and receiving actions.
13 """
14
15 - def __init__(self, name="actor", descr=None):
16 """
17 create a new GameActor
18 @param name: display name of this actor
19 @param descr: human description of this actor
20 """
21 super(GameActor, self).__init__(name, descr)
22 self.context = None
23 self.alive = True
24 self.incapacitated = False
25
27 """
28 Accept an attack, figure out if it hits, and how bad
29 @param action: (GameAction) being performed
30 @param actor: (GameActor) initiating the action
31 @param context: (GameContext) in which action is being taken
32 @return: (boolean, string) succewss and description of the effect
33
34 """
35
36
37 evade = self.get("EVASION")
38 evasion = 0 if evade is None else int(evade)
39 if "ATTACK." in action.verb:
40 evade = self.get("EVASION." + action.verb.split(".")[1])
41 if evade is not None:
42 evasion += int(evade)
43
44
45 to_hit = action.get("TO_HIT") - evasion
46 if to_hit < 100 and randint(1, 100) > to_hit:
47 return (False, "{} evades {} {}"
48 .format(self.name, action.source.name, action.verb))
49
50
51 prot = self.get("PROTECTION")
52 protection = 0 if prot is None else int(prot)
53 if "ATTACK." in action.verb:
54 prot = self.get("PROTECTION." + action.verb.split(".")[1])
55 if prot is not None:
56 protection += int(prot)
57
58
59 delivered = action.get("HIT_POINTS")
60 if protection >= delivered:
61 return (False, "{}'s protection absorbs all damage from {}"
62 .format(self.name, action.verb))
63
64
65 old_hp = self.get("LIFE")
66 if old_hp is None:
67 old_hp = 0
68 new_hp = old_hp - (delivered - protection)
69 self.set("LIFE", new_hp)
70
71 result = "{} hit by {} from {} using {} for {}-{} life-points in {}" \
72 .format(self.name, action.verb, actor.name,
73 action.source.name,
74 delivered, protection, context.name) \
75 + "\n {} life: {} - {} = {}" \
76 .format(self.name, old_hp, delivered - protection, new_hp)
77
78
79 if new_hp <= 0:
80 result += ", and is killed"
81 self.alive = False
82 self.incapacitated = True
83 return (True, result)
84
86 """
87 receive and process the effects of an ATTACK
88 (other actions are passed to our super-class)
89
90 A standard attack comes with at-least two standard attributes:
91 - TO_HIT ... the (pre defense) to-hit probability
92 - HIT_POINTS ... the (pre-armor) damage being delivered
93
94 1. use D100+EVASION to determine if attack hits
95 2. use PROTECTION to see how much damage gets through
96 3. update LIFE_POINTS
97
98 @param action: (GameAction) being performed
99 @param actor: (GameActor) initiating the action
100 @param context: (GameContext) in which action is being taken
101 @return: (boolean, string) description of the effect
102 """
103
104 base_verb = action.verb.split('.')[0] \
105 if '.' in action.verb else action.verb
106
107
108 if base_verb == "ATTACK":
109 return self._accept_attack(action, actor, context)
110
111
112 return super(GameActor, self).accept_action(action, actor, context)
113
115 """
116 return a list of possible interactions (w/this GameActor)
117
118 @param actor: (GameActor) initiating the interactions
119 @return: Interaction object
120
121 GameObjects have a (ACTIONS) list of verbs that can be turned into
122 a list of the GameActions that they enable.
123 GameActors have a (INTERACTIONS) list of verbs that a requesting
124 GameActor can turn into interaction GameActions that can be
125 exchanged with that character.
126
127 The interaction object will have an ACTIONS attribute,
128 containing a comma-separated list of the supported interaction verbs
129 (which can be used to instantiate and deliver GameActions).
130 """
131 interactions = GameObject("interactions w/" + actor.name)
132 verbs = self.get("INTERACTIONS")
133 actions = ""
134 if verbs is not None:
135 for verb in verbs.split(','):
136 actions += "VERBAL." if actions == "" else ",VERBAL."
137 actions += verb
138 interactions.set("ACTIONS", actions)
139 return interactions
140
141 - def set_context(self, context):
142 """
143 establish the local context
144 """
145 self.context = context
146
148 """
149 Initiate an action against a target
150 @param action: (GameAction) to be initiated
151 @param target: (GameObject) target of the action
152 @return: (boolean, string) result of the action
153 """
154
155 return action.act(self, target, self.context)
156
158 """
159 called once per round in initiative order
160 (must be implemented in sub-classes)
161 """
162 return self.name + " takes no action"
163
164
165
166
168 """
169 Base attacks with assured outcomes
170 """
171 attacker = GameActor("attacker")
172 target = GameActor("target")
173 context = GameContext("unit-test")
174
175 tried = 0
176 passed = 0
177
178
179 target.set("LIFE", 10)
180 source = GameObject("weak-attack")
181 action = GameAction(source, "ATTACK")
182 action.set("ACCURACY", -100)
183 action.set("DAMAGE", "1")
184 print("{} tries to {} {} with {}".
185 format(attacker, action, target, source))
186 (_, desc) = action.act(attacker, target, context)
187 tried += 1
188 assert target.get("LIFE") == 10, \
189 "{} took damage, LIFE: {} -> {}". \
190 format(target, 10, target.get("LIFE"))
191 passed += 1
192 print(" " + desc)
193 print()
194
195
196 source = GameObject("strong-attack")
197 action = GameAction(source, "ATTACK")
198 action.set("ACCURACY", 100)
199 action.set("DAMAGE", "1")
200 print("{} tries to {} {} with {}".
201 format(attacker, action, target, source))
202 tried += 1
203 (_, desc) = action.act(attacker, target, context)
204 assert target.get("LIFE") == 9, \
205 "{} took incorrect damage, LIFE: {} -> {}". \
206 format(target, 10, target.get("LIFE"))
207 passed += 1
208 print(" " + desc)
209 print()
210
211
212 source = GameObject("evadable-attack")
213 action = GameAction(source, "ATTACK")
214 action.set("ACCURACY", 0)
215 action.set("DAMAGE", "1")
216 target.set("EVASION", 100)
217 target.set("LIFE", 10)
218 print("{} tries to {} {} with {}".
219 format(attacker, action, target, source))
220 tried += 1
221 (_, desc) = action.act(attacker, target, context)
222 assert target.get("LIFE") == 10, \
223 "{} took damage, LIFE: {} -> {}". \
224 format(target, 10, target.get("LIFE"))
225 passed += 1
226 print(" " + desc)
227 print()
228
229
230 source = GameObject("absorbable-attack")
231 action = GameAction(source, "ATTACK")
232 action.set("ACCURACY", 100)
233 action.set("DAMAGE", "1")
234 target.set("EVASION", 0)
235 target.set("LIFE", 10)
236 target.set("PROTECTION", 1)
237 print("{} tries to {} {} with {}".
238 format(attacker, action, target, source))
239 tried += 1
240 (_, desc) = action.act(attacker, target, context)
241 assert target.get("LIFE") == 10, \
242 "{} took damage, LIFE: {} -> {}". \
243 format(target, 10, target.get("LIFE"))
244 passed += 1
245 print(" " + desc)
246 print()
247 return (tried, passed)
248
249
251 """
252 Attacks that draw on sub-type EVASION and PROTECTION
253 """
254 attacker = GameActor("attacker")
255 target = GameActor("target")
256 context = GameContext("unit-test")
257
258 tried = 0
259 passed = 0
260
261
262 source = GameObject("evadable")
263 action = GameAction(source, "ATTACK.subtype")
264 action.set("ACCURACY", 0)
265 action.set("DAMAGE", "1")
266
267 target.set("LIFE", 10)
268 target.set("EVASION", 50)
269 target.set("EVASION.subtype", 50)
270
271 print("{} tries to {} {} with {}".
272 format(attacker, action, target, source))
273 (_, desc) = action.act(attacker, target, context)
274 tried += 1
275 assert target.get("LIFE") == 10, \
276 "{} took damage, LIFE: {} -> {}". \
277 format(target, 10, target.get("LIFE"))
278 passed += 1
279 print(" " + desc)
280
281
282 source = GameObject("absorbable")
283 action = GameAction(source, "ATTACK.subtype")
284 action.set("ACCURACY", 0)
285 action.set("DAMAGE", "4")
286
287 target.set("LIFE", 10)
288 target.set("EVASION", 0)
289 target.set("EVASION.subtype", 0)
290 target.set("PROTECTION", 1)
291 target.set("PROTECTION.subtype", 1)
292
293 print("{} tries to {} {} with {}".
294 format(attacker, action, target, source))
295 (_, desc) = action.act(attacker, target, context)
296 tried += 1
297 assert target.get("LIFE") == 8, \
298 "{} took damage, LIFE: {} -> {}". \
299 format(target, 8, target.get("LIFE"))
300 passed += 1
301 print(" " + desc)
302 print()
303 return (tried, passed)
304
305
307 """
308 attacks that depend on dice-rolls
309 """
310 attacker = GameActor("attacker")
311 target = GameActor("target")
312 context = GameContext("unit-test")
313
314 target.set("LIFE", 10)
315 source = GameObject("fair-fight")
316 action = GameAction(source, "ATTACK")
317 action.set("ACCURACY", 0)
318 action.set("DAMAGE", "1")
319 target.set("EVASION", 50)
320 target.set("LIFE", 10)
321 target.set("PROTECTION", 0)
322 rounds = 10
323 for _ in range(rounds):
324 print("{} tries to {} {} with {}".
325 format(attacker, action, target, source))
326 (_, desc) = action.act(attacker, target, context)
327 print(" " + desc)
328
329 life = target.get("LIFE")
330 assert life < 10, "{} took no damage in {} rounds".format(target, rounds)
331 assert life > 10 - rounds, "{} took damage every round".format(target)
332 print("{} was hit {} times in {} rounds".format(target, 10 - life, rounds))
333 print()
334 return (2, 2)
335
336
338 """
339 conditions that are guaranteed to happen or not
340 """
341 sender = GameActor("sender")
342 target = GameActor("target")
343 context = GameContext("unit-test")
344
345 tried = 0
346 passed = 0
347
348
349 source = GameObject("weak-condition")
350 action = GameAction(source, "MENTAL.CONDITION-1")
351 action.set("POWER", -100)
352 action.set("STACKS", "10")
353 print("{} tries to {} {} with {}".
354 format(sender, action, target, source))
355 (success, desc) = action.act(sender, target, context)
356 assert not success, \
357 "{} was successful against {}".\
358 format(action.verb, target)
359 assert target.get(action.verb) is None, \
360 "{} RECEIVED {}={}". \
361 format(target, action.verb, target.get(action.verb))
362 print(" " + desc)
363 tried += 2
364 passed += 2
365
366
367 source = GameObject("strong-condition")
368 action = GameAction(source, "MENTAL.CONDITION-2")
369 action.set("POWER", 0)
370 action.set("STACKS", "10")
371 print("{} tries to {} {} with {}".
372 format(sender, action, target, source))
373 (success, desc) = action.act(sender, target, context)
374 assert success, \
375 "{} was unsuccessful against {}".\
376 format(action.verb, target)
377 assert target.get(action.verb) == 10, \
378 "{} RECEIVED {}={}". \
379 format(target, action.verb, target.get(action.verb))
380 print(" " + desc)
381 tried += 2
382 passed += 2
383
384
385 source = GameObject("base-class-resisted-condition")
386 action = GameAction(source, "MENTAL.CONDITION-3")
387 action.set("POWER", 0)
388 action.set("STACKS", "10")
389 target.set("RESISTANCE.MENTAL", 100)
390 print("{} tries to {} {} with {}".
391 format(sender, action, target, source))
392 (success, desc) = action.act(sender, target, context)
393 assert not success, \
394 "{} was successful against {}".\
395 format(action.verb, target)
396 assert target.get(action.verb) is None, \
397 "{} RECEIVED {}={}". \
398 format(target, action.verb, target.get(action.verb))
399 print(" " + desc)
400 tried += 2
401 passed += 2
402
403 print()
404 return (tried, passed)
405
406
408 """
409 conditions that draw on sub-type RESISTANCE
410 """
411 sender = GameActor("sender")
412 target = GameActor("target")
413 context = GameContext("unit-test")
414
415
416 source = GameObject("sub-type-resisted-condition")
417 action = GameAction(source, "MENTAL.CONDITION-4")
418 action.set("POWER", 0)
419 action.set("STACKS", "10")
420 target.set("RESISTANCE.MENTAL", 50)
421 target.set("RESISTANCE.MENTAL.CONDITION-4", 50)
422 print("{} tries to {} {} with {}".
423 format(sender, action, target, source))
424 (success, desc) = action.act(sender, target, context)
425 assert not success, \
426 "{} was successful against {}".\
427 format(action.verb, target)
428 assert target.get(action.verb) is None, \
429 "{} RECEIVED {}={}". \
430 format(target, action.verb, target.get(action.verb))
431 print(" " + desc)
432
433 print()
434 return (2, 2)
435
436
438 """
439 conditions that depend on dice rolls
440 """
441 sender = GameActor("sender")
442 target = GameActor("target")
443 context = GameContext("unit-test")
444
445 source = GameObject("partially-resisted-condition")
446 action = GameAction(source, "MENTAL.CONDITION-5")
447 action.set("POWER", 0)
448 action.set("STACKS", "10")
449 target.set("RESISTANCE.MENTAL", 25)
450 target.set("RESISTANCE.MENTAL.CONDITION-5", 25)
451
452 rounds = 5
453 for _ in range(rounds):
454 print("{} tries to {} {} with {}".
455 format(sender, action, target, source))
456 (success, desc) = action.act(sender, target, context)
457 assert success, \
458 "none of 10 stacks of {} got through".format(action.verb)
459 print(" " + desc)
460
461 delivered = rounds * 10
462 expected = delivered / 2
463 received = target.get(action.verb)
464 assert received > 0.7 * expected, \
465 "{} took {}/{} stacks".format(target, received, delivered)
466 assert received < 1.3 * expected, \
467 "{} took {}/{} stacks". format(target, received, delivered)
468 print("{} took {}/{} stacks (vs {} expected)".
469 format(target, received, delivered, int(expected)))
470
471 print()
472 return (2, 2)
473
474
476 """
477 Run all unit-test cases and print out summary of results
478 """
479 (t_1, p_1) = simple_attack_tests()
480 (t_2, p_2) = sub_attack_tests()
481 (t_3, p_3) = random_attack_tests()
482 (t_4, p_4) = simple_condition_tests()
483 (t_5, p_5) = sub_condition_tests()
484 (t_6, p_6) = random_condition_tests()
485 tried = t_1 + t_2 + t_3 + t_4 + t_5 + t_6
486 passed = p_1 + p_2 + p_3 + p_4 + p_5 + p_6
487 if tried == passed:
488 print("Passed all {} GameActor tests".format(passed))
489 else:
490 print("FAILED {}/{} GameActor tests".format(tried-passed, tried))
491
492
493 if __name__ == "__main__":
494 main()
495