exploration.analysis
- Authors: Peter Mawhorter
- Consulted:
- Date: 2022-10-24
- Purpose: Analysis functions for decision graphs an explorations.
1""" 2- Authors: Peter Mawhorter 3- Consulted: 4- Date: 2022-10-24 5- Purpose: Analysis functions for decision graphs an explorations. 6""" 7 8from typing import ( 9 List, Dict, Tuple, Optional, TypeVar, Callable, Union, Any, 10 ParamSpec, Concatenate, Set, cast 11) 12 13from . import base, core, parsing 14 15import textwrap 16 17 18#-------------------# 19# Text descriptions # 20#-------------------# 21 22def describeConsequence(consequence: base.Consequence) -> str: 23 """ 24 Returns a string which concisely describes a consequence list. 25 Returns an empty string if given an empty consequence. Examples: 26 27 >>> describeConsequence([]) 28 '' 29 >>> describeConsequence([ 30 ... base.effect(gain=('gold', 5), delay=2, charges=3), 31 ... base.effect(lose='flight') 32 ... ]) 33 'gain gold*5 ,2 =3; lose flight' 34 >>> from . import commands 35 >>> d = describeConsequence([ 36 ... base.effect(edit=[ 37 ... [ 38 ... commands.command('val', '5'), 39 ... commands.command('empty', 'list'), 40 ... commands.command('append') 41 ... ], 42 ... [ 43 ... commands.command('val', '11'), 44 ... commands.command('assign', 'var'), 45 ... commands.command('op', '+', '$var', '$var') 46 ... ], 47 ... ]) 48 ... ]) 49 >>> d 50 'with consequences:\ 51\\n edit {\ 52\\n val 5;\ 53\\n empty list;\ 54\\n append $_;\ 55\\n } {\ 56\\n val 11;\ 57\\n assign var $_;\ 58\\n op + $var $var;\ 59\\n }\ 60\\n' 61 >>> for line in d.splitlines(): 62 ... print(line) 63 with consequences: 64 edit { 65 val 5; 66 empty list; 67 append $_; 68 } { 69 val 11; 70 assign var $_; 71 op + $var $var; 72 } 73 """ 74 edesc = '' 75 pf = parsing.ParseFormat() 76 if consequence: 77 parts = [] 78 for item in consequence: 79 # TODO: Challenges and Conditions here! 80 if 'skills' in item: # a Challenge 81 item = cast(base.Challenge, item) 82 parts.append(pf.unparseChallenge(item)) 83 elif 'value' in item: # an Effect 84 item = cast(base.Effect, item) 85 parts.append(pf.unparseEffect(item)) 86 elif 'condition' in item: # a Condition 87 item = cast(base.Condition, item) 88 parts.append(pf.unparseCondition(item)) 89 else: 90 raise TypeError( 91 f"Invalid consequence item (no 'skills', 'value', or" 92 f" 'condition' key found):\n{repr(item)}" 93 ) 94 edesc = '; '.join(parts) 95 if len(edesc) > 60 or '\n' in edesc: 96 edesc = 'with consequences:\n' + ';\n'.join( 97 textwrap.indent(part, ' ') 98 for part in parts 99 ) + '\n' 100 101 return edesc 102 103 104def describeProgress(exploration: core.DiscreteExploration) -> str: 105 """ 106 Describes the progress of an exploration by noting each room/zone 107 visited and explaining the options visible at each point plus which 108 option was taken. Notes powers/tokens gained/lost along the way. 109 Returns a string. 110 111 Example: 112 >>> from exploration import journal 113 >>> e = journal.convertJournal('''\\ 114 ... S Start::pit 115 ... A gain jump 116 ... A gain attack 117 ... n button check 118 ... zz Wilds 119 ... o up 120 ... q _flight 121 ... o left 122 ... x left left_nook right 123 ... a geo_rock 124 ... At gain geo*15 125 ... At deactivate 126 ... o up 127 ... q _tall_narrow 128 ... t right 129 ... o right 130 ... q attack 131 ... ''') 132 >>> for line in describeProgress(e).splitlines(): 133 ... print(line) 134 Start of the exploration 135 Start exploring domain main at 0 (Start::pit) 136 Gained capability 'attack' 137 Gained capability 'jump' 138 At decision 0 (Start::pit) 139 In zone Start 140 In region Wilds 141 There are transitions: 142 left to unconfirmed 143 up to unconfirmed; requires _flight 144 1 note(s) at this step 145 Explore left from decision 0 (Start::pit) to 2 (now Start::left_nook) 146 At decision 2 (Start::left_nook) 147 There are transitions: 148 right to 0 (Start::pit) 149 There are actions: 150 geo_rock 151 Do action geo_rock 152 Gained 15 geo(s) 153 Take right from decision 2 (Start::left_nook) to 0 (Start::pit) 154 At decision 0 (Start::pit) 155 There are transitions: 156 left to 2 (Start::left_nook) 157 right to unconfirmed; requires attack 158 up to unconfirmed; requires _flight 159 Waiting for another action... 160 End of the exploration. 161 """ 162 result = '' 163 164 regions: Set[base.Zone] = set() 165 zones: Set[base.Zone] = set() 166 last: Union[base.DecisionID, Set[base.DecisionID], None] = None 167 lastState: base.State = base.emptyState() 168 prevCapabilities = base.effectiveCapabilitySet(lastState) 169 prevMechanisms = lastState['mechanisms'] 170 oldActiveDecisions: Set[base.DecisionID] = set() 171 for i, situation in enumerate(exploration): 172 if i == 0: 173 result += "Start of the exploration\n" 174 175 # Extract info 176 graph = situation.graph 177 activeDecisions = exploration.getActiveDecisions(i) 178 newActive = activeDecisions - oldActiveDecisions 179 departedFrom = exploration.movementAtStep(i)[0] 180 # TODO: use the other parts of this? 181 nowZones: Set[base.Zone] = set() 182 for active in activeDecisions: 183 nowZones |= graph.zoneAncestors(active) 184 regionsHere = set( 185 z 186 for z in nowZones 187 if graph.zoneHierarchyLevel(z) == 1 188 ) 189 zonesHere = set( 190 z 191 for z in nowZones 192 if graph.zoneHierarchyLevel(z) == 0 193 ) 194 here = departedFrom 195 state = situation.state 196 capabilities = base.effectiveCapabilitySet(state) 197 mechanisms = state['mechanisms'] 198 199 # Describe capabilities gained/lost relative to previous step 200 # (i.e., as a result of the previous action) 201 gained = ( 202 capabilities['capabilities'] 203 - prevCapabilities['capabilities'] 204 ) 205 gainedTokens = [] 206 for tokenType in capabilities['tokens']: 207 net = ( 208 capabilities['tokens'][tokenType] 209 - prevCapabilities['tokens'].get(tokenType, 0) 210 ) 211 if net != 0: 212 gainedTokens.append((tokenType, net)) 213 changed = [ 214 mID 215 for mID in list(mechanisms.keys()) + list(prevMechanisms.keys()) 216 if mechanisms.get(mID) != prevMechanisms.get(mID) 217 ] 218 219 for capability in sorted(gained): 220 result += f" Gained capability '{capability}'\n" 221 222 for tokenType, net in gainedTokens: 223 if net > 0: 224 result += f" Gained {net} {tokenType}(s)\n" 225 else: 226 result += f" Lost {-net} {tokenType}(s)\n" 227 228 for mID in changed: 229 oldState = prevMechanisms.get(mID, base.DEFAULT_MECHANISM_STATE) 230 newState = mechanisms.get(mID, base.DEFAULT_MECHANISM_STATE) 231 232 details = graph.mechanismDetails(mID) 233 if details is None: 234 mName = "(unknown)" 235 else: 236 mName = details[1] 237 result += ( 238 f" Set mechanism {mID} ({mName}) to {newState} (was" 239 f" {oldState})" 240 ) 241 # TODO: Test this! 242 243 if isinstance(departedFrom, base.DecisionID): 244 # Print location info 245 if here != last: 246 if here is None: 247 result += "Without a position...\n" 248 elif isinstance(here, set): 249 result += f"With {len(here)} active decisions\n" 250 # TODO: List them using namesListing? 251 else: 252 result += f"At decision {graph.identityOf(here)}\n" 253 newZones = zonesHere - zones 254 for zone in sorted(newZones): 255 result += f" In zone {zone}\n" 256 newRegions = regionsHere - regions 257 for region in sorted(newRegions): 258 result += f" In region {region}\n" 259 260 elif isinstance(departedFrom, set): # active in spreading domain 261 spreadingDomain = graph.domainFor(list(departedFrom)[0]) 262 result += ( 263 f" In domain {spreadingDomain} with {len(departedFrom)}" 264 f" active decisions...\n" 265 ) 266 267 else: 268 assert departedFrom is None 269 270 # Describe new position/positions at start of this step 271 if len(newActive) > 1: 272 newListing = ', '.join( 273 sorted(graph.identityOf(n) for n in newActive) 274 ) 275 result += ( 276 f" There are {len(newActive)} new active decisions:" 277 f"\n {newListing}" 278 ) 279 280 elif len(newActive) == 1: 281 here = list(newActive)[0] 282 283 outgoing = graph.destinationsFrom(here) 284 285 transitions = {t: d for (t, d) in outgoing.items() if d != here} 286 actions = {t: d for (t, d) in outgoing.items() if d == here} 287 if transitions: 288 result += " There are transitions:\n" 289 for transition in sorted(transitions): 290 dest = transitions[transition] 291 if not graph.isConfirmed(dest): 292 destSpec = 'unconfirmed' 293 else: 294 destSpec = graph.identityOf(dest) 295 req = graph.getTransitionRequirement(here, transition) 296 rDesc = '' 297 if req != base.ReqNothing(): 298 rDesc = f"; requires {req.unparse()}" 299 cDesc = describeConsequence( 300 graph.getConsequence(here, transition) 301 ) 302 if cDesc: 303 cDesc = '; ' + cDesc 304 result += ( 305 f" {transition} to {destSpec}{rDesc}{cDesc}\n" 306 ) 307 308 if actions: 309 result += " There are actions:\n" 310 for action in sorted(actions): 311 req = graph.getTransitionRequirement(here, action) 312 rDesc = '' 313 if req != base.ReqNothing(): 314 rDesc = f"; requires {req.unparse()}" 315 cDesc = describeConsequence( 316 graph.getConsequence(here, action) 317 ) 318 if cDesc: 319 cDesc = '; ' + cDesc 320 if rDesc or cDesc: 321 desc = (rDesc + cDesc)[2:] # chop '; ' from either 322 result += f" {action} ({desc})\n" 323 else: 324 result += f" {action}\n" 325 326 # note annotations 327 if len(situation.annotations) > 0: 328 result += ( 329 f" {len(situation.annotations)} note(s) at this step\n" 330 ) 331 332 # Describe action taken 333 if situation.action is None and situation.type == "pending": 334 result += "Waiting for another action...\n" 335 else: 336 desc = base.describeExplorationAction(situation, situation.action) 337 desc = desc[0].capitalize() + desc[1:] 338 result += desc + '\n' 339 340 if i == len(exploration) - 1: 341 result += "End of the exploration.\n" 342 343 # Update state variables 344 oldActiveDecisions = activeDecisions 345 prevCapabilities = capabilities 346 prevMechanisms = mechanisms 347 regions = regionsHere 348 zones = zonesHere 349 if here is not None: 350 last = here 351 lastState = state 352 353 return result 354 355 356#--------------------# 357# Analysis functions # 358#--------------------# 359 360def lastIdentity( 361 exploration: core.DiscreteExploration, 362 decision: base.DecisionID 363): 364 """ 365 Returns the `identityOf` result for the specified decision in the 366 last step in which that decision existed. 367 """ 368 for i in range(-1, -len(exploration) - 1, -1): 369 situation = exploration.getSituation(i) 370 try: 371 return situation.graph.identityOf(decision) 372 except core.MissingDecisionError: 373 pass 374 raise core.MissingDecisionError( 375 f"Decision {decision!r} never existed." 376 ) 377 378 379def unexploredBranches( 380 graph: core.DecisionGraph, 381 context: Optional[base.RequirementContext] = None 382) -> List[Tuple[base.DecisionID, base.Transition]]: 383 """ 384 Returns a list of from-decision, transition-at-that-decision pairs 385 which each identify an unexplored branch in the given graph. 386 387 When a `context` is provided it only counts options whose 388 requirements are satisfied in that `RequirementContext`, and the 389 'searchFrom' part of the context will be replaced by both ends of 390 each transition tested. This doesn't perfectly map onto actually 391 reachability since nodes between where the player is and where the 392 option is might force changes in the game state that make it 393 un-takeable. 394 395 TODO: add logic to detect trivially-unblocked edges? 396 """ 397 result = [] 398 # TODO: Fix networkx type stubs for MultiDiGraph! 399 for (src, dst, transition) in graph.edges(keys=True): # type:ignore 400 req = graph.getTransitionRequirement(src, transition) 401 localContext: Optional[base.RequirementContext] = None 402 if context is not None: 403 localContext = base.RequirementContext( 404 state=context.state, 405 graph=context.graph, 406 searchFrom=graph.bothEnds(src, transition) 407 ) 408 # Check if this edge goes from a confirmed to an unconfirmed node 409 if ( 410 graph.isConfirmed(src) 411 and not graph.isConfirmed(dst) 412 and (localContext is None or req.satisfied(localContext)) 413 ): 414 result.append((src, transition)) 415 return result 416 417 418def currentDecision(situation: base.Situation) -> str: 419 """ 420 Returns the `identityOf` string for the current decision in a given 421 situation. 422 """ 423 return situation.graph.identityOf(situation.state['primaryDecision']) 424 425 426def countAllUnexploredBranches(situation: base.Situation) -> int: 427 """ 428 Counts the number of unexplored branches in the given situation's 429 graph, regardless of traversibility (see `unexploredBranches`). 430 """ 431 return len(unexploredBranches(situation.graph)) 432 433 434def countTraversableUnexploredBranches(situation: base.Situation) -> int: 435 """ 436 Counts the number of traversable unexplored branches (see 437 `unexploredBranches`) in a given situation, using the situation's 438 game state to determine which branches are traversable or not 439 (although this isn't strictly perfect TODO: Fix that). 440 """ 441 context = base.genericContextForSituation( 442 situation, 443 base.combinedDecisionSet(situation.state) 444 ) 445 return len(unexploredBranches(situation.graph, context)) 446 447 448def countActionsAtDecision( 449 graph: core.DecisionGraph, 450 decision: base.DecisionID 451) -> Optional[int]: 452 """ 453 Given a graph and a particular decision within that graph, returns 454 the number of actions available at that decision. Returns None if the 455 specified decision does not exist. 456 """ 457 if decision not in graph: 458 return None 459 return len(graph.decisionActions(decision)) 460 461 462def countBranches( 463 graph: core.DecisionGraph, 464 decision: base.DecisionID 465) -> Optional[int]: 466 """ 467 Computes the number of branches at a particular decision, not 468 counting actions. Returns `None` for unvisited and nonexistent 469 decisions so that they aren't counted as part of averages. 470 """ 471 if decision not in graph or not graph.isConfirmed(decision): 472 return None 473 474 dests = graph.destinationsFrom(decision) 475 branches = 0 476 for transition, dest in dests.items(): 477 if dest != decision: 478 branches += 1 479 480 return branches 481 482 483def countRevisits( 484 exploration: core.DiscreteExploration, 485 decision: base.DecisionID 486) -> int: 487 """ 488 Given an `DiscreteExploration` object and a particular `Decision` 489 which exists at some point during that exploration, counts the number 490 of times that decision was activated after its initial discovery (not 491 counting steps where we remain in it due to a wait or action). 492 493 Returns 0 even for decisions that aren't part of the exploration. 494 """ 495 result = 0 496 wasActive = False 497 for i in range(len(exploration)): 498 active = exploration.getActiveDecisions(i) 499 if decision in active: 500 if not wasActive: 501 result += 1 502 wasActive = True 503 else: 504 wasActive = False 505 506 # Make sure not to return -1 for decisions that were never visited 507 if result >= 1: 508 return result - 1 509 else: 510 return 0 511 512 513#-----------------------# 514# Generalizer Functions # 515#-----------------------# 516 517# Some type variables to make type annotations work 518T = TypeVar('T') 519P = ParamSpec('P') 520 521 522def analyzeGraph( 523 routine: Callable[Concatenate[core.DecisionGraph, P], T] 524) -> Callable[Concatenate[base.Situation, P], T]: 525 """ 526 Wraps a `DecisionGraph` analysis routine (possibly with extra 527 arguments), returning a function which applies that analysis to a 528 `Situation`. 529 """ 530 def analyzesGraph( 531 situation: base.Situation, 532 *args: P.args, 533 **kwargs: P.kwargs 534 ) -> T: 535 "Created by `analyzeGraph`." 536 return routine(situation.graph, *args, **kwargs) 537 538 analyzesGraph.__name__ = routine.__name__ + "InSituation" 539 analyzesGraph.__doc__ = f""" 540 Application of a graph analysis routine to a situation. 541 542 The analysis routine applied is: {routine.__name__} 543 """ + (routine.__doc__ or '') 544 return analyzesGraph 545 546 547def perDecision( 548 routine: Callable[[base.Situation, base.DecisionID], T] 549) -> Callable[[base.Situation], Dict[base.DecisionID, T]]: 550 """ 551 Returns a wrapped function that applies the given 552 individual-decision analysis routine to each decision in a 553 situation, returning a dictionary mapping decisions to results. 554 """ 555 def appliedPerDecision( 556 situation: base.Situation, 557 ) -> Dict[base.DecisionID, T]: 558 'Created by `perDecision`.' 559 result = {} 560 for decision in situation.graph: 561 result[decision] = routine(situation, decision) 562 return result 563 appliedPerDecision.__name__ = routine.__name__ + "PerDecision" 564 appliedPerDecision.__doc__ = f""" 565 Application of an analysis routine to each decision in a situation, 566 returning a dictionary mapping decisions to results. The analysis 567 routine applied is: {routine.__name__} 568 """ + (routine.__doc__ or '') 569 return appliedPerDecision 570 571 572def perExplorationDecision( 573 routine: Callable[[core.DiscreteExploration, base.DecisionID], T], 574 mode: str = "all" 575) -> Callable[[core.DiscreteExploration], Dict[base.DecisionID, T]]: 576 """ 577 Returns a wrapped function that applies the given 578 decision-in-exploration analysis routine to each decision in an 579 exploration, returning a dictionary mapping decisions to results. 580 581 The `mode` argument controls what we mean by "each decision:" use 582 "all" to apply it to all decisions which ever existed, "known" to 583 apply it to all decisions which were known at any point, "visited" 584 to apply it to all visited decisions, and "final" to apply it to 585 each decision in the final decision graph. 586 """ 587 def appliedPerDecision( 588 exploration: core.DiscreteExploration, 589 ) -> Dict[base.DecisionID, T]: 590 'Created by `perExplorationDecision`.' 591 result = {} 592 now = exploration.getSituation() 593 graph = now.graph 594 if mode == "all": 595 applyTo = exploration.allDecisions() 596 elif mode == "known": 597 applyTo = exploration.allExploredDecisions() 598 elif mode == "visited": 599 applyTo = exploration.allVisitedDecisions() 600 elif mode == "final": 601 applyTo = list(graph) 602 603 for decision in applyTo: 604 result[decision] = routine(exploration, decision) 605 606 return result 607 608 appliedPerDecision.__name__ = routine.__name__ + "PerExplorationDecision" 609 desc = mode + ' ' 610 if desc == "all ": 611 desc = '' 612 appliedPerDecision.__doc__ = f""" 613 Application of an analysis routine to each {desc}decision in an 614 exploration, returning a dictionary mapping decisions to results. The 615 analysis routine applied is: {routine.__name__} 616 """ + (routine.__doc__ or '') 617 return appliedPerDecision 618 619 620Base = TypeVar('Base', base.Situation, core.DiscreteExploration) 621"Either a situation or an exploration." 622 623 624def sumOfResults( 625 routine: Callable[ 626 [Base], 627 Dict[Any, Union[int, float, complex, None]] 628 ] 629) -> Callable[[Base], Union[int, float, complex]]: 630 """ 631 Given an analysis routine that applies to either a situation or an 632 exploration and which returns a dictionary mapping some analysis 633 units to individual numerical results, returns a new analysis 634 routine which applies to the same input and which returns a single 635 number that's the sum of the individual results, ignoring `None`s. 636 Returns 0 if there are no results. 637 """ 638 def sumResults(base: Base) -> Union[int, float, complex]: 639 "Created by sumOfResults" 640 results = routine(base) 641 return sum(v for v in results.values() if v is not None) 642 643 sumResults.__name__ = routine.__name__ + "Sum" 644 sumResults.__doc__ = f""" 645 Sum of analysis results over analysis units. 646 The analysis routine applied is: {routine.__name__} 647 """ + (routine.__doc__ or '') 648 return sumResults 649 650 651def meanOfResults( 652 routine: Callable[ 653 [Base], 654 Dict[Any, Union[int, float, complex, None]] 655 ] 656) -> Callable[[Base], Union[int, float, complex, None]]: 657 """ 658 Works like `sumOfResults` but returns a function which gives the 659 mean, not the sum. The function will return `None` if there are no 660 results. 661 """ 662 def meanResult(base: Base) -> Union[int, float, complex, None]: 663 "Created by meanOfResults" 664 results = routine(base) 665 nums = [v for v in results.values() if v is not None] 666 if len(nums) == 0: 667 return None 668 else: 669 return sum(nums) / len(nums) 670 671 meanResult.__name__ = routine.__name__ + "Mean" 672 meanResult.__doc__ = f""" 673 Mean of analysis results over analysis units. 674 The analysis routine applied is: {routine.__name__} 675 """ + (routine.__doc__ or '') 676 return meanResult 677 678 679def medianOfResults( 680 routine: Callable[ 681 [Base], 682 Dict[Any, Union[int, float, None]] 683 ] 684) -> Callable[[Base], Union[int, float, None]]: 685 """ 686 Works like `sumOfResults` but returns a function which gives the 687 median, not the sum. The function will return `None` if there are no 688 results. 689 """ 690 def medianResult(base: Base) -> Union[int, float, None]: 691 "Created by medianOfResults" 692 results = routine(base) 693 nums = sorted(v for v in results.values() if v is not None) 694 half = len(nums) // 2 695 if len(nums) == 0: 696 return None 697 elif len(nums) % 2 == 0: 698 return (nums[half] + nums[half + 1]) / 2 699 else: 700 return nums[half] 701 702 medianResult.__name__ = routine.__name__ + "Mean" 703 medianResult.__doc__ = f""" 704 Mean of analysis results over analysis units. 705 The analysis routine applied is: {routine.__name__} 706 """ + (routine.__doc__ or '') 707 return medianResult 708 709 710def perSituation( 711 routine: Callable[[base.Situation], T] 712) -> Callable[[core.DiscreteExploration], List[T]]: 713 """ 714 Returns a function which will apply an analysis routine to each 715 situation in an exploration, returning a list of results. 716 """ 717 def appliedPerSituation( 718 exploration: core.DiscreteExploration 719 ) -> List[T]: 720 result = [] 721 for situ in exploration: 722 result.append(routine(situ)) 723 return result 724 725 appliedPerSituation.__name__ = routine.__name__ + "PerSituation" 726 appliedPerSituation.__doc__ = f""" 727 Analysis routine applied to each situation in an exploration, 728 returning a list of results. 729 730 The analysis routine applied is: {routine.__name__} 731 """ + (routine.__doc__ or '') 732 return appliedPerSituation
23def describeConsequence(consequence: base.Consequence) -> str: 24 """ 25 Returns a string which concisely describes a consequence list. 26 Returns an empty string if given an empty consequence. Examples: 27 28 >>> describeConsequence([]) 29 '' 30 >>> describeConsequence([ 31 ... base.effect(gain=('gold', 5), delay=2, charges=3), 32 ... base.effect(lose='flight') 33 ... ]) 34 'gain gold*5 ,2 =3; lose flight' 35 >>> from . import commands 36 >>> d = describeConsequence([ 37 ... base.effect(edit=[ 38 ... [ 39 ... commands.command('val', '5'), 40 ... commands.command('empty', 'list'), 41 ... commands.command('append') 42 ... ], 43 ... [ 44 ... commands.command('val', '11'), 45 ... commands.command('assign', 'var'), 46 ... commands.command('op', '+', '$var', '$var') 47 ... ], 48 ... ]) 49 ... ]) 50 >>> d 51 'with consequences:\ 52\\n edit {\ 53\\n val 5;\ 54\\n empty list;\ 55\\n append $_;\ 56\\n } {\ 57\\n val 11;\ 58\\n assign var $_;\ 59\\n op + $var $var;\ 60\\n }\ 61\\n' 62 >>> for line in d.splitlines(): 63 ... print(line) 64 with consequences: 65 edit { 66 val 5; 67 empty list; 68 append $_; 69 } { 70 val 11; 71 assign var $_; 72 op + $var $var; 73 } 74 """ 75 edesc = '' 76 pf = parsing.ParseFormat() 77 if consequence: 78 parts = [] 79 for item in consequence: 80 # TODO: Challenges and Conditions here! 81 if 'skills' in item: # a Challenge 82 item = cast(base.Challenge, item) 83 parts.append(pf.unparseChallenge(item)) 84 elif 'value' in item: # an Effect 85 item = cast(base.Effect, item) 86 parts.append(pf.unparseEffect(item)) 87 elif 'condition' in item: # a Condition 88 item = cast(base.Condition, item) 89 parts.append(pf.unparseCondition(item)) 90 else: 91 raise TypeError( 92 f"Invalid consequence item (no 'skills', 'value', or" 93 f" 'condition' key found):\n{repr(item)}" 94 ) 95 edesc = '; '.join(parts) 96 if len(edesc) > 60 or '\n' in edesc: 97 edesc = 'with consequences:\n' + ';\n'.join( 98 textwrap.indent(part, ' ') 99 for part in parts 100 ) + '\n' 101 102 return edesc
Returns a string which concisely describes a consequence list. Returns an empty string if given an empty consequence. Examples:
>>> describeConsequence([])
''
>>> describeConsequence([
... base.effect(gain=('gold', 5), delay=2, charges=3),
... base.effect(lose='flight')
... ])
'gain gold*5 ,2 =3; lose flight'
>>> from . import commands
>>> d = describeConsequence([
... base.effect(edit=[
... [
... commands.command('val', '5'),
... commands.command('empty', 'list'),
... commands.command('append')
... ],
... [
... commands.command('val', '11'),
... commands.command('assign', 'var'),
... commands.command('op', '+', '$var', '$var')
... ],
... ])
... ])
>>> d
'with consequences:\n edit {\n val 5;\n empty list;\n append $_;\n } {\n val 11;\n assign var $_;\n op + $var $var;\n }\n'
>>> for line in d.splitlines():
... print(line)
with consequences:
edit {
val 5;
empty list;
append $_;
} {
val 11;
assign var $_;
op + $var $var;
}
105def describeProgress(exploration: core.DiscreteExploration) -> str: 106 """ 107 Describes the progress of an exploration by noting each room/zone 108 visited and explaining the options visible at each point plus which 109 option was taken. Notes powers/tokens gained/lost along the way. 110 Returns a string. 111 112 Example: 113 >>> from exploration import journal 114 >>> e = journal.convertJournal('''\\ 115 ... S Start::pit 116 ... A gain jump 117 ... A gain attack 118 ... n button check 119 ... zz Wilds 120 ... o up 121 ... q _flight 122 ... o left 123 ... x left left_nook right 124 ... a geo_rock 125 ... At gain geo*15 126 ... At deactivate 127 ... o up 128 ... q _tall_narrow 129 ... t right 130 ... o right 131 ... q attack 132 ... ''') 133 >>> for line in describeProgress(e).splitlines(): 134 ... print(line) 135 Start of the exploration 136 Start exploring domain main at 0 (Start::pit) 137 Gained capability 'attack' 138 Gained capability 'jump' 139 At decision 0 (Start::pit) 140 In zone Start 141 In region Wilds 142 There are transitions: 143 left to unconfirmed 144 up to unconfirmed; requires _flight 145 1 note(s) at this step 146 Explore left from decision 0 (Start::pit) to 2 (now Start::left_nook) 147 At decision 2 (Start::left_nook) 148 There are transitions: 149 right to 0 (Start::pit) 150 There are actions: 151 geo_rock 152 Do action geo_rock 153 Gained 15 geo(s) 154 Take right from decision 2 (Start::left_nook) to 0 (Start::pit) 155 At decision 0 (Start::pit) 156 There are transitions: 157 left to 2 (Start::left_nook) 158 right to unconfirmed; requires attack 159 up to unconfirmed; requires _flight 160 Waiting for another action... 161 End of the exploration. 162 """ 163 result = '' 164 165 regions: Set[base.Zone] = set() 166 zones: Set[base.Zone] = set() 167 last: Union[base.DecisionID, Set[base.DecisionID], None] = None 168 lastState: base.State = base.emptyState() 169 prevCapabilities = base.effectiveCapabilitySet(lastState) 170 prevMechanisms = lastState['mechanisms'] 171 oldActiveDecisions: Set[base.DecisionID] = set() 172 for i, situation in enumerate(exploration): 173 if i == 0: 174 result += "Start of the exploration\n" 175 176 # Extract info 177 graph = situation.graph 178 activeDecisions = exploration.getActiveDecisions(i) 179 newActive = activeDecisions - oldActiveDecisions 180 departedFrom = exploration.movementAtStep(i)[0] 181 # TODO: use the other parts of this? 182 nowZones: Set[base.Zone] = set() 183 for active in activeDecisions: 184 nowZones |= graph.zoneAncestors(active) 185 regionsHere = set( 186 z 187 for z in nowZones 188 if graph.zoneHierarchyLevel(z) == 1 189 ) 190 zonesHere = set( 191 z 192 for z in nowZones 193 if graph.zoneHierarchyLevel(z) == 0 194 ) 195 here = departedFrom 196 state = situation.state 197 capabilities = base.effectiveCapabilitySet(state) 198 mechanisms = state['mechanisms'] 199 200 # Describe capabilities gained/lost relative to previous step 201 # (i.e., as a result of the previous action) 202 gained = ( 203 capabilities['capabilities'] 204 - prevCapabilities['capabilities'] 205 ) 206 gainedTokens = [] 207 for tokenType in capabilities['tokens']: 208 net = ( 209 capabilities['tokens'][tokenType] 210 - prevCapabilities['tokens'].get(tokenType, 0) 211 ) 212 if net != 0: 213 gainedTokens.append((tokenType, net)) 214 changed = [ 215 mID 216 for mID in list(mechanisms.keys()) + list(prevMechanisms.keys()) 217 if mechanisms.get(mID) != prevMechanisms.get(mID) 218 ] 219 220 for capability in sorted(gained): 221 result += f" Gained capability '{capability}'\n" 222 223 for tokenType, net in gainedTokens: 224 if net > 0: 225 result += f" Gained {net} {tokenType}(s)\n" 226 else: 227 result += f" Lost {-net} {tokenType}(s)\n" 228 229 for mID in changed: 230 oldState = prevMechanisms.get(mID, base.DEFAULT_MECHANISM_STATE) 231 newState = mechanisms.get(mID, base.DEFAULT_MECHANISM_STATE) 232 233 details = graph.mechanismDetails(mID) 234 if details is None: 235 mName = "(unknown)" 236 else: 237 mName = details[1] 238 result += ( 239 f" Set mechanism {mID} ({mName}) to {newState} (was" 240 f" {oldState})" 241 ) 242 # TODO: Test this! 243 244 if isinstance(departedFrom, base.DecisionID): 245 # Print location info 246 if here != last: 247 if here is None: 248 result += "Without a position...\n" 249 elif isinstance(here, set): 250 result += f"With {len(here)} active decisions\n" 251 # TODO: List them using namesListing? 252 else: 253 result += f"At decision {graph.identityOf(here)}\n" 254 newZones = zonesHere - zones 255 for zone in sorted(newZones): 256 result += f" In zone {zone}\n" 257 newRegions = regionsHere - regions 258 for region in sorted(newRegions): 259 result += f" In region {region}\n" 260 261 elif isinstance(departedFrom, set): # active in spreading domain 262 spreadingDomain = graph.domainFor(list(departedFrom)[0]) 263 result += ( 264 f" In domain {spreadingDomain} with {len(departedFrom)}" 265 f" active decisions...\n" 266 ) 267 268 else: 269 assert departedFrom is None 270 271 # Describe new position/positions at start of this step 272 if len(newActive) > 1: 273 newListing = ', '.join( 274 sorted(graph.identityOf(n) for n in newActive) 275 ) 276 result += ( 277 f" There are {len(newActive)} new active decisions:" 278 f"\n {newListing}" 279 ) 280 281 elif len(newActive) == 1: 282 here = list(newActive)[0] 283 284 outgoing = graph.destinationsFrom(here) 285 286 transitions = {t: d for (t, d) in outgoing.items() if d != here} 287 actions = {t: d for (t, d) in outgoing.items() if d == here} 288 if transitions: 289 result += " There are transitions:\n" 290 for transition in sorted(transitions): 291 dest = transitions[transition] 292 if not graph.isConfirmed(dest): 293 destSpec = 'unconfirmed' 294 else: 295 destSpec = graph.identityOf(dest) 296 req = graph.getTransitionRequirement(here, transition) 297 rDesc = '' 298 if req != base.ReqNothing(): 299 rDesc = f"; requires {req.unparse()}" 300 cDesc = describeConsequence( 301 graph.getConsequence(here, transition) 302 ) 303 if cDesc: 304 cDesc = '; ' + cDesc 305 result += ( 306 f" {transition} to {destSpec}{rDesc}{cDesc}\n" 307 ) 308 309 if actions: 310 result += " There are actions:\n" 311 for action in sorted(actions): 312 req = graph.getTransitionRequirement(here, action) 313 rDesc = '' 314 if req != base.ReqNothing(): 315 rDesc = f"; requires {req.unparse()}" 316 cDesc = describeConsequence( 317 graph.getConsequence(here, action) 318 ) 319 if cDesc: 320 cDesc = '; ' + cDesc 321 if rDesc or cDesc: 322 desc = (rDesc + cDesc)[2:] # chop '; ' from either 323 result += f" {action} ({desc})\n" 324 else: 325 result += f" {action}\n" 326 327 # note annotations 328 if len(situation.annotations) > 0: 329 result += ( 330 f" {len(situation.annotations)} note(s) at this step\n" 331 ) 332 333 # Describe action taken 334 if situation.action is None and situation.type == "pending": 335 result += "Waiting for another action...\n" 336 else: 337 desc = base.describeExplorationAction(situation, situation.action) 338 desc = desc[0].capitalize() + desc[1:] 339 result += desc + '\n' 340 341 if i == len(exploration) - 1: 342 result += "End of the exploration.\n" 343 344 # Update state variables 345 oldActiveDecisions = activeDecisions 346 prevCapabilities = capabilities 347 prevMechanisms = mechanisms 348 regions = regionsHere 349 zones = zonesHere 350 if here is not None: 351 last = here 352 lastState = state 353 354 return result
Describes the progress of an exploration by noting each room/zone visited and explaining the options visible at each point plus which option was taken. Notes powers/tokens gained/lost along the way. Returns a string.
Example:
>>> from exploration import journal
>>> e = journal.convertJournal('''\
... S Start::pit
... A gain jump
... A gain attack
... n button check
... zz Wilds
... o up
... q _flight
... o left
... x left left_nook right
... a geo_rock
... At gain geo*15
... At deactivate
... o up
... q _tall_narrow
... t right
... o right
... q attack
... ''')
>>> for line in describeProgress(e).splitlines():
... print(line)
Start of the exploration
Start exploring domain main at 0 (Start::pit)
Gained capability 'attack'
Gained capability 'jump'
At decision 0 (Start::pit)
In zone Start
In region Wilds
There are transitions:
left to unconfirmed
up to unconfirmed; requires _flight
1 note(s) at this step
Explore left from decision 0 (Start::pit) to 2 (now Start::left_nook)
At decision 2 (Start::left_nook)
There are transitions:
right to 0 (Start::pit)
There are actions:
geo_rock
Do action geo_rock
Gained 15 geo(s)
Take right from decision 2 (Start::left_nook) to 0 (Start::pit)
At decision 0 (Start::pit)
There are transitions:
left to 2 (Start::left_nook)
right to unconfirmed; requires attack
up to unconfirmed; requires _flight
Waiting for another action...
End of the exploration.
361def lastIdentity( 362 exploration: core.DiscreteExploration, 363 decision: base.DecisionID 364): 365 """ 366 Returns the `identityOf` result for the specified decision in the 367 last step in which that decision existed. 368 """ 369 for i in range(-1, -len(exploration) - 1, -1): 370 situation = exploration.getSituation(i) 371 try: 372 return situation.graph.identityOf(decision) 373 except core.MissingDecisionError: 374 pass 375 raise core.MissingDecisionError( 376 f"Decision {decision!r} never existed." 377 )
Returns the identityOf
result for the specified decision in the
last step in which that decision existed.
380def unexploredBranches( 381 graph: core.DecisionGraph, 382 context: Optional[base.RequirementContext] = None 383) -> List[Tuple[base.DecisionID, base.Transition]]: 384 """ 385 Returns a list of from-decision, transition-at-that-decision pairs 386 which each identify an unexplored branch in the given graph. 387 388 When a `context` is provided it only counts options whose 389 requirements are satisfied in that `RequirementContext`, and the 390 'searchFrom' part of the context will be replaced by both ends of 391 each transition tested. This doesn't perfectly map onto actually 392 reachability since nodes between where the player is and where the 393 option is might force changes in the game state that make it 394 un-takeable. 395 396 TODO: add logic to detect trivially-unblocked edges? 397 """ 398 result = [] 399 # TODO: Fix networkx type stubs for MultiDiGraph! 400 for (src, dst, transition) in graph.edges(keys=True): # type:ignore 401 req = graph.getTransitionRequirement(src, transition) 402 localContext: Optional[base.RequirementContext] = None 403 if context is not None: 404 localContext = base.RequirementContext( 405 state=context.state, 406 graph=context.graph, 407 searchFrom=graph.bothEnds(src, transition) 408 ) 409 # Check if this edge goes from a confirmed to an unconfirmed node 410 if ( 411 graph.isConfirmed(src) 412 and not graph.isConfirmed(dst) 413 and (localContext is None or req.satisfied(localContext)) 414 ): 415 result.append((src, transition)) 416 return result
Returns a list of from-decision, transition-at-that-decision pairs which each identify an unexplored branch in the given graph.
When a context
is provided it only counts options whose
requirements are satisfied in that RequirementContext
, and the
'searchFrom' part of the context will be replaced by both ends of
each transition tested. This doesn't perfectly map onto actually
reachability since nodes between where the player is and where the
option is might force changes in the game state that make it
un-takeable.
TODO: add logic to detect trivially-unblocked edges?
419def currentDecision(situation: base.Situation) -> str: 420 """ 421 Returns the `identityOf` string for the current decision in a given 422 situation. 423 """ 424 return situation.graph.identityOf(situation.state['primaryDecision'])
Returns the identityOf
string for the current decision in a given
situation.
427def countAllUnexploredBranches(situation: base.Situation) -> int: 428 """ 429 Counts the number of unexplored branches in the given situation's 430 graph, regardless of traversibility (see `unexploredBranches`). 431 """ 432 return len(unexploredBranches(situation.graph))
Counts the number of unexplored branches in the given situation's
graph, regardless of traversibility (see unexploredBranches
).
435def countTraversableUnexploredBranches(situation: base.Situation) -> int: 436 """ 437 Counts the number of traversable unexplored branches (see 438 `unexploredBranches`) in a given situation, using the situation's 439 game state to determine which branches are traversable or not 440 (although this isn't strictly perfect TODO: Fix that). 441 """ 442 context = base.genericContextForSituation( 443 situation, 444 base.combinedDecisionSet(situation.state) 445 ) 446 return len(unexploredBranches(situation.graph, context))
Counts the number of traversable unexplored branches (see
unexploredBranches
) in a given situation, using the situation's
game state to determine which branches are traversable or not
(although this isn't strictly perfect TODO: Fix that).
449def countActionsAtDecision( 450 graph: core.DecisionGraph, 451 decision: base.DecisionID 452) -> Optional[int]: 453 """ 454 Given a graph and a particular decision within that graph, returns 455 the number of actions available at that decision. Returns None if the 456 specified decision does not exist. 457 """ 458 if decision not in graph: 459 return None 460 return len(graph.decisionActions(decision))
Given a graph and a particular decision within that graph, returns the number of actions available at that decision. Returns None if the specified decision does not exist.
463def countBranches( 464 graph: core.DecisionGraph, 465 decision: base.DecisionID 466) -> Optional[int]: 467 """ 468 Computes the number of branches at a particular decision, not 469 counting actions. Returns `None` for unvisited and nonexistent 470 decisions so that they aren't counted as part of averages. 471 """ 472 if decision not in graph or not graph.isConfirmed(decision): 473 return None 474 475 dests = graph.destinationsFrom(decision) 476 branches = 0 477 for transition, dest in dests.items(): 478 if dest != decision: 479 branches += 1 480 481 return branches
Computes the number of branches at a particular decision, not
counting actions. Returns None
for unvisited and nonexistent
decisions so that they aren't counted as part of averages.
484def countRevisits( 485 exploration: core.DiscreteExploration, 486 decision: base.DecisionID 487) -> int: 488 """ 489 Given an `DiscreteExploration` object and a particular `Decision` 490 which exists at some point during that exploration, counts the number 491 of times that decision was activated after its initial discovery (not 492 counting steps where we remain in it due to a wait or action). 493 494 Returns 0 even for decisions that aren't part of the exploration. 495 """ 496 result = 0 497 wasActive = False 498 for i in range(len(exploration)): 499 active = exploration.getActiveDecisions(i) 500 if decision in active: 501 if not wasActive: 502 result += 1 503 wasActive = True 504 else: 505 wasActive = False 506 507 # Make sure not to return -1 for decisions that were never visited 508 if result >= 1: 509 return result - 1 510 else: 511 return 0
Given an DiscreteExploration
object and a particular Decision
which exists at some point during that exploration, counts the number
of times that decision was activated after its initial discovery (not
counting steps where we remain in it due to a wait or action).
Returns 0 even for decisions that aren't part of the exploration.
523def analyzeGraph( 524 routine: Callable[Concatenate[core.DecisionGraph, P], T] 525) -> Callable[Concatenate[base.Situation, P], T]: 526 """ 527 Wraps a `DecisionGraph` analysis routine (possibly with extra 528 arguments), returning a function which applies that analysis to a 529 `Situation`. 530 """ 531 def analyzesGraph( 532 situation: base.Situation, 533 *args: P.args, 534 **kwargs: P.kwargs 535 ) -> T: 536 "Created by `analyzeGraph`." 537 return routine(situation.graph, *args, **kwargs) 538 539 analyzesGraph.__name__ = routine.__name__ + "InSituation" 540 analyzesGraph.__doc__ = f""" 541 Application of a graph analysis routine to a situation. 542 543 The analysis routine applied is: {routine.__name__} 544 """ + (routine.__doc__ or '') 545 return analyzesGraph
Wraps a DecisionGraph
analysis routine (possibly with extra
arguments), returning a function which applies that analysis to a
Situation
.
548def perDecision( 549 routine: Callable[[base.Situation, base.DecisionID], T] 550) -> Callable[[base.Situation], Dict[base.DecisionID, T]]: 551 """ 552 Returns a wrapped function that applies the given 553 individual-decision analysis routine to each decision in a 554 situation, returning a dictionary mapping decisions to results. 555 """ 556 def appliedPerDecision( 557 situation: base.Situation, 558 ) -> Dict[base.DecisionID, T]: 559 'Created by `perDecision`.' 560 result = {} 561 for decision in situation.graph: 562 result[decision] = routine(situation, decision) 563 return result 564 appliedPerDecision.__name__ = routine.__name__ + "PerDecision" 565 appliedPerDecision.__doc__ = f""" 566 Application of an analysis routine to each decision in a situation, 567 returning a dictionary mapping decisions to results. The analysis 568 routine applied is: {routine.__name__} 569 """ + (routine.__doc__ or '') 570 return appliedPerDecision
Returns a wrapped function that applies the given individual-decision analysis routine to each decision in a situation, returning a dictionary mapping decisions to results.
573def perExplorationDecision( 574 routine: Callable[[core.DiscreteExploration, base.DecisionID], T], 575 mode: str = "all" 576) -> Callable[[core.DiscreteExploration], Dict[base.DecisionID, T]]: 577 """ 578 Returns a wrapped function that applies the given 579 decision-in-exploration analysis routine to each decision in an 580 exploration, returning a dictionary mapping decisions to results. 581 582 The `mode` argument controls what we mean by "each decision:" use 583 "all" to apply it to all decisions which ever existed, "known" to 584 apply it to all decisions which were known at any point, "visited" 585 to apply it to all visited decisions, and "final" to apply it to 586 each decision in the final decision graph. 587 """ 588 def appliedPerDecision( 589 exploration: core.DiscreteExploration, 590 ) -> Dict[base.DecisionID, T]: 591 'Created by `perExplorationDecision`.' 592 result = {} 593 now = exploration.getSituation() 594 graph = now.graph 595 if mode == "all": 596 applyTo = exploration.allDecisions() 597 elif mode == "known": 598 applyTo = exploration.allExploredDecisions() 599 elif mode == "visited": 600 applyTo = exploration.allVisitedDecisions() 601 elif mode == "final": 602 applyTo = list(graph) 603 604 for decision in applyTo: 605 result[decision] = routine(exploration, decision) 606 607 return result 608 609 appliedPerDecision.__name__ = routine.__name__ + "PerExplorationDecision" 610 desc = mode + ' ' 611 if desc == "all ": 612 desc = '' 613 appliedPerDecision.__doc__ = f""" 614 Application of an analysis routine to each {desc}decision in an 615 exploration, returning a dictionary mapping decisions to results. The 616 analysis routine applied is: {routine.__name__} 617 """ + (routine.__doc__ or '') 618 return appliedPerDecision
Returns a wrapped function that applies the given decision-in-exploration analysis routine to each decision in an exploration, returning a dictionary mapping decisions to results.
The mode
argument controls what we mean by "each decision:" use
"all" to apply it to all decisions which ever existed, "known" to
apply it to all decisions which were known at any point, "visited"
to apply it to all visited decisions, and "final" to apply it to
each decision in the final decision graph.
Either a situation or an exploration.
625def sumOfResults( 626 routine: Callable[ 627 [Base], 628 Dict[Any, Union[int, float, complex, None]] 629 ] 630) -> Callable[[Base], Union[int, float, complex]]: 631 """ 632 Given an analysis routine that applies to either a situation or an 633 exploration and which returns a dictionary mapping some analysis 634 units to individual numerical results, returns a new analysis 635 routine which applies to the same input and which returns a single 636 number that's the sum of the individual results, ignoring `None`s. 637 Returns 0 if there are no results. 638 """ 639 def sumResults(base: Base) -> Union[int, float, complex]: 640 "Created by sumOfResults" 641 results = routine(base) 642 return sum(v for v in results.values() if v is not None) 643 644 sumResults.__name__ = routine.__name__ + "Sum" 645 sumResults.__doc__ = f""" 646 Sum of analysis results over analysis units. 647 The analysis routine applied is: {routine.__name__} 648 """ + (routine.__doc__ or '') 649 return sumResults
Given an analysis routine that applies to either a situation or an
exploration and which returns a dictionary mapping some analysis
units to individual numerical results, returns a new analysis
routine which applies to the same input and which returns a single
number that's the sum of the individual results, ignoring None
s.
Returns 0 if there are no results.
652def meanOfResults( 653 routine: Callable[ 654 [Base], 655 Dict[Any, Union[int, float, complex, None]] 656 ] 657) -> Callable[[Base], Union[int, float, complex, None]]: 658 """ 659 Works like `sumOfResults` but returns a function which gives the 660 mean, not the sum. The function will return `None` if there are no 661 results. 662 """ 663 def meanResult(base: Base) -> Union[int, float, complex, None]: 664 "Created by meanOfResults" 665 results = routine(base) 666 nums = [v for v in results.values() if v is not None] 667 if len(nums) == 0: 668 return None 669 else: 670 return sum(nums) / len(nums) 671 672 meanResult.__name__ = routine.__name__ + "Mean" 673 meanResult.__doc__ = f""" 674 Mean of analysis results over analysis units. 675 The analysis routine applied is: {routine.__name__} 676 """ + (routine.__doc__ or '') 677 return meanResult
Works like sumOfResults
but returns a function which gives the
mean, not the sum. The function will return None
if there are no
results.
680def medianOfResults( 681 routine: Callable[ 682 [Base], 683 Dict[Any, Union[int, float, None]] 684 ] 685) -> Callable[[Base], Union[int, float, None]]: 686 """ 687 Works like `sumOfResults` but returns a function which gives the 688 median, not the sum. The function will return `None` if there are no 689 results. 690 """ 691 def medianResult(base: Base) -> Union[int, float, None]: 692 "Created by medianOfResults" 693 results = routine(base) 694 nums = sorted(v for v in results.values() if v is not None) 695 half = len(nums) // 2 696 if len(nums) == 0: 697 return None 698 elif len(nums) % 2 == 0: 699 return (nums[half] + nums[half + 1]) / 2 700 else: 701 return nums[half] 702 703 medianResult.__name__ = routine.__name__ + "Mean" 704 medianResult.__doc__ = f""" 705 Mean of analysis results over analysis units. 706 The analysis routine applied is: {routine.__name__} 707 """ + (routine.__doc__ or '') 708 return medianResult
Works like sumOfResults
but returns a function which gives the
median, not the sum. The function will return None
if there are no
results.
711def perSituation( 712 routine: Callable[[base.Situation], T] 713) -> Callable[[core.DiscreteExploration], List[T]]: 714 """ 715 Returns a function which will apply an analysis routine to each 716 situation in an exploration, returning a list of results. 717 """ 718 def appliedPerSituation( 719 exploration: core.DiscreteExploration 720 ) -> List[T]: 721 result = [] 722 for situ in exploration: 723 result.append(routine(situ)) 724 return result 725 726 appliedPerSituation.__name__ = routine.__name__ + "PerSituation" 727 appliedPerSituation.__doc__ = f""" 728 Analysis routine applied to each situation in an exploration, 729 returning a list of results. 730 731 The analysis routine applied is: {routine.__name__} 732 """ + (routine.__doc__ or '') 733 return appliedPerSituation
Returns a function which will apply an analysis routine to each situation in an exploration, returning a list of results.