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...
@pytest.fixture
def pf() -> exploration.parsing.ParseFormat:
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.

@pytest.fixture
def trc() -> exploration.base.RequirementContext:
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.

def rcWith( original: exploration.base.RequirementContext, where: Optional[Iterable[Union[int, exploration.base.DecisionSpecifier, str]]] = None, eq: Optional[Iterable[Tuple[exploration.base.Requirement, Union[str, Tuple[int, str]]]]] = None, **kwargs: Union[bool, int, str, Tuple[Literal['skill'], int], Tuple[Literal['tag'], Union[bool, int, float, str, list, dict, NoneType, exploration.base.Requirement, List[Union[exploration.base.Challenge, exploration.base.Effect, exploration.base.Condition]]]]]):
 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.

def test_Requirements(pf, trc) -> None:
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.

def test_DecisionGraph() -> None:
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.

def test_DGTagsAndAnnotations() -> None:
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.

def test_DiscreteExploration() -> None:
 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.

def test_exploring_with_zones() -> None:
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.

def test_triggers() -> None:
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...