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

Source Code for Module gameactor

  1  #!/usr/bin/python3 
  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   
9 -class GameActor(GameObject):
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
26 - def _accept_attack(self, action, actor, context):
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 # get the victim's base and sub-type EVASION 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 # see if EVASION+D100 can beat the incoming TO_HIT 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 # get the recipient's base and sub-class PROTECTION 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 # see if PROTECTION can absorb all the incoming HIT_POINTS 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 # subtract received HIT_POINTS from our LIFE 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 # if LIFE<=0 we are incapacitated, and no longer alive 79 if new_hp <= 0: 80 result += ", and is killed" 81 self.alive = False 82 self.incapacitated = True 83 return (True, result)
84
85 - def accept_action(self, action, actor, context):
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 # get the base action verb 104 base_verb = action.verb.split('.')[0] \ 105 if '.' in action.verb else action.verb 106 107 # we handle ATTACK (based on HIT/DAMAGE vs EVASION/PROTECTION) 108 if base_verb == "ATTACK": 109 return self._accept_attack(action, actor, context) 110 111 # otherwise let our (GameObject) super-class handle it 112 return super(GameActor, self).accept_action(action, actor, context)
113
114 - def interact(self, actor):
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
147 - def take_action(self, action, target):
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 # call C{action.act()} with me as initiator, in my context 155 return action.act(self, target, self.context)
156
157 - def take_turn(self):
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 # UNIT TESTING 166 # pylint: disable=too-many-statements
167 -def simple_attack_tests():
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 # attack guarnteed to fail 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 # attack guaranteed to succeed 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 # attack that will be evaded 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 # attack that will be absorbabed 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
250 -def sub_attack_tests():
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 # evasion succeeds because base and sub-type add 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 # protection is sum of base and sub-type 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
306 -def random_attack_tests():
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
337 -def simple_condition_tests():
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 # impossibly weak condition will not happen 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 # un-resisted condition will always happen 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 # fully resisted condition will never happen 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
407 -def sub_condition_tests():
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 # MENTAL + sub-type are sufficient to resist it 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
437 -def random_condition_tests():
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 # TO_HIT=100, RESISTANCE=50 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
475 -def main():
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