""" Authors: Peter Mawhorter Consulted: Date: 2022-3-12 Purpose: Tests for the core functionality and types. """ from typing import Optional, Union, Iterable, Tuple, Literal import json import copy import pytest from .. import base from .. import core from .. import parsing @pytest.fixture def pf() -> parsing.ParseFormat: """ A fixture that provides the default `parsing.ParseFormat`. """ return parsing.ParseFormat() @pytest.fixture def trc() -> base.RequirementContext: """ A fixture providing an empty requirement context. """ baseGraph = core.DecisionGraph.example('simple') # Has nodes A/B/C in a triangle with "next"/"prev" transitions baseState = base.emptyState() # Set up node A as the current position baseState['common']['focalization']['main'] = 'singular' baseState['common']['activeDomains'] = { 'main' } baseState['common']['activeDecisions']['main'] = 0 baseState['primaryDecision'] = 0 # A return base.RequirementContext( state=baseState, graph=baseGraph, searchFrom=set() ) def rcWith( original: base.RequirementContext, where: Optional[Iterable[base.AnyDecisionSpecifier]] = None, eq: Optional[ Iterable[ Tuple[ base.Requirement, Union[ base.Capability, Tuple[base.MechanismID, base.MechanismState] ] ] ] ] = None, **kwargs: Union[ bool, base.TokenCount, base.MechanismState, Tuple[Literal['skill'], int], Tuple[Literal['tag'], base.TagValue] ] ): """ 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. """ if where is not None: searchFrom = set( original.graph.resolveDecision(x) for x in where ) else: searchFrom = copy.deepcopy(original.searchFrom) newState = copy.deepcopy(original.state) newGraph = copy.deepcopy(original.graph) if eq is not None: for (req, equivalentTo) in eq: newGraph.addEquivalence(req, equivalentTo) commonFC = newState['common'] for kw in kwargs: val = kwargs[kw] if isinstance(val, bool): if val: commonFC['capabilities']['capabilities'].add(kw) else: try: commonFC['capabilities']['capabilities'].remove(kw) except KeyError: pass elif isinstance(val, base.TokenCount): commonFC['capabilities']['tokens'][kw] = val elif isinstance(val, base.MechanismState): mID = original.graph.resolveMechanism(kw, original.searchFrom) newState['mechanisms'][mID] = val elif ( isinstance(val, tuple) and len(val) == 2 and val[0] == 'skill' and isinstance(val[1], int) ): commonFC['capabilities']['skills'][kw] = val[1] elif ( isinstance(val, tuple) and len(val) == 2 and val[0] == 'tag' ): pr = original.state['primaryDecision'] if pr is None: raise ValueError( "Base context has no primary decision so we can't" " tag anything." ) newGraph.tagDecision(pr, kw, val[1]) else: raise ValueError(f"Invalid context addition value: {val!r}") return base.RequirementContext( state=newState, graph=newGraph, searchFrom=searchFrom ) def test_Requirements(pf, trc) -> None: """ Multi-method test for `exploration.core.Requirement` and sub-classes. """ # Tests of comparison r: base.Requirement = base.ReqAll([ base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), base.ReqTokens('key', 1) ]) r2: base.Requirement = base.ReqAll([ base.ReqAny([base.ReqCapability('p1'), base.ReqCapability('p2')]), base.ReqTokens('key', 1) ]) assert r == r2 assert base.ReqNothing() == base.ReqNothing() assert base.ReqImpossible() == base.ReqImpossible() # Tests of satisfied assert not r.satisfied(trc) assert r.satisfied(rcWith(trc, p1=True, key=1)) assert not r.satisfied(rcWith(trc, key=1)) assert not r.satisfied(rcWith(trc, p1=True, key=0)) assert not r.satisfied(rcWith(trc, p1=True)) assert r.satisfied( rcWith(trc, p1=True, p2=True, key=2) ) r = base.ReqAny([ base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]), base.ReqTokens('key', 3) ]) assert r.satisfied(rcWith(trc, p1=True, key=3)) assert not r.satisfied(rcWith(trc, key=1)) assert not r.satisfied(rcWith(trc, p2=True, key=0)) assert not r.satisfied(rcWith(trc, p1=True)) assert r.satisfied(rcWith(trc, p1=True, p2=True)) assert r.satisfied(rcWith(trc, p1=True, p2=True, key=2)) assert r.satisfied(rcWith(trc, p1=True, p2=True, key=5)) assert r.satisfied(rcWith(trc, key=5)) assert not base.hasCapabilityOrEquivalent('p3', trc) assert base.hasCapabilityOrEquivalent('p3', rcWith(trc, p3=True)) p2IsP3 = rcWith(trc, eq=[(base.ReqCapability('p2'), 'p3')]) assert not base.hasCapabilityOrEquivalent('p3', p2IsP3) assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p2=True)) assert base.hasCapabilityOrEquivalent('p3', rcWith(p2IsP3, p3=True)) assert not base.hasCapabilityOrEquivalent('p2', rcWith(p2IsP3, p3=True)) assert base.ReqCapability('p3').satisfied(rcWith(p2IsP3, p2=True)) r = base.ReqAll( [base.ReqCapability('p1'), base.ReqCapability('p3')] ) assert r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) assert not r.satisfied(rcWith(p2IsP3, p2=True)) assert not r.satisfied(rcWith(p2IsP3, p1=True)) assert not r.satisfied(rcWith(p2IsP3, p3=True)) assert r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) r = base.ReqImpossible() assert not r.satisfied(trc) r = base.ReqNothing() assert r.satisfied(trc) r = base.ReqNot( base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p2')]) ) assert r.satisfied(trc) assert r.satisfied(rcWith(trc, p1=True)) assert r.satisfied(rcWith(trc, p2=True)) assert not r.satisfied(rcWith(trc, p1=True, p2=True)) assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) r = base.ReqNot( base.ReqAll([base.ReqCapability('p1'), base.ReqCapability('p3')]) ) assert r.satisfied(p2IsP3) assert not r.satisfied(rcWith(p2IsP3, p1=True, p2=True)) assert not r.satisfied(rcWith(p2IsP3, p1=True, p3=True)) assert r.satisfied(rcWith(p2IsP3, p1=True)) assert r.satisfied(rcWith(p2IsP3, p2=True)) assert r.satisfied(rcWith(p2IsP3, p3=True)) # Mechanism requirements withSwitch = copy.deepcopy(trc) withSwitch.graph.addMechanism('switch', 0) # at A r = base.ReqMechanism('switch', 'on') assert not r.satisfied(withSwitch) assert not r.satisfied(rcWith(withSwitch, switch="off")) assert r.satisfied(rcWith(withSwitch, switch="on")) leverNearby = copy.deepcopy(trc) leverNearby.graph.addMechanism('lever', 1) # at B r = base.ReqMechanism('lever', 'pulled') assert not r.satisfied(leverNearby) assert not r.satisfied(rcWith(leverNearby, lever="default")) assert r.satisfied(rcWith(leverNearby, lever="pulled")) # Mechanism requirement w/ decision ID switchAt0 = base.MechanismSpecifier(None, None, 0, 'switch') switchAt1 = base.MechanismSpecifier(None, None, 1, 'switch') r = base.ReqMechanism(switchAt0, 'on') assert not r.satisfied(withSwitch) assert r.satisfied(rcWith(withSwitch, switch="on")) # 'switch' at different specific decision won't search r2 = base.ReqMechanism(switchAt1, 'on') assert not r.satisfied(withSwitch) assert r.satisfied(rcWith(withSwitch, switch="on")) # Skill level requirements r = base.ReqLevel('skill', 1) r2 = base.ReqNot(base.ReqLevel('skill', 2)) assert not r.satisfied(trc) assert not r.satisfied(rcWith(trc, skill=("skill", 0))) assert r.satisfied(rcWith(trc, skill=("skill", 1))) assert r.satisfied(rcWith(trc, skill=("skill", 2))) assert r.satisfied(rcWith(trc, skill=("skill", 10))) assert r2.satisfied(rcWith(trc, skill=("skill", 1))) assert not r2.satisfied(rcWith(trc, skill=("skill", 2))) assert not r2.satisfied(rcWith(trc, skill=("skill", 10))) # Tag requirements r = base.ReqTag('tag', 1) r2 = base.ReqTag('tag2', 'value') assert not r.satisfied(trc) assert not r2.satisfied(trc) tagged = rcWith(trc, tag=("tag", 1)) assert tagged.graph.decisionTags(0) == {'tag': 1} assert r.satisfied(tagged) assert not r.satisfied(rcWith(trc, tag=("tag", 2))) assert not r.satisfied(rcWith(trc, tag=("tag", 0))) assert r2.satisfied(rcWith(trc, tag2=("tag", 'value'))) taggedAside = copy.deepcopy(trc) taggedAside.graph.tagDecision(1, 'tag', 1) taggedAside.graph.tagDecision(2, 'tag2', 'value') assert not r.satisfied(taggedAside) assert not r2.satisfied(taggedAside) taggedAside.state['common']['activeDecisions']['main'] = 1 assert r.satisfied(taggedAside) assert not r2.satisfied(taggedAside) taggedAside.state['common']['activeDecisions']['main'] = 2 assert not r.satisfied(taggedAside) assert r2.satisfied(taggedAside) zoneTagged = copy.deepcopy(trc) zoneTagged.graph.createZone('zone') zoneTagged.graph.addDecisionToZone(0, 'zone') zoneTagged.graph.tagZone('zone', 'tag', 1) assert r.satisfied(zoneTagged) assert not r2.satisfied(zoneTagged) zoneTagged.graph.removeDecisionFromZone(0, 'zone') assert not r.satisfied(zoneTagged) # Tests of parsing: assert pf.parseRequirement('a') == base.ReqCapability('a') assert pf.parseRequirement('(a)') == base.ReqCapability('a') assert pf.parseRequirement('a*5') == base.ReqTokens('a', 5) assert pf.parseRequirement('((a*5))') == base.ReqTokens('a', 5) assert pf.parseRequirement('(a|b)&c*3') == base.ReqAll([ base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), base.ReqTokens('c', 3) ]) assert pf.parseRequirement(' ( a | b )\t& c * 3 ') == base.ReqAll([ base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]), base.ReqTokens('c', 3) ]) assert pf.parseRequirement('a|(b&c*3)') == base.ReqAny([ base.ReqCapability('a'), base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), ]) assert pf.parseRequirement('a|b&c*3') == base.ReqAny([ base.ReqCapability('a'), base.ReqAll([base.ReqCapability('b'), base.ReqTokens('c', 3)]), ]) assert pf.parseRequirement('a&b|c*3') == base.ReqAny([ base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]), base.ReqTokens('c', 3) ]) assert pf.parseRequirement('a|b|c') == base.ReqAny([ base.ReqCapability('a'), base.ReqCapability('b'), base.ReqCapability('c'), ]) assert pf.parseRequirement('a&b&c&d') == base.ReqAll([ base.ReqCapability('a'), base.ReqCapability('b'), base.ReqCapability('c'), base.ReqCapability('d'), ]) assert pf.parseRequirement('a&b|c&d') == base.ReqAny([ base.ReqAll([ base.ReqCapability('a'), base.ReqCapability('b') ]), base.ReqAll([ base.ReqCapability('c'), base.ReqCapability('d') ]) ]) assert pf.parseRequirement('a&!b|!c&d') == base.ReqAny([ base.ReqAll([ base.ReqCapability('a'), base.ReqNot(base.ReqCapability('b')) ]), base.ReqAll([ base.ReqNot(base.ReqCapability('c')), base.ReqCapability('d') ]) ]) assert pf.parseRequirement('!(a|b)&c') == base.ReqAll([ base.ReqNot( base.ReqAny([base.ReqCapability('a'), base.ReqCapability('b')]) ), base.ReqCapability('c') ]) assert pf.parseRequirement('!a&b&c') == base.ReqAll([ base.ReqNot(base.ReqCapability('a')), base.ReqCapability('b'), base.ReqCapability('c') ]) assert pf.parseRequirement('!(a&b)&c') == base.ReqAll([ base.ReqNot( base.ReqAll([base.ReqCapability('a'), base.ReqCapability('b')]) ), base.ReqCapability('c') ]) assert pf.parseRequirement('!c*3') == base.ReqNot(base.ReqTokens('c', 3)) assert pf.parseRequirement('X') == base.ReqImpossible() assert pf.parseRequirement('O') == base.ReqNothing() assert pf.parseRequirement('X|a') == base.ReqAny([ base.ReqImpossible(), base.ReqCapability('a') ]) assert pf.parseRequirement('door:open') == base.ReqMechanism('door', 'open') assert pf.parseRequirement('z::d::door:open') == base.ReqMechanism( base.MechanismSpecifier(None, 'z', 'd', 'door'), 'open' ) assert pf.parseRequirement('3::door:open') == base.ReqMechanism( base.MechanismSpecifier(None, None, 3, 'door'), 'open' ) with pytest.raises(parsing.ParseError): pf.parseRequirement('a*3*2') with pytest.raises(parsing.ParseError): pf.parseRequirement('(a):2') with pytest.raises(parsing.ParseError): pf.parseRequirement('(a|b&c):3') with pytest.raises(parsing.ParseError): pf.parseRequirement('a|&b') with pytest.raises(parsing.ParseError): pf.parseRequirement('(a|b') with pytest.raises(parsing.ParseError): pf.parseRequirement('a|b)') assert (pf.parseRequirement('a*-3') == base.ReqTokens('a', -3)) with pytest.raises(parsing.ParseError): pf.parseRequirement('a*!3') with pytest.raises(parsing.ParseError): pf.parseRequirement('a!b') with pytest.raises(parsing.ParseError): pf.parseRequirement('a*-b') def test_DecisionGraph() -> None: "Multi-method test for `exploration.core.DecisionGraph`." m = core.DecisionGraph() m.addDecision('a') m.addDecision('b') m.addDecision('c') assert len(m) == 3 assert set(m) == {0, 1, 2} m.addTransition('a', 'East', 'b', 'West') m.addTransition('a', 'Northeast', 'c') m.addTransition('c', 'Southeast', 'b') assert m.destinationsFrom('a') == {'East': 1, 'Northeast': 2} assert m.destinationsFrom('b') == {'West': 0} assert m.destinationsFrom('c') == {'Southeast': 1} assert m.getReciprocal('a', 'East') == 'West' assert m.getReciprocal('a', 'Northeast') is None assert m.getReciprocal('b', 'Southwest') is None assert m.getReciprocal('c', 'Southeast') is None m.addUnexploredEdge('a', 'South') m.addUnexploredEdge('b', 'East') m.addUnexploredEdge('c', 'North') assert len(m) == 6 assert set(m) == set(range(6)) assert m.namesListing(m) == """\ 0 (a) 1 (b) 2 (c) 3 (_u.0) 4 (_u.1) 5 (_u.2) """ assert ( m.destinationsFrom('a') == {'East': 1, 'Northeast': 2, 'South': 3} ) assert (m.destinationsFrom('b') == {'West': 0, 'East': 4}) assert (m.destinationsFrom('c') == {'Southeast': 1, 'North': 5}) m.replaceUnconfirmed('c', 'North', 'd', 'South') assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} assert m.nameFor(5) == 'd' assert len(m) == 6 assert set(m) == set(range(6)) assert m.namesListing(m) == """\ 0 (a) 1 (b) 2 (c) 3 (_u.0) 4 (_u.1) 5 (d) """ m.addTransition('d', 'West', 'a', 'North') assert ( m.destinationsFrom('a') == {'East': 1, 'Northeast': 2, 'South': 3, 'North': 5} ) assert ( m.destinationsFrom('d') == {'West': 0, 'South': 2} ) with pytest.raises(core.MissingDecisionError): _ = m.destinationsFrom('z') with pytest.raises(core.TransitionCollisionError): m.addTransition('a', 'East', 'b', 'West') with pytest.raises(core.TransitionCollisionError): m.addTransition('c', 'East', 'b', 'West') with pytest.raises(core.TransitionCollisionError): m.addUnexploredEdge('a', 'East') with pytest.raises(core.TransitionCollisionError): m.addUnexploredEdge('c', 'North') with pytest.raises(core.MissingTransitionError): m.replaceUnconfirmed('a', 'Up', 'z') with pytest.raises(core.ExplorationStatusError): m.replaceUnconfirmed('a', 'East', 'z') assert m.destinationsFrom('c') == {'Southeast': 1, 'North': 5} m.addTransition('a', 'EastBelow', 'b', 'WestBelow') assert (m.destinationsFrom('a') == { 'East': 1, 'EastBelow': 1, 'Northeast': 2, 'South': 3, 'North': 5 }) assert ( m.destinationsFrom('b') == {'West': 0, 'WestBelow': 0, 'East': 4} ) # Two edges that could be but are not reciprocals m.addTransition('d', 'East', 'b') m.addTransition('b', 'North', 'd') assert ( m.destinationsFrom('b') == {'West': 0, 'WestBelow': 0, 'East': 4, 'North': 5} ) assert ( m.destinationsFrom('d') == {'West': 0, 'South': 2, 'East': 1} ) assert m.getReciprocal('b', 'North') is None assert m.getReciprocal('d', 'East') is None # Establish a reciprocal relationship m.setReciprocal('b', 'North', 'East') assert m.getReciprocal('b', 'North') == 'East' assert m.getReciprocal('d', 'East') == 'North' with pytest.raises(core.MissingDecisionError): m.setReciprocal('z', 'Nope', 'None') with pytest.raises(core.MissingTransitionError): m.setReciprocal('b', 'Nope', 'None') # Remove the reciprocal relationship again (from the other side) m.setReciprocal('d', 'East', None) assert m.getReciprocal('b', 'North') is None assert m.getReciprocal('d', 'East') is None with pytest.raises(core.InvalidDestinationError): m.setReciprocal('b', 'North', 'West') with pytest.raises(core.MissingTransitionError): m.setReciprocal('b', 'North', 'None') assert (m.isConfirmed("_u.0") is False) assert (m.isConfirmed("_u.1") is False) assert (m.isConfirmed("a") is True) assert (m.isConfirmed("d") is True) assert ( json.dumps(m.textMapObj(), indent=4) == """\ { "0::East": { "1::West": "0", "1::East": {}, "1::WestBelow": "0", "1::North": { "5::South": { "2::Southeast": "1", "2::North": "5" }, "5::West": "0", "5::East": "1" } }, "0::Northeast": "2", "0::South": {}, "0::North": "5", "0::EastBelow": "1" }""" ) assert ( json.dumps( m.textMapObj( explorationOrder=( 0, [ 'East', 'West', 'Northeast', 'Southeast', 'North', 'West', 'South', ] ) ), indent=4 ) == """\ { "0::East": { "1::West": "0", "1::North": { "5::West": "0", "5::South": { "2::Southeast": "1", "2::North": "5" }, "5::East": "1" }, "1::East": {}, "1::WestBelow": "0" }, "0::Northeast": "2", "0::South": {}, "0::North": "5", "0::EastBelow": "1" }""" ) m.addTransition( 'd', 'failure', m.endingID('failure') ) assert set(m) == set(range(7)) assert m.namesListing(m) == """\ 0 (a) 1 (b) 2 (c) 3 (_u.0) 4 (_u.1) 5 (d) 6 (endings//failure) """ assert ( m.destinationsFrom('d') == {'West': 0, 'South': 2, 'East': 1, 'failure': 6} ) assert m.destinationsFrom('failure') == {} def test_DGTagsAndAnnotations() -> None: """ Test for tagging decisions and transitions in an `exploration.core.DecisionGraph`. """ m = core.DecisionGraph() m.addDecision('a') m.addDecision('b', tags={'grass': 1}) m.addDecision('c') m.tagDecision('c', {'water': 1, 'big': 1}) m.annotateDecision('c', "This is a note.") m.addTransition('a', 'East', 'b', 'West') m.addTransition( 'a', 'South', 'c', 'North', {'green': 1}, ["Requires green key"], {'blue': 1}, ["Requires blue key"] ) m.tagTransition('a', 'South', 'green') m.annotateTransition('a', 'South', "a2") m.tagTransition('c', 'North', 'blue', 1) m.annotateTransition('c', 'North', ["Requires", "blue key"]) assert m.decisionTags('a') == {} assert m.decisionTags('b') == {'grass': 1} assert m.decisionTags('c') == {'water': 1, 'big': 1} assert m.transitionTags('a', 'East') == {} assert m.transitionTags('a', 'South') == {'green': 1} assert m.transitionTags('b', 'West') == {} assert m.transitionTags('c', 'North') == {'blue': 1} assert m.decisionAnnotations('a') == [] assert m.decisionAnnotations('b') == [] assert m.decisionAnnotations('c') == ["This is a note."] assert m.transitionAnnotations('a', 'East') == [] assert m.transitionAnnotations('a', 'South') == [ "Requires green key", "a2" ] assert m.transitionAnnotations('b', 'West') == [] assert m.transitionAnnotations('c', 'North') == [ "Requires blue key", "Requires", "blue key" ] m.tagDecision('a', 'grass') assert m.decisionTags('a') == {'grass': 1} m.untagDecision('b', 'grass') assert m.decisionTags('b') == {} m.annotateDecision('a', "Starting location.") assert m.decisionAnnotations('a') == ["Starting location."] m.annotateDecision('c', "Blue key here.") assert m.decisionAnnotations('c') == ["This is a note.", "Blue key here."] assert m.decisionAnnotations('b') == [] m.tagTransition('a', 'East', 'open') assert m.transitionTags('a', 'East') == {'open': 1} m.tagTransition('a', 'South', 'open') assert m.transitionTags('a', 'South') == {'green': 1, 'open': 1} m.untagTransition('a', 'South', 'green') assert m.transitionTags('a', 'South') == {'open': 1} m.tagTransition('a', 'South', 'open', [1, 2, 3]) assert m.transitionTags('a', 'South') == {'open': [1, 2, 3]} m.annotateTransition('c', 'North', "This was difficult.") assert ( m.transitionAnnotations('c', 'North') == ['Requires blue key', 'Requires', 'blue key', 'This was difficult.'] ) cna = m.transitionAnnotations('c', 'North') cna.clear() cna.append('hi') assert m.transitionAnnotations('c', 'North') == ['hi'] with pytest.raises(core.MissingTransitionError): _ = m.transitionAnnotations('a', 'West') def test_DiscreteExploration() -> None: "Multi-method test for `exploration.core.DiscreteExploration`." e = core.DiscreteExploration() s0 = e.getSituation(0) assert e.getSituation() is s0 assert len(s0.graph) == 0 assert e.getActiveDecisions(0) == set() assert s0.state == base.emptyState() assert s0.type == "pending" assert s0.action is None assert len(e) == 1 e.start('a') e.observeAll('a', 'North', 'East', 'South') assert len(e) == 2 s0 = e.getSituation(0) s1 = e.getSituation(1) assert s1 is e.getSituation() assert len(s1.graph) == 4 assert set(s1.graph) == set(range(4)) assert s1.graph.namesListing(s1.graph) == """\ 0 (a) 1 (_u.0) 2 (_u.1) 3 (_u.2) """ assert e.getActiveDecisions() == e.getActiveDecisions(1) assert e.getActiveDecisions() == {0} assert s0.type == "imposed" assert s0.action == ( 'start', 0, 0, 'main', None, None, None ) e.explore('East', 'b', 'West') e.observeAll('b', 'North', 'South') assert len(e) == 3 assert set(s1.graph) == set(range(4)) assert s1.graph.namesListing(s1.graph) == """\ 0 (a) 1 (_u.0) 2 (_u.1) 3 (_u.2) """ assert e.getActiveDecisions(1) == {0} s1 = e.getSituation(1) assert s1.type == 'active' dz = base.DefaultZone assert s1.action == ( 'explore', 'active', 0, ('East', []), 'b', 'West', dz ) s2 = e.getSituation(2) assert s2 is e.getSituation() assert set(s2.graph) == set(range(6)) assert s2.graph.namesListing(s2.graph) == """\ 0 (a) 1 (_u.0) 2 (b) 3 (_u.2) 4 (_u.3) 5 (_u.4) """ assert e.getActiveDecisions() == {2} assert s2.type == "pending" assert s2.action is None assert (s2.graph.destinationsFrom('a') == { 'North': 1, 'East': 2, 'South': 3 }) assert (s2.graph.destinationsFrom('b') == { 'North': 4, 'West': 0, 'South': 5 }) assert (s2.graph.destinationsFrom('_u.3') == {}) assert (s2.graph.destinationsFrom('_u.4') == {}) with pytest.raises(core.ExplorationStatusError): e.returnTo('West', 'a', 'North') with pytest.raises(core.ExplorationStatusError): e.returnTo('West', 'a', 'East') # (would need to use retrace instead) e.explore('North', 'c', None) e.observe('c', 'West') assert len(e) == 4 assert e.getSituation(0) == s0 assert e.getSituation(1) == s1 assert set(s1.graph) == set(range(4)) assert s1.graph.namesListing(s1.graph) == """\ 0 (a) 1 (_u.0) 2 (_u.1) 3 (_u.2) """ assert e.getActiveDecisions(1) == {0} assert s1.type == "active" assert s1.action == ('explore', 'active', 0, ('East', []), 'b', 'West', dz) ns2 = e.getSituation(2) assert ns2 != s2 assert ns2.graph == s2.graph assert ns2.state == s2.state assert s2.type, ns2.type == ('pending', 'active') assert s2.action is None assert ns2.tags == s2.tags assert ns2.annotations == s2.annotations s2 = ns2 assert set(s2.graph) == set(range(6)) assert s2.graph.namesListing(s2.graph) == """\ 0 (a) 1 (_u.0) 2 (b) 3 (_u.2) 4 (_u.3) 5 (_u.4) """ assert e.getActiveDecisions(2) == {2} assert s2.type == "active" assert s2.action == ('explore', 'active', 2, ('North', []), 'c', None, dz) assert (s2.graph.destinationsFrom('a') == { 'North': 1, 'East': 2, 'South': 3 }) assert (s2.graph.destinationsFrom('b') == { 'North': 4, 'West': 0, 'South': 5 }) s3 = e.getSituation(3) assert set(s3.graph) == set(range(7)) assert s3.graph.namesListing(s3.graph) == """\ 0 (a) 1 (_u.0) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (_u.5) """ assert e.getActiveDecisions(3) == {4} assert s3.type == "pending" assert s3.action is None assert (s3.graph.destinationsFrom('a') == { 'North': 1, 'East': 2, 'South': 3 }) assert (s3.graph.destinationsFrom('b') == { 'North': 4, 'West': 0, 'South': 5 }) assert s3.graph.destinationsFrom('c') == {'West': 6} assert s3.graph.destinationsFrom('_u.0') == {} assert s3.graph.destinationsFrom('_u.2') == {} assert s3.graph.destinationsFrom('_u.4') == {} assert s3.graph.destinationsFrom('_u.5') == {} assert s3.graph.degree(0) == 4 assert s3.graph.degree(2) == 4 assert s3.graph.degree(4) == 2 assert s3.graph.degree(6) == 1 e.explore('West', 'd', 'East') assert len(e) == 5 s3 = e.getSituation(3) s4 = e.getSituation(4) assert set(s4.graph) == set(range(7)) assert s4.graph.namesListing(s4.graph) == """\ 0 (a) 1 (_u.0) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(4) == {6} assert s4.type == "pending" assert s4.action is None assert s4.graph.destinationsFrom('c') == {'West': 6} assert s4.graph.destinationsFrom('d') == {'East': 4} assert s4.graph.degree(4) == 3 assert s4.graph.degree(6) == 2 # Can't return if there's no outgoing edge yet with pytest.raises(core.MissingTransitionError): e.returnTo('South', 'a', 'North') with pytest.raises(core.ExplorationStatusError): e.returnTo('East', 'a', 'North') # Add the edge and then we can use it to return g = s4.graph g.addUnexploredEdge('d', 'South') assert set(g) == set(range(8)) assert g.namesListing(g) == """\ 0 (a) 1 (_u.0) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) 7 (_u.6) """ e.returnTo('South', 'a', 'North') assert len(e) == 6 s4 = e.getSituation(4) s5 = e.getSituation(5) assert s5 == e.getSituation() assert set(s5.graph) == set([0, 2, 3, 4, 5, 6]) assert s5.graph.namesListing(s5.graph) == """\ 0 (a) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(5) == {0} assert s5.type == "pending" assert s5.action is None assert s5.graph.destinationsFrom('a') == { 'East': 2, 'North': 6, 'South': 3 } assert s5.graph.destinationsFrom('d') == {'East': 4, 'South': 0} assert s5.graph.degree(4) == 3 assert s5.graph.degree(6) == 4 assert s5.graph.degree(0) == 5 e.wait() assert len(e) == 7 s5 = e.getSituation(5) s6 = e.getSituation(6) assert set(s6.graph) == set([0, 2, 3, 4, 5, 6]) assert s6.graph.namesListing(s6.graph) == """\ 0 (a) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(6) == {0} assert s6.type == "pending" assert s6.action is None assert s5.action == ('noAction',) e.takeAction( 'powerUp', consequence=[ base.effect(gain='power'), base.effect(gain=('token', 2)), ], fromDecision=0 ) assert len(e) == 8 s6 = e.getSituation(6) s7 = e.getSituation(7) assert set(s7.graph) == set([0, 2, 3, 4, 5, 6]) assert s7.graph.namesListing(s7.graph) == """\ 0 (a) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(7) == {0} assert base.hasCapabilityOrEquivalent( 'power', base.genericContextForSituation(s7) ) assert base.combinedTokenCount(s7.state, 'token') == 2 assert base.effectiveCapabilitySet(s7.state) == { "capabilities": {"power"}, "tokens": {"token": 2}, "skills": {} } assert s7.type == "pending" assert s7.action is None assert (s7.graph.destinationsFrom('a') == { 'North': 6, 'East': 2, 'South': 3, 'powerUp': 0 }) assert s6.action == ('take', 'active', 0, ('powerUp', [])) e.retrace('East') assert len(e) == 9 s7 = e.getSituation(7) s8 = e.getSituation(8) assert set(s8.graph) == set([0, 2, 3, 4, 5, 6]) assert s8.graph.namesListing(s8.graph) == """\ 0 (a) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(8) == {2} assert s8.type == "pending" assert s8.action is None assert base.effectiveCapabilitySet(s8.state) == { "capabilities": {"power"}, "tokens": {"token": 2}, "skills": {} } assert s7.action == ('take', 'active', 0, ('East', [])) e.observeMechanisms('d', ('gate', 'closed')) gateD = base.mechanismAt('gate', decision='d') gateA = base.mechanismAt('gate', decision='a') assert gateD == (None, None, 'd', 'gate') assert gateA == (None, None, 'a', 'gate') assert base.mechanismInStateOrEquivalent( 'gate', 'closed', base.genericContextForSituation(s8) ) assert e.mechanismState('gate') == "closed" assert e.mechanismState(gateD) == "closed" # Can't get mechanism state in step before it's been observed with pytest.raises(core.MissingMechanismError): e.mechanismState('gate', step=6) # Can't get mechanism state for mechanism at a different decision assert e.mechanismState(gateA) == "closed" e.observeMechanisms('a', 'gate') # without starting state with pytest.raises(core.AmbiguousMechanismError): e.mechanismState('gate') with pytest.raises(core.AmbiguousMechanismError): e.mechanismState(base.mechanismAt('gate', decision='c')) # Default state assert e.mechanismState(gateA) == 'off' assert e.mechanismState(gateD) == 'closed' e.warp( 'd', consequence=[ base.effect(lose=('token', 1)), base.effect(set=('gate', 'open')) # knows to open gate at 'd' ] ) assert len(e) == 10 s8 = e.getSituation(8) s9 = e.getSituation(9) assert s8.action == ("warp", "active", 6) assert set(s9.graph) == set([0, 2, 3, 4, 5, 6]) assert s9.graph.namesListing(s9.graph) == """\ 0 (a) 2 (b) 3 (_u.2) 4 (c) 5 (_u.4) 6 (d) """ assert e.getActiveDecisions(9) == {6} assert base.effectiveCapabilitySet(s9.state) == { "capabilities": {"power"}, "tokens": {"token": 1}, "skills": {} } assert s9.type == "pending" assert s9.action is None assert (s9.graph.destinationsFrom('a') == { 'North': 6, 'East': 2, 'South': 3, 'powerUp': 0 }) assert (s9.graph.destinationsFrom('b') == { 'North': 4, 'West': 0, 'South': 5 }) assert (s9.graph.destinationsFrom('c') == {'West': 6}) assert (s9.graph.destinationsFrom('d') == {'East': 4, 'South': 0}) ctx1 = base.genericContextForSituation(s1) ctx8 = base.genericContextForSituation(s8) ctx9 = base.genericContextForSituation(s9) with pytest.raises(core.MissingMechanismError): base.mechanismInStateOrEquivalent(gateD, 'open', ctx1) # 'gate mechanism doesn't exist back then (neither does decision 'd') assert not base.mechanismInStateOrEquivalent(gateD, 'open', ctx8) assert base.mechanismInStateOrEquivalent(gateD, 'open', ctx9) with pytest.raises(core.MissingMechanismError): e.mechanismState(gateA, step=1) assert e.mechanismState(gateA) == 'off' assert e.mechanismState(gateD) == 'open' assert e.mechanismState(gateD, step=8) == 'closed' def test_exploring_with_zones() -> None: """ A test for exploring with zones being applied. """ e = core.DiscreteExploration() assert e.start('start') == 0 graph = e.getSituation().graph graph.createZone('zone', 0) graph.addDecisionToZone('start', 'zone') e.observe(0, 'transition') assert e.explore('transition', 'room') == 1 s = e.getSituation() g = s.graph assert g.zoneParents(0) == {'zone'} assert g.zoneParents(1) == {'zone'} e.observeAll(1, 'out', 'down') unknown = g.destination('room', 'down') g.renameDecision(unknown, 'fourth_room') assert g.nameFor(3) == 'fourth_room' assert g.zoneParents(3) == set() assert not g.isConfirmed('fourth_room') assert not e.hasBeenVisited('fourth_room') assert e.explore('out', 'another_room', 'back', 'zone2') == 2 e.retrace('back') e.explore('down', None, 'up') # already named 'fourth_room' g = e.getSituation().graph assert g.nameFor(3) == 'fourth_room' assert g.zoneParents(0) == {'zone'} assert g.zoneParents(1) == {'zone'} assert g.zoneParents(2) == {'zone2'} assert g.zoneParents(3) == {'zone'} def test_triggers() -> None: e = core.DiscreteExploration() e.start('start') assert e.primaryDecision() == 0 e.takeAction( 'shiver', requires=base.ReqNot(base.ReqCapability('jacket')), consequence=[base.effect(gain=('cold', 1))], fromDecision='start' ) e.getSituation().graph.tagTransition('start', 'shiver', 'trigger') assert e.tokenCountNow('cold') == 1 e.observe('start', 'right') e.explore('right', 'room', 'left') assert e.primaryDecision() == 1 assert e.tokenCountNow('cold') == 1 e.takeAction( 'warmUp', requires=base.ReqTokens('cold', 1), consequence=[base.effect(lose=('cold', 1))], fromDecision='room' ) assert e.tokenCountNow('cold') == 0 e.getSituation().graph.tagTransition('room', 'warmUp', 'trigger') e.wait() assert e.tokenCountNow('cold') == 0 e.retrace('left') assert e.primaryDecision() == 0 assert e.tokenCountNow('cold') == 1 e.wait() assert e.tokenCountNow('cold') == 2 e.wait() assert e.tokenCountNow('cold') == 3 e.retrace('right') assert e.tokenCountNow('cold') == 2 e.wait() assert e.tokenCountNow('cold') == 1 e.wait() # No issue with trigger when out of tokens because of its requirement assert e.tokenCountNow('cold') == 0 e.wait() assert e.tokenCountNow('cold') == 0 # Get a jacket e.applyExtraneousEffect(base.effect(gain='jacket')) e.retrace('left') # Jacket prevents trigger assert e.primaryDecision() == 0 assert e.tokenCountNow('cold') == 0 e.wait() assert e.tokenCountNow('cold') == 0 # TODO: Add a trigger group...