1
2 """ This module implements the GameAction class """
3 from dice import Dice
4 from base import Base
5
6
7
9 """
10 Every GameActor has, at any given time, a list of possible actions
11 from which they can choose. Some of these possibilities may be
12 intrinsic in the actor, while others are made possible by GameObjects
13 (e.g. a weapon), by other actors (e.g. interaction) or a GameContext
14 (e.g. searching a room).
15
16 A GameAction is an action possibility that has been associated with
17 a GameActor. It has attributes that describe and quantify its effects.
18 When the GameAction's C{act()} method is called, it computes probabilities
19 and strengths (based on attributes of the actor and enabling artifact)
20 and calls the target's C{accept_action()} handler, which will determine
21 and return the results.
22
23 @ivar attributes: (dict) of <name,value> pairs
24 @ivar source: (GameObject) that delivers this action
25 @ivar verb: (string) simple or sub-classed name(s)
26
27 verbs may be simple or sub-classed (with subtypes):
28 - SEARCH
29 - VERBAL.INTIMMIDATE
30 - ATTACK.STAB
31
32 attributes to be set by client after instantiation:
33 - ATTACK verbs are expected to have ACCURACY (to-hit percentage) and
34 DAMAGE (dice formula) attributes.
35 - non-ATTACK verbs are expected to have POWER (to-hit percentage) and
36 STACKS (dice formulat for instances to attempt/deliver) attributes.
37
38 A verb can also be a plus-separated concatenation of simple and/or
39 sub-classed verbs. If a passed verb contains multiple (plus-separated)
40 terms, the ACCURACY, DAMAGE, POWER, and STACKS attributes are each assumed
41 to be comma-separated lists with the same number of terms.
42
43 Example:
44 - verb="ATTACK.STAB+ATTACK.POISON"
45 - ACCURACY=60,40
46 - DAMAGE=D6,3D4
47
48 If there are multiple verbs but only a single value, that single value
49 should be used for each verb.
50 """
52 """
53 create a new C{GameAction}
54 @param source: (GameObject) instrument for the action
55 @param verb: (string) the name of the action(s)
56 """
57 super(GameAction, self).__init__(verb)
58 self.source = source
59 self.verb = verb
60 self.attributes = {}
61
62
63 if "ATTACK" not in verb:
64 self.set("STACKS", "1")
65
67 """
68 return a string representation of our verb and its attributes
69 """
70 if "ATTACK" in self.verb:
71 return "{} (ACCURACY={}%, DAMAGE={})".\
72 format(self.verb, self.get("ACCURACY"), self.get("DAMAGE"))
73 return "{} (POWER={}%, STACKS={})".\
74 format(self.verb, self.get("POWER"), self.get("STACKS"))
75
76
77 - def act(self, initiator, target, context):
78 """
79 Initiate an action against a target
80
81 If the verb is a concatenation (list of simple/sub-classed verbs),
82 split it and process each action separately (stopping if one of
83 them fails).
84
85 This (base-class) C{act()} method knows how to process (a list of)
86 simple or sub-typed verbs that are simply a matter of looking up
87 initiator bonus values, adding them up, and calling the target's
88 C{accept_action()} handler, passing:
89 - the C{GameAction} (with all of its attributes)
90 - the initiating C{GameActor}
91 - the C{GameContext} in which this is taking place
92
93 When the target is called, the C{GameAction} attributes will
94 include (for ATTACK verbs):
95 - C{TO_HIT}: 100 + sum of action's and actor's C{ACCURACY}
96 - C{DAMAGE}: sum of rolls against actions's and actor's C{DAMAGE}
97 for non-attack verbs:
98 - C{TO_HIT}: 100 + the action's and initiator's C{POWER}
99 - C{STACKS}: sum of rolls against actions's & initiator's C{STACKS}
100
101 Actions that require more complex processing (before
102 calling the target) must be implemented (by additional
103 code in a sub-class that extends this method (at least
104 for the verbs in question)
105
106 @param initiator: (GameActor) initiating the action
107 @param target: (GameObject) target of the action
108 @param context: (GameContext) in which this is happening
109 @return: (string) description of the results (returned from the target)
110
111 """
112
113 verbs = self.verb.split('+') if '+' in self.verb else [self.verb]
114
115 accuracies = self.__get_list("ACCURACY", len(verbs))
116 damages = self.__get_list("DAMAGE", len(verbs))
117 powers = self.__get_list("POWER", len(verbs))
118 stacks = self.__get_list("STACKS", len(verbs))
119
120
121 results = ""
122 attacks = 0
123 conditions = 0
124 for verb in verbs:
125
126 self.verb = verb
127 if "ATTACK" in verb:
128 self.set("TO_HIT", 100 +
129 self.__accuracy(verb, accuracies[attacks], initiator))
130 self.set("HIT_POINTS",
131 self.__damage(verb, damages[attacks], initiator))
132 attacks += 1
133 else:
134 self.set("TO_HIT", 100 +
135 self.__power(verb, powers[conditions], initiator))
136 self.set("TOTAL",
137 self.__stacks(verb, stacks[conditions], initiator))
138 conditions += 1
139
140 (success, result) = target.accept_action(self, initiator, context)
141 if results == "":
142 results = result
143 else:
144 results += "\n" + result
145
146 if not success:
147 return (False, results)
148
149 return (True, results)
150
152 """
153 read specified attribute, lex its (comma-separated) values into a list
154
155 @param name: (string) name of attribute to be looked up and split
156 @param size: (int) length of desired list
157 @return: (list of strings)
158 - if no value is found, a list of Nones will be returned
159 - if there is only single value, it will be replicated the
160 specified number of times
161 """
162 atr = self.get(name)
163 if atr is None:
164 return [None] * size
165 if not isinstance(atr, str):
166 return [atr] * size
167 if ',' not in atr:
168 return [atr] * size
169 return atr.split(',')
170
172 """
173 helper to compute accuracy of this attack, based on the supplied
174 base ACCURACY plus any initiator ACCURACY(.subtype) bonus(es).
175
176 @param verb: (string) attack verb
177 @param base: (int) accuracy (from the action)
178 @param initiator: (GameActor) who is initiating the attack
179 @return: (int) probability of hitting
180 """
181
182 if base is None:
183 w_accuracy = 0
184 else:
185 w_accuracy = int(base)
186
187
188 acc = initiator.get("ACCURACY")
189 if acc is None:
190 i_accuracy = 0
191 else:
192 i_accuracy = int(acc)
193
194
195 if 'ATTACK.' in verb:
196 sub_type = verb.split('.')[1]
197 if sub_type is not None:
198 acc = initiator.get("ACCURACY." + sub_type)
199 if acc is not None:
200 i_accuracy += int(acc)
201
202 return w_accuracy + i_accuracy
203
204 - def __damage(self, verb, base, initiator):
205 """
206 helper to compute the damage of this attack, based on the supplied
207 base DAMAGE plus any initiator DAMAGE(.subtype) bonus(es).
208
209 @param verb: (string) attack verb
210 @param base: (string) dice formula for base damage
211 @param initiator: (GameActor) who is initiating the attack
212 @return: (int) total damage
213 """
214
215 if base is None:
216 w_damage = 0
217 else:
218 dice = Dice(base)
219 w_damage = dice.roll()
220
221
222 dmg = initiator.get("DAMAGE")
223 if dmg is None:
224 i_damage = 0
225 else:
226 dice = Dice(dmg)
227 i_damage = dice.roll()
228
229
230 if 'ATTACK.' in verb:
231 sub_type = verb.split('.')[1]
232 if sub_type is not None:
233 dmg = initiator.get("DAMAGE." + sub_type)
234 if dmg is not None:
235 dice = Dice(dmg)
236 i_damage += dice.roll()
237
238 return w_damage + i_damage
239
240 - def __power(self, verb, base, initiator):
241 """
242 helper to compute the power of this action, based on the supplied
243 base POWER plus any initiator POWER.verb/POWER.verb.subtype bonuses
244
245 @param verb: (string) action verb
246 @param base: (int) base power (from action)
247 @param initiator: (GameActor) who is sending the condition
248 @return: (int) total probability of hitting
249 """
250
251 if '.' in verb:
252 base_type = verb.split('.')[0]
253 sub_type = verb.split('.')[1]
254 else:
255 base_type = verb
256 sub_type = None
257
258
259 if base is None:
260 power = 0
261 else:
262 power = int(base)
263
264
265 pwr = initiator.get("POWER." + base_type)
266 if pwr is not None:
267 power += int(pwr)
268
269
270 if sub_type is not None:
271 pwr = initiator.get("POWER." + base_type + '.' + sub_type)
272 if pwr is not None:
273 power += int(pwr)
274
275 return power
276
277 - def __stacks(self, verb, base, initiator):
278 """
279 helper to compute the stacks of this action, based on the supplied
280 base STACKS plus any initiator STACKS.verb/STACKS.verb.subtype bonuses
281
282 @param verb: (string) action verb
283 @param base: (string) dice formula for base stacks
284 @param initiator: (GameActor) who is sending the condition
285 @return: (int) total number of stacks
286 """
287
288 if '.' in verb:
289 base_type = verb.split('.')[0]
290 sub_type = verb.split('.')[1]
291 else:
292 base_type = verb
293 sub_type = None
294
295
296 if base is None:
297 stacks = 1
298 else:
299 dice = Dice(base)
300 stacks = dice.roll()
301
302
303 stx = initiator.get("STACKS." + base_type)
304 if stx is not None:
305 dice = Dice(stx)
306 stacks += dice.roll()
307
308
309 if sub_type is not None:
310 stx = initiator.get("STACKS." + base_type + '.' + sub_type)
311 if stx is not None:
312 dice = Dice(stx)
313 stacks += dice.roll()
314
315 return stacks
316
317
318
320 """
321 a minimal object that can receive, and report on actions
322
323 The returned result string will include the passed TO_HIT and POWER/STACKS
324 attributes so that the test case can confirm that they were properly
325 computed and passed.
326 """
327
329 """
330 report on the action we received
331
332 @param action: GameAction being sent
333 @param actor: GameActor who set it
334 @param context: GameContext in which this happened
335 @return (boolean success, string results)
336 """
337 if "ATTACK" in action.verb:
338 return (True,
339 "{} receives {} (TO_HIT={}, DAMAGE={}) from {} in {}".
340 format(self, action.verb,
341 action.get("TO_HIT"), action.get("HIT_POINTS"),
342 actor, context))
343 result = "resists" if action.verb == "FAIL" else "receives"
344 return (action.verb != "FAIL",
345 "{} {} {} (TO_HIT={}, STACKS={}) from {} in {}".
346 format(self, result, action.verb,
347 action.get("TO_HIT"), action.get("TOTAL"),
348 actor, context))
349
350
351
353 """
354 GameAction base ability ATTACK test cases:
355 - test correctness of TO_HIT and DAMAGE computations
356 for base and sub-type attacks
357 but with no sub-type or initiator bonuses
358 """
359
360
361 victim = TestRecipient("victim")
362 context = Base("unit-test")
363
364
365 artifact = Base("test-case")
366
367
368 lame = Base("lame attacker")
369
370 lame_attacks = [
371
372 ("ATTACK", None, "1", 100, 1),
373 ("ATTACK.ten", 10, "10", 110, 10),
374 ("ATTACK.twenty", 20, "20", 120, 20),
375 ("ATTACK.thirty", 30, "30", 130, 30)]
376
377 tried = 0
378 passed = 0
379 for (verb, accuracy, damage, exp_hit, exp_dmg) in lame_attacks:
380 action = GameAction(artifact, verb)
381 if accuracy is not None:
382 action.set("ACCURACY", accuracy)
383 action.set("DAMAGE", damage)
384 (_, result) = action.act(lame, victim, context)
385
386
387 to_hit = action.get("TO_HIT")
388 hit_points = action.get("HIT_POINTS")
389 tried += 3
390 if action.verb == verb and to_hit == exp_hit and hit_points == exp_dmg:
391 print(result + " ... CORRECT")
392 passed += 3
393 else:
394 print(result)
395 assert action.verb == verb, \
396 "incorrect action verb: expected " + verb
397 passed += 1
398 assert action.get("TO_HIT") == exp_hit, \
399 "incorrect base accuracy: expected " + str(exp_hit)
400 passed += 1
401 assert action.get("HIT_POINTS") == exp_dmg, \
402 "incorrect base damage: expected " + str(exp_dmg)
403 passed += 1
404
405 print()
406 return (tried, passed)
407
408
409
411 """
412 GameAction sub-type ability ATTACK test cases:
413 - test correctness of TO_HIT and DAMAGE computations
414 for base and sub-type attacks
415 both action and initiator have both base and sub-type bonuses
416 """
417
418
419 victim = TestRecipient("victim")
420 context = Base("unit-test")
421
422
423 artifact = Base("test-case")
424
425
426 skilled = Base("skilled attacker")
427 skilled.set("ACCURACY", 10)
428 skilled.set("DAMAGE", "10")
429 skilled.set("ACCURACY.twenty", 20)
430 skilled.set("DAMAGE.twenty", "20")
431 skilled.set("ACCURACY.thirty", 30)
432 skilled.set("DAMAGE.thirty", "30")
433
434
435 skilled_attacks = [
436
437 ("ATTACK", None, "1", 110, 11),
438 ("ATTACK.ten", 10, "10", 120, 20),
439 ("ATTACK.twenty", 20, "20", 150, 50),
440 ("ATTACK.thirty", 30, "30", 170, 70)]
441
442 tried = 0
443 passed = 0
444 for (verb, accuracy, damage, exp_hit, exp_dmg) in skilled_attacks:
445 action = GameAction(artifact, verb)
446 if accuracy is not None:
447 action.set("ACCURACY", accuracy)
448 action.set("DAMAGE", damage)
449 (_, result) = action.act(skilled, victim, context)
450
451
452 tried += 3
453 to_hit = action.get("TO_HIT")
454 hit_points = action.get("HIT_POINTS")
455 if action.verb == verb and to_hit == exp_hit and hit_points == exp_dmg:
456 print(result + " ... CORRECT")
457 passed += 3
458 else:
459 print(result)
460 assert action.verb == verb, \
461 "incorrect action verb: expected " + verb
462 passed += 1
463 assert action.get("TO_HIT") == exp_hit, \
464 "incorrect base accuracy: expected " + str(exp_hit)
465 passed += 1
466 assert action.get("HIT_POINTS") == exp_dmg, \
467 "incorrect base damage: expected " + str(exp_dmg)
468 passed += 1
469 print()
470 return (tried, passed)
471
472
473
475 """
476 GameAction base ability condition test cases:
477 - test correctness of TO_HIT and STACKS computations
478 for base and sub-type verbs
479 but with no sub-type or initiator bonuses
480 """
481
482
483 victim = TestRecipient("victim")
484 context = Base("unit-test")
485
486
487 artifact = Base("test-case")
488
489
490 lame = Base("unskilled sender")
491
492 lame_attacks = [
493
494 ("MENTAL", None, "1", 100, 1),
495 ("MENTAL.X", 10, "10", 110, 10),
496 ("MENTAL.Y", 20, "20", 120, 20),
497 ("MENTAL.Z", 30, "30", 130, 30)]
498
499 tried = 0
500 passed = 0
501 for (verb, power, stacks, exp_hit, exp_stx) in lame_attacks:
502 action = GameAction(artifact, verb)
503 if power is not None:
504 action.set("POWER", power)
505 action.set("STACKS", stacks)
506 (_, result) = action.act(lame, victim, context)
507
508
509 tried += 3
510 to_hit = action.get("TO_HIT")
511 stacks = action.get("TOTAL")
512 if action.verb == verb and to_hit == exp_hit and stacks == exp_stx:
513 print(result + " ... CORRECT")
514 passed += 3
515 else:
516 print(result)
517 assert action.verb == verb, \
518 "incorrect action verb: expected " + verb
519 passed += 1
520 assert action.get("TO_HIT") == exp_hit, \
521 "incorrect base accuracy: expected " + str(exp_hit)
522 passed += 1
523 assert action.get("TOTAL") == exp_stx, \
524 "incorrect base stacks: expected " + str(exp_stx)
525 passed += 1
526 print()
527 return (tried, passed)
528
529
530
532 """
533 GameAction sub-type ability condition test cases:
534 - test correctness of TO_HIT and STACKS computations
535 for base and sub-type verbs
536 both action and initiator have both base and sub-type bonuses
537 """
538
539
540 victim = TestRecipient("victim")
541 context = Base("unit-test")
542
543
544 artifact = Base("test-case")
545
546
547 skilled = Base("skilled sender")
548 skilled.set("POWER.MENTAL", 10)
549 skilled.set("STACKS.MENTAL", "10")
550 skilled.set("POWER.MENTAL.Y", 20)
551 skilled.set("STACKS.MENTAL.Y", "20")
552 skilled.set("POWER.MENTAL.Z", 30)
553 skilled.set("STACKS.MENTAL.Z", "30")
554
555
556 skilled_attacks = [
557
558 ("MENTAL", None, "1", 110, 11),
559 ("MENTAL.X", 10, "10", 120, 20),
560 ("MENTAL.Y", 20, "20", 150, 50),
561 ("MENTAL.Z", 30, "30", 170, 70)]
562
563 tried = 0
564 passed = 0
565 for (verb, power, stacks, exp_hit, exp_stx) in skilled_attacks:
566 action = GameAction(artifact, verb)
567 if power is not None:
568 action.set("POWER", power)
569 action.set("STACKS", stacks)
570 (_, result) = action.act(skilled, victim, context)
571
572
573 tried += 3
574 to_hit = action.get("TO_HIT")
575 stacks = action.get("TOTAL")
576 if action.verb == verb and to_hit == exp_hit and stacks == exp_stx:
577 print(result + " ... CORRECT")
578 passed += 3
579 else:
580 print(result)
581 assert action.verb == verb, \
582 "incorrect action verb: expected " + verb
583 passed += 1
584 assert action.get("TO_HIT") == exp_hit, \
585 "incorrect base accuracy: expected " + str(exp_hit)
586 passed += 1
587 assert action.get("TOTAL") == exp_stx, \
588 "incorrect base stacks: expected " + str(exp_stx)
589 passed += 1
590 print()
591 return (tried, passed)
592
593
595 """
596 GameAction compound-verb test cases:
597 - recognition and handling of compound verbs
598 - correct association with ACCURACY, DAMAGE, POWER and STACKS lists
599 - stopping list processing on first action that fails
600 """
601 context = Base("unit-test")
602 artifact = Base("test-case")
603 victim = TestRecipient("victim")
604 lame = Base("unskilled sender")
605
606 verbs = "ATTACK.one+MENTAL.two+ATTACK.three+PHYSICAL.four+VERBAL.five" + \
607 "+FAIL+WONT-HAPPEN"
608 action = GameAction(artifact, verbs)
609 action.set("ACCURACY", "1,3")
610 action.set("DAMAGE", "10,30")
611 action.set("POWER", "2,4,5,0")
612 action.set("STACKS", "2,4,5,0")
613
614 print("Compound verb: " + verbs)
615 for attr in ["ACCURACY", "DAMAGE", "POWER", "STACKS"]:
616 print(" " + attr + ":\t" + action.get(attr))
617
618 (success, results) = action.act(lame, victim, context)
619 print(results)
620 assert "ATTACK.one (TO_HIT=101, DAMAGE=10)" in results, \
621 "ATTACK.one was not correctly passed"
622 assert "two (TO_HIT=102, STACKS=2)" in results, \
623 "MENTAL.two was not correctly passed"
624 assert "ATTACK.three (TO_HIT=103, DAMAGE=30)" in results, \
625 "ATTACK.three was not correctly passed"
626 assert "four (TO_HIT=104, STACKS=4)" in results, \
627 "PHYSICAL.four was not correctly passed"
628 assert "five (TO_HIT=105, STACKS=5)" in results, \
629 "VERBAL.five was not correctly passed"
630 assert "resists FAIL" in results, \
631 "sixth (FAIL) condition was not correctly passed"
632 assert not success, \
633 "sixth (FAIL) condition did not cause action to fail"
634 assert "WONT-HAPPEN" not in results, \
635 "seventh (WONT-HAPPEN) action should not have been sent"
636
637 print()
638 return (8, 8)
639
640
642 """
643 Run all unit-test cases and print out summary of results
644 """
645 (t_1, p_1) = base_attacks()
646 (t_2, p_2) = subtype_attacks()
647 (t_3, p_3) = base_conditions()
648 (t_4, p_4) = subtype_conditions()
649 (t_5, p_5) = compound_verbs()
650 tried = t_1 + t_2 + t_3 + t_4 + t_5
651 passed = p_1 + p_2 + p_3 + p_4 + p_5
652 if tried == passed:
653 print("Passed all {} GameAction tests".format(passed))
654 else:
655 print("FAILED {}/{} GameAction tests".format(tried-passed, tried))
656
657
658 if __name__ == "__main__":
659 main()
660