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...
@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    # 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.

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

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

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

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

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