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 # Mechanism requirement w/ decision ID 254 switchAt0 = base.MechanismSpecifier(None, None, 0, 'switch') 255 switchAt1 = base.MechanismSpecifier(None, None, 1, 'switch') 256 r = base.ReqMechanism(switchAt0, 'on') 257 assert not r.satisfied(withSwitch) 258 assert r.satisfied(rcWith(withSwitch, switch="on")) 259 260 # 'switch' at different specific decision won't search 261 r2 = base.ReqMechanism(switchAt1, 'on') 262 assert not r.satisfied(withSwitch) 263 assert r.satisfied(rcWith(withSwitch, switch="on")) 264 265 # Skill level requirements 266 r = base.ReqLevel('skill', 1) 267 r2 = base.ReqNot(base.ReqLevel('skill', 2)) 268 assert not r.satisfied(trc) 269 assert not r.satisfied(rcWith(trc, skill=("skill", 0))) 270 assert r.satisfied(rcWith(trc, skill=("skill", 1))) 271 assert r.satisfied(rcWith(trc, skill=("skill", 2))) 272 assert r.satisfied(rcWith(trc, skill=("skill", 10))) 273 assert r2.satisfied(rcWith(trc, skill=("skill", 1))) 274 assert not r2.satisfied(rcWith(trc, skill=("skill", 2))) 275 assert not r2.satisfied(rcWith(trc, skill=("skill", 10))) 276 277 # Tag requirements 278 r = base.ReqTag('tag', 1) 279 r2 = base.ReqTag('tag2', 'value') 280 assert not r.satisfied(trc) 281 assert not r2.satisfied(trc) 282 tagged = rcWith(trc, tag=("tag", 1)) 283 assert tagged.graph.decisionTags(0) == {'tag': 1} 284 assert r.satisfied(tagged) 285 assert not r.satisfied(rcWith(trc, tag=("tag", 2))) 286 assert not r.satisfied(rcWith(trc, tag=("tag", 0))) 287 assert r2.satisfied(rcWith(trc, tag2=("tag", 'value'))) 288 taggedAside = copy.deepcopy(trc) 289 taggedAside.graph.tagDecision(1, 'tag', 1) 290 taggedAside.graph.tagDecision(2, 'tag2', 'value') 291 assert not r.satisfied(taggedAside) 292 assert not r2.satisfied(taggedAside) 293 taggedAside.state['common']['activeDecisions']['main'] = 1 294 assert r.satisfied(taggedAside) 295 assert not r2.satisfied(taggedAside) 296 taggedAside.state['common']['activeDecisions']['main'] = 2 297 assert not r.satisfied(taggedAside) 298 assert r2.satisfied(taggedAside) 299 zoneTagged = copy.deepcopy(trc) 300 zoneTagged.graph.createZone('zone') 301 zoneTagged.graph.addDecisionToZone(0, 'zone') 302 zoneTagged.graph.tagZone('zone', 'tag', 1) 303 assert r.satisfied(zoneTagged) 304 assert not r2.satisfied(zoneTagged) 305 zoneTagged.graph.removeDecisionFromZone(0, 'zone') 306 assert not r.satisfied(zoneTagged) 307 308 # Tests of parsing: 309 assert pf.parseRequirement('a') == base.ReqCapability('a') 310 311 assert pf.parseRequirement('(a)') == base.ReqCapability('a') 312 313 assert pf.parseRequirement('a*5') == base.ReqTokens('a', 5) 314 315 assert pf.parseRequirement('((a*5))') == base.ReqTokens('a', 5) 316 317 assert pf.parseRequirement('(a|b)&c*3') == base.ReqAll([ 318 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 319 base.ReqTokens('c', 3) 320 ]) 321 322 assert pf.parseRequirement(' ( a | b )\t& c * 3 ') == base.ReqAll([ 323 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 324 base.ReqTokens('c', 3) 325 ]) 326 327 assert pf.parseRequirement('a|(b&c*3)') == base.ReqAny([ 328 base.ReqCapability('a'), 329 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 330 ]) 331 332 assert pf.parseRequirement('a|b&c*3') == base.ReqAny([ 333 base.ReqCapability('a'), 334 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 335 ]) 336 337 assert pf.parseRequirement('a&b|c*3') == base.ReqAny([ 338 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]), 339 base.ReqTokens('c', 3) 340 ]) 341 342 assert pf.parseRequirement('a|b|c') == base.ReqAny([ 343 base.ReqCapability('a'), 344 base.ReqCapability('b'), 345 base.ReqCapability('c'), 346 ]) 347 348 assert pf.parseRequirement('a&b&c&d') == base.ReqAll([ 349 base.ReqCapability('a'), 350 base.ReqCapability('b'), 351 base.ReqCapability('c'), 352 base.ReqCapability('d'), 353 ]) 354 355 assert pf.parseRequirement('a&b|c&d') == base.ReqAny([ 356 base.ReqAll([ 357 base.ReqCapability('a'), 358 base.ReqCapability('b') 359 ]), 360 base.ReqAll([ 361 base.ReqCapability('c'), 362 base.ReqCapability('d') 363 ]) 364 ]) 365 366 assert pf.parseRequirement('a&!b|!c&d') == base.ReqAny([ 367 base.ReqAll([ 368 base.ReqCapability('a'), 369 base.ReqNot(base.ReqCapability('b')) 370 ]), 371 base.ReqAll([ 372 base.ReqNot(base.ReqCapability('c')), 373 base.ReqCapability('d') 374 ]) 375 ]) 376 377 assert pf.parseRequirement('!(a|b)&c') == base.ReqAll([ 378 base.ReqNot( 379 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]) 380 ), 381 base.ReqCapability('c') 382 ]) 383 384 assert pf.parseRequirement('!a&b&c') == base.ReqAll([ 385 base.ReqNot(base.ReqCapability('a')), 386 base.ReqCapability('b'), 387 base.ReqCapability('c') 388 ]) 389 390 assert pf.parseRequirement('!(a&b)&c') == base.ReqAll([ 391 base.ReqNot( 392 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]) 393 ), 394 base.ReqCapability('c') 395 ]) 396 397 assert pf.parseRequirement('!c*3') == base.ReqNot(base.ReqTokens('c', 3)) 398 399 assert pf.parseRequirement('X') == base.ReqImpossible() 400 401 assert pf.parseRequirement('O') == base.ReqNothing() 402 403 assert pf.parseRequirement('X|a') == base.ReqAny([ 404 base.ReqImpossible(), 405 base.ReqCapability('a') 406 ]) 407 408 assert pf.parseRequirement('door:open') == base.ReqMechanism('door', 'open') 409 410 assert pf.parseRequirement('z::d::door:open') == base.ReqMechanism( 411 base.MechanismSpecifier(None, 'z', 'd', 'door'), 412 'open' 413 ) 414 415 assert pf.parseRequirement('3::door:open') == base.ReqMechanism( 416 base.MechanismSpecifier(None, None, 3, 'door'), 417 'open' 418 ) 419 420 with pytest.raises(parsing.ParseError): 421 pf.parseRequirement('a*3*2') 422 423 with pytest.raises(parsing.ParseError): 424 pf.parseRequirement('(a):2') 425 426 with pytest.raises(parsing.ParseError): 427 pf.parseRequirement('(a|b&c):3') 428 429 with pytest.raises(parsing.ParseError): 430 pf.parseRequirement('a|&b') 431 432 with pytest.raises(parsing.ParseError): 433 pf.parseRequirement('(a|b') 434 435 with pytest.raises(parsing.ParseError): 436 pf.parseRequirement('a|b)') 437 438 assert (pf.parseRequirement('a*-3') == base.ReqTokens('a', -3)) 439 440 with pytest.raises(parsing.ParseError): 441 pf.parseRequirement('a*!3') 442 443 with pytest.raises(parsing.ParseError): 444 pf.parseRequirement('a!b') 445 446 with pytest.raises(parsing.ParseError): 447 pf.parseRequirement('a*-b') 448 449 450def test_DecisionGraph() -> None: 451 "Multi-method test for `exploration.core.DecisionGraph`." 452 m = core.DecisionGraph() 453 m.addDecision('a') 454 m.addDecision('b') 455 m.addDecision('c') 456 assert len(m) == 3 457 assert set(m) == {0, 1, 2} 458 459 m.addTransition('a', 'East', 'b', 'West') 460 m.addTransition('a', 'Northeast', 'c') 461 m.addTransition('c', 'Southeast', 'b') 462 463 assert m.destinationsFrom('a') == {'East': 1, 'Northeast': 2} 464 assert m.destinationsFrom('b') == {'West': 0} 465 assert m.destinationsFrom('c') == {'Southeast': 1} 466 467 assert m.getReciprocal('a', 'East') == 'West' 468 assert m.getReciprocal('a', 'Northeast') is None 469 assert m.getReciprocal('b', 'Southwest') is None 470 assert m.getReciprocal('c', 'Southeast') is None 471 472 m.addUnexploredEdge('a', 'South') 473 m.addUnexploredEdge('b', 'East') 474 m.addUnexploredEdge('c', 'North') 475 476 assert len(m) == 6 477 assert set(m) == set(range(6)) 478 assert m.namesListing(m) == """\ 479 0 (a) 480 1 (b) 481 2 (c) 482 3 (_u.0) 483 4 (_u.1) 484 5 (_u.2) 485""" 486 assert ( 487 m.destinationsFrom('a') 488 == {'East': 1, 'Northeast': 2, 'South': 3} 489 ) 490 assert (m.destinationsFrom('b') == {'West': 0, 'East': 4}) 491 assert (m.destinationsFrom('c') == {'Southeast': 1, 'North': 5}) 492 493 m.replaceUnconfirmed('c', 'North', 'd', 'South') 494 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 495 assert m.nameFor(5) == 'd' 496 assert len(m) == 6 497 assert set(m) == set(range(6)) 498 assert m.namesListing(m) == """\ 499 0 (a) 500 1 (b) 501 2 (c) 502 3 (_u.0) 503 4 (_u.1) 504 5 (d) 505""" 506 507 m.addTransition('d', 'West', 'a', 'North') 508 assert ( 509 m.destinationsFrom('a') 510 == {'East': 1, 'Northeast': 2, 'South': 3, 'North': 5} 511 ) 512 assert ( 513 m.destinationsFrom('d') 514 == {'West': 0, 'South': 2} 515 ) 516 517 with pytest.raises(core.MissingDecisionError): 518 _ = m.destinationsFrom('z') 519 520 with pytest.raises(core.TransitionCollisionError): 521 m.addTransition('a', 'East', 'b', 'West') 522 523 with pytest.raises(core.TransitionCollisionError): 524 m.addTransition('c', 'East', 'b', 'West') 525 526 with pytest.raises(core.TransitionCollisionError): 527 m.addUnexploredEdge('a', 'East') 528 529 with pytest.raises(core.TransitionCollisionError): 530 m.addUnexploredEdge('c', 'North') 531 532 with pytest.raises(core.MissingTransitionError): 533 m.replaceUnconfirmed('a', 'Up', 'z') 534 535 with pytest.raises(core.ExplorationStatusError): 536 m.replaceUnconfirmed('a', 'East', 'z') 537 538 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 539 540 m.addTransition('a', 'EastBelow', 'b', 'WestBelow') 541 assert (m.destinationsFrom('a') == { 542 'East': 1, 543 'EastBelow': 1, 544 'Northeast': 2, 545 'South': 3, 546 'North': 5 547 }) 548 assert ( 549 m.destinationsFrom('b') 550 == {'West': 0, 'WestBelow': 0, 'East': 4} 551 ) 552 553 # Two edges that could be but are not reciprocals 554 m.addTransition('d', 'East', 'b') 555 m.addTransition('b', 'North', 'd') 556 assert ( 557 m.destinationsFrom('b') 558 == {'West': 0, 'WestBelow': 0, 'East': 4, 'North': 5} 559 ) 560 assert ( 561 m.destinationsFrom('d') 562 == {'West': 0, 'South': 2, 'East': 1} 563 ) 564 assert m.getReciprocal('b', 'North') is None 565 assert m.getReciprocal('d', 'East') is None 566 567 # Establish a reciprocal relationship 568 m.setReciprocal('b', 'North', 'East') 569 assert m.getReciprocal('b', 'North') == 'East' 570 assert m.getReciprocal('d', 'East') == 'North' 571 572 with pytest.raises(core.MissingDecisionError): 573 m.setReciprocal('z', 'Nope', 'None') 574 575 with pytest.raises(core.MissingTransitionError): 576 m.setReciprocal('b', 'Nope', 'None') 577 578 # Remove the reciprocal relationship again (from the other side) 579 m.setReciprocal('d', 'East', None) 580 assert m.getReciprocal('b', 'North') is None 581 assert m.getReciprocal('d', 'East') is None 582 583 with pytest.raises(core.InvalidDestinationError): 584 m.setReciprocal('b', 'North', 'West') 585 586 with pytest.raises(core.MissingTransitionError): 587 m.setReciprocal('b', 'North', 'None') 588 589 assert (m.isConfirmed("_u.0") is False) 590 assert (m.isConfirmed("_u.1") is False) 591 assert (m.isConfirmed("a") is True) 592 assert (m.isConfirmed("d") is True) 593 594 assert ( 595 json.dumps(m.textMapObj(), indent=4) 596 == """\ 597{ 598 "0::East": { 599 "1::West": "0", 600 "1::East": {}, 601 "1::WestBelow": "0", 602 "1::North": { 603 "5::South": { 604 "2::Southeast": "1", 605 "2::North": "5" 606 }, 607 "5::West": "0", 608 "5::East": "1" 609 } 610 }, 611 "0::Northeast": "2", 612 "0::South": {}, 613 "0::North": "5", 614 "0::EastBelow": "1" 615}""" 616 ) 617 618 assert ( 619 json.dumps( 620 m.textMapObj( 621 explorationOrder=( 622 0, 623 [ 624 'East', 625 'West', 626 'Northeast', 627 'Southeast', 628 'North', 629 'West', 630 'South', 631 ] 632 ) 633 ), 634 indent=4 635 ) 636 == """\ 637{ 638 "0::East": { 639 "1::West": "0", 640 "1::North": { 641 "5::West": "0", 642 "5::South": { 643 "2::Southeast": "1", 644 "2::North": "5" 645 }, 646 "5::East": "1" 647 }, 648 "1::East": {}, 649 "1::WestBelow": "0" 650 }, 651 "0::Northeast": "2", 652 "0::South": {}, 653 "0::North": "5", 654 "0::EastBelow": "1" 655}""" 656 ) 657 658 m.addTransition( 659 'd', 660 'failure', 661 m.endingID('failure') 662 ) 663 assert set(m) == set(range(7)) 664 assert m.namesListing(m) == """\ 665 0 (a) 666 1 (b) 667 2 (c) 668 3 (_u.0) 669 4 (_u.1) 670 5 (d) 671 6 (endings//failure) 672""" 673 assert ( 674 m.destinationsFrom('d') 675 == {'West': 0, 'South': 2, 'East': 1, 'failure': 6} 676 ) 677 assert m.destinationsFrom('failure') == {} 678 679 680def test_DGTagsAndAnnotations() -> None: 681 """ 682 Test for tagging decisions and transitions in an 683 `exploration.core.DecisionGraph`. 684 """ 685 m = core.DecisionGraph() 686 m.addDecision('a') 687 m.addDecision('b', tags={'grass': 1}) 688 m.addDecision('c') 689 m.tagDecision('c', {'water': 1, 'big': 1}) 690 m.annotateDecision('c', "This is a note.") 691 m.addTransition('a', 'East', 'b', 'West') 692 m.addTransition( 693 'a', 'South', 'c', 'North', 694 {'green': 1}, ["Requires green key"], 695 {'blue': 1}, ["Requires blue key"] 696 ) 697 m.tagTransition('a', 'South', 'green') 698 m.annotateTransition('a', 'South', "a2") 699 m.tagTransition('c', 'North', 'blue', 1) 700 m.annotateTransition('c', 'North', ["Requires", "blue key"]) 701 702 assert m.decisionTags('a') == {} 703 assert m.decisionTags('b') == {'grass': 1} 704 assert m.decisionTags('c') == {'water': 1, 'big': 1} 705 assert m.transitionTags('a', 'East') == {} 706 assert m.transitionTags('a', 'South') == {'green': 1} 707 assert m.transitionTags('b', 'West') == {} 708 assert m.transitionTags('c', 'North') == {'blue': 1} 709 710 assert m.decisionAnnotations('a') == [] 711 assert m.decisionAnnotations('b') == [] 712 assert m.decisionAnnotations('c') == ["This is a note."] 713 assert m.transitionAnnotations('a', 'East') == [] 714 assert m.transitionAnnotations('a', 'South') == [ 715 "Requires green key", 716 "a2" 717 ] 718 assert m.transitionAnnotations('b', 'West') == [] 719 assert m.transitionAnnotations('c', 'North') == [ 720 "Requires blue key", 721 "Requires", 722 "blue key" 723 ] 724 725 m.tagDecision('a', 'grass') 726 assert m.decisionTags('a') == {'grass': 1} 727 728 m.untagDecision('b', 'grass') 729 assert m.decisionTags('b') == {} 730 731 m.annotateDecision('a', "Starting location.") 732 assert m.decisionAnnotations('a') == ["Starting location."] 733 734 m.annotateDecision('c', "Blue key here.") 735 assert m.decisionAnnotations('c') == ["This is a note.", "Blue key here."] 736 737 assert m.decisionAnnotations('b') == [] 738 739 m.tagTransition('a', 'East', 'open') 740 assert m.transitionTags('a', 'East') == {'open': 1} 741 m.tagTransition('a', 'South', 'open') 742 assert m.transitionTags('a', 'South') == {'green': 1, 'open': 1} 743 m.untagTransition('a', 'South', 'green') 744 assert m.transitionTags('a', 'South') == {'open': 1} 745 m.tagTransition('a', 'South', 'open', [1, 2, 3]) 746 assert m.transitionTags('a', 'South') == {'open': [1, 2, 3]} 747 748 m.annotateTransition('c', 'North', "This was difficult.") 749 assert ( 750 m.transitionAnnotations('c', 'North') 751 == ['Requires blue key', 'Requires', 'blue key', 'This was difficult.'] 752 ) 753 cna = m.transitionAnnotations('c', 'North') 754 cna.clear() 755 cna.append('hi') 756 assert m.transitionAnnotations('c', 'North') == ['hi'] 757 758 with pytest.raises(core.MissingTransitionError): 759 _ = m.transitionAnnotations('a', 'West') 760 761 762def test_DiscreteExploration() -> None: 763 "Multi-method test for `exploration.core.DiscreteExploration`." 764 e = core.DiscreteExploration() 765 766 s0 = e.getSituation(0) 767 assert e.getSituation() is s0 768 769 assert len(s0.graph) == 0 770 assert e.getActiveDecisions(0) == set() 771 assert s0.state == base.emptyState() 772 assert s0.type == "pending" 773 assert s0.action is None 774 775 assert len(e) == 1 776 777 e.start('a') 778 e.observeAll('a', 'North', 'East', 'South') 779 780 assert len(e) == 2 781 782 s0 = e.getSituation(0) 783 s1 = e.getSituation(1) 784 assert s1 is e.getSituation() 785 assert len(s1.graph) == 4 786 assert set(s1.graph) == set(range(4)) 787 assert s1.graph.namesListing(s1.graph) == """\ 788 0 (a) 789 1 (_u.0) 790 2 (_u.1) 791 3 (_u.2) 792""" 793 assert e.getActiveDecisions() == e.getActiveDecisions(1) 794 assert e.getActiveDecisions() == {0} 795 assert s0.type == "imposed" 796 assert s0.action == ( 797 'start', 798 0, 799 0, 800 'main', 801 None, 802 None, 803 None 804 ) 805 806 e.explore('East', 'b', 'West') 807 e.observeAll('b', 'North', 'South') 808 809 assert len(e) == 3 810 assert set(s1.graph) == set(range(4)) 811 assert s1.graph.namesListing(s1.graph) == """\ 812 0 (a) 813 1 (_u.0) 814 2 (_u.1) 815 3 (_u.2) 816""" 817 assert e.getActiveDecisions(1) == {0} 818 s1 = e.getSituation(1) 819 assert s1.type == 'active' 820 dz = base.DefaultZone 821 assert s1.action == ( 822 'explore', 823 'active', 824 0, 825 ('East', []), 826 'b', 827 'West', 828 dz 829 ) 830 831 s2 = e.getSituation(2) 832 assert s2 is e.getSituation() 833 assert set(s2.graph) == set(range(6)) 834 assert s2.graph.namesListing(s2.graph) == """\ 835 0 (a) 836 1 (_u.0) 837 2 (b) 838 3 (_u.2) 839 4 (_u.3) 840 5 (_u.4) 841""" 842 assert e.getActiveDecisions() == {2} 843 assert s2.type == "pending" 844 assert s2.action is None 845 assert (s2.graph.destinationsFrom('a') == { 846 'North': 1, 847 'East': 2, 848 'South': 3 849 }) 850 assert (s2.graph.destinationsFrom('b') == { 851 'North': 4, 852 'West': 0, 853 'South': 5 854 }) 855 assert (s2.graph.destinationsFrom('_u.3') == {}) 856 assert (s2.graph.destinationsFrom('_u.4') == {}) 857 858 with pytest.raises(core.ExplorationStatusError): 859 e.returnTo('West', 'a', 'North') 860 861 with pytest.raises(core.ExplorationStatusError): 862 e.returnTo('West', 'a', 'East') 863 # (would need to use retrace instead) 864 865 e.explore('North', 'c', None) 866 e.observe('c', 'West') 867 868 assert len(e) == 4 869 assert e.getSituation(0) == s0 870 assert e.getSituation(1) == s1 871 assert set(s1.graph) == set(range(4)) 872 assert s1.graph.namesListing(s1.graph) == """\ 873 0 (a) 874 1 (_u.0) 875 2 (_u.1) 876 3 (_u.2) 877""" 878 assert e.getActiveDecisions(1) == {0} 879 assert s1.type == "active" 880 assert s1.action == ('explore', 'active', 0, ('East', []), 'b', 'West', dz) 881 ns2 = e.getSituation(2) 882 assert ns2 != s2 883 assert ns2.graph == s2.graph 884 assert ns2.state == s2.state 885 assert s2.type, ns2.type == ('pending', 'active') 886 assert s2.action is None 887 assert ns2.tags == s2.tags 888 assert ns2.annotations == s2.annotations 889 s2 = ns2 890 assert set(s2.graph) == set(range(6)) 891 assert s2.graph.namesListing(s2.graph) == """\ 892 0 (a) 893 1 (_u.0) 894 2 (b) 895 3 (_u.2) 896 4 (_u.3) 897 5 (_u.4) 898""" 899 assert e.getActiveDecisions(2) == {2} 900 assert s2.type == "active" 901 assert s2.action == ('explore', 'active', 2, ('North', []), 'c', None, dz) 902 assert (s2.graph.destinationsFrom('a') == { 903 'North': 1, 904 'East': 2, 905 'South': 3 906 }) 907 assert (s2.graph.destinationsFrom('b') == { 908 'North': 4, 909 'West': 0, 910 'South': 5 911 }) 912 s3 = e.getSituation(3) 913 assert set(s3.graph) == set(range(7)) 914 assert s3.graph.namesListing(s3.graph) == """\ 915 0 (a) 916 1 (_u.0) 917 2 (b) 918 3 (_u.2) 919 4 (c) 920 5 (_u.4) 921 6 (_u.5) 922""" 923 assert e.getActiveDecisions(3) == {4} 924 assert s3.type == "pending" 925 assert s3.action is None 926 assert (s3.graph.destinationsFrom('a') == { 927 'North': 1, 928 'East': 2, 929 'South': 3 930 }) 931 assert (s3.graph.destinationsFrom('b') == { 932 'North': 4, 933 'West': 0, 934 'South': 5 935 }) 936 assert s3.graph.destinationsFrom('c') == {'West': 6} 937 assert s3.graph.destinationsFrom('_u.0') == {} 938 assert s3.graph.destinationsFrom('_u.2') == {} 939 assert s3.graph.destinationsFrom('_u.4') == {} 940 assert s3.graph.destinationsFrom('_u.5') == {} 941 assert s3.graph.degree(0) == 4 942 assert s3.graph.degree(2) == 4 943 assert s3.graph.degree(4) == 2 944 assert s3.graph.degree(6) == 1 945 946 e.explore('West', 'd', 'East') 947 948 assert len(e) == 5 949 s3 = e.getSituation(3) 950 s4 = e.getSituation(4) 951 assert set(s4.graph) == set(range(7)) 952 assert s4.graph.namesListing(s4.graph) == """\ 953 0 (a) 954 1 (_u.0) 955 2 (b) 956 3 (_u.2) 957 4 (c) 958 5 (_u.4) 959 6 (d) 960""" 961 assert e.getActiveDecisions(4) == {6} 962 assert s4.type == "pending" 963 assert s4.action is None 964 assert s4.graph.destinationsFrom('c') == {'West': 6} 965 assert s4.graph.destinationsFrom('d') == {'East': 4} 966 assert s4.graph.degree(4) == 3 967 assert s4.graph.degree(6) == 2 968 969 # Can't return if there's no outgoing edge yet 970 with pytest.raises(core.MissingTransitionError): 971 e.returnTo('South', 'a', 'North') 972 973 with pytest.raises(core.ExplorationStatusError): 974 e.returnTo('East', 'a', 'North') 975 976 # Add the edge and then we can use it to return 977 g = s4.graph 978 g.addUnexploredEdge('d', 'South') 979 assert set(g) == set(range(8)) 980 assert g.namesListing(g) == """\ 981 0 (a) 982 1 (_u.0) 983 2 (b) 984 3 (_u.2) 985 4 (c) 986 5 (_u.4) 987 6 (d) 988 7 (_u.6) 989""" 990 e.returnTo('South', 'a', 'North') 991 992 assert len(e) == 6 993 s4 = e.getSituation(4) 994 s5 = e.getSituation(5) 995 assert s5 == e.getSituation() 996 assert set(s5.graph) == set([0, 2, 3, 4, 5, 6]) 997 assert s5.graph.namesListing(s5.graph) == """\ 998 0 (a) 999 2 (b) 1000 3 (_u.2) 1001 4 (c) 1002 5 (_u.4) 1003 6 (d) 1004""" 1005 assert e.getActiveDecisions(5) == {0} 1006 assert s5.type == "pending" 1007 assert s5.action is None 1008 assert s5.graph.destinationsFrom('a') == { 1009 'East': 2, 1010 'North': 6, 1011 'South': 3 1012 } 1013 assert s5.graph.destinationsFrom('d') == {'East': 4, 'South': 0} 1014 assert s5.graph.degree(4) == 3 1015 assert s5.graph.degree(6) == 4 1016 assert s5.graph.degree(0) == 5 1017 1018 e.wait() 1019 1020 assert len(e) == 7 1021 s5 = e.getSituation(5) 1022 s6 = e.getSituation(6) 1023 assert set(s6.graph) == set([0, 2, 3, 4, 5, 6]) 1024 assert s6.graph.namesListing(s6.graph) == """\ 1025 0 (a) 1026 2 (b) 1027 3 (_u.2) 1028 4 (c) 1029 5 (_u.4) 1030 6 (d) 1031""" 1032 assert e.getActiveDecisions(6) == {0} 1033 assert s6.type == "pending" 1034 assert s6.action is None 1035 1036 assert s5.action == ('noAction',) 1037 1038 e.takeAction( 1039 'powerUp', 1040 consequence=[ 1041 base.effect(gain='power'), 1042 base.effect(gain=('token', 2)), 1043 ], 1044 fromDecision=0 1045 ) 1046 1047 assert len(e) == 8 1048 s6 = e.getSituation(6) 1049 s7 = e.getSituation(7) 1050 assert set(s7.graph) == set([0, 2, 3, 4, 5, 6]) 1051 assert s7.graph.namesListing(s7.graph) == """\ 1052 0 (a) 1053 2 (b) 1054 3 (_u.2) 1055 4 (c) 1056 5 (_u.4) 1057 6 (d) 1058""" 1059 assert e.getActiveDecisions(7) == {0} 1060 assert base.hasCapabilityOrEquivalent( 1061 'power', 1062 base.genericContextForSituation(s7) 1063 ) 1064 assert base.combinedTokenCount(s7.state, 'token') == 2 1065 assert base.effectiveCapabilitySet(s7.state) == { 1066 "capabilities": {"power"}, 1067 "tokens": {"token": 2}, 1068 "skills": {} 1069 } 1070 assert s7.type == "pending" 1071 assert s7.action is None 1072 assert (s7.graph.destinationsFrom('a') == { 1073 'North': 6, 1074 'East': 2, 1075 'South': 3, 1076 'powerUp': 0 1077 }) 1078 assert s6.action == ('take', 'active', 0, ('powerUp', [])) 1079 1080 e.retrace('East') 1081 1082 assert len(e) == 9 1083 s7 = e.getSituation(7) 1084 s8 = e.getSituation(8) 1085 assert set(s8.graph) == set([0, 2, 3, 4, 5, 6]) 1086 assert s8.graph.namesListing(s8.graph) == """\ 1087 0 (a) 1088 2 (b) 1089 3 (_u.2) 1090 4 (c) 1091 5 (_u.4) 1092 6 (d) 1093""" 1094 assert e.getActiveDecisions(8) == {2} 1095 assert s8.type == "pending" 1096 assert s8.action is None 1097 assert base.effectiveCapabilitySet(s8.state) == { 1098 "capabilities": {"power"}, 1099 "tokens": {"token": 2}, 1100 "skills": {} 1101 } 1102 assert s7.action == ('take', 'active', 0, ('East', [])) 1103 1104 e.observeMechanisms('d', ('gate', 'closed')) 1105 gateD = base.mechanismAt('gate', decision='d') 1106 gateA = base.mechanismAt('gate', decision='a') 1107 assert gateD == (None, None, 'd', 'gate') 1108 assert gateA == (None, None, 'a', 'gate') 1109 assert base.mechanismInStateOrEquivalent( 1110 'gate', 1111 'closed', 1112 base.genericContextForSituation(s8) 1113 ) 1114 assert e.mechanismState('gate') == "closed" 1115 assert e.mechanismState(gateD) == "closed" 1116 1117 # Can't get mechanism state in step before it's been observed 1118 with pytest.raises(core.MissingMechanismError): 1119 e.mechanismState('gate', step=6) 1120 1121 # Can't get mechanism state for mechanism at a different decision 1122 assert e.mechanismState(gateA) == "closed" 1123 1124 e.observeMechanisms('a', 'gate') # without starting state 1125 with pytest.raises(core.AmbiguousMechanismError): 1126 e.mechanismState('gate') 1127 1128 with pytest.raises(core.AmbiguousMechanismError): 1129 e.mechanismState(base.mechanismAt('gate', decision='c')) 1130 1131 # Default state 1132 assert e.mechanismState(gateA) == 'off' 1133 assert e.mechanismState(gateD) == 'closed' 1134 1135 e.warp( 1136 'd', 1137 consequence=[ 1138 base.effect(lose=('token', 1)), 1139 base.effect(set=('gate', 'open')) # knows to open gate at 'd' 1140 ] 1141 ) 1142 1143 assert len(e) == 10 1144 s8 = e.getSituation(8) 1145 s9 = e.getSituation(9) 1146 assert s8.action == ("warp", "active", 6) 1147 assert set(s9.graph) == set([0, 2, 3, 4, 5, 6]) 1148 assert s9.graph.namesListing(s9.graph) == """\ 1149 0 (a) 1150 2 (b) 1151 3 (_u.2) 1152 4 (c) 1153 5 (_u.4) 1154 6 (d) 1155""" 1156 assert e.getActiveDecisions(9) == {6} 1157 assert base.effectiveCapabilitySet(s9.state) == { 1158 "capabilities": {"power"}, 1159 "tokens": {"token": 1}, 1160 "skills": {} 1161 } 1162 assert s9.type == "pending" 1163 assert s9.action is None 1164 assert (s9.graph.destinationsFrom('a') == { 1165 'North': 6, 1166 'East': 2, 1167 'South': 3, 1168 'powerUp': 0 1169 }) 1170 assert (s9.graph.destinationsFrom('b') == { 1171 'North': 4, 1172 'West': 0, 1173 'South': 5 1174 }) 1175 assert (s9.graph.destinationsFrom('c') == {'West': 6}) 1176 assert (s9.graph.destinationsFrom('d') == {'East': 4, 'South': 0}) 1177 1178 ctx1 = base.genericContextForSituation(s1) 1179 ctx8 = base.genericContextForSituation(s8) 1180 ctx9 = base.genericContextForSituation(s9) 1181 with pytest.raises(core.MissingMechanismError): 1182 base.mechanismInStateOrEquivalent(gateD, 'open', ctx1) 1183 # 'gate mechanism doesn't exist back then (neither does decision 'd') 1184 assert not base.mechanismInStateOrEquivalent(gateD, 'open', ctx8) 1185 assert base.mechanismInStateOrEquivalent(gateD, 'open', ctx9) 1186 1187 with pytest.raises(core.MissingMechanismError): 1188 e.mechanismState(gateA, step=1) 1189 1190 assert e.mechanismState(gateA) == 'off' 1191 assert e.mechanismState(gateD) == 'open' 1192 assert e.mechanismState(gateD, step=8) == 'closed' 1193 1194 1195def test_exploring_with_zones() -> None: 1196 """ 1197 A test for exploring with zones being applied. 1198 """ 1199 e = core.DiscreteExploration() 1200 1201 assert e.start('start') == 0 1202 graph = e.getSituation().graph 1203 graph.createZone('zone', 0) 1204 graph.addDecisionToZone('start', 'zone') 1205 e.observe(0, 'transition') 1206 assert e.explore('transition', 'room') == 1 1207 1208 s = e.getSituation() 1209 g = s.graph 1210 assert g.zoneParents(0) == {'zone'} 1211 assert g.zoneParents(1) == {'zone'} 1212 1213 e.observeAll(1, 'out', 'down') 1214 unknown = g.destination('room', 'down') 1215 g.renameDecision(unknown, 'fourth_room') 1216 assert g.nameFor(3) == 'fourth_room' 1217 assert g.zoneParents(3) == set() 1218 assert not g.isConfirmed('fourth_room') 1219 assert not e.hasBeenVisited('fourth_room') 1220 assert e.explore('out', 'another_room', 'back', 'zone2') == 2 1221 e.retrace('back') 1222 e.explore('down', None, 'up') # already named 'fourth_room' 1223 1224 g = e.getSituation().graph 1225 assert g.nameFor(3) == 'fourth_room' 1226 assert g.zoneParents(0) == {'zone'} 1227 assert g.zoneParents(1) == {'zone'} 1228 assert g.zoneParents(2) == {'zone2'} 1229 assert g.zoneParents(3) == {'zone'} 1230 1231 1232def test_triggers() -> None: 1233 e = core.DiscreteExploration() 1234 e.start('start') 1235 assert e.primaryDecision() == 0 1236 e.takeAction( 1237 'shiver', 1238 requires=base.ReqNot(base.ReqCapability('jacket')), 1239 consequence=[base.effect(gain=('cold', 1))], 1240 fromDecision='start' 1241 ) 1242 e.getSituation().graph.tagTransition('start', 'shiver', 'trigger') 1243 assert e.tokenCountNow('cold') == 1 1244 e.observe('start', 'right') 1245 e.explore('right', 'room', 'left') 1246 assert e.primaryDecision() == 1 1247 assert e.tokenCountNow('cold') == 1 1248 e.takeAction( 1249 'warmUp', 1250 requires=base.ReqTokens('cold', 1), 1251 consequence=[base.effect(lose=('cold', 1))], 1252 fromDecision='room' 1253 ) 1254 assert e.tokenCountNow('cold') == 0 1255 e.getSituation().graph.tagTransition('room', 'warmUp', 'trigger') 1256 e.wait() 1257 assert e.tokenCountNow('cold') == 0 1258 e.retrace('left') 1259 assert e.primaryDecision() == 0 1260 assert e.tokenCountNow('cold') == 1 1261 e.wait() 1262 assert e.tokenCountNow('cold') == 2 1263 e.wait() 1264 assert e.tokenCountNow('cold') == 3 1265 e.retrace('right') 1266 assert e.tokenCountNow('cold') == 2 1267 e.wait() 1268 assert e.tokenCountNow('cold') == 1 1269 e.wait() 1270 # No issue with trigger when out of tokens because of its requirement 1271 assert e.tokenCountNow('cold') == 0 1272 e.wait() 1273 assert e.tokenCountNow('cold') == 0 1274 # Get a jacket 1275 e.applyExtraneousEffect(base.effect(gain='jacket')) 1276 e.retrace('left') 1277 # Jacket prevents trigger 1278 assert e.primaryDecision() == 0 1279 assert e.tokenCountNow('cold') == 0 1280 e.wait() 1281 assert e.tokenCountNow('cold') == 0 1282 # 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 # Mechanism requirement w/ decision ID 255 switchAt0 = base.MechanismSpecifier(None, None, 0, 'switch') 256 switchAt1 = base.MechanismSpecifier(None, None, 1, 'switch') 257 r = base.ReqMechanism(switchAt0, 'on') 258 assert not r.satisfied(withSwitch) 259 assert r.satisfied(rcWith(withSwitch, switch="on")) 260 261 # 'switch' at different specific decision won't search 262 r2 = base.ReqMechanism(switchAt1, 'on') 263 assert not r.satisfied(withSwitch) 264 assert r.satisfied(rcWith(withSwitch, switch="on")) 265 266 # Skill level requirements 267 r = base.ReqLevel('skill', 1) 268 r2 = base.ReqNot(base.ReqLevel('skill', 2)) 269 assert not r.satisfied(trc) 270 assert not r.satisfied(rcWith(trc, skill=("skill", 0))) 271 assert r.satisfied(rcWith(trc, skill=("skill", 1))) 272 assert r.satisfied(rcWith(trc, skill=("skill", 2))) 273 assert r.satisfied(rcWith(trc, skill=("skill", 10))) 274 assert r2.satisfied(rcWith(trc, skill=("skill", 1))) 275 assert not r2.satisfied(rcWith(trc, skill=("skill", 2))) 276 assert not r2.satisfied(rcWith(trc, skill=("skill", 10))) 277 278 # Tag requirements 279 r = base.ReqTag('tag', 1) 280 r2 = base.ReqTag('tag2', 'value') 281 assert not r.satisfied(trc) 282 assert not r2.satisfied(trc) 283 tagged = rcWith(trc, tag=("tag", 1)) 284 assert tagged.graph.decisionTags(0) == {'tag': 1} 285 assert r.satisfied(tagged) 286 assert not r.satisfied(rcWith(trc, tag=("tag", 2))) 287 assert not r.satisfied(rcWith(trc, tag=("tag", 0))) 288 assert r2.satisfied(rcWith(trc, tag2=("tag", 'value'))) 289 taggedAside = copy.deepcopy(trc) 290 taggedAside.graph.tagDecision(1, 'tag', 1) 291 taggedAside.graph.tagDecision(2, 'tag2', 'value') 292 assert not r.satisfied(taggedAside) 293 assert not r2.satisfied(taggedAside) 294 taggedAside.state['common']['activeDecisions']['main'] = 1 295 assert r.satisfied(taggedAside) 296 assert not r2.satisfied(taggedAside) 297 taggedAside.state['common']['activeDecisions']['main'] = 2 298 assert not r.satisfied(taggedAside) 299 assert r2.satisfied(taggedAside) 300 zoneTagged = copy.deepcopy(trc) 301 zoneTagged.graph.createZone('zone') 302 zoneTagged.graph.addDecisionToZone(0, 'zone') 303 zoneTagged.graph.tagZone('zone', 'tag', 1) 304 assert r.satisfied(zoneTagged) 305 assert not r2.satisfied(zoneTagged) 306 zoneTagged.graph.removeDecisionFromZone(0, 'zone') 307 assert not r.satisfied(zoneTagged) 308 309 # Tests of parsing: 310 assert pf.parseRequirement('a') == base.ReqCapability('a') 311 312 assert pf.parseRequirement('(a)') == base.ReqCapability('a') 313 314 assert pf.parseRequirement('a*5') == base.ReqTokens('a', 5) 315 316 assert pf.parseRequirement('((a*5))') == base.ReqTokens('a', 5) 317 318 assert pf.parseRequirement('(a|b)&c*3') == base.ReqAll([ 319 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 320 base.ReqTokens('c', 3) 321 ]) 322 323 assert pf.parseRequirement(' ( a | b )\t& c * 3 ') == base.ReqAll([ 324 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), 325 base.ReqTokens('c', 3) 326 ]) 327 328 assert pf.parseRequirement('a|(b&c*3)') == base.ReqAny([ 329 base.ReqCapability('a'), 330 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 331 ]) 332 333 assert pf.parseRequirement('a|b&c*3') == base.ReqAny([ 334 base.ReqCapability('a'), 335 base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), 336 ]) 337 338 assert pf.parseRequirement('a&b|c*3') == base.ReqAny([ 339 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]), 340 base.ReqTokens('c', 3) 341 ]) 342 343 assert pf.parseRequirement('a|b|c') == base.ReqAny([ 344 base.ReqCapability('a'), 345 base.ReqCapability('b'), 346 base.ReqCapability('c'), 347 ]) 348 349 assert pf.parseRequirement('a&b&c&d') == base.ReqAll([ 350 base.ReqCapability('a'), 351 base.ReqCapability('b'), 352 base.ReqCapability('c'), 353 base.ReqCapability('d'), 354 ]) 355 356 assert pf.parseRequirement('a&b|c&d') == base.ReqAny([ 357 base.ReqAll([ 358 base.ReqCapability('a'), 359 base.ReqCapability('b') 360 ]), 361 base.ReqAll([ 362 base.ReqCapability('c'), 363 base.ReqCapability('d') 364 ]) 365 ]) 366 367 assert pf.parseRequirement('a&!b|!c&d') == base.ReqAny([ 368 base.ReqAll([ 369 base.ReqCapability('a'), 370 base.ReqNot(base.ReqCapability('b')) 371 ]), 372 base.ReqAll([ 373 base.ReqNot(base.ReqCapability('c')), 374 base.ReqCapability('d') 375 ]) 376 ]) 377 378 assert pf.parseRequirement('!(a|b)&c') == base.ReqAll([ 379 base.ReqNot( 380 base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]) 381 ), 382 base.ReqCapability('c') 383 ]) 384 385 assert pf.parseRequirement('!a&b&c') == base.ReqAll([ 386 base.ReqNot(base.ReqCapability('a')), 387 base.ReqCapability('b'), 388 base.ReqCapability('c') 389 ]) 390 391 assert pf.parseRequirement('!(a&b)&c') == base.ReqAll([ 392 base.ReqNot( 393 base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]) 394 ), 395 base.ReqCapability('c') 396 ]) 397 398 assert pf.parseRequirement('!c*3') == base.ReqNot(base.ReqTokens('c', 3)) 399 400 assert pf.parseRequirement('X') == base.ReqImpossible() 401 402 assert pf.parseRequirement('O') == base.ReqNothing() 403 404 assert pf.parseRequirement('X|a') == base.ReqAny([ 405 base.ReqImpossible(), 406 base.ReqCapability('a') 407 ]) 408 409 assert pf.parseRequirement('door:open') == base.ReqMechanism('door', 'open') 410 411 assert pf.parseRequirement('z::d::door:open') == base.ReqMechanism( 412 base.MechanismSpecifier(None, 'z', 'd', 'door'), 413 'open' 414 ) 415 416 assert pf.parseRequirement('3::door:open') == base.ReqMechanism( 417 base.MechanismSpecifier(None, None, 3, 'door'), 418 'open' 419 ) 420 421 with pytest.raises(parsing.ParseError): 422 pf.parseRequirement('a*3*2') 423 424 with pytest.raises(parsing.ParseError): 425 pf.parseRequirement('(a):2') 426 427 with pytest.raises(parsing.ParseError): 428 pf.parseRequirement('(a|b&c):3') 429 430 with pytest.raises(parsing.ParseError): 431 pf.parseRequirement('a|&b') 432 433 with pytest.raises(parsing.ParseError): 434 pf.parseRequirement('(a|b') 435 436 with pytest.raises(parsing.ParseError): 437 pf.parseRequirement('a|b)') 438 439 assert (pf.parseRequirement('a*-3') == base.ReqTokens('a', -3)) 440 441 with pytest.raises(parsing.ParseError): 442 pf.parseRequirement('a*!3') 443 444 with pytest.raises(parsing.ParseError): 445 pf.parseRequirement('a!b') 446 447 with pytest.raises(parsing.ParseError): 448 pf.parseRequirement('a*-b')
Multi-method test for exploration.core.Requirement and sub-classes.
451def test_DecisionGraph() -> None: 452 "Multi-method test for `exploration.core.DecisionGraph`." 453 m = core.DecisionGraph() 454 m.addDecision('a') 455 m.addDecision('b') 456 m.addDecision('c') 457 assert len(m) == 3 458 assert set(m) == {0, 1, 2} 459 460 m.addTransition('a', 'East', 'b', 'West') 461 m.addTransition('a', 'Northeast', 'c') 462 m.addTransition('c', 'Southeast', 'b') 463 464 assert m.destinationsFrom('a') == {'East': 1, 'Northeast': 2} 465 assert m.destinationsFrom('b') == {'West': 0} 466 assert m.destinationsFrom('c') == {'Southeast': 1} 467 468 assert m.getReciprocal('a', 'East') == 'West' 469 assert m.getReciprocal('a', 'Northeast') is None 470 assert m.getReciprocal('b', 'Southwest') is None 471 assert m.getReciprocal('c', 'Southeast') is None 472 473 m.addUnexploredEdge('a', 'South') 474 m.addUnexploredEdge('b', 'East') 475 m.addUnexploredEdge('c', 'North') 476 477 assert len(m) == 6 478 assert set(m) == set(range(6)) 479 assert m.namesListing(m) == """\ 480 0 (a) 481 1 (b) 482 2 (c) 483 3 (_u.0) 484 4 (_u.1) 485 5 (_u.2) 486""" 487 assert ( 488 m.destinationsFrom('a') 489 == {'East': 1, 'Northeast': 2, 'South': 3} 490 ) 491 assert (m.destinationsFrom('b') == {'West': 0, 'East': 4}) 492 assert (m.destinationsFrom('c') == {'Southeast': 1, 'North': 5}) 493 494 m.replaceUnconfirmed('c', 'North', 'd', 'South') 495 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 496 assert m.nameFor(5) == 'd' 497 assert len(m) == 6 498 assert set(m) == set(range(6)) 499 assert m.namesListing(m) == """\ 500 0 (a) 501 1 (b) 502 2 (c) 503 3 (_u.0) 504 4 (_u.1) 505 5 (d) 506""" 507 508 m.addTransition('d', 'West', 'a', 'North') 509 assert ( 510 m.destinationsFrom('a') 511 == {'East': 1, 'Northeast': 2, 'South': 3, 'North': 5} 512 ) 513 assert ( 514 m.destinationsFrom('d') 515 == {'West': 0, 'South': 2} 516 ) 517 518 with pytest.raises(core.MissingDecisionError): 519 _ = m.destinationsFrom('z') 520 521 with pytest.raises(core.TransitionCollisionError): 522 m.addTransition('a', 'East', 'b', 'West') 523 524 with pytest.raises(core.TransitionCollisionError): 525 m.addTransition('c', 'East', 'b', 'West') 526 527 with pytest.raises(core.TransitionCollisionError): 528 m.addUnexploredEdge('a', 'East') 529 530 with pytest.raises(core.TransitionCollisionError): 531 m.addUnexploredEdge('c', 'North') 532 533 with pytest.raises(core.MissingTransitionError): 534 m.replaceUnconfirmed('a', 'Up', 'z') 535 536 with pytest.raises(core.ExplorationStatusError): 537 m.replaceUnconfirmed('a', 'East', 'z') 538 539 assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} 540 541 m.addTransition('a', 'EastBelow', 'b', 'WestBelow') 542 assert (m.destinationsFrom('a') == { 543 'East': 1, 544 'EastBelow': 1, 545 'Northeast': 2, 546 'South': 3, 547 'North': 5 548 }) 549 assert ( 550 m.destinationsFrom('b') 551 == {'West': 0, 'WestBelow': 0, 'East': 4} 552 ) 553 554 # Two edges that could be but are not reciprocals 555 m.addTransition('d', 'East', 'b') 556 m.addTransition('b', 'North', 'd') 557 assert ( 558 m.destinationsFrom('b') 559 == {'West': 0, 'WestBelow': 0, 'East': 4, 'North': 5} 560 ) 561 assert ( 562 m.destinationsFrom('d') 563 == {'West': 0, 'South': 2, 'East': 1} 564 ) 565 assert m.getReciprocal('b', 'North') is None 566 assert m.getReciprocal('d', 'East') is None 567 568 # Establish a reciprocal relationship 569 m.setReciprocal('b', 'North', 'East') 570 assert m.getReciprocal('b', 'North') == 'East' 571 assert m.getReciprocal('d', 'East') == 'North' 572 573 with pytest.raises(core.MissingDecisionError): 574 m.setReciprocal('z', 'Nope', 'None') 575 576 with pytest.raises(core.MissingTransitionError): 577 m.setReciprocal('b', 'Nope', 'None') 578 579 # Remove the reciprocal relationship again (from the other side) 580 m.setReciprocal('d', 'East', None) 581 assert m.getReciprocal('b', 'North') is None 582 assert m.getReciprocal('d', 'East') is None 583 584 with pytest.raises(core.InvalidDestinationError): 585 m.setReciprocal('b', 'North', 'West') 586 587 with pytest.raises(core.MissingTransitionError): 588 m.setReciprocal('b', 'North', 'None') 589 590 assert (m.isConfirmed("_u.0") is False) 591 assert (m.isConfirmed("_u.1") is False) 592 assert (m.isConfirmed("a") is True) 593 assert (m.isConfirmed("d") is True) 594 595 assert ( 596 json.dumps(m.textMapObj(), indent=4) 597 == """\ 598{ 599 "0::East": { 600 "1::West": "0", 601 "1::East": {}, 602 "1::WestBelow": "0", 603 "1::North": { 604 "5::South": { 605 "2::Southeast": "1", 606 "2::North": "5" 607 }, 608 "5::West": "0", 609 "5::East": "1" 610 } 611 }, 612 "0::Northeast": "2", 613 "0::South": {}, 614 "0::North": "5", 615 "0::EastBelow": "1" 616}""" 617 ) 618 619 assert ( 620 json.dumps( 621 m.textMapObj( 622 explorationOrder=( 623 0, 624 [ 625 'East', 626 'West', 627 'Northeast', 628 'Southeast', 629 'North', 630 'West', 631 'South', 632 ] 633 ) 634 ), 635 indent=4 636 ) 637 == """\ 638{ 639 "0::East": { 640 "1::West": "0", 641 "1::North": { 642 "5::West": "0", 643 "5::South": { 644 "2::Southeast": "1", 645 "2::North": "5" 646 }, 647 "5::East": "1" 648 }, 649 "1::East": {}, 650 "1::WestBelow": "0" 651 }, 652 "0::Northeast": "2", 653 "0::South": {}, 654 "0::North": "5", 655 "0::EastBelow": "1" 656}""" 657 ) 658 659 m.addTransition( 660 'd', 661 'failure', 662 m.endingID('failure') 663 ) 664 assert set(m) == set(range(7)) 665 assert m.namesListing(m) == """\ 666 0 (a) 667 1 (b) 668 2 (c) 669 3 (_u.0) 670 4 (_u.1) 671 5 (d) 672 6 (endings//failure) 673""" 674 assert ( 675 m.destinationsFrom('d') 676 == {'West': 0, 'South': 2, 'East': 1, 'failure': 6} 677 ) 678 assert m.destinationsFrom('failure') == {}
Multi-method test for exploration.core.DecisionGraph.
681def test_DGTagsAndAnnotations() -> None: 682 """ 683 Test for tagging decisions and transitions in an 684 `exploration.core.DecisionGraph`. 685 """ 686 m = core.DecisionGraph() 687 m.addDecision('a') 688 m.addDecision('b', tags={'grass': 1}) 689 m.addDecision('c') 690 m.tagDecision('c', {'water': 1, 'big': 1}) 691 m.annotateDecision('c', "This is a note.") 692 m.addTransition('a', 'East', 'b', 'West') 693 m.addTransition( 694 'a', 'South', 'c', 'North', 695 {'green': 1}, ["Requires green key"], 696 {'blue': 1}, ["Requires blue key"] 697 ) 698 m.tagTransition('a', 'South', 'green') 699 m.annotateTransition('a', 'South', "a2") 700 m.tagTransition('c', 'North', 'blue', 1) 701 m.annotateTransition('c', 'North', ["Requires", "blue key"]) 702 703 assert m.decisionTags('a') == {} 704 assert m.decisionTags('b') == {'grass': 1} 705 assert m.decisionTags('c') == {'water': 1, 'big': 1} 706 assert m.transitionTags('a', 'East') == {} 707 assert m.transitionTags('a', 'South') == {'green': 1} 708 assert m.transitionTags('b', 'West') == {} 709 assert m.transitionTags('c', 'North') == {'blue': 1} 710 711 assert m.decisionAnnotations('a') == [] 712 assert m.decisionAnnotations('b') == [] 713 assert m.decisionAnnotations('c') == ["This is a note."] 714 assert m.transitionAnnotations('a', 'East') == [] 715 assert m.transitionAnnotations('a', 'South') == [ 716 "Requires green key", 717 "a2" 718 ] 719 assert m.transitionAnnotations('b', 'West') == [] 720 assert m.transitionAnnotations('c', 'North') == [ 721 "Requires blue key", 722 "Requires", 723 "blue key" 724 ] 725 726 m.tagDecision('a', 'grass') 727 assert m.decisionTags('a') == {'grass': 1} 728 729 m.untagDecision('b', 'grass') 730 assert m.decisionTags('b') == {} 731 732 m.annotateDecision('a', "Starting location.") 733 assert m.decisionAnnotations('a') == ["Starting location."] 734 735 m.annotateDecision('c', "Blue key here.") 736 assert m.decisionAnnotations('c') == ["This is a note.", "Blue key here."] 737 738 assert m.decisionAnnotations('b') == [] 739 740 m.tagTransition('a', 'East', 'open') 741 assert m.transitionTags('a', 'East') == {'open': 1} 742 m.tagTransition('a', 'South', 'open') 743 assert m.transitionTags('a', 'South') == {'green': 1, 'open': 1} 744 m.untagTransition('a', 'South', 'green') 745 assert m.transitionTags('a', 'South') == {'open': 1} 746 m.tagTransition('a', 'South', 'open', [1, 2, 3]) 747 assert m.transitionTags('a', 'South') == {'open': [1, 2, 3]} 748 749 m.annotateTransition('c', 'North', "This was difficult.") 750 assert ( 751 m.transitionAnnotations('c', 'North') 752 == ['Requires blue key', 'Requires', 'blue key', 'This was difficult.'] 753 ) 754 cna = m.transitionAnnotations('c', 'North') 755 cna.clear() 756 cna.append('hi') 757 assert m.transitionAnnotations('c', 'North') == ['hi'] 758 759 with pytest.raises(core.MissingTransitionError): 760 _ = m.transitionAnnotations('a', 'West')
Test for tagging decisions and transitions in an
exploration.core.DecisionGraph.
763def test_DiscreteExploration() -> None: 764 "Multi-method test for `exploration.core.DiscreteExploration`." 765 e = core.DiscreteExploration() 766 767 s0 = e.getSituation(0) 768 assert e.getSituation() is s0 769 770 assert len(s0.graph) == 0 771 assert e.getActiveDecisions(0) == set() 772 assert s0.state == base.emptyState() 773 assert s0.type == "pending" 774 assert s0.action is None 775 776 assert len(e) == 1 777 778 e.start('a') 779 e.observeAll('a', 'North', 'East', 'South') 780 781 assert len(e) == 2 782 783 s0 = e.getSituation(0) 784 s1 = e.getSituation(1) 785 assert s1 is e.getSituation() 786 assert len(s1.graph) == 4 787 assert set(s1.graph) == set(range(4)) 788 assert s1.graph.namesListing(s1.graph) == """\ 789 0 (a) 790 1 (_u.0) 791 2 (_u.1) 792 3 (_u.2) 793""" 794 assert e.getActiveDecisions() == e.getActiveDecisions(1) 795 assert e.getActiveDecisions() == {0} 796 assert s0.type == "imposed" 797 assert s0.action == ( 798 'start', 799 0, 800 0, 801 'main', 802 None, 803 None, 804 None 805 ) 806 807 e.explore('East', 'b', 'West') 808 e.observeAll('b', 'North', 'South') 809 810 assert len(e) == 3 811 assert set(s1.graph) == set(range(4)) 812 assert s1.graph.namesListing(s1.graph) == """\ 813 0 (a) 814 1 (_u.0) 815 2 (_u.1) 816 3 (_u.2) 817""" 818 assert e.getActiveDecisions(1) == {0} 819 s1 = e.getSituation(1) 820 assert s1.type == 'active' 821 dz = base.DefaultZone 822 assert s1.action == ( 823 'explore', 824 'active', 825 0, 826 ('East', []), 827 'b', 828 'West', 829 dz 830 ) 831 832 s2 = e.getSituation(2) 833 assert s2 is e.getSituation() 834 assert set(s2.graph) == set(range(6)) 835 assert s2.graph.namesListing(s2.graph) == """\ 836 0 (a) 837 1 (_u.0) 838 2 (b) 839 3 (_u.2) 840 4 (_u.3) 841 5 (_u.4) 842""" 843 assert e.getActiveDecisions() == {2} 844 assert s2.type == "pending" 845 assert s2.action is None 846 assert (s2.graph.destinationsFrom('a') == { 847 'North': 1, 848 'East': 2, 849 'South': 3 850 }) 851 assert (s2.graph.destinationsFrom('b') == { 852 'North': 4, 853 'West': 0, 854 'South': 5 855 }) 856 assert (s2.graph.destinationsFrom('_u.3') == {}) 857 assert (s2.graph.destinationsFrom('_u.4') == {}) 858 859 with pytest.raises(core.ExplorationStatusError): 860 e.returnTo('West', 'a', 'North') 861 862 with pytest.raises(core.ExplorationStatusError): 863 e.returnTo('West', 'a', 'East') 864 # (would need to use retrace instead) 865 866 e.explore('North', 'c', None) 867 e.observe('c', 'West') 868 869 assert len(e) == 4 870 assert e.getSituation(0) == s0 871 assert e.getSituation(1) == s1 872 assert set(s1.graph) == set(range(4)) 873 assert s1.graph.namesListing(s1.graph) == """\ 874 0 (a) 875 1 (_u.0) 876 2 (_u.1) 877 3 (_u.2) 878""" 879 assert e.getActiveDecisions(1) == {0} 880 assert s1.type == "active" 881 assert s1.action == ('explore', 'active', 0, ('East', []), 'b', 'West', dz) 882 ns2 = e.getSituation(2) 883 assert ns2 != s2 884 assert ns2.graph == s2.graph 885 assert ns2.state == s2.state 886 assert s2.type, ns2.type == ('pending', 'active') 887 assert s2.action is None 888 assert ns2.tags == s2.tags 889 assert ns2.annotations == s2.annotations 890 s2 = ns2 891 assert set(s2.graph) == set(range(6)) 892 assert s2.graph.namesListing(s2.graph) == """\ 893 0 (a) 894 1 (_u.0) 895 2 (b) 896 3 (_u.2) 897 4 (_u.3) 898 5 (_u.4) 899""" 900 assert e.getActiveDecisions(2) == {2} 901 assert s2.type == "active" 902 assert s2.action == ('explore', 'active', 2, ('North', []), 'c', None, dz) 903 assert (s2.graph.destinationsFrom('a') == { 904 'North': 1, 905 'East': 2, 906 'South': 3 907 }) 908 assert (s2.graph.destinationsFrom('b') == { 909 'North': 4, 910 'West': 0, 911 'South': 5 912 }) 913 s3 = e.getSituation(3) 914 assert set(s3.graph) == set(range(7)) 915 assert s3.graph.namesListing(s3.graph) == """\ 916 0 (a) 917 1 (_u.0) 918 2 (b) 919 3 (_u.2) 920 4 (c) 921 5 (_u.4) 922 6 (_u.5) 923""" 924 assert e.getActiveDecisions(3) == {4} 925 assert s3.type == "pending" 926 assert s3.action is None 927 assert (s3.graph.destinationsFrom('a') == { 928 'North': 1, 929 'East': 2, 930 'South': 3 931 }) 932 assert (s3.graph.destinationsFrom('b') == { 933 'North': 4, 934 'West': 0, 935 'South': 5 936 }) 937 assert s3.graph.destinationsFrom('c') == {'West': 6} 938 assert s3.graph.destinationsFrom('_u.0') == {} 939 assert s3.graph.destinationsFrom('_u.2') == {} 940 assert s3.graph.destinationsFrom('_u.4') == {} 941 assert s3.graph.destinationsFrom('_u.5') == {} 942 assert s3.graph.degree(0) == 4 943 assert s3.graph.degree(2) == 4 944 assert s3.graph.degree(4) == 2 945 assert s3.graph.degree(6) == 1 946 947 e.explore('West', 'd', 'East') 948 949 assert len(e) == 5 950 s3 = e.getSituation(3) 951 s4 = e.getSituation(4) 952 assert set(s4.graph) == set(range(7)) 953 assert s4.graph.namesListing(s4.graph) == """\ 954 0 (a) 955 1 (_u.0) 956 2 (b) 957 3 (_u.2) 958 4 (c) 959 5 (_u.4) 960 6 (d) 961""" 962 assert e.getActiveDecisions(4) == {6} 963 assert s4.type == "pending" 964 assert s4.action is None 965 assert s4.graph.destinationsFrom('c') == {'West': 6} 966 assert s4.graph.destinationsFrom('d') == {'East': 4} 967 assert s4.graph.degree(4) == 3 968 assert s4.graph.degree(6) == 2 969 970 # Can't return if there's no outgoing edge yet 971 with pytest.raises(core.MissingTransitionError): 972 e.returnTo('South', 'a', 'North') 973 974 with pytest.raises(core.ExplorationStatusError): 975 e.returnTo('East', 'a', 'North') 976 977 # Add the edge and then we can use it to return 978 g = s4.graph 979 g.addUnexploredEdge('d', 'South') 980 assert set(g) == set(range(8)) 981 assert g.namesListing(g) == """\ 982 0 (a) 983 1 (_u.0) 984 2 (b) 985 3 (_u.2) 986 4 (c) 987 5 (_u.4) 988 6 (d) 989 7 (_u.6) 990""" 991 e.returnTo('South', 'a', 'North') 992 993 assert len(e) == 6 994 s4 = e.getSituation(4) 995 s5 = e.getSituation(5) 996 assert s5 == e.getSituation() 997 assert set(s5.graph) == set([0, 2, 3, 4, 5, 6]) 998 assert s5.graph.namesListing(s5.graph) == """\ 999 0 (a) 1000 2 (b) 1001 3 (_u.2) 1002 4 (c) 1003 5 (_u.4) 1004 6 (d) 1005""" 1006 assert e.getActiveDecisions(5) == {0} 1007 assert s5.type == "pending" 1008 assert s5.action is None 1009 assert s5.graph.destinationsFrom('a') == { 1010 'East': 2, 1011 'North': 6, 1012 'South': 3 1013 } 1014 assert s5.graph.destinationsFrom('d') == {'East': 4, 'South': 0} 1015 assert s5.graph.degree(4) == 3 1016 assert s5.graph.degree(6) == 4 1017 assert s5.graph.degree(0) == 5 1018 1019 e.wait() 1020 1021 assert len(e) == 7 1022 s5 = e.getSituation(5) 1023 s6 = e.getSituation(6) 1024 assert set(s6.graph) == set([0, 2, 3, 4, 5, 6]) 1025 assert s6.graph.namesListing(s6.graph) == """\ 1026 0 (a) 1027 2 (b) 1028 3 (_u.2) 1029 4 (c) 1030 5 (_u.4) 1031 6 (d) 1032""" 1033 assert e.getActiveDecisions(6) == {0} 1034 assert s6.type == "pending" 1035 assert s6.action is None 1036 1037 assert s5.action == ('noAction',) 1038 1039 e.takeAction( 1040 'powerUp', 1041 consequence=[ 1042 base.effect(gain='power'), 1043 base.effect(gain=('token', 2)), 1044 ], 1045 fromDecision=0 1046 ) 1047 1048 assert len(e) == 8 1049 s6 = e.getSituation(6) 1050 s7 = e.getSituation(7) 1051 assert set(s7.graph) == set([0, 2, 3, 4, 5, 6]) 1052 assert s7.graph.namesListing(s7.graph) == """\ 1053 0 (a) 1054 2 (b) 1055 3 (_u.2) 1056 4 (c) 1057 5 (_u.4) 1058 6 (d) 1059""" 1060 assert e.getActiveDecisions(7) == {0} 1061 assert base.hasCapabilityOrEquivalent( 1062 'power', 1063 base.genericContextForSituation(s7) 1064 ) 1065 assert base.combinedTokenCount(s7.state, 'token') == 2 1066 assert base.effectiveCapabilitySet(s7.state) == { 1067 "capabilities": {"power"}, 1068 "tokens": {"token": 2}, 1069 "skills": {} 1070 } 1071 assert s7.type == "pending" 1072 assert s7.action is None 1073 assert (s7.graph.destinationsFrom('a') == { 1074 'North': 6, 1075 'East': 2, 1076 'South': 3, 1077 'powerUp': 0 1078 }) 1079 assert s6.action == ('take', 'active', 0, ('powerUp', [])) 1080 1081 e.retrace('East') 1082 1083 assert len(e) == 9 1084 s7 = e.getSituation(7) 1085 s8 = e.getSituation(8) 1086 assert set(s8.graph) == set([0, 2, 3, 4, 5, 6]) 1087 assert s8.graph.namesListing(s8.graph) == """\ 1088 0 (a) 1089 2 (b) 1090 3 (_u.2) 1091 4 (c) 1092 5 (_u.4) 1093 6 (d) 1094""" 1095 assert e.getActiveDecisions(8) == {2} 1096 assert s8.type == "pending" 1097 assert s8.action is None 1098 assert base.effectiveCapabilitySet(s8.state) == { 1099 "capabilities": {"power"}, 1100 "tokens": {"token": 2}, 1101 "skills": {} 1102 } 1103 assert s7.action == ('take', 'active', 0, ('East', [])) 1104 1105 e.observeMechanisms('d', ('gate', 'closed')) 1106 gateD = base.mechanismAt('gate', decision='d') 1107 gateA = base.mechanismAt('gate', decision='a') 1108 assert gateD == (None, None, 'd', 'gate') 1109 assert gateA == (None, None, 'a', 'gate') 1110 assert base.mechanismInStateOrEquivalent( 1111 'gate', 1112 'closed', 1113 base.genericContextForSituation(s8) 1114 ) 1115 assert e.mechanismState('gate') == "closed" 1116 assert e.mechanismState(gateD) == "closed" 1117 1118 # Can't get mechanism state in step before it's been observed 1119 with pytest.raises(core.MissingMechanismError): 1120 e.mechanismState('gate', step=6) 1121 1122 # Can't get mechanism state for mechanism at a different decision 1123 assert e.mechanismState(gateA) == "closed" 1124 1125 e.observeMechanisms('a', 'gate') # without starting state 1126 with pytest.raises(core.AmbiguousMechanismError): 1127 e.mechanismState('gate') 1128 1129 with pytest.raises(core.AmbiguousMechanismError): 1130 e.mechanismState(base.mechanismAt('gate', decision='c')) 1131 1132 # Default state 1133 assert e.mechanismState(gateA) == 'off' 1134 assert e.mechanismState(gateD) == 'closed' 1135 1136 e.warp( 1137 'd', 1138 consequence=[ 1139 base.effect(lose=('token', 1)), 1140 base.effect(set=('gate', 'open')) # knows to open gate at 'd' 1141 ] 1142 ) 1143 1144 assert len(e) == 10 1145 s8 = e.getSituation(8) 1146 s9 = e.getSituation(9) 1147 assert s8.action == ("warp", "active", 6) 1148 assert set(s9.graph) == set([0, 2, 3, 4, 5, 6]) 1149 assert s9.graph.namesListing(s9.graph) == """\ 1150 0 (a) 1151 2 (b) 1152 3 (_u.2) 1153 4 (c) 1154 5 (_u.4) 1155 6 (d) 1156""" 1157 assert e.getActiveDecisions(9) == {6} 1158 assert base.effectiveCapabilitySet(s9.state) == { 1159 "capabilities": {"power"}, 1160 "tokens": {"token": 1}, 1161 "skills": {} 1162 } 1163 assert s9.type == "pending" 1164 assert s9.action is None 1165 assert (s9.graph.destinationsFrom('a') == { 1166 'North': 6, 1167 'East': 2, 1168 'South': 3, 1169 'powerUp': 0 1170 }) 1171 assert (s9.graph.destinationsFrom('b') == { 1172 'North': 4, 1173 'West': 0, 1174 'South': 5 1175 }) 1176 assert (s9.graph.destinationsFrom('c') == {'West': 6}) 1177 assert (s9.graph.destinationsFrom('d') == {'East': 4, 'South': 0}) 1178 1179 ctx1 = base.genericContextForSituation(s1) 1180 ctx8 = base.genericContextForSituation(s8) 1181 ctx9 = base.genericContextForSituation(s9) 1182 with pytest.raises(core.MissingMechanismError): 1183 base.mechanismInStateOrEquivalent(gateD, 'open', ctx1) 1184 # 'gate mechanism doesn't exist back then (neither does decision 'd') 1185 assert not base.mechanismInStateOrEquivalent(gateD, 'open', ctx8) 1186 assert base.mechanismInStateOrEquivalent(gateD, 'open', ctx9) 1187 1188 with pytest.raises(core.MissingMechanismError): 1189 e.mechanismState(gateA, step=1) 1190 1191 assert e.mechanismState(gateA) == 'off' 1192 assert e.mechanismState(gateD) == 'open' 1193 assert e.mechanismState(gateD, step=8) == 'closed'
Multi-method test for exploration.core.DiscreteExploration.
1196def test_exploring_with_zones() -> None: 1197 """ 1198 A test for exploring with zones being applied. 1199 """ 1200 e = core.DiscreteExploration() 1201 1202 assert e.start('start') == 0 1203 graph = e.getSituation().graph 1204 graph.createZone('zone', 0) 1205 graph.addDecisionToZone('start', 'zone') 1206 e.observe(0, 'transition') 1207 assert e.explore('transition', 'room') == 1 1208 1209 s = e.getSituation() 1210 g = s.graph 1211 assert g.zoneParents(0) == {'zone'} 1212 assert g.zoneParents(1) == {'zone'} 1213 1214 e.observeAll(1, 'out', 'down') 1215 unknown = g.destination('room', 'down') 1216 g.renameDecision(unknown, 'fourth_room') 1217 assert g.nameFor(3) == 'fourth_room' 1218 assert g.zoneParents(3) == set() 1219 assert not g.isConfirmed('fourth_room') 1220 assert not e.hasBeenVisited('fourth_room') 1221 assert e.explore('out', 'another_room', 'back', 'zone2') == 2 1222 e.retrace('back') 1223 e.explore('down', None, 'up') # already named 'fourth_room' 1224 1225 g = e.getSituation().graph 1226 assert g.nameFor(3) == 'fourth_room' 1227 assert g.zoneParents(0) == {'zone'} 1228 assert g.zoneParents(1) == {'zone'} 1229 assert g.zoneParents(2) == {'zone2'} 1230 assert g.zoneParents(3) == {'zone'}
A test for exploring with zones being applied.
1233def test_triggers() -> None: 1234 e = core.DiscreteExploration() 1235 e.start('start') 1236 assert e.primaryDecision() == 0 1237 e.takeAction( 1238 'shiver', 1239 requires=base.ReqNot(base.ReqCapability('jacket')), 1240 consequence=[base.effect(gain=('cold', 1))], 1241 fromDecision='start' 1242 ) 1243 e.getSituation().graph.tagTransition('start', 'shiver', 'trigger') 1244 assert e.tokenCountNow('cold') == 1 1245 e.observe('start', 'right') 1246 e.explore('right', 'room', 'left') 1247 assert e.primaryDecision() == 1 1248 assert e.tokenCountNow('cold') == 1 1249 e.takeAction( 1250 'warmUp', 1251 requires=base.ReqTokens('cold', 1), 1252 consequence=[base.effect(lose=('cold', 1))], 1253 fromDecision='room' 1254 ) 1255 assert e.tokenCountNow('cold') == 0 1256 e.getSituation().graph.tagTransition('room', 'warmUp', 'trigger') 1257 e.wait() 1258 assert e.tokenCountNow('cold') == 0 1259 e.retrace('left') 1260 assert e.primaryDecision() == 0 1261 assert e.tokenCountNow('cold') == 1 1262 e.wait() 1263 assert e.tokenCountNow('cold') == 2 1264 e.wait() 1265 assert e.tokenCountNow('cold') == 3 1266 e.retrace('right') 1267 assert e.tokenCountNow('cold') == 2 1268 e.wait() 1269 assert e.tokenCountNow('cold') == 1 1270 e.wait() 1271 # No issue with trigger when out of tokens because of its requirement 1272 assert e.tokenCountNow('cold') == 0 1273 e.wait() 1274 assert e.tokenCountNow('cold') == 0 1275 # Get a jacket 1276 e.applyExtraneousEffect(base.effect(gain='jacket')) 1277 e.retrace('left') 1278 # Jacket prevents trigger 1279 assert e.primaryDecision() == 0 1280 assert e.tokenCountNow('cold') == 0 1281 e.wait() 1282 assert e.tokenCountNow('cold') == 0 1283 # TODO: Add a trigger group...