Module gameaction
[hide private]
[frames] | no frames]

Source Code for Module gameaction

  1  #!/usr/bin/python3 
  2  """ This module implements the GameAction class """ 
  3  from dice import Dice 
  4  from base import Base 
  5   
  6   
  7  # pylint: disable=no-self-use;   these are still appropriately class-private 
8 -class GameAction(Base):
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 """
51 - def __init__(self, source, verb):
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 # non-attacks automatically have STACKS=1 63 if "ATTACK" not in verb: 64 self.set("STACKS", "1")
65
66 - def __str__(self):
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 # pylint: disable=too-many-locals; I claim I need them all
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 # split verb into a list of individual plus-separated verbs 113 verbs = self.verb.split('+') if '+' in self.verb else [self.verb] 114 # split the ACCURACY, DAMAGE, POWER and STACKS into corresponding lists 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 # carry out each of the verbs in the list 121 results = "" 122 attacks = 0 123 conditions = 0 124 for verb in verbs: 125 # gather the verb and associated base/initiator attributes 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 # pass them on to target, and accumulate results 140 (success, result) = target.accept_action(self, initiator, context) 141 if results == "": 142 results = result 143 else: 144 results += "\n" + result 145 # immediately return false if any action fails 146 if not success: 147 return (False, results) 148 149 return (True, results)
150
151 - def __get_list(self, name, size):
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
171 - def __accuracy(self, verb, base, initiator):
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 # get base accuracy from the action 182 if base is None: 183 w_accuracy = 0 184 else: 185 w_accuracy = int(base) 186 187 # get the initiator base accuracy 188 acc = initiator.get("ACCURACY") 189 if acc is None: 190 i_accuracy = 0 191 else: 192 i_accuracy = int(acc) 193 194 # add any initiator sub-type accuracy 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 # get the basic action damage formula and roll it 215 if base is None: 216 w_damage = 0 217 else: 218 dice = Dice(base) 219 w_damage = dice.roll() 220 221 # get initiator base damage formula and roll it 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 # add any initiator sub-type damage 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 # figure out the condition type and subtype 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 # get base power from the action 259 if base is None: 260 power = 0 261 else: 262 power = int(base) 263 264 # add the initiator base power 265 pwr = initiator.get("POWER." + base_type) 266 if pwr is not None: 267 power += int(pwr) 268 269 # add any initiator sub-type power 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 # figure out the condition type and subtype 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 # get base stacks from the action 296 if base is None: 297 stacks = 1 298 else: 299 dice = Dice(base) 300 stacks = dice.roll() 301 302 # add the initiator base stacks 303 stx = initiator.get("STACKS." + base_type) 304 if stx is not None: 305 dice = Dice(stx) 306 stacks += dice.roll() 307 308 # add any initiator sub-type stacks 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 # UNIT TESTING
319 -class TestRecipient(Base):
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
328 - def accept_action(self, action, actor, context):
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 # pylint: disable=too-many-locals; I claim I need them all
352 -def base_attacks():
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 # create a victim and context 361 victim = TestRecipient("victim") 362 context = Base("unit-test") 363 364 # create an artifact with actions 365 artifact = Base("test-case") 366 367 # test attacks from an un-skilled attacker (base values) 368 lame = Base("lame attacker") # attacker w/no skills 369 # pylint: disable=bad-whitespace 370 lame_attacks = [ 371 # verb, accuracy, damage, exp hit, exp dmg 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 # see if the action contained the expected values 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 # pylint: disable=too-many-locals; I claim I need them all
410 -def subtype_attacks():
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 # create a victim and context 419 victim = TestRecipient("victim") 420 context = Base("unit-test") 421 422 # create an artifact with actions 423 artifact = Base("test-case") 424 425 # test attacks from a skilled attacker, w/bonus values 426 skilled = Base("skilled attacker") # attacker w/many skills 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 # pylint: disable=bad-whitespace 435 skilled_attacks = [ 436 # verb, accuracy, damage, exp hit, exp dmg 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 # see if the action contained the expected values 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 # pylint: disable=too-many-locals; I claim I need them all
474 -def base_conditions():
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 # create a victim and context 483 victim = TestRecipient("victim") 484 context = Base("unit-test") 485 486 # create an artifact with actions 487 artifact = Base("test-case") 488 489 # test attacks from an un-skilled attacker (base values) 490 lame = Base("unskilled sender") # sender w/no skills 491 # pylint: disable=bad-whitespace 492 lame_attacks = [ 493 # verb, power, stacks, exp hit, exp stx 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 # see if the action contained the expected values 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 # pylint: disable=too-many-locals; I claim I need them all
531 -def subtype_conditions():
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 # create a victim and context 540 victim = TestRecipient("victim") 541 context = Base("unit-test") 542 543 # create an artifact with actions 544 artifact = Base("test-case") 545 546 # test attacks from a skilled attacker, w/bonus values 547 skilled = Base("skilled sender") # sender w/many skills 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 # pylint: disable=bad-whitespace 556 skilled_attacks = [ 557 # verb, power, stacks, exp hit, exp stx 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 # see if the action contained the expected values 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
594 -def compound_verbs():
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") # sender w/no skills 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
641 -def main():
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