exploration.tests.test_core
Authors: Peter Mawhorter Consulted: Date: 2022-3-12 Purpose: Tests for the core functionality and types.
1""" 2Authors: Peter Mawhorter 3Consulted: 4Date: 2022-3-12 5Purpose: Tests for the core functionality and types. 6""" 7 8from typing import Optional, Union, Iterable, Tuple, Literal 9 10import json 11import copy 12 13import pytest 14 15from .. import base 16from .. import core 17from .. import parsing 18 19 20@pytest.fixture 21def pf() -> parsing.ParseFormat: 22 """ 23 A fixture that provides the default `parsing.ParseFormat`. 24 """ 25 return parsing.ParseFormat() 26 27 28@pytest.fixture 29def trc() -> base.RequirementContext: 30 """ 31 A fixture providing an empty requirement context. 32 """ 33 baseGraph = core.DecisionGraph.example('simple') 34 # Has nodes A/B/C in a triangle with "next"/"prev" transitions 35 baseState = base.emptyState() 36 # Set up node A as the current position 37 baseState['common']['focalization']['main'] = 'singular' 38 baseState['common']['activeDomains'] = { 'main' } 39 baseState['common']['activeDecisions']['main'] = 0 40 baseState['primaryDecision'] = 0 # A 41 return base.RequirementContext( 42 state=baseState, 43 graph=baseGraph, 44 searchFrom=set() 45 ) 46 47 48def rcWith( 49 original: base.RequirementContext, 50 where: Optional[Iterable[base.AnyDecisionSpecifier]] = None, 51 eq: Optional[ 52 Iterable[ 53 Tuple[ 54 base.Requirement, 55 Union[ 56 base.Capability, 57 Tuple[base.MechanismID, base.MechanismState] 58 ] 59 ] 60 ] 61 ] = None, 62 **kwargs: Union[ 63 bool, 64 base.TokenCount, 65 base.MechanismState, 66 Tuple[Literal['skill'], int], 67 Tuple[Literal['tag'], base.TagValue] 68 ] 69): 70 """ 71 Clones the given `base.RequirementContext` and returns a clone with 72 additional powers, tokens, and/or mechanism states set according to 73 keyword arguments provided, where the type of each value determines 74 what is being changed: True/False sets a `base.Capability`, an 75 integer sets a `base.Token`'s count, and a string sets the state of 76 the named mechanism. Tuples starting with 'skill' (followed by an 77 integer) or "tag" (followed by any kind of tag value) set up skill 78 levels or tags. 79 80 Capabilities, tokens, and skills are set up in the common focal 81 context. 82 83 Tags values are added to the current primary decision, and mechanism 84 states are set for the mechanism found via mechanism search for 85 that name (default is global search). 86 87 In addition to changing token, capability, and/or mechanism states, 88 a new search from location set can be provided via the `where`, and 89 a list of new equivalences can be provided via the `eq` argument. 90 """ 91 if where is not None: 92 searchFrom = set( 93 original.graph.resolveDecision(x) for x in where 94 ) 95 else: 96 searchFrom = copy.deepcopy(original.searchFrom) 97 98 newState = copy.deepcopy(original.state) 99 newGraph = copy.deepcopy(original.graph) 100 101 if eq is not None: 102 for (req, equivalentTo) in eq: 103 newGraph.addEquivalence(req, equivalentTo) 104 105 commonFC = newState['common'] 106 107 for kw in kwargs: 108 val = kwargs[kw] 109 if isinstance(val, bool): 110 if val: 111 commonFC['capabilities']['capabilities'].add(kw) 112 else: 113 try: 114 commonFC['capabilities']['capabilities'].remove(kw) 115 except KeyError: 116 pass 117 118 elif isinstance(val, base.TokenCount): 119 commonFC['capabilities']['tokens'][kw] = val 120 121 elif isinstance(val, base.MechanismState): 122 mID = original.graph.resolveMechanism(kw, original.searchFrom) 123 newState['mechanisms'][mID] = val 124 125 elif ( 126 isinstance(val, tuple) 127 and len(val) == 2 128 and val[0] == 'skill' 129 and isinstance(val[1], int) 130 ): 131 commonFC['capabilities']['skills'][kw] = val[1] 132 133 elif ( 134 isinstance(val, tuple) 135 and len(val) == 2 136 and val[0] == 'tag' 137 ): 138 pr = original.state['primaryDecision'] 139 if pr is None: 140 raise ValueError( 141 "Base context has no primary decision so we can't" 142 " tag anything." 143 ) 144 newGraph.tagDecision(pr, kw, val[1]) 145 146 else: 147 raise ValueError(f"Invalid context addition value: {val!r}") 148 149 return base.RequirementContext( 150 state=newState, 151 graph=newGraph, 152 searchFrom=searchFrom 153 ) 154 155 156def test_Requirements(pf, trc) -> None: 157 """ 158 Multi-method test for `exploration.core.Requirement` and sub-classes. 159 """ 160 # Tests of comparison 161 r: base.Requirement = base.ReqAll([ 162 base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), 163 base.ReqTokens('key', 1) 164 ]) 165 r2: base.Requirement = base.ReqAll([ 166 base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), 167 base.ReqTokens('key', 1) 168 ]) 169 assert r == r2 170 assert base.ReqNothing() == base.ReqNothing() 171 assert base.ReqImpossible() == base.ReqImpossible() 172 173 # Tests of satisfied 174 assert not r.satisfied(trc) 175 assert r.satisfied(rcWith(trc, p1=True, key=1)) 176 assert not r.satisfied(rcWith(trc, key=1)) 177 assert not r.satisfied(rcWith(trc, p1=True, key=0)) 178 assert not r.satisfied(rcWith(trc, p1=True)) 179 assert r.satisfied( 180 rcWith(trc, p1=True, p2=True, key=2) 181 ) 182 183 r = base.ReqAny([ 184 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]), 185 base.ReqTokens('key', 3) 186 ]) 187 assert r.satisfied(rcWith(trc, p1=True, key=3)) 188 assert not r.satisfied(rcWith(trc, key=1)) 189 assert not r.satisfied(rcWith(trc, p2=True, key=0)) 190 assert not r.satisfied(rcWith(trc, p1=True)) 191 assert r.satisfied(rcWith(trc, p1=True, p2=True)) 192 assert r.satisfied(rcWith(trc, p1=True, p2=True, key=2)) 193 assert r.satisfied(rcWith(trc, p1=True, p2=True, key=5)) 194 assert r.satisfied(rcWith(trc, key=5)) 195 196 assert not base.hasCapabilityOrEquivalent('p3', trc) 197 assert base.hasCapabilityOrEquivalent('p3', rcWith(trc, p3=True)) 198 p2IsP3 = rcWith(trc, eq=[(base.ReqCapability('p2'), 'p3')]) 199 assert not base.hasCapabilityOrEquivalent('p3', p2IsP3) 200 assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p2=True)) 201 assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p3=True)) 202 assert not base.hasCapabilityOrEquivalent('p2', rcWith(p2IsP3, p3=True)) 203 204 assert base.ReqCapability('p3').satisfied(rcWith(p2IsP3, p2=True)) 205 r = base.ReqAll( 206 [base.ReqCapability('p1'), base.ReqCapability('p3')] 207 ) 208 assert r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 209 assert not r.satisfied(rcWith(p2IsP3, p2=True)) 210 assert not r.satisfied(rcWith(p2IsP3, p1=True)) 211 assert not r.satisfied(rcWith(p2IsP3, p3=True)) 212 assert r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) 213 214 r = base.ReqImpossible() 215 assert not r.satisfied(trc) 216 217 r = base.ReqNothing() 218 assert r.satisfied(trc) 219 220 r = base.ReqNot( 221 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]) 222 ) 223 assert r.satisfied(trc) 224 assert r.satisfied(rcWith(trc, p1=True)) 225 assert r.satisfied(rcWith(trc, p2=True)) 226 assert not r.satisfied(rcWith(trc, p1=True, p2=True)) 227 assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 228 r = base.ReqNot( 229 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p3')]) 230 ) 231 assert r.satisfied(p2IsP3) 232 assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 233 assert not r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) 234 assert r.satisfied(rcWith(p2IsP3, p1=True)) 235 assert r.satisfied(rcWith(p2IsP3, p2=True)) 236 assert r.satisfied(rcWith(p2IsP3, p3=True)) 237 238 # Mechanism requirements 239 withSwitch = copy.deepcopy(trc) 240 withSwitch.graph.addMechanism('switch', 0) # at A 241 r = base.ReqMechanism('switch', 'on') 242 assert not r.satisfied(withSwitch) 243 assert not r.satisfied(rcWith(withSwitch, switch="off")) 244 assert r.satisfied(rcWith(withSwitch, switch="on")) 245 246 leverNearby = copy.deepcopy(trc) 247 leverNearby.graph.addMechanism('lever', 1) # at B 248 r = base.ReqMechanism('lever', 'pulled') 249 assert not r.satisfied(leverNearby) 250 assert not r.satisfied(rcWith(leverNearby, lever="default")) 251 assert r.satisfied(rcWith(leverNearby, lever="pulled")) 252 253 # Skill level requirements 254 r = base.ReqLevel('skill', 1) 255 r2 = base.ReqNot(base.ReqLevel('skill', 2)) 256 assert not r.satisfied(trc) 257 assert not r.satisfied(rcWith(trc, skill=("skill", 0))) 258 assert r.satisfied(rcWith(trc, skill=("skill", 1))) 259 assert r.satisfied(rcWith(trc, skill=("skill", 2))) 260 assert r.satisfied(rcWith(trc, skill=("skill", 10))) 261 assert r2.satisfied(rcWith(trc, skill=("skill", 1))) 262 assert not r2.satisfied(rcWith(trc, skill=("skill", 2))) 263 assert not r2.satisfied(rcWith(trc, skill=("skill", 10))) 264 265 # Tag requirements 266 r = base.ReqTag('tag', 1) 267 r2 = base.ReqTag('tag2', 'value') 268 assert not r.satisfied(trc) 269 assert not r2.satisfied(trc) 270 tagged = rcWith(trc, tag=("tag", 1)) 271 assert tagged.graph.decisionTags(0) == {'tag': 1} 272 assert r.satisfied(tagged) 273 assert not r.satisfied(rcWith(trc, tag=("tag", 2))) 274 assert not r.satisfied(rcWith(trc, tag=("tag", 0))) 275 assert r2.satisfied(rcWith(trc, tag2=("tag", 'value'))) 276 taggedAside = copy.deepcopy(trc) 277 taggedAside.graph.tagDecision(1, 'tag', 1) 278 taggedAside.graph.tagDecision(2, 'tag2', 'value') 279 assert not r.satisfied(taggedAside) 280 assert not r2.satisfied(taggedAside) 281 taggedAside.state['common']['activeDecisions']['main'] = 1 282 assert r.satisfied(taggedAside) 283 assert not r2.satisfied(taggedAside) 284 taggedAside.state['common']['activeDecisions']['main'] = 2 285 assert not r.satisfied(taggedAside) 286 assert r2.satisfied(taggedAside) 287 zoneTagged = copy.deepcopy(trc) 288 zoneTagged.graph.createZone('zone') 289 zoneTagged.graph.addDecisionToZone(0, 'zone') 290 zoneTagged.graph.tagZone('zone', 'tag', 1) 291 assert r.satisfied(zoneTagged) 292 assert not r2.satisfied(zoneTagged) 293 zoneTagged.graph.removeDecisionFromZone(0, 'zone') 294 assert not r.satisfied(zoneTagged) 295 296 # Tests of parsing: 297 assert pf.parseRequirement('a') == base.ReqCapability('a') 298 299 assert pf.parseRequirement('(a)') == base.ReqCapability('a') 300 301 assert pf.parseRequirement('a*5') == base.ReqTokens('a', 5) 302 303 assert pf.parseRequirement('((a*5))') == base.ReqTokens('a', 5) 304 305 assert pf.parseRequirement('(a|b)&c*3') == base.ReqAll([ 306 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 307 base.ReqTokens('c', 3) 308 ]) 309 310 assert pf.parseRequirement(' ( a | b )\t& c * 3 ') == base.ReqAll([ 311 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 312 base.ReqTokens('c', 3) 313 ]) 314 315 assert pf.parseRequirement('a|(b&c*3)') == base.ReqAny([ 316 base.ReqCapability('a'), 317 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 318 ]) 319 320 assert pf.parseRequirement('a|b&c*3') == base.ReqAny([ 321 base.ReqCapability('a'), 322 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 323 ]) 324 325 assert pf.parseRequirement('a&b|c*3') == base.ReqAny([ 326 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]), 327 base.ReqTokens('c', 3) 328 ]) 329 330 assert pf.parseRequirement('a|b|c') == base.ReqAny([ 331 base.ReqCapability('a'), 332 base.ReqCapability('b'), 333 base.ReqCapability('c'), 334 ]) 335 336 assert pf.parseRequirement('a&b&c&d') == base.ReqAll([ 337 base.ReqCapability('a'), 338 base.ReqCapability('b'), 339 base.ReqCapability('c'), 340 base.ReqCapability('d'), 341 ]) 342 343 assert pf.parseRequirement('a&b|c&d') == base.ReqAny([ 344 base.ReqAll([ 345 base.ReqCapability('a'), 346 base.ReqCapability('b') 347 ]), 348 base.ReqAll([ 349 base.ReqCapability('c'), 350 base.ReqCapability('d') 351 ]) 352 ]) 353 354 assert pf.parseRequirement('a&!b|!c&d') == base.ReqAny([ 355 base.ReqAll([ 356 base.ReqCapability('a'), 357 base.ReqNot(base.ReqCapability('b')) 358 ]), 359 base.ReqAll([ 360 base.ReqNot(base.ReqCapability('c')), 361 base.ReqCapability('d') 362 ]) 363 ]) 364 365 assert pf.parseRequirement('!(a|b)&c') == base.ReqAll([ 366 base.ReqNot( 367 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]) 368 ), 369 base.ReqCapability('c') 370 ]) 371 372 assert pf.parseRequirement('!a&b&c') == base.ReqAll([ 373 base.ReqNot(base.ReqCapability('a')), 374 base.ReqCapability('b'), 375 base.ReqCapability('c') 376 ]) 377 378 assert pf.parseRequirement('!(a&b)&c') == base.ReqAll([ 379 base.ReqNot( 380 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]) 381 ), 382 base.ReqCapability('c') 383 ]) 384 385 assert pf.parseRequirement('!c*3') == base.ReqNot(base.ReqTokens('c', 3)) 386 387 assert pf.parseRequirement('X') == base.ReqImpossible() 388 389 assert pf.parseRequirement('O') == base.ReqNothing() 390 391 assert pf.parseRequirement('X|a') == base.ReqAny([ 392 base.ReqImpossible(), 393 base.ReqCapability('a') 394 ]) 395 396 with pytest.raises(parsing.ParseError): 397 pf.parseRequirement('a*3*2') 398 399 with pytest.raises(parsing.ParseError): 400 pf.parseRequirement('(a):2') 401 402 with pytest.raises(parsing.ParseError): 403 pf.parseRequirement('(a|b&c):3') 404 405 with pytest.raises(parsing.ParseError): 406 pf.parseRequirement('a|&b') 407 408 with pytest.raises(parsing.ParseError): 409 pf.parseRequirement('(a|b') 410 411 with pytest.raises(parsing.ParseError): 412 pf.parseRequirement('a|b)') 413 414 assert (pf.parseRequirement('a*-3') == base.ReqTokens('a', -3)) 415 416 with pytest.raises(parsing.ParseError): 417 pf.parseRequirement('a*!3') 418 419 with pytest.raises(parsing.ParseError): 420 pf.parseRequirement('a!b') 421 422 with pytest.raises(parsing.ParseError): 423 pf.parseRequirement('a*-b') 424 425 426def test_DecisionGraph() -> None: 427 "Multi-method test for `exploration.core.DecisionGraph`." 428 m = core.DecisionGraph() 429 m.addDecision('a') 430 m.addDecision('b') 431 m.addDecision('c') 432 assert len(m) == 3 433 assert set(m) == {0, 1, 2} 434 435 m.addTransition('a', 'East', 'b', 'West') 436 m.addTransition('a', 'Northeast', 'c') 437 m.addTransition('c', 'Southeast', 'b') 438 439 assert m.destinationsFrom('a') == {'East': 1, 'Northeast': 2} 440 assert m.destinationsFrom('b') == {'West': 0} 441 assert m.destinationsFrom('c') == {'Southeast': 1} 442 443 assert m.getReciprocal('a', 'East') == 'West' 444 assert m.getReciprocal('a', 'Northeast') is None 445 assert m.getReciprocal('b', 'Southwest') is None 446 assert m.getReciprocal('c', 'Southeast') is None 447 448 m.addUnexploredEdge('a', 'South') 449 m.addUnexploredEdge('b', 'East') 450 m.addUnexploredEdge('c', 'North') 451 452 assert len(m) == 6 453 assert set(m) == set(range(6)) 454 assert m.namesListing(m) == """\ 455 0 (a) 456 1 (b) 457 2 (c) 458 3 (_u.0) 459 4 (_u.1) 460 5 (_u.2) 461""" 462 assert ( 463 m.destinationsFrom('a') 464 == {'East': 1, 'Northeast': 2, 'South': 3} 465 ) 466 assert (m.destinationsFrom('b') == {'West': 0, 'East': 4}) 467 assert (m.destinationsFrom('c') == {'Southeast': 1, 'North': 5}) 468 469 m.replaceUnconfirmed('c', 'North', 'd', 'South') 470 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 471 assert m.nameFor(5) == 'd' 472 assert len(m) == 6 473 assert set(m) == set(range(6)) 474 assert m.namesListing(m) == """\ 475 0 (a) 476 1 (b) 477 2 (c) 478 3 (_u.0) 479 4 (_u.1) 480 5 (d) 481""" 482 483 m.addTransition('d', 'West', 'a', 'North') 484 assert ( 485 m.destinationsFrom('a') 486 == {'East': 1, 'Northeast': 2, 'South': 3, 'North': 5} 487 ) 488 assert ( 489 m.destinationsFrom('d') 490 == {'West': 0, 'South': 2} 491 ) 492 493 with pytest.raises(core.MissingDecisionError): 494 _ = m.destinationsFrom('z') 495 496 with pytest.raises(core.TransitionCollisionError): 497 m.addTransition('a', 'East', 'b', 'West') 498 499 with pytest.raises(core.TransitionCollisionError): 500 m.addTransition('c', 'East', 'b', 'West') 501 502 with pytest.raises(core.TransitionCollisionError): 503 m.addUnexploredEdge('a', 'East') 504 505 with pytest.raises(core.TransitionCollisionError): 506 m.addUnexploredEdge('c', 'North') 507 508 with pytest.raises(core.MissingTransitionError): 509 m.replaceUnconfirmed('a', 'Up', 'z') 510 511 with pytest.raises(core.ExplorationStatusError): 512 m.replaceUnconfirmed('a', 'East', 'z') 513 514 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 515 516 m.addTransition('a', 'EastBelow', 'b', 'WestBelow') 517 assert (m.destinationsFrom('a') == { 518 'East': 1, 519 'EastBelow': 1, 520 'Northeast': 2, 521 'South': 3, 522 'North': 5 523 }) 524 assert ( 525 m.destinationsFrom('b') 526 == {'West': 0, 'WestBelow': 0, 'East': 4} 527 ) 528 529 # Two edges that could be but are not reciprocals 530 m.addTransition('d', 'East', 'b') 531 m.addTransition('b', 'North', 'd') 532 assert ( 533 m.destinationsFrom('b') 534 == {'West': 0, 'WestBelow': 0, 'East': 4, 'North': 5} 535 ) 536 assert ( 537 m.destinationsFrom('d') 538 == {'West': 0, 'South': 2, 'East': 1} 539 ) 540 assert m.getReciprocal('b', 'North') is None 541 assert m.getReciprocal('d', 'East') is None 542 543 # Establish a reciprocal relationship 544 m.setReciprocal('b', 'North', 'East') 545 assert m.getReciprocal('b', 'North') == 'East' 546 assert m.getReciprocal('d', 'East') == 'North' 547 548 with pytest.raises(core.MissingDecisionError): 549 m.setReciprocal('z', 'Nope', 'None') 550 551 with pytest.raises(core.MissingTransitionError): 552 m.setReciprocal('b', 'Nope', 'None') 553 554 # Remove the reciprocal relationship again (from the other side) 555 m.setReciprocal('d', 'East', None) 556 assert m.getReciprocal('b', 'North') is None 557 assert m.getReciprocal('d', 'East') is None 558 559 with pytest.raises(core.InvalidDestinationError): 560 m.setReciprocal('b', 'North', 'West') 561 562 with pytest.raises(core.MissingTransitionError): 563 m.setReciprocal('b', 'North', 'None') 564 565 assert (m.isConfirmed("_u.0") is False) 566 assert (m.isConfirmed("_u.1") is False) 567 assert (m.isConfirmed("a") is True) 568 assert (m.isConfirmed("d") is True) 569 570 assert ( 571 json.dumps(m.textMapObj(), indent=4) 572 == """\ 573{ 574 "0::East": { 575 "1::West": "0", 576 "1::East": { 577 "4::return": "1" 578 }, 579 "1::WestBelow": "0", 580 "1::North": { 581 "5::South": { 582 "2::Southeast": "1", 583 "2::North": "5" 584 }, 585 "5::West": "0", 586 "5::East": "1" 587 } 588 }, 589 "0::Northeast": "2", 590 "0::South": { 591 "3::return": "0" 592 }, 593 "0::North": "5", 594 "0::EastBelow": "1" 595}""" 596 ) 597 598 assert ( 599 json.dumps( 600 m.textMapObj( 601 explorationOrder=( 602 0, 603 [ 604 'East', 605 'West', 606 'South', 607 'return', 608 'Northeast', 609 'Southeast', 610 'North', 611 ] 612 ) 613 ), 614 indent=4 615 ) 616 == """\ 617{ 618 "0::East": { 619 "1::West": "0", 620 "1::North": { 621 "5::South": { 622 "2::Southeast": "1", 623 "2::North": "5" 624 }, 625 "5::West": "0", 626 "5::East": "1" 627 }, 628 "1::East": { 629 "4::return": "1" 630 }, 631 "1::WestBelow": "0" 632 }, 633 "0::South": { 634 "3::return": "0" 635 }, 636 "0::Northeast": "2", 637 "0::North": "5", 638 "0::EastBelow": "1" 639}""" 640 ) 641 642 m.addTransition( 643 'd', 644 'failure', 645 m.endingID('failure') 646 ) 647 assert set(m) == set(range(7)) 648 assert m.namesListing(m) == """\ 649 0 (a) 650 1 (b) 651 2 (c) 652 3 (_u.0) 653 4 (_u.1) 654 5 (d) 655 6 (endings//failure) 656""" 657 assert ( 658 m.destinationsFrom('d') 659 == {'West': 0, 'South': 2, 'East': 1, 'failure': 6} 660 ) 661 assert m.destinationsFrom('failure') == {} 662 663 664def test_DGTagsAndAnnotations() -> None: 665 """ 666 Test for tagging decisions and transitions in an 667 `exploration.core.DecisionGraph`. 668 """ 669 m = core.DecisionGraph() 670 m.addDecision('a') 671 m.addDecision('b', tags={'grass': 1}) 672 m.addDecision('c') 673 m.tagDecision('c', {'water': 1, 'big': 1}) 674 m.annotateDecision('c', "This is a note.") 675 m.addTransition('a', 'East', 'b', 'West') 676 m.addTransition( 677 'a', 'South', 'c', 'North', 678 {'green': 1}, ["Requires green key"], 679 {'blue': 1}, ["Requires blue key"] 680 ) 681 m.tagTransition('a', 'South', 'green') 682 m.annotateTransition('a', 'South', "a2") 683 m.tagTransition('c', 'North', 'blue', 1) 684 m.annotateTransition('c', 'North', ["Requires", "blue key"]) 685 686 assert m.decisionTags('a') == {} 687 assert m.decisionTags('b') == {'grass': 1} 688 assert m.decisionTags('c') == {'water': 1, 'big': 1} 689 assert m.transitionTags('a', 'East') == {} 690 assert m.transitionTags('a', 'South') == {'green': 1} 691 assert m.transitionTags('b', 'West') == {} 692 assert m.transitionTags('c', 'North') == {'blue': 1} 693 694 assert m.decisionAnnotations('a') == [] 695 assert m.decisionAnnotations('b') == [] 696 assert m.decisionAnnotations('c') == ["This is a note."] 697 assert m.transitionAnnotations('a', 'East') == [] 698 assert m.transitionAnnotations('a', 'South') == [ 699 "Requires green key", 700 "a2" 701 ] 702 assert m.transitionAnnotations('b', 'West') == [] 703 assert m.transitionAnnotations('c', 'North') == [ 704 "Requires blue key", 705 "Requires", 706 "blue key" 707 ] 708 709 m.tagDecision('a', 'grass') 710 assert m.decisionTags('a') == {'grass': 1} 711 712 m.untagDecision('b', 'grass') 713 assert m.decisionTags('b') == {} 714 715 m.annotateDecision('a', "Starting location.") 716 assert m.decisionAnnotations('a') == ["Starting location."] 717 718 m.annotateDecision('c', "Blue key here.") 719 assert m.decisionAnnotations('c') == ["This is a note.", "Blue key here."] 720 721 assert m.decisionAnnotations('b') == [] 722 723 m.tagTransition('a', 'East', 'open') 724 assert m.transitionTags('a', 'East') == {'open': 1} 725 m.tagTransition('a', 'South', 'open') 726 assert m.transitionTags('a', 'South') == {'green': 1, 'open': 1} 727 m.untagTransition('a', 'South', 'green') 728 assert m.transitionTags('a', 'South') == {'open': 1} 729 m.tagTransition('a', 'South', 'open', [1, 2, 3]) 730 assert m.transitionTags('a', 'South') == {'open': [1, 2, 3]} 731 732 m.annotateTransition('c', 'North', "This was difficult.") 733 assert ( 734 m.transitionAnnotations('c', 'North') 735 == ['Requires blue key', 'Requires', 'blue key', 'This was difficult.'] 736 ) 737 cna = m.transitionAnnotations('c', 'North') 738 cna.clear() 739 cna.append('hi') 740 assert m.transitionAnnotations('c', 'North') == ['hi'] 741 742 with pytest.raises(core.MissingTransitionError): 743 _ = m.transitionAnnotations('a', 'West') 744 745 746def test_DiscreteExploration() -> None: 747 "Multi-method test for `exploration.core.DiscreteExploration`." 748 e = core.DiscreteExploration() 749 750 s0 = e.getSituation(0) 751 assert e.getSituation() is s0 752 753 assert len(s0.graph) == 0 754 assert e.getActiveDecisions(0) == set() 755 assert s0.state == base.emptyState() 756 assert s0.type == "pending" 757 assert s0.action is None 758 759 assert len(e) == 1 760 761 e.start('a') 762 e.observeAll('a', 'North', 'East', 'South') 763 764 assert len(e) == 2 765 766 s0 = e.getSituation(0) 767 s1 = e.getSituation(1) 768 assert s1 is e.getSituation() 769 assert len(s1.graph) == 4 770 assert set(s1.graph) == set(range(4)) 771 assert s1.graph.namesListing(s1.graph) == """\ 772 0 (a) 773 1 (_u.0) 774 2 (_u.1) 775 3 (_u.2) 776""" 777 assert e.getActiveDecisions() == e.getActiveDecisions(1) 778 assert e.getActiveDecisions() == {0} 779 assert s0.type == "imposed" 780 assert s0.action == ( 781 'start', 782 0, 783 0, 784 'main', 785 None, 786 None, 787 None 788 ) 789 790 e.explore('East', 'b', 'West') 791 e.observeAll('b', 'North', 'South') 792 e.getSituation().graph.removeTransition('_u.3', 'return') # truly one-way 793 794 assert len(e) == 3 795 assert set(s1.graph) == set(range(4)) 796 assert s1.graph.namesListing(s1.graph) == """\ 797 0 (a) 798 1 (_u.0) 799 2 (_u.1) 800 3 (_u.2) 801""" 802 assert e.getActiveDecisions(1) == {0} 803 s1 = e.getSituation(1) 804 assert s1.type == 'active' 805 dz = base.DefaultZone 806 assert s1.action == ( 807 'explore', 808 'active', 809 0, 810 ('East', []), 811 'b', 812 'West', 813 dz 814 ) 815 816 s2 = e.getSituation(2) 817 assert s2 is e.getSituation() 818 assert set(s2.graph) == set(range(6)) 819 assert s2.graph.namesListing(s2.graph) == """\ 820 0 (a) 821 1 (_u.0) 822 2 (b) 823 3 (_u.2) 824 4 (_u.3) 825 5 (_u.4) 826""" 827 assert e.getActiveDecisions() == {2} 828 assert s2.type == "pending" 829 assert s2.action is None 830 assert (s2.graph.destinationsFrom('a') == { 831 'North': 1, 832 'East': 2, 833 'South': 3 834 }) 835 assert (s2.graph.destinationsFrom('b') == { 836 'North': 4, 837 'West': 0, 838 'South': 5 839 }) 840 assert (s2.graph.destinationsFrom('_u.3') == {}) 841 assert (s2.graph.destinationsFrom('_u.4') == {'return': 2}) 842 843 with pytest.raises(core.ExplorationStatusError): 844 e.returnTo('West', 'a', 'North') 845 846 with pytest.raises(core.ExplorationStatusError): 847 e.returnTo('West', 'a', 'East') 848 # (would need to use retrace instead) 849 850 e.explore('North', 'c', None) 851 e.observe('c', 'West') 852 853 assert len(e) == 4 854 assert e.getSituation(0) == s0 855 assert e.getSituation(1) == s1 856 assert set(s1.graph) == set(range(4)) 857 assert s1.graph.namesListing(s1.graph) == """\ 858 0 (a) 859 1 (_u.0) 860 2 (_u.1) 861 3 (_u.2) 862""" 863 assert e.getActiveDecisions(1) == {0} 864 assert s1.type == "active" 865 assert s1.action == ('explore', 'active', 0, ('East', []), 'b', 'West', dz) 866 ns2 = e.getSituation(2) 867 assert ns2 != s2 868 assert ns2.graph == s2.graph 869 assert ns2.state == s2.state 870 assert s2.type, ns2.type == ('pending', 'active') 871 assert s2.action is None 872 assert ns2.tags == s2.tags 873 assert ns2.annotations == s2.annotations 874 s2 = ns2 875 assert set(s2.graph) == set(range(6)) 876 assert s2.graph.namesListing(s2.graph) == """\ 877 0 (a) 878 1 (_u.0) 879 2 (b) 880 3 (_u.2) 881 4 (_u.3) 882 5 (_u.4) 883""" 884 assert e.getActiveDecisions(2) == {2} 885 assert s2.type == "active" 886 assert s2.action == ('explore', 'active', 2, ('North', []), 'c', None, dz) 887 assert (s2.graph.destinationsFrom('a') == { 888 'North': 1, 889 'East': 2, 890 'South': 3 891 }) 892 assert (s2.graph.destinationsFrom('b') == { 893 'North': 4, 894 'West': 0, 895 'South': 5 896 }) 897 s3 = e.getSituation(3) 898 assert set(s3.graph) == set(range(7)) 899 assert s3.graph.namesListing(s3.graph) == """\ 900 0 (a) 901 1 (_u.0) 902 2 (b) 903 3 (_u.2) 904 4 (c) 905 5 (_u.4) 906 6 (_u.5) 907""" 908 assert e.getActiveDecisions(3) == {4} 909 assert s3.type == "pending" 910 assert s3.action is None 911 assert (s3.graph.destinationsFrom('a') == { 912 'North': 1, 913 'East': 2, 914 'South': 3 915 }) 916 assert (s3.graph.destinationsFrom('b') == { 917 'North': 4, 918 'West': 0, 919 'South': 5 920 }) 921 assert s3.graph.destinationsFrom('c') == {'West': 6} 922 assert s3.graph.destinationsFrom('_u.0') == {'return': 0} 923 assert s3.graph.destinationsFrom('_u.2') == {'return': 0} 924 assert s3.graph.destinationsFrom('_u.4') == {'return': 2} 925 assert s3.graph.destinationsFrom('_u.5') == {'return': 4} 926 assert s3.graph.degree(0) == 6 927 assert s3.graph.degree(2) == 5 928 assert s3.graph.degree(4) == 3 929 assert s3.graph.degree(6) == 2 930 931 e.explore('West', 'd', 'East') 932 933 assert len(e) == 5 934 s3 = e.getSituation(3) 935 s4 = e.getSituation(4) 936 assert set(s4.graph) == set(range(7)) 937 assert s4.graph.namesListing(s4.graph) == """\ 938 0 (a) 939 1 (_u.0) 940 2 (b) 941 3 (_u.2) 942 4 (c) 943 5 (_u.4) 944 6 (d) 945""" 946 assert e.getActiveDecisions(4) == {6} 947 assert s4.type == "pending" 948 assert s4.action is None 949 assert s4.graph.destinationsFrom('c') == {'West': 6} 950 assert s4.graph.destinationsFrom('d') == {'East': 4} 951 assert s4.graph.degree(4) == 3 952 assert s4.graph.degree(6) == 2 953 954 # Can't return if there's no outgoing edge yet 955 with pytest.raises(core.MissingTransitionError): 956 e.returnTo('South', 'a', 'North') 957 958 with pytest.raises(core.ExplorationStatusError): 959 e.returnTo('East', 'a', 'North') 960 961 # Add the edge and then we can use it to return 962 g = s4.graph 963 g.addUnexploredEdge('d', 'South') 964 assert set(g) == set(range(8)) 965 assert g.namesListing(g) == """\ 966 0 (a) 967 1 (_u.0) 968 2 (b) 969 3 (_u.2) 970 4 (c) 971 5 (_u.4) 972 6 (d) 973 7 (_u.6) 974""" 975 e.returnTo('South', 'a', 'North') 976 977 assert len(e) == 6 978 s4 = e.getSituation(4) 979 s5 = e.getSituation(5) 980 assert s5 == e.getSituation() 981 assert set(s5.graph) == set([0, 2, 3, 4, 5, 6]) 982 assert s5.graph.namesListing(s5.graph) == """\ 983 0 (a) 984 2 (b) 985 3 (_u.2) 986 4 (c) 987 5 (_u.4) 988 6 (d) 989""" 990 assert e.getActiveDecisions(5) == {0} 991 assert s5.type == "pending" 992 assert s5.action is None 993 assert s5.graph.destinationsFrom('a') == { 994 'East': 2, 995 'North': 6, 996 'South': 3 997 } 998 assert s5.graph.destinationsFrom('d') == {'East': 4, 'South': 0} 999 assert s5.graph.degree(4) == 3 1000 assert s5.graph.degree(6) == 4 1001 assert s5.graph.degree(0) == 6 1002 1003 e.wait() 1004 1005 assert len(e) == 7 1006 s5 = e.getSituation(5) 1007 s6 = e.getSituation(6) 1008 assert set(s6.graph) == set([0, 2, 3, 4, 5, 6]) 1009 assert s6.graph.namesListing(s6.graph) == """\ 1010 0 (a) 1011 2 (b) 1012 3 (_u.2) 1013 4 (c) 1014 5 (_u.4) 1015 6 (d) 1016""" 1017 assert e.getActiveDecisions(6) == {0} 1018 assert s6.type == "pending" 1019 assert s6.action is None 1020 1021 assert s5.action == ('noAction',) 1022 1023 e.takeAction( 1024 'powerUp', 1025 consequence=[ 1026 base.effect(gain='power'), 1027 base.effect(gain=('token', 2)), 1028 ], 1029 fromDecision=0 1030 ) 1031 1032 assert len(e) == 8 1033 s6 = e.getSituation(6) 1034 s7 = e.getSituation(7) 1035 assert set(s7.graph) == set([0, 2, 3, 4, 5, 6]) 1036 assert s7.graph.namesListing(s7.graph) == """\ 1037 0 (a) 1038 2 (b) 1039 3 (_u.2) 1040 4 (c) 1041 5 (_u.4) 1042 6 (d) 1043""" 1044 assert e.getActiveDecisions(7) == {0} 1045 assert base.hasCapabilityOrEquivalent( 1046 'power', 1047 base.genericContextForSituation(s7) 1048 ) 1049 assert base.combinedTokenCount(s7.state, 'token') == 2 1050 assert base.effectiveCapabilitySet(s7.state) == { 1051 "capabilities": {"power"}, 1052 "tokens": {"token": 2}, 1053 "skills": {} 1054 } 1055 assert s7.type == "pending" 1056 assert s7.action is None 1057 assert (s7.graph.destinationsFrom('a') == { 1058 'North': 6, 1059 'East': 2, 1060 'South': 3, 1061 'powerUp': 0 1062 }) 1063 assert s6.action == ('take', 'active', 0, ('powerUp', [])) 1064 1065 e.retrace('East') 1066 1067 assert len(e) == 9 1068 s7 = e.getSituation(7) 1069 s8 = e.getSituation(8) 1070 assert set(s8.graph) == set([0, 2, 3, 4, 5, 6]) 1071 assert s8.graph.namesListing(s8.graph) == """\ 1072 0 (a) 1073 2 (b) 1074 3 (_u.2) 1075 4 (c) 1076 5 (_u.4) 1077 6 (d) 1078""" 1079 assert e.getActiveDecisions(8) == {2} 1080 assert s8.type == "pending" 1081 assert s8.action is None 1082 assert base.effectiveCapabilitySet(s8.state) == { 1083 "capabilities": {"power"}, 1084 "tokens": {"token": 2}, 1085 "skills": {} 1086 } 1087 assert s7.action == ('take', 'active', 0, ('East', [])) 1088 1089 e.observeMechanisms('d', ('gate', 'closed')) 1090 gateD = base.mechanismAt('gate', decision='d') 1091 gateA = base.mechanismAt('gate', decision='a') 1092 assert gateD == (None, None, 'd', 'gate') 1093 assert gateA == (None, None, 'a', 'gate') 1094 assert base.mechanismInStateOrEquivalent( 1095 'gate', 1096 'closed', 1097 base.genericContextForSituation(s8) 1098 ) 1099 assert e.mechanismState('gate') == "closed" 1100 assert e.mechanismState(gateD) == "closed" 1101 1102 # Can't get mechanism state in step before it's been observed 1103 with pytest.raises(core.MissingMechanismError): 1104 e.mechanismState('gate', step=6) 1105 1106 # Can't get mechanism state for mechanism at a different decision 1107 assert e.mechanismState(gateA) == "closed" 1108 1109 e.observeMechanisms('a', 'gate') # without starting state 1110 with pytest.raises(core.MechanismCollisionError): 1111 e.mechanismState('gate') 1112 1113 with pytest.raises(core.MechanismCollisionError): 1114 e.mechanismState(base.mechanismAt('gate', decision='c')) 1115 1116 # Default state 1117 assert e.mechanismState(gateA) == 'off' 1118 assert e.mechanismState(gateD) == 'closed' 1119 1120 e.warp( 1121 'd', 1122 consequence=[ 1123 base.effect(lose=('token', 1)), 1124 base.effect(set=('gate', 'open')) # knows to open gate at 'd' 1125 ] 1126 ) 1127 1128 assert len(e) == 10 1129 s8 = e.getSituation(8) 1130 s9 = e.getSituation(9) 1131 assert s8.action == ("warp", "active", 6) 1132 assert set(s9.graph) == set([0, 2, 3, 4, 5, 6]) 1133 assert s9.graph.namesListing(s9.graph) == """\ 1134 0 (a) 1135 2 (b) 1136 3 (_u.2) 1137 4 (c) 1138 5 (_u.4) 1139 6 (d) 1140""" 1141 assert e.getActiveDecisions(9) == {6} 1142 assert base.effectiveCapabilitySet(s9.state) == { 1143 "capabilities": {"power"}, 1144 "tokens": {"token": 1}, 1145 "skills": {} 1146 } 1147 assert s9.type == "pending" 1148 assert s9.action is None 1149 assert (s9.graph.destinationsFrom('a') == { 1150 'North': 6, 1151 'East': 2, 1152 'South': 3, 1153 'powerUp': 0 1154 }) 1155 assert (s9.graph.destinationsFrom('b') == { 1156 'North': 4, 1157 'West': 0, 1158 'South': 5 1159 }) 1160 assert (s9.graph.destinationsFrom('c') == {'West': 6}) 1161 assert (s9.graph.destinationsFrom('d') == {'East': 4, 'South': 0}) 1162 1163 ctx1 = base.genericContextForSituation(s1) 1164 ctx8 = base.genericContextForSituation(s8) 1165 ctx9 = base.genericContextForSituation(s9) 1166 with pytest.raises(core.MissingDecisionError): 1167 base.mechanismInStateOrEquivalent(gateD, 'open', ctx1) 1168 # Decision 'd' doesn't exist back then (neither does the gate) 1169 assert not base.mechanismInStateOrEquivalent(gateD, 'open', ctx8) 1170 assert base.mechanismInStateOrEquivalent(gateD, 'open', ctx9) 1171 1172 with pytest.raises(core.MissingMechanismError): 1173 e.mechanismState(gateA, step=1) 1174 1175 assert e.mechanismState(gateA) == 'off' 1176 assert e.mechanismState(gateD) == 'open' 1177 assert e.mechanismState(gateD, step=8) == 'closed' 1178 1179 1180def test_exploring_with_zones() -> None: 1181 """ 1182 A test for exploring with zones being applied. 1183 """ 1184 e = core.DiscreteExploration() 1185 1186 assert e.start('start') == 0 1187 graph = e.getSituation().graph 1188 graph.createZone('zone', 0) 1189 graph.addDecisionToZone('start', 'zone') 1190 e.observe(0, 'transition') 1191 assert e.explore('transition', 'room') == 1 1192 1193 s = e.getSituation() 1194 g = s.graph 1195 assert g.zoneParents(0) == {'zone'} 1196 assert g.zoneParents(1) == {'zone'} 1197 1198 e.observeAll(1, 'out', 'down') 1199 unknown = g.destination('room', 'down') 1200 g.renameDecision(unknown, 'fourth_room') 1201 assert g.nameFor(3) == 'fourth_room' 1202 assert g.zoneParents(3) == set() 1203 assert not g.isConfirmed('fourth_room') 1204 assert not e.hasBeenVisited('fourth_room') 1205 assert e.explore('out', 'another_room', 'back', 'zone2') == 2 1206 e.retrace('back') 1207 e.explore('down', None, 'up') # already named 'fourth_room' 1208 1209 g = e.getSituation().graph 1210 assert g.nameFor(3) == 'fourth_room' 1211 assert g.zoneParents(0) == {'zone'} 1212 assert g.zoneParents(1) == {'zone'} 1213 assert g.zoneParents(2) == {'zone2'} 1214 assert g.zoneParents(3) == {'zone'} 1215 1216 1217def test_triggers() -> None: 1218 e = core.DiscreteExploration() 1219 e.start('start') 1220 assert e.primaryDecision() == 0 1221 e.takeAction( 1222 'shiver', 1223 requires=base.ReqNot(base.ReqCapability('jacket')), 1224 consequence=[base.effect(gain=('cold', 1))], 1225 fromDecision='start' 1226 ) 1227 e.getSituation().graph.tagTransition('start', 'shiver', 'trigger') 1228 assert e.tokenCountNow('cold') == 1 1229 e.observe('start', 'right') 1230 e.explore('right', 'room', 'left') 1231 assert e.primaryDecision() == 1 1232 assert e.tokenCountNow('cold') == 1 1233 e.takeAction( 1234 'warmUp', 1235 requires=base.ReqTokens('cold', 1), 1236 consequence=[base.effect(lose=('cold', 1))], 1237 fromDecision='room' 1238 ) 1239 assert e.tokenCountNow('cold') == 0 1240 e.getSituation().graph.tagTransition('room', 'warmUp', 'trigger') 1241 e.wait() 1242 assert e.tokenCountNow('cold') == 0 1243 e.retrace('left') 1244 assert e.primaryDecision() == 0 1245 assert e.tokenCountNow('cold') == 1 1246 e.wait() 1247 assert e.tokenCountNow('cold') == 2 1248 e.wait() 1249 assert e.tokenCountNow('cold') == 3 1250 e.retrace('right') 1251 assert e.tokenCountNow('cold') == 2 1252 e.wait() 1253 assert e.tokenCountNow('cold') == 1 1254 e.wait() 1255 # No issue with trigger when out of tokens because of its requirement 1256 assert e.tokenCountNow('cold') == 0 1257 e.wait() 1258 assert e.tokenCountNow('cold') == 0 1259 # Get a jacket 1260 e.applyExtraneousEffect(base.effect(gain='jacket')) 1261 e.retrace('left') 1262 # Jacket prevents trigger 1263 assert e.primaryDecision() == 0 1264 assert e.tokenCountNow('cold') == 0 1265 e.wait() 1266 assert e.tokenCountNow('cold') == 0 1267 # TODO: Add a trigger group...
21@pytest.fixture 22def pf() -> parsing.ParseFormat: 23 """ 24 A fixture that provides the default `parsing.ParseFormat`. 25 """ 26 return parsing.ParseFormat()
A fixture that provides the default parsing.ParseFormat
.
29@pytest.fixture 30def trc() -> base.RequirementContext: 31 """ 32 A fixture providing an empty requirement context. 33 """ 34 baseGraph = core.DecisionGraph.example('simple') 35 # Has nodes A/B/C in a triangle with "next"/"prev" transitions 36 baseState = base.emptyState() 37 # Set up node A as the current position 38 baseState['common']['focalization']['main'] = 'singular' 39 baseState['common']['activeDomains'] = { 'main' } 40 baseState['common']['activeDecisions']['main'] = 0 41 baseState['primaryDecision'] = 0 # A 42 return base.RequirementContext( 43 state=baseState, 44 graph=baseGraph, 45 searchFrom=set() 46 )
A fixture providing an empty requirement context.
49def rcWith( 50 original: base.RequirementContext, 51 where: Optional[Iterable[base.AnyDecisionSpecifier]] = None, 52 eq: Optional[ 53 Iterable[ 54 Tuple[ 55 base.Requirement, 56 Union[ 57 base.Capability, 58 Tuple[base.MechanismID, base.MechanismState] 59 ] 60 ] 61 ] 62 ] = None, 63 **kwargs: Union[ 64 bool, 65 base.TokenCount, 66 base.MechanismState, 67 Tuple[Literal['skill'], int], 68 Tuple[Literal['tag'], base.TagValue] 69 ] 70): 71 """ 72 Clones the given `base.RequirementContext` and returns a clone with 73 additional powers, tokens, and/or mechanism states set according to 74 keyword arguments provided, where the type of each value determines 75 what is being changed: True/False sets a `base.Capability`, an 76 integer sets a `base.Token`'s count, and a string sets the state of 77 the named mechanism. Tuples starting with 'skill' (followed by an 78 integer) or "tag" (followed by any kind of tag value) set up skill 79 levels or tags. 80 81 Capabilities, tokens, and skills are set up in the common focal 82 context. 83 84 Tags values are added to the current primary decision, and mechanism 85 states are set for the mechanism found via mechanism search for 86 that name (default is global search). 87 88 In addition to changing token, capability, and/or mechanism states, 89 a new search from location set can be provided via the `where`, and 90 a list of new equivalences can be provided via the `eq` argument. 91 """ 92 if where is not None: 93 searchFrom = set( 94 original.graph.resolveDecision(x) for x in where 95 ) 96 else: 97 searchFrom = copy.deepcopy(original.searchFrom) 98 99 newState = copy.deepcopy(original.state) 100 newGraph = copy.deepcopy(original.graph) 101 102 if eq is not None: 103 for (req, equivalentTo) in eq: 104 newGraph.addEquivalence(req, equivalentTo) 105 106 commonFC = newState['common'] 107 108 for kw in kwargs: 109 val = kwargs[kw] 110 if isinstance(val, bool): 111 if val: 112 commonFC['capabilities']['capabilities'].add(kw) 113 else: 114 try: 115 commonFC['capabilities']['capabilities'].remove(kw) 116 except KeyError: 117 pass 118 119 elif isinstance(val, base.TokenCount): 120 commonFC['capabilities']['tokens'][kw] = val 121 122 elif isinstance(val, base.MechanismState): 123 mID = original.graph.resolveMechanism(kw, original.searchFrom) 124 newState['mechanisms'][mID] = val 125 126 elif ( 127 isinstance(val, tuple) 128 and len(val) == 2 129 and val[0] == 'skill' 130 and isinstance(val[1], int) 131 ): 132 commonFC['capabilities']['skills'][kw] = val[1] 133 134 elif ( 135 isinstance(val, tuple) 136 and len(val) == 2 137 and val[0] == 'tag' 138 ): 139 pr = original.state['primaryDecision'] 140 if pr is None: 141 raise ValueError( 142 "Base context has no primary decision so we can't" 143 " tag anything." 144 ) 145 newGraph.tagDecision(pr, kw, val[1]) 146 147 else: 148 raise ValueError(f"Invalid context addition value: {val!r}") 149 150 return base.RequirementContext( 151 state=newState, 152 graph=newGraph, 153 searchFrom=searchFrom 154 )
Clones the given base.RequirementContext
and returns a clone with
additional powers, tokens, and/or mechanism states set according to
keyword arguments provided, where the type of each value determines
what is being changed: True/False sets a base.Capability
, an
integer sets a base.Token
's count, and a string sets the state of
the named mechanism. Tuples starting with 'skill' (followed by an
integer) or "tag" (followed by any kind of tag value) set up skill
levels or tags.
Capabilities, tokens, and skills are set up in the common focal context.
Tags values are added to the current primary decision, and mechanism states are set for the mechanism found via mechanism search for that name (default is global search).
In addition to changing token, capability, and/or mechanism states,
a new search from location set can be provided via the where
, and
a list of new equivalences can be provided via the eq
argument.
157def test_Requirements(pf, trc) -> None: 158 """ 159 Multi-method test for `exploration.core.Requirement` and sub-classes. 160 """ 161 # Tests of comparison 162 r: base.Requirement = base.ReqAll([ 163 base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), 164 base.ReqTokens('key', 1) 165 ]) 166 r2: base.Requirement = base.ReqAll([ 167 base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), 168 base.ReqTokens('key', 1) 169 ]) 170 assert r == r2 171 assert base.ReqNothing() == base.ReqNothing() 172 assert base.ReqImpossible() == base.ReqImpossible() 173 174 # Tests of satisfied 175 assert not r.satisfied(trc) 176 assert r.satisfied(rcWith(trc, p1=True, key=1)) 177 assert not r.satisfied(rcWith(trc, key=1)) 178 assert not r.satisfied(rcWith(trc, p1=True, key=0)) 179 assert not r.satisfied(rcWith(trc, p1=True)) 180 assert r.satisfied( 181 rcWith(trc, p1=True, p2=True, key=2) 182 ) 183 184 r = base.ReqAny([ 185 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]), 186 base.ReqTokens('key', 3) 187 ]) 188 assert r.satisfied(rcWith(trc, p1=True, key=3)) 189 assert not r.satisfied(rcWith(trc, key=1)) 190 assert not r.satisfied(rcWith(trc, p2=True, key=0)) 191 assert not r.satisfied(rcWith(trc, p1=True)) 192 assert r.satisfied(rcWith(trc, p1=True, p2=True)) 193 assert r.satisfied(rcWith(trc, p1=True, p2=True, key=2)) 194 assert r.satisfied(rcWith(trc, p1=True, p2=True, key=5)) 195 assert r.satisfied(rcWith(trc, key=5)) 196 197 assert not base.hasCapabilityOrEquivalent('p3', trc) 198 assert base.hasCapabilityOrEquivalent('p3', rcWith(trc, p3=True)) 199 p2IsP3 = rcWith(trc, eq=[(base.ReqCapability('p2'), 'p3')]) 200 assert not base.hasCapabilityOrEquivalent('p3', p2IsP3) 201 assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p2=True)) 202 assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p3=True)) 203 assert not base.hasCapabilityOrEquivalent('p2', rcWith(p2IsP3, p3=True)) 204 205 assert base.ReqCapability('p3').satisfied(rcWith(p2IsP3, p2=True)) 206 r = base.ReqAll( 207 [base.ReqCapability('p1'), base.ReqCapability('p3')] 208 ) 209 assert r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 210 assert not r.satisfied(rcWith(p2IsP3, p2=True)) 211 assert not r.satisfied(rcWith(p2IsP3, p1=True)) 212 assert not r.satisfied(rcWith(p2IsP3, p3=True)) 213 assert r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) 214 215 r = base.ReqImpossible() 216 assert not r.satisfied(trc) 217 218 r = base.ReqNothing() 219 assert r.satisfied(trc) 220 221 r = base.ReqNot( 222 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]) 223 ) 224 assert r.satisfied(trc) 225 assert r.satisfied(rcWith(trc, p1=True)) 226 assert r.satisfied(rcWith(trc, p2=True)) 227 assert not r.satisfied(rcWith(trc, p1=True, p2=True)) 228 assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 229 r = base.ReqNot( 230 base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p3')]) 231 ) 232 assert r.satisfied(p2IsP3) 233 assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) 234 assert not r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) 235 assert r.satisfied(rcWith(p2IsP3, p1=True)) 236 assert r.satisfied(rcWith(p2IsP3, p2=True)) 237 assert r.satisfied(rcWith(p2IsP3, p3=True)) 238 239 # Mechanism requirements 240 withSwitch = copy.deepcopy(trc) 241 withSwitch.graph.addMechanism('switch', 0) # at A 242 r = base.ReqMechanism('switch', 'on') 243 assert not r.satisfied(withSwitch) 244 assert not r.satisfied(rcWith(withSwitch, switch="off")) 245 assert r.satisfied(rcWith(withSwitch, switch="on")) 246 247 leverNearby = copy.deepcopy(trc) 248 leverNearby.graph.addMechanism('lever', 1) # at B 249 r = base.ReqMechanism('lever', 'pulled') 250 assert not r.satisfied(leverNearby) 251 assert not r.satisfied(rcWith(leverNearby, lever="default")) 252 assert r.satisfied(rcWith(leverNearby, lever="pulled")) 253 254 # Skill level requirements 255 r = base.ReqLevel('skill', 1) 256 r2 = base.ReqNot(base.ReqLevel('skill', 2)) 257 assert not r.satisfied(trc) 258 assert not r.satisfied(rcWith(trc, skill=("skill", 0))) 259 assert r.satisfied(rcWith(trc, skill=("skill", 1))) 260 assert r.satisfied(rcWith(trc, skill=("skill", 2))) 261 assert r.satisfied(rcWith(trc, skill=("skill", 10))) 262 assert r2.satisfied(rcWith(trc, skill=("skill", 1))) 263 assert not r2.satisfied(rcWith(trc, skill=("skill", 2))) 264 assert not r2.satisfied(rcWith(trc, skill=("skill", 10))) 265 266 # Tag requirements 267 r = base.ReqTag('tag', 1) 268 r2 = base.ReqTag('tag2', 'value') 269 assert not r.satisfied(trc) 270 assert not r2.satisfied(trc) 271 tagged = rcWith(trc, tag=("tag", 1)) 272 assert tagged.graph.decisionTags(0) == {'tag': 1} 273 assert r.satisfied(tagged) 274 assert not r.satisfied(rcWith(trc, tag=("tag", 2))) 275 assert not r.satisfied(rcWith(trc, tag=("tag", 0))) 276 assert r2.satisfied(rcWith(trc, tag2=("tag", 'value'))) 277 taggedAside = copy.deepcopy(trc) 278 taggedAside.graph.tagDecision(1, 'tag', 1) 279 taggedAside.graph.tagDecision(2, 'tag2', 'value') 280 assert not r.satisfied(taggedAside) 281 assert not r2.satisfied(taggedAside) 282 taggedAside.state['common']['activeDecisions']['main'] = 1 283 assert r.satisfied(taggedAside) 284 assert not r2.satisfied(taggedAside) 285 taggedAside.state['common']['activeDecisions']['main'] = 2 286 assert not r.satisfied(taggedAside) 287 assert r2.satisfied(taggedAside) 288 zoneTagged = copy.deepcopy(trc) 289 zoneTagged.graph.createZone('zone') 290 zoneTagged.graph.addDecisionToZone(0, 'zone') 291 zoneTagged.graph.tagZone('zone', 'tag', 1) 292 assert r.satisfied(zoneTagged) 293 assert not r2.satisfied(zoneTagged) 294 zoneTagged.graph.removeDecisionFromZone(0, 'zone') 295 assert not r.satisfied(zoneTagged) 296 297 # Tests of parsing: 298 assert pf.parseRequirement('a') == base.ReqCapability('a') 299 300 assert pf.parseRequirement('(a)') == base.ReqCapability('a') 301 302 assert pf.parseRequirement('a*5') == base.ReqTokens('a', 5) 303 304 assert pf.parseRequirement('((a*5))') == base.ReqTokens('a', 5) 305 306 assert pf.parseRequirement('(a|b)&c*3') == base.ReqAll([ 307 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 308 base.ReqTokens('c', 3) 309 ]) 310 311 assert pf.parseRequirement(' ( a | b )\t& c * 3 ') == base.ReqAll([ 312 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 313 base.ReqTokens('c', 3) 314 ]) 315 316 assert pf.parseRequirement('a|(b&c*3)') == base.ReqAny([ 317 base.ReqCapability('a'), 318 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 319 ]) 320 321 assert pf.parseRequirement('a|b&c*3') == base.ReqAny([ 322 base.ReqCapability('a'), 323 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 324 ]) 325 326 assert pf.parseRequirement('a&b|c*3') == base.ReqAny([ 327 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]), 328 base.ReqTokens('c', 3) 329 ]) 330 331 assert pf.parseRequirement('a|b|c') == base.ReqAny([ 332 base.ReqCapability('a'), 333 base.ReqCapability('b'), 334 base.ReqCapability('c'), 335 ]) 336 337 assert pf.parseRequirement('a&b&c&d') == base.ReqAll([ 338 base.ReqCapability('a'), 339 base.ReqCapability('b'), 340 base.ReqCapability('c'), 341 base.ReqCapability('d'), 342 ]) 343 344 assert pf.parseRequirement('a&b|c&d') == base.ReqAny([ 345 base.ReqAll([ 346 base.ReqCapability('a'), 347 base.ReqCapability('b') 348 ]), 349 base.ReqAll([ 350 base.ReqCapability('c'), 351 base.ReqCapability('d') 352 ]) 353 ]) 354 355 assert pf.parseRequirement('a&!b|!c&d') == base.ReqAny([ 356 base.ReqAll([ 357 base.ReqCapability('a'), 358 base.ReqNot(base.ReqCapability('b')) 359 ]), 360 base.ReqAll([ 361 base.ReqNot(base.ReqCapability('c')), 362 base.ReqCapability('d') 363 ]) 364 ]) 365 366 assert pf.parseRequirement('!(a|b)&c') == base.ReqAll([ 367 base.ReqNot( 368 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]) 369 ), 370 base.ReqCapability('c') 371 ]) 372 373 assert pf.parseRequirement('!a&b&c') == base.ReqAll([ 374 base.ReqNot(base.ReqCapability('a')), 375 base.ReqCapability('b'), 376 base.ReqCapability('c') 377 ]) 378 379 assert pf.parseRequirement('!(a&b)&c') == base.ReqAll([ 380 base.ReqNot( 381 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]) 382 ), 383 base.ReqCapability('c') 384 ]) 385 386 assert pf.parseRequirement('!c*3') == base.ReqNot(base.ReqTokens('c', 3)) 387 388 assert pf.parseRequirement('X') == base.ReqImpossible() 389 390 assert pf.parseRequirement('O') == base.ReqNothing() 391 392 assert pf.parseRequirement('X|a') == base.ReqAny([ 393 base.ReqImpossible(), 394 base.ReqCapability('a') 395 ]) 396 397 with pytest.raises(parsing.ParseError): 398 pf.parseRequirement('a*3*2') 399 400 with pytest.raises(parsing.ParseError): 401 pf.parseRequirement('(a):2') 402 403 with pytest.raises(parsing.ParseError): 404 pf.parseRequirement('(a|b&c):3') 405 406 with pytest.raises(parsing.ParseError): 407 pf.parseRequirement('a|&b') 408 409 with pytest.raises(parsing.ParseError): 410 pf.parseRequirement('(a|b') 411 412 with pytest.raises(parsing.ParseError): 413 pf.parseRequirement('a|b)') 414 415 assert (pf.parseRequirement('a*-3') == base.ReqTokens('a', -3)) 416 417 with pytest.raises(parsing.ParseError): 418 pf.parseRequirement('a*!3') 419 420 with pytest.raises(parsing.ParseError): 421 pf.parseRequirement('a!b') 422 423 with pytest.raises(parsing.ParseError): 424 pf.parseRequirement('a*-b')
Multi-method test for exploration.core.Requirement
and sub-classes.
427def test_DecisionGraph() -> None: 428 "Multi-method test for `exploration.core.DecisionGraph`." 429 m = core.DecisionGraph() 430 m.addDecision('a') 431 m.addDecision('b') 432 m.addDecision('c') 433 assert len(m) == 3 434 assert set(m) == {0, 1, 2} 435 436 m.addTransition('a', 'East', 'b', 'West') 437 m.addTransition('a', 'Northeast', 'c') 438 m.addTransition('c', 'Southeast', 'b') 439 440 assert m.destinationsFrom('a') == {'East': 1, 'Northeast': 2} 441 assert m.destinationsFrom('b') == {'West': 0} 442 assert m.destinationsFrom('c') == {'Southeast': 1} 443 444 assert m.getReciprocal('a', 'East') == 'West' 445 assert m.getReciprocal('a', 'Northeast') is None 446 assert m.getReciprocal('b', 'Southwest') is None 447 assert m.getReciprocal('c', 'Southeast') is None 448 449 m.addUnexploredEdge('a', 'South') 450 m.addUnexploredEdge('b', 'East') 451 m.addUnexploredEdge('c', 'North') 452 453 assert len(m) == 6 454 assert set(m) == set(range(6)) 455 assert m.namesListing(m) == """\ 456 0 (a) 457 1 (b) 458 2 (c) 459 3 (_u.0) 460 4 (_u.1) 461 5 (_u.2) 462""" 463 assert ( 464 m.destinationsFrom('a') 465 == {'East': 1, 'Northeast': 2, 'South': 3} 466 ) 467 assert (m.destinationsFrom('b') == {'West': 0, 'East': 4}) 468 assert (m.destinationsFrom('c') == {'Southeast': 1, 'North': 5}) 469 470 m.replaceUnconfirmed('c', 'North', 'd', 'South') 471 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 472 assert m.nameFor(5) == 'd' 473 assert len(m) == 6 474 assert set(m) == set(range(6)) 475 assert m.namesListing(m) == """\ 476 0 (a) 477 1 (b) 478 2 (c) 479 3 (_u.0) 480 4 (_u.1) 481 5 (d) 482""" 483 484 m.addTransition('d', 'West', 'a', 'North') 485 assert ( 486 m.destinationsFrom('a') 487 == {'East': 1, 'Northeast': 2, 'South': 3, 'North': 5} 488 ) 489 assert ( 490 m.destinationsFrom('d') 491 == {'West': 0, 'South': 2} 492 ) 493 494 with pytest.raises(core.MissingDecisionError): 495 _ = m.destinationsFrom('z') 496 497 with pytest.raises(core.TransitionCollisionError): 498 m.addTransition('a', 'East', 'b', 'West') 499 500 with pytest.raises(core.TransitionCollisionError): 501 m.addTransition('c', 'East', 'b', 'West') 502 503 with pytest.raises(core.TransitionCollisionError): 504 m.addUnexploredEdge('a', 'East') 505 506 with pytest.raises(core.TransitionCollisionError): 507 m.addUnexploredEdge('c', 'North') 508 509 with pytest.raises(core.MissingTransitionError): 510 m.replaceUnconfirmed('a', 'Up', 'z') 511 512 with pytest.raises(core.ExplorationStatusError): 513 m.replaceUnconfirmed('a', 'East', 'z') 514 515 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 516 517 m.addTransition('a', 'EastBelow', 'b', 'WestBelow') 518 assert (m.destinationsFrom('a') == { 519 'East': 1, 520 'EastBelow': 1, 521 'Northeast': 2, 522 'South': 3, 523 'North': 5 524 }) 525 assert ( 526 m.destinationsFrom('b') 527 == {'West': 0, 'WestBelow': 0, 'East': 4} 528 ) 529 530 # Two edges that could be but are not reciprocals 531 m.addTransition('d', 'East', 'b') 532 m.addTransition('b', 'North', 'd') 533 assert ( 534 m.destinationsFrom('b') 535 == {'West': 0, 'WestBelow': 0, 'East': 4, 'North': 5} 536 ) 537 assert ( 538 m.destinationsFrom('d') 539 == {'West': 0, 'South': 2, 'East': 1} 540 ) 541 assert m.getReciprocal('b', 'North') is None 542 assert m.getReciprocal('d', 'East') is None 543 544 # Establish a reciprocal relationship 545 m.setReciprocal('b', 'North', 'East') 546 assert m.getReciprocal('b', 'North') == 'East' 547 assert m.getReciprocal('d', 'East') == 'North' 548 549 with pytest.raises(core.MissingDecisionError): 550 m.setReciprocal('z', 'Nope', 'None') 551 552 with pytest.raises(core.MissingTransitionError): 553 m.setReciprocal('b', 'Nope', 'None') 554 555 # Remove the reciprocal relationship again (from the other side) 556 m.setReciprocal('d', 'East', None) 557 assert m.getReciprocal('b', 'North') is None 558 assert m.getReciprocal('d', 'East') is None 559 560 with pytest.raises(core.InvalidDestinationError): 561 m.setReciprocal('b', 'North', 'West') 562 563 with pytest.raises(core.MissingTransitionError): 564 m.setReciprocal('b', 'North', 'None') 565 566 assert (m.isConfirmed("_u.0") is False) 567 assert (m.isConfirmed("_u.1") is False) 568 assert (m.isConfirmed("a") is True) 569 assert (m.isConfirmed("d") is True) 570 571 assert ( 572 json.dumps(m.textMapObj(), indent=4) 573 == """\ 574{ 575 "0::East": { 576 "1::West": "0", 577 "1::East": { 578 "4::return": "1" 579 }, 580 "1::WestBelow": "0", 581 "1::North": { 582 "5::South": { 583 "2::Southeast": "1", 584 "2::North": "5" 585 }, 586 "5::West": "0", 587 "5::East": "1" 588 } 589 }, 590 "0::Northeast": "2", 591 "0::South": { 592 "3::return": "0" 593 }, 594 "0::North": "5", 595 "0::EastBelow": "1" 596}""" 597 ) 598 599 assert ( 600 json.dumps( 601 m.textMapObj( 602 explorationOrder=( 603 0, 604 [ 605 'East', 606 'West', 607 'South', 608 'return', 609 'Northeast', 610 'Southeast', 611 'North', 612 ] 613 ) 614 ), 615 indent=4 616 ) 617 == """\ 618{ 619 "0::East": { 620 "1::West": "0", 621 "1::North": { 622 "5::South": { 623 "2::Southeast": "1", 624 "2::North": "5" 625 }, 626 "5::West": "0", 627 "5::East": "1" 628 }, 629 "1::East": { 630 "4::return": "1" 631 }, 632 "1::WestBelow": "0" 633 }, 634 "0::South": { 635 "3::return": "0" 636 }, 637 "0::Northeast": "2", 638 "0::North": "5", 639 "0::EastBelow": "1" 640}""" 641 ) 642 643 m.addTransition( 644 'd', 645 'failure', 646 m.endingID('failure') 647 ) 648 assert set(m) == set(range(7)) 649 assert m.namesListing(m) == """\ 650 0 (a) 651 1 (b) 652 2 (c) 653 3 (_u.0) 654 4 (_u.1) 655 5 (d) 656 6 (endings//failure) 657""" 658 assert ( 659 m.destinationsFrom('d') 660 == {'West': 0, 'South': 2, 'East': 1, 'failure': 6} 661 ) 662 assert m.destinationsFrom('failure') == {}
Multi-method test for exploration.core.DecisionGraph
.
665def test_DGTagsAndAnnotations() -> None: 666 """ 667 Test for tagging decisions and transitions in an 668 `exploration.core.DecisionGraph`. 669 """ 670 m = core.DecisionGraph() 671 m.addDecision('a') 672 m.addDecision('b', tags={'grass': 1}) 673 m.addDecision('c') 674 m.tagDecision('c', {'water': 1, 'big': 1}) 675 m.annotateDecision('c', "This is a note.") 676 m.addTransition('a', 'East', 'b', 'West') 677 m.addTransition( 678 'a', 'South', 'c', 'North', 679 {'green': 1}, ["Requires green key"], 680 {'blue': 1}, ["Requires blue key"] 681 ) 682 m.tagTransition('a', 'South', 'green') 683 m.annotateTransition('a', 'South', "a2") 684 m.tagTransition('c', 'North', 'blue', 1) 685 m.annotateTransition('c', 'North', ["Requires", "blue key"]) 686 687 assert m.decisionTags('a') == {} 688 assert m.decisionTags('b') == {'grass': 1} 689 assert m.decisionTags('c') == {'water': 1, 'big': 1} 690 assert m.transitionTags('a', 'East') == {} 691 assert m.transitionTags('a', 'South') == {'green': 1} 692 assert m.transitionTags('b', 'West') == {} 693 assert m.transitionTags('c', 'North') == {'blue': 1} 694 695 assert m.decisionAnnotations('a') == [] 696 assert m.decisionAnnotations('b') == [] 697 assert m.decisionAnnotations('c') == ["This is a note."] 698 assert m.transitionAnnotations('a', 'East') == [] 699 assert m.transitionAnnotations('a', 'South') == [ 700 "Requires green key", 701 "a2" 702 ] 703 assert m.transitionAnnotations('b', 'West') == [] 704 assert m.transitionAnnotations('c', 'North') == [ 705 "Requires blue key", 706 "Requires", 707 "blue key" 708 ] 709 710 m.tagDecision('a', 'grass') 711 assert m.decisionTags('a') == {'grass': 1} 712 713 m.untagDecision('b', 'grass') 714 assert m.decisionTags('b') == {} 715 716 m.annotateDecision('a', "Starting location.") 717 assert m.decisionAnnotations('a') == ["Starting location."] 718 719 m.annotateDecision('c', "Blue key here.") 720 assert m.decisionAnnotations('c') == ["This is a note.", "Blue key here."] 721 722 assert m.decisionAnnotations('b') == [] 723 724 m.tagTransition('a', 'East', 'open') 725 assert m.transitionTags('a', 'East') == {'open': 1} 726 m.tagTransition('a', 'South', 'open') 727 assert m.transitionTags('a', 'South') == {'green': 1, 'open': 1} 728 m.untagTransition('a', 'South', 'green') 729 assert m.transitionTags('a', 'South') == {'open': 1} 730 m.tagTransition('a', 'South', 'open', [1, 2, 3]) 731 assert m.transitionTags('a', 'South') == {'open': [1, 2, 3]} 732 733 m.annotateTransition('c', 'North', "This was difficult.") 734 assert ( 735 m.transitionAnnotations('c', 'North') 736 == ['Requires blue key', 'Requires', 'blue key', 'This was difficult.'] 737 ) 738 cna = m.transitionAnnotations('c', 'North') 739 cna.clear() 740 cna.append('hi') 741 assert m.transitionAnnotations('c', 'North') == ['hi'] 742 743 with pytest.raises(core.MissingTransitionError): 744 _ = m.transitionAnnotations('a', 'West')
Test for tagging decisions and transitions in an
exploration.core.DecisionGraph
.
747def test_DiscreteExploration() -> None: 748 "Multi-method test for `exploration.core.DiscreteExploration`." 749 e = core.DiscreteExploration() 750 751 s0 = e.getSituation(0) 752 assert e.getSituation() is s0 753 754 assert len(s0.graph) == 0 755 assert e.getActiveDecisions(0) == set() 756 assert s0.state == base.emptyState() 757 assert s0.type == "pending" 758 assert s0.action is None 759 760 assert len(e) == 1 761 762 e.start('a') 763 e.observeAll('a', 'North', 'East', 'South') 764 765 assert len(e) == 2 766 767 s0 = e.getSituation(0) 768 s1 = e.getSituation(1) 769 assert s1 is e.getSituation() 770 assert len(s1.graph) == 4 771 assert set(s1.graph) == set(range(4)) 772 assert s1.graph.namesListing(s1.graph) == """\ 773 0 (a) 774 1 (_u.0) 775 2 (_u.1) 776 3 (_u.2) 777""" 778 assert e.getActiveDecisions() == e.getActiveDecisions(1) 779 assert e.getActiveDecisions() == {0} 780 assert s0.type == "imposed" 781 assert s0.action == ( 782 'start', 783 0, 784 0, 785 'main', 786 None, 787 None, 788 None 789 ) 790 791 e.explore('East', 'b', 'West') 792 e.observeAll('b', 'North', 'South') 793 e.getSituation().graph.removeTransition('_u.3', 'return') # truly one-way 794 795 assert len(e) == 3 796 assert set(s1.graph) == set(range(4)) 797 assert s1.graph.namesListing(s1.graph) == """\ 798 0 (a) 799 1 (_u.0) 800 2 (_u.1) 801 3 (_u.2) 802""" 803 assert e.getActiveDecisions(1) == {0} 804 s1 = e.getSituation(1) 805 assert s1.type == 'active' 806 dz = base.DefaultZone 807 assert s1.action == ( 808 'explore', 809 'active', 810 0, 811 ('East', []), 812 'b', 813 'West', 814 dz 815 ) 816 817 s2 = e.getSituation(2) 818 assert s2 is e.getSituation() 819 assert set(s2.graph) == set(range(6)) 820 assert s2.graph.namesListing(s2.graph) == """\ 821 0 (a) 822 1 (_u.0) 823 2 (b) 824 3 (_u.2) 825 4 (_u.3) 826 5 (_u.4) 827""" 828 assert e.getActiveDecisions() == {2} 829 assert s2.type == "pending" 830 assert s2.action is None 831 assert (s2.graph.destinationsFrom('a') == { 832 'North': 1, 833 'East': 2, 834 'South': 3 835 }) 836 assert (s2.graph.destinationsFrom('b') == { 837 'North': 4, 838 'West': 0, 839 'South': 5 840 }) 841 assert (s2.graph.destinationsFrom('_u.3') == {}) 842 assert (s2.graph.destinationsFrom('_u.4') == {'return': 2}) 843 844 with pytest.raises(core.ExplorationStatusError): 845 e.returnTo('West', 'a', 'North') 846 847 with pytest.raises(core.ExplorationStatusError): 848 e.returnTo('West', 'a', 'East') 849 # (would need to use retrace instead) 850 851 e.explore('North', 'c', None) 852 e.observe('c', 'West') 853 854 assert len(e) == 4 855 assert e.getSituation(0) == s0 856 assert e.getSituation(1) == s1 857 assert set(s1.graph) == set(range(4)) 858 assert s1.graph.namesListing(s1.graph) == """\ 859 0 (a) 860 1 (_u.0) 861 2 (_u.1) 862 3 (_u.2) 863""" 864 assert e.getActiveDecisions(1) == {0} 865 assert s1.type == "active" 866 assert s1.action == ('explore', 'active', 0, ('East', []), 'b', 'West', dz) 867 ns2 = e.getSituation(2) 868 assert ns2 != s2 869 assert ns2.graph == s2.graph 870 assert ns2.state == s2.state 871 assert s2.type, ns2.type == ('pending', 'active') 872 assert s2.action is None 873 assert ns2.tags == s2.tags 874 assert ns2.annotations == s2.annotations 875 s2 = ns2 876 assert set(s2.graph) == set(range(6)) 877 assert s2.graph.namesListing(s2.graph) == """\ 878 0 (a) 879 1 (_u.0) 880 2 (b) 881 3 (_u.2) 882 4 (_u.3) 883 5 (_u.4) 884""" 885 assert e.getActiveDecisions(2) == {2} 886 assert s2.type == "active" 887 assert s2.action == ('explore', 'active', 2, ('North', []), 'c', None, dz) 888 assert (s2.graph.destinationsFrom('a') == { 889 'North': 1, 890 'East': 2, 891 'South': 3 892 }) 893 assert (s2.graph.destinationsFrom('b') == { 894 'North': 4, 895 'West': 0, 896 'South': 5 897 }) 898 s3 = e.getSituation(3) 899 assert set(s3.graph) == set(range(7)) 900 assert s3.graph.namesListing(s3.graph) == """\ 901 0 (a) 902 1 (_u.0) 903 2 (b) 904 3 (_u.2) 905 4 (c) 906 5 (_u.4) 907 6 (_u.5) 908""" 909 assert e.getActiveDecisions(3) == {4} 910 assert s3.type == "pending" 911 assert s3.action is None 912 assert (s3.graph.destinationsFrom('a') == { 913 'North': 1, 914 'East': 2, 915 'South': 3 916 }) 917 assert (s3.graph.destinationsFrom('b') == { 918 'North': 4, 919 'West': 0, 920 'South': 5 921 }) 922 assert s3.graph.destinationsFrom('c') == {'West': 6} 923 assert s3.graph.destinationsFrom('_u.0') == {'return': 0} 924 assert s3.graph.destinationsFrom('_u.2') == {'return': 0} 925 assert s3.graph.destinationsFrom('_u.4') == {'return': 2} 926 assert s3.graph.destinationsFrom('_u.5') == {'return': 4} 927 assert s3.graph.degree(0) == 6 928 assert s3.graph.degree(2) == 5 929 assert s3.graph.degree(4) == 3 930 assert s3.graph.degree(6) == 2 931 932 e.explore('West', 'd', 'East') 933 934 assert len(e) == 5 935 s3 = e.getSituation(3) 936 s4 = e.getSituation(4) 937 assert set(s4.graph) == set(range(7)) 938 assert s4.graph.namesListing(s4.graph) == """\ 939 0 (a) 940 1 (_u.0) 941 2 (b) 942 3 (_u.2) 943 4 (c) 944 5 (_u.4) 945 6 (d) 946""" 947 assert e.getActiveDecisions(4) == {6} 948 assert s4.type == "pending" 949 assert s4.action is None 950 assert s4.graph.destinationsFrom('c') == {'West': 6} 951 assert s4.graph.destinationsFrom('d') == {'East': 4} 952 assert s4.graph.degree(4) == 3 953 assert s4.graph.degree(6) == 2 954 955 # Can't return if there's no outgoing edge yet 956 with pytest.raises(core.MissingTransitionError): 957 e.returnTo('South', 'a', 'North') 958 959 with pytest.raises(core.ExplorationStatusError): 960 e.returnTo('East', 'a', 'North') 961 962 # Add the edge and then we can use it to return 963 g = s4.graph 964 g.addUnexploredEdge('d', 'South') 965 assert set(g) == set(range(8)) 966 assert g.namesListing(g) == """\ 967 0 (a) 968 1 (_u.0) 969 2 (b) 970 3 (_u.2) 971 4 (c) 972 5 (_u.4) 973 6 (d) 974 7 (_u.6) 975""" 976 e.returnTo('South', 'a', 'North') 977 978 assert len(e) == 6 979 s4 = e.getSituation(4) 980 s5 = e.getSituation(5) 981 assert s5 == e.getSituation() 982 assert set(s5.graph) == set([0, 2, 3, 4, 5, 6]) 983 assert s5.graph.namesListing(s5.graph) == """\ 984 0 (a) 985 2 (b) 986 3 (_u.2) 987 4 (c) 988 5 (_u.4) 989 6 (d) 990""" 991 assert e.getActiveDecisions(5) == {0} 992 assert s5.type == "pending" 993 assert s5.action is None 994 assert s5.graph.destinationsFrom('a') == { 995 'East': 2, 996 'North': 6, 997 'South': 3 998 } 999 assert s5.graph.destinationsFrom('d') == {'East': 4, 'South': 0} 1000 assert s5.graph.degree(4) == 3 1001 assert s5.graph.degree(6) == 4 1002 assert s5.graph.degree(0) == 6 1003 1004 e.wait() 1005 1006 assert len(e) == 7 1007 s5 = e.getSituation(5) 1008 s6 = e.getSituation(6) 1009 assert set(s6.graph) == set([0, 2, 3, 4, 5, 6]) 1010 assert s6.graph.namesListing(s6.graph) == """\ 1011 0 (a) 1012 2 (b) 1013 3 (_u.2) 1014 4 (c) 1015 5 (_u.4) 1016 6 (d) 1017""" 1018 assert e.getActiveDecisions(6) == {0} 1019 assert s6.type == "pending" 1020 assert s6.action is None 1021 1022 assert s5.action == ('noAction',) 1023 1024 e.takeAction( 1025 'powerUp', 1026 consequence=[ 1027 base.effect(gain='power'), 1028 base.effect(gain=('token', 2)), 1029 ], 1030 fromDecision=0 1031 ) 1032 1033 assert len(e) == 8 1034 s6 = e.getSituation(6) 1035 s7 = e.getSituation(7) 1036 assert set(s7.graph) == set([0, 2, 3, 4, 5, 6]) 1037 assert s7.graph.namesListing(s7.graph) == """\ 1038 0 (a) 1039 2 (b) 1040 3 (_u.2) 1041 4 (c) 1042 5 (_u.4) 1043 6 (d) 1044""" 1045 assert e.getActiveDecisions(7) == {0} 1046 assert base.hasCapabilityOrEquivalent( 1047 'power', 1048 base.genericContextForSituation(s7) 1049 ) 1050 assert base.combinedTokenCount(s7.state, 'token') == 2 1051 assert base.effectiveCapabilitySet(s7.state) == { 1052 "capabilities": {"power"}, 1053 "tokens": {"token": 2}, 1054 "skills": {} 1055 } 1056 assert s7.type == "pending" 1057 assert s7.action is None 1058 assert (s7.graph.destinationsFrom('a') == { 1059 'North': 6, 1060 'East': 2, 1061 'South': 3, 1062 'powerUp': 0 1063 }) 1064 assert s6.action == ('take', 'active', 0, ('powerUp', [])) 1065 1066 e.retrace('East') 1067 1068 assert len(e) == 9 1069 s7 = e.getSituation(7) 1070 s8 = e.getSituation(8) 1071 assert set(s8.graph) == set([0, 2, 3, 4, 5, 6]) 1072 assert s8.graph.namesListing(s8.graph) == """\ 1073 0 (a) 1074 2 (b) 1075 3 (_u.2) 1076 4 (c) 1077 5 (_u.4) 1078 6 (d) 1079""" 1080 assert e.getActiveDecisions(8) == {2} 1081 assert s8.type == "pending" 1082 assert s8.action is None 1083 assert base.effectiveCapabilitySet(s8.state) == { 1084 "capabilities": {"power"}, 1085 "tokens": {"token": 2}, 1086 "skills": {} 1087 } 1088 assert s7.action == ('take', 'active', 0, ('East', [])) 1089 1090 e.observeMechanisms('d', ('gate', 'closed')) 1091 gateD = base.mechanismAt('gate', decision='d') 1092 gateA = base.mechanismAt('gate', decision='a') 1093 assert gateD == (None, None, 'd', 'gate') 1094 assert gateA == (None, None, 'a', 'gate') 1095 assert base.mechanismInStateOrEquivalent( 1096 'gate', 1097 'closed', 1098 base.genericContextForSituation(s8) 1099 ) 1100 assert e.mechanismState('gate') == "closed" 1101 assert e.mechanismState(gateD) == "closed" 1102 1103 # Can't get mechanism state in step before it's been observed 1104 with pytest.raises(core.MissingMechanismError): 1105 e.mechanismState('gate', step=6) 1106 1107 # Can't get mechanism state for mechanism at a different decision 1108 assert e.mechanismState(gateA) == "closed" 1109 1110 e.observeMechanisms('a', 'gate') # without starting state 1111 with pytest.raises(core.MechanismCollisionError): 1112 e.mechanismState('gate') 1113 1114 with pytest.raises(core.MechanismCollisionError): 1115 e.mechanismState(base.mechanismAt('gate', decision='c')) 1116 1117 # Default state 1118 assert e.mechanismState(gateA) == 'off' 1119 assert e.mechanismState(gateD) == 'closed' 1120 1121 e.warp( 1122 'd', 1123 consequence=[ 1124 base.effect(lose=('token', 1)), 1125 base.effect(set=('gate', 'open')) # knows to open gate at 'd' 1126 ] 1127 ) 1128 1129 assert len(e) == 10 1130 s8 = e.getSituation(8) 1131 s9 = e.getSituation(9) 1132 assert s8.action == ("warp", "active", 6) 1133 assert set(s9.graph) == set([0, 2, 3, 4, 5, 6]) 1134 assert s9.graph.namesListing(s9.graph) == """\ 1135 0 (a) 1136 2 (b) 1137 3 (_u.2) 1138 4 (c) 1139 5 (_u.4) 1140 6 (d) 1141""" 1142 assert e.getActiveDecisions(9) == {6} 1143 assert base.effectiveCapabilitySet(s9.state) == { 1144 "capabilities": {"power"}, 1145 "tokens": {"token": 1}, 1146 "skills": {} 1147 } 1148 assert s9.type == "pending" 1149 assert s9.action is None 1150 assert (s9.graph.destinationsFrom('a') == { 1151 'North': 6, 1152 'East': 2, 1153 'South': 3, 1154 'powerUp': 0 1155 }) 1156 assert (s9.graph.destinationsFrom('b') == { 1157 'North': 4, 1158 'West': 0, 1159 'South': 5 1160 }) 1161 assert (s9.graph.destinationsFrom('c') == {'West': 6}) 1162 assert (s9.graph.destinationsFrom('d') == {'East': 4, 'South': 0}) 1163 1164 ctx1 = base.genericContextForSituation(s1) 1165 ctx8 = base.genericContextForSituation(s8) 1166 ctx9 = base.genericContextForSituation(s9) 1167 with pytest.raises(core.MissingDecisionError): 1168 base.mechanismInStateOrEquivalent(gateD, 'open', ctx1) 1169 # Decision 'd' doesn't exist back then (neither does the gate) 1170 assert not base.mechanismInStateOrEquivalent(gateD, 'open', ctx8) 1171 assert base.mechanismInStateOrEquivalent(gateD, 'open', ctx9) 1172 1173 with pytest.raises(core.MissingMechanismError): 1174 e.mechanismState(gateA, step=1) 1175 1176 assert e.mechanismState(gateA) == 'off' 1177 assert e.mechanismState(gateD) == 'open' 1178 assert e.mechanismState(gateD, step=8) == 'closed'
Multi-method test for exploration.core.DiscreteExploration
.
1181def test_exploring_with_zones() -> None: 1182 """ 1183 A test for exploring with zones being applied. 1184 """ 1185 e = core.DiscreteExploration() 1186 1187 assert e.start('start') == 0 1188 graph = e.getSituation().graph 1189 graph.createZone('zone', 0) 1190 graph.addDecisionToZone('start', 'zone') 1191 e.observe(0, 'transition') 1192 assert e.explore('transition', 'room') == 1 1193 1194 s = e.getSituation() 1195 g = s.graph 1196 assert g.zoneParents(0) == {'zone'} 1197 assert g.zoneParents(1) == {'zone'} 1198 1199 e.observeAll(1, 'out', 'down') 1200 unknown = g.destination('room', 'down') 1201 g.renameDecision(unknown, 'fourth_room') 1202 assert g.nameFor(3) == 'fourth_room' 1203 assert g.zoneParents(3) == set() 1204 assert not g.isConfirmed('fourth_room') 1205 assert not e.hasBeenVisited('fourth_room') 1206 assert e.explore('out', 'another_room', 'back', 'zone2') == 2 1207 e.retrace('back') 1208 e.explore('down', None, 'up') # already named 'fourth_room' 1209 1210 g = e.getSituation().graph 1211 assert g.nameFor(3) == 'fourth_room' 1212 assert g.zoneParents(0) == {'zone'} 1213 assert g.zoneParents(1) == {'zone'} 1214 assert g.zoneParents(2) == {'zone2'} 1215 assert g.zoneParents(3) == {'zone'}
A test for exploring with zones being applied.
1218def test_triggers() -> None: 1219 e = core.DiscreteExploration() 1220 e.start('start') 1221 assert e.primaryDecision() == 0 1222 e.takeAction( 1223 'shiver', 1224 requires=base.ReqNot(base.ReqCapability('jacket')), 1225 consequence=[base.effect(gain=('cold', 1))], 1226 fromDecision='start' 1227 ) 1228 e.getSituation().graph.tagTransition('start', 'shiver', 'trigger') 1229 assert e.tokenCountNow('cold') == 1 1230 e.observe('start', 'right') 1231 e.explore('right', 'room', 'left') 1232 assert e.primaryDecision() == 1 1233 assert e.tokenCountNow('cold') == 1 1234 e.takeAction( 1235 'warmUp', 1236 requires=base.ReqTokens('cold', 1), 1237 consequence=[base.effect(lose=('cold', 1))], 1238 fromDecision='room' 1239 ) 1240 assert e.tokenCountNow('cold') == 0 1241 e.getSituation().graph.tagTransition('room', 'warmUp', 'trigger') 1242 e.wait() 1243 assert e.tokenCountNow('cold') == 0 1244 e.retrace('left') 1245 assert e.primaryDecision() == 0 1246 assert e.tokenCountNow('cold') == 1 1247 e.wait() 1248 assert e.tokenCountNow('cold') == 2 1249 e.wait() 1250 assert e.tokenCountNow('cold') == 3 1251 e.retrace('right') 1252 assert e.tokenCountNow('cold') == 2 1253 e.wait() 1254 assert e.tokenCountNow('cold') == 1 1255 e.wait() 1256 # No issue with trigger when out of tokens because of its requirement 1257 assert e.tokenCountNow('cold') == 0 1258 e.wait() 1259 assert e.tokenCountNow('cold') == 0 1260 # Get a jacket 1261 e.applyExtraneousEffect(base.effect(gain='jacket')) 1262 e.retrace('left') 1263 # Jacket prevents trigger 1264 assert e.primaryDecision() == 0 1265 assert e.tokenCountNow('cold') == 0 1266 e.wait() 1267 assert e.tokenCountNow('cold') == 0 1268 # TODO: Add a trigger group...