""" Authors: Peter Mawhorter Consulted: Date: 2022-9-30 Purpose: Tests for journal functionality. """ import json import warnings import pytest from .. import core, journal, base def listingAtStep(expl: core.DiscreteExploration, step: int = -1) -> str: """ Calls `namesListing` for all decisions in the graph at the specified step of the given exploration. """ graph = expl.getSituation(step).graph return graph.namesListing(graph) def test_simpleConversion() -> None: """ A simple test of journal conversion. """ simple = journal.convertJournal("""\ S First_Room x Exit Second_Room Entrance gt tag o Onwards E END """) assert len(simple) == 4 finalGraph = simple.getSituation().graph assert len(finalGraph) == 4 assert finalGraph.namesListing(finalGraph) == """\ 0 (First_Room) 1 (Second_Room) 2 (_u.1) 3 (endings//END) """ assert finalGraph.destinationsFrom("First_Room") == {'Exit': 1} assert finalGraph.destinationsFrom("Second_Room") == { 'Entrance': 0, 'Onwards': 2 } assert finalGraph.destinationsFrom("END") == {} assert json.dumps( finalGraph.textMapObj(), indent=4 ) == """\ { "0::Exit": { "1::Entrance": "0", "1::Onwards": { "2::return": "1" } } }""" # TODO: We'd like to include the ending here! # TODO: Specific case coverage tests... def test_exploring_zones() -> None: """ A test for exploration with zones. """ expl = journal.convertJournal("""\ S zone::room # decision 0 x ahead room2 back # decision 1 o hatch basement ladder # decision 2 x ahead zone2::room3 back # decision 3 x ahead room4 back # decision 4 t back t back xt hatch """) assert expl.getActiveDecisions() == {2} now = expl.getSituation() assert now.graph.getDecision('basement') == 2 assert now.graph.getDecision( base.DecisionSpecifier(domain=None, zone='zone', name='basement') ) == 2 assert now.graph.getDecision( base.DecisionSpecifier(domain=None, zone='zone2', name='basement') ) is None assert now.graph.getDecision( base.DecisionSpecifier(domain=None, zone='zone2', name='room3') ) == 3 assert now.graph.namesListing(now.graph) == """\ 0 (zone::room) 1 (zone::room2) 2 (zone::basement) 3 (zone2::room3) 4 (zone2::room4) """ assert now.graph.zoneParents(0) == {'zone'} assert now.graph.zoneParents(2) == {'zone'} assert now.graph.zoneParents(3) == {'zone2'} assert now.graph.zoneParents(4) == {'zone2'} assert now.graph.zoneParents(1) == {'zone'} # Returning to a noticed unverified decision... expl2 = journal.convertJournal("""\ S inside::room # decision 0 o right outside::yard left # decision 1 x up attic down # decision 2 x up outside::roof down # decision 3 x jumpOff yard climbUp o right # decision 5 for reasons I don't 100% understand """) assert expl2.getActiveDecisions() == {1} now = expl2.getSituation() assert now.graph.zoneParents(0) == {'inside'} assert now.graph.zoneParents(1) == {'outside'} assert now.graph.zoneParents(2) == {'inside'} assert now.graph.zoneParents(3) == {'outside'} assert now.graph.zoneParents(5) == set() def test_zoneNaming() -> None: """ Tests some situations with zone propagation in journals. """ expl = journal.convertJournal("""\ S West::start x right second left o down third up x right East::fourth left t left xt down # third should be in zone 'West' o left New::fifth right xt left # fifth should remain in zone 'New' """) g1 = expl.getSituation(1).graph assert g1.namesListing(g1) == """\ 0 (West::start) 1 (_u.0) """ g2 = expl.getSituation(2).graph assert g2.namesListing(g2) == """\ 0 (West::start) 1 (West::second) 2 (West::third) 3 (_u.2) """ assert g2.zoneParents(0) == {'West'} assert g2.zoneParents(1) == {'West'} assert g2.zoneParents(2) == {'West'} assert g2.zoneParents(3) == set() g3 = expl.getSituation(3).graph assert g3.namesListing(g3) == """\ 0 (West::start) 1 (West::second) 2 (West::third) 3 (East::fourth) """ assert g3.zoneParents(0) == {'West'} assert g3.zoneParents(1) == {'West'} assert g3.zoneParents(2) == {'West'} assert g3.zoneParents(3) == {'East'} g4 = expl.getSituation(4).graph assert g4.namesListing(g4) == """\ 0 (West::start) 1 (West::second) 2 (West::third) 3 (East::fourth) """ assert g4.zoneParents(0) == {'West'} assert g4.zoneParents(1) == {'West'} assert g3.zoneParents(2) == {'West'} assert g4.zoneParents(3) == {'East'} g5 = expl.getSituation(5).graph assert g5.namesListing(g5) == """\ 0 (West::start) 1 (West::second) 2 (West::third) 3 (East::fourth) 4 (New::fifth) """ assert g5.zoneParents(0) == {'West'} assert g5.zoneParents(1) == {'West'} assert g5.zoneParents(2) == {'West'} assert g5.zoneParents(3) == {'East'} assert g5.zoneParents(4) == {'New'} g6 = expl.getSituation(6).graph assert g6.namesListing(g6) == """\ 0 (West::start) 1 (West::second) 2 (West::third) 3 (East::fourth) 4 (New::fifth) """ assert g6.zoneParents(0) == {'West'} assert g6.zoneParents(1) == {'West'} assert g6.zoneParents(2) == {'West'} assert g6.zoneParents(3) == {'East'} assert g6.zoneParents(4) == {'New'} def test_startOfCotM() -> None: """ A test for converting part of a journal from Castlevania: Circle of the Moon. This journal covers just the first few rooms of the game. """ # Note that we're expecting a `TransitionBlockedWarning` because the # requirement 'q distance' does not match the power gained 'At gain # dash_boots'... expl = journal.convertJournal("""\ # Setup # ----- # A save-point alias that creates, explores and tags a new decision but # then returns to where we just were. = savePoint [ x {_1} save{_2} return g save t return ] = dagger [ a candle{_} At gain dagger At lose axe At lose holy_water At lose cross ] = axe [ a candle{_} At lose dagger At gain axe At lose holy_water At lose cross ] = holy_water [ a candle{_} At lose dagger At lose axe At gain holy_water At lose cross ] = cross [ a candle{_} At lose dagger At lose axe At lose holy_water At gain cross ] # Journal # ------- S Confrontation_Site::main # Stars the exploration, and applies a zone zz Catacombs # Creates and applies a level-1 zone above the existing zone o Top_Left # Observes a transition at the current decision x Bottom Rubble_Tower_Base::top Top # Explores a transition (newly created) # The 'x' entry above notes the transition taken, the room name on # the other end, the transition name for the reciprocal, and the # new level-0 zone for the destination (could be omitted if it's in # the same level-0 zone). gt forced # Tags the transition we just explored o Crack # We see a crack in the wall as we go down q ?short_and_high # But we can't get through it right now # Spoilers: we'll never be able to get through it from this side n Fought -Skeleton_Bomber- # A note, applied to the exploration step A gain Salamander # An applied effect: gaining a power # Note: We're not modelling random drops but could with a challenge... n Literally first enemy I touched! # Another note x Bottom_Right Catacombs_Entrance::top Top_Left # Another exploration o up n Can't jump up here yet q height o top_stairs mid top_stairs # The stairs to the middle level # Here we define the room name on the other side and the reciprocal # transition name. x First_Right Catacombs_Treasury::left Left # But first a detour o box # Can't get through this box yet qb heavy # Requirement applies in both directions t Left # This 't' retraces a known transition at the current position xt top_stairs # Here the room and reciprocal names have already been # defined, and the zone is the same o bottom_stairs bottom bottom_stairs A gain Mercury n Bone head along the way > savePoint xt bottom_stairs > dagger o Bot_Left o across_bottom xt Bot_Left Blockwall_Cave::bottom Right o ledge q height t Right xt across_bottom bottom_right back_across_bottom o Bot_Right # TODO: Is the zone necessary here? Feels like it shouldn't be u Catacombs_Entrance::bottom # Nothing super interesting across the bottom of the room, so we might # as well treat the entire bottom as one decision. xt Bot_Right Green_Gates::entrance Left n Fought -Earth_Demon- o jump q distance a get_dash_boots At gain dash_boots F dash_boots distance xt jump upper left o right_stairs_down right_side o climb xt climb attic fall v fall_left entrance fly_up qr flight n at: Green_Gates::attic o fall_right right_side fly_up qr flight x Upper_Right Catacombs_Treasury_2::entrance Left o block qb crumble t Left n at: Green_Gates::attic xt fall_right n at: Green_Gates::right_side x Lower_Right Zombie_Corridor::main Left n [ Fought -Poison_Worm- combo'd with -Zombie- and -Skeleton-. This makes for an interesting room. ] # This annotation spans multiple lines of text x Right Pinkstone_Tower::bottom_left Left_Mid # And that's the end of this fragment for testing purposes! """) assert len(expl) == 23 firstSituation = expl.getSituation(0) firstDecisions = expl.getActiveDecisions(0) secondSituation = expl.getSituation(1) secondDecisions = expl.getActiveDecisions(1) sixthSituation = expl.getSituation(5) sixthDecisions = expl.getActiveDecisions(5) finalSituation = expl.getSituation() finalDecisions = expl.getActiveDecisions() assert len(firstSituation.graph) == 1 assert firstDecisions == set() assert len(secondSituation.graph) == 3 # room + 2 unknowns assert listingAtStep(expl, 1) == """\ 0 (Confrontation_Site::main) 1 (_u.0) 2 (_u.1) """ assert secondDecisions == {0} g6 = sixthSituation.graph assert len(g6) == 9 assert listingAtStep(expl, 5) == """\ 0 (Confrontation_Site::main) 1 (_u.0) 2 (Rubble_Tower_Base::top) 3 (_u.2) 4 (Catacombs_Entrance::top) 5 (_u.4) 6 (Catacombs_Entrance::mid) 7 (Catacombs_Treasury::left) 8 (_u.7) """ # 'mid' has not been explored yet so isn't in a zone gZones = [g6.zoneParents(d) for d in sorted(g6)] assert gZones == [ {'Confrontation_Site'}, set(), {'Rubble_Tower_Base'}, set(), {'Catacombs_Entrance'}, set(), {'Catacombs_Entrance'}, {'Catacombs_Treasury'}, set() ] assert g6.destination('_u.0', 'return') == 0 assert g6.destination('_u.2', 'return') == 2 assert g6.destination('_u.4', 'return') == 4 assert g6.destination('_u.7', 'return') == 7 assert sixthDecisions == {4} gL = finalSituation.graph assert len(gL) == 21 assert listingAtStep(expl, -1) == """\ 0 (Confrontation_Site::main) 1 (_u.0) 2 (Rubble_Tower_Base::top) 3 (_u.2) 4 (Catacombs_Entrance::top) 5 (_u.4) 6 (Catacombs_Entrance::mid) 7 (Catacombs_Treasury::left) 8 (_u.7) 9 (Catacombs_Entrance::bottom) 10 (Catacombs_Entrance::save_1) 11 (Blockwall_Cave::bottom) 13 (_u.12) 14 (Green_Gates::entrance) 15 (Green_Gates::upper) 16 (Green_Gates::right_side) 17 (Green_Gates::attic) 20 (Catacombs_Treasury_2::entrance) 21 (_u.20) 22 (Zombie_Corridor::main) 23 (Pinkstone_Tower::bottom_left) """ assert finalDecisions == {23} def _test_CotMToCerberus() -> None: """ A test for converting part of a journal from Castlevania: Circle of the Moon. This journal covers an experienced re-playthrough up to the first boss (Cerberus) and boss reward (double-jump). TODO: This test is not finished yet! """ expl = journal.convertJournal("""\ # Setup # ----- P on # Turns on zone-prefixing (we're using level-0 zones for rooms) # A save-point alias that creates, explores and tags a new decision but # then returns to where we just were. = savePoint [ x {_1} save{_2} return g save t return ] = dagger [ a candle{_} At gain dagger At lose axe At lose holy_water At lose cross ] = axe [ a candle{_} At lose dagger At gain axe At lose holy_water At lose cross ] = holy_water [ a candle{_} At lose dagger At lose axe At gain holy_water At lose cross ] = cross [ a candle{_} At lose dagger At lose axe At lose holy_water At gain cross ] # Journal # ------- S main Confrontation_Site # Stars the exploration, and applies a zone zz Catacombs # Creates and applies a level-1 zone above the existing zone o Top_Left # Observes a transition at the current decision x Bottom top Top Rubble_Tower_Base # Explores a transition (newly created) # The 'x' entry above notes the transition taken, the room name on # the other end, the transition name for the reciprocal, and the # new level-0 zone for the destination (could be omitted if it's in # the same level-0 zone). gt forced # Tags the transition we just explored o Crack # We see a crack in the wall as we go down q short_and_high # But we can't get through it right now # Spoilers: we'll never be able to get through it from this side n Fought -Skeleton_Bomber- # A note, applied to the exploration step A gain Salamander # An applied effect: gaining a power # Note we're not modelling random drops here but we could with # challenges n Literally first enemy I touched! # Another note x Bottom_Right Catacombs_Entrance::top Top_Left # Another exploration o up n Can't jump up here yet q height o top_stairs mid top_stairs # The stairs to the middle level # Here we define the room name on the other side and the reciprocal # transition name. x First_Right Catacombs_Treasury::left Left # But first a detour o box # Can't get through this box yet qb heavy # Requirement applies in both directions t Left # This 't' retraces a known transition at the current position x top_stairs # Here the room and reciprocal names have already been # defined, and the zone is the same o bottom_stairs bottom bottom_stairs A gain Mercury n Bone head along the way > savePoint x bottom_stairs > dagger o Bot_Left o across_bottom xt Bot_Left Blockwall_Cave::bottom Right o ledge q height t Right x across_bottom bottom_right back_across_bottom o Bot_Right u Catacombs_Entrance::bottom # Nothing super interesting across the bottom of the room, so we might # as well treat the entire bottom as one decision. xt Bot_Right entrance Left Green_Gates n Fought -Earth_Demon- o jump q distance a get_dash_boots At gain dash_boots xt jump upper left o right_stairs_down right_side o climb xt climb attic fall v fall_left entrance fly_up qr flight o fall_right right_side fly_up qr flight x Upper_Right Catacombs_Treasury_2::entrance Left o block qb crumble t Left xt fall_right x Lower_Right main Left Zombie_Corridor n [ Fought -Poison_Worm- combo'd with -Zombie- and -Skeleton-. This makes for an interesting room. ] # This annotation spans multiple lines of text x Right bottom_left Left_Mid Pinkstone_Tower # TODO: HERE [Pinkstone_Tower] < Left_Mid ? down ? up ? across >< Left_Crumble [. ] @ down > Bottom_Right [Green_Shaft_Cave] < Left # -Earth_Demon- . x< up (tall_and_narrow) > Left [Pinkstone_Tower] < Bottom_Right - across > Mid_Right [Squeeze_Under] < Right . > Right [Pinkstone_Tower] < Mid_Right - up ? up2 > Upper_Left [Pinkstone_Connector_Crack] < Right x< crack (short_and_high) > Right [Pinkstone_Tower] < Upper_Left - up2 ? across2 ? up3 >< Top_Left [. ] - up3 ? Top - across2 >< Top_Right [save] > Top [FlameMouth_Chamber] < Bottom ? right > Left [Pinkstone_Tower_2] < Entrance_Crack . ? Right >< Left [. ] > Right [Pinkstone_Connector_Crack] < Left -> crack > Right [Pinkstone_Tower] < Upper_Left > Top [FlameMouth_Chamber] < Bottom - right ? up - right2 . x< ledge (height) - up > Top_Left [Muddy_Pinkstone] < Right ? down >< Top_Left [save] - down > Bottom_Left [Greenstone_Double_Shaft] < Top_Right # -Gremlin- >< Mid_Right [. ] ? up > Bottom_Left [Mummies_Hallway] < Right # Mummy . x< ledge (height) > Right [Greenstone_Double_Shaft] < Bottom_Left - up ?b Top_Left > Mid_Left [Hopper_Treasury] < Right # -Hopper- . > Right [Greenstone_Double_Shaft] < Mid_Left > Top_Left {boss} [Cerberus] < Right {boss} # -Cerberus- >< Left [. (/height|distance)] > Right """) # TODO: Fix this up? Or swap for another example? assert len(expl) == 3 firstSituation = expl.getSituation(0) firstDecisions = expl.getActiveDecisions(0) sixthSituation = expl.getSituation(5) sixthDecisions = expl.getActiveDecisions(5) finalSituation = expl.getSituation() finalDecisions = expl.getActiveDecisions() assert len(firstSituation.graph) == 1 assert firstDecisions == {0} assert len(sixthSituation.graph) == 10 assert sixthDecisions == {3} assert len(finalSituation.graph) == 100 assert finalDecisions == {41} def test_entry_types() -> None: pf = journal.JournalParseFormat() for ebits, expect in [ (["o", "action"], ("observe", 'active', None, ["action"])), (["observe", "action"], ("observe", 'active', None, ["action"])), (["oa", "action"], ("observe", 'active', "actionPart", ["action"])), (["ot", "action"], ("observe", 'active', "transitionPart", ["action"])), (["observe@a", "action"], ("observe", 'active', "actionPart", ["action"])), (["observe@action", "action"], ("observe", 'active', "actionPart", ["action"])), (["observe@actionPart", "action"], ("observe", 'active', "actionPart", ["action"])), (["imposed/observe@actionPart", "action"], ("observe", 'imposed', "actionPart", ["action"])), (["!oa", "action"], ("observe", 'unintended', "actionPart", ["action"])), ]: assert pf.determineEntryType(ebits) == expect, ebits for broken in ( ["obse"], ["obse", "action"], ["observe@madeUp"], ["oq"], ): with pytest.raises(journal.JournalParseError): assert pf.determineEntryType(broken) is None, broken def test_long_entry_types() -> None: """ Tests the use of long entry types. """ expl = journal.convertJournal("""\ START start zone Start zone@zone Region observe@action action observe door explore@transition door Room1::room1 door tag@both blue """) expl2 = journal.convertJournal("""\ S start z Start zz Region oa action o door xt door Room1::room1 door gb blue """) assert expl == expl2 def test_revisions() -> None: """ Tests revision-type entries. """ expl = journal.convertJournal("""\ S A o left o right extinguish left xt right B left unify A x down C up complicate up D down2 up2 t up x right E left """) assert len(expl) == 6 # Ignoring empty starting graph first = expl.getSituation(1).graph assert expl.getActiveDecisions(1) == {0} assert listingAtStep(expl, 1) == """\ 0 (A) 2 (_u.1) """ assert first.getDestination('A', 'left') is None assert first.getDestination('A', 'right') is not None assert not first.isConfirmed(first.destination('A', 'right')) second = expl.getSituation(2).graph assert listingAtStep(expl, 2) == """\ 0 (A) 3 (_u.2) """ assert expl.getActiveDecisions(2) == {0} assert 'B' not in [second.nameFor(d) for d in second] assert second.getDestination('A', 'right') == 0 assert second.getDestination('A', 'left') == 0 assert not second.isConfirmed(second.destination('A', 'down')) third = expl.getSituation(3).graph assert listingAtStep(expl, 3) == """\ 0 (A) 3 (C) 4 (D) """ assert expl.getActiveDecisions(3) == {3} assert third.getDestination('A', 'down') == 4 assert third.getDestination('D', 'up2') == 0 assert third.getDestination('C', 'up') == 4 assert third.getDestination('D', 'down2') == 3 fourth = expl.getSituation(4).graph assert listingAtStep(expl, 4) == """\ 0 (A) 3 (C) 4 (D) 5 (_u.3) """ assert expl.getActiveDecisions(4) == {4} assert fourth.getDestination('A', 'down') == 4 assert fourth.getDestination('D', 'up2') == 0 assert fourth.getDestination('C', 'up') == 4 assert fourth.getDestination('D', 'down2') == 3 assert not fourth.isConfirmed(fourth.destination('D', 'right')) fifth = expl.getSituation(5).graph assert listingAtStep(expl, 5) == """\ 0 (A) 3 (C) 4 (D) 5 (E) """ assert expl.getActiveDecisions(5) == {5} assert fifth.getDestination('A', 'down') == 4 assert fifth.getDestination('D', 'up2') == 0 assert fifth.getDestination('C', 'up') == 4 assert fifth.getDestination('D', 'down2') == 3 assert fifth.getDestination('D', 'right') == 5 def test_journal_outcomes() -> None: """ Tests challenge outcome mechanisms. """ warnings.filterwarnings("error") expl = journal.convertJournal("""\ S A A gain coin*5 o right e [ {<0>luck>{gain coin*1}{lose coin*1}} ] # gain outcome is selected xt right B left n has: coin*6 # gain outcome triggers by default er [ {<0>luck{gain coin*2}>{lose coin*2}} ] # lose outcome is selected t left n has: coin*4 # lose outcome triggered by default t right%f # explicit outcome overrules n has: coin*3 t left%s # explicit again n has: coin*5 t right # remembers most-recent outcomes n has: coin*4 t left%f # specified again n has: coin*2 t right%s n has: coin*3 t left # remembers most-recent n has: coin*1 """) # (should not warn about mismatched 'has:' notes) assert len(expl) == 10 def tokensAtStep(e, t, n): return base.combinedTokenCount(e.getSituation(n).state, t) assert tokensAtStep(expl, 'coin', 0) == 0 assert tokensAtStep(expl, 'coin', 1) == 5 assert tokensAtStep(expl, 'coin', 2) == 6 assert tokensAtStep(expl, 'coin', 3) == 4 assert tokensAtStep(expl, 'coin', 4) == 3 assert tokensAtStep(expl, 'coin', 5) == 5 assert tokensAtStep(expl, 'coin', 6) == 4 assert tokensAtStep(expl, 'coin', 7) == 2 assert tokensAtStep(expl, 'coin', 8) == 3 assert tokensAtStep(expl, 'coin', 9) == 1 expl2 = journal.convertJournal("""\ S Aa A gain coin*5 n has: coin*5 n has: flower*0 o right Bb left e [ { <0>luck{<0>luck{gain coin*1}{lose coin*1}; bounce} {<0>luck{gain coin*2}{lose coin*2}; follow left}; ??(coin*5){gain flower*1}{<0>luck{gain flower*1}{}} } ] # no pre-selected outcomes xt right%fs # effectively bounces each time so we can just keep taking it n has: coin*7 n has: flower*1 n at: Aa t right%sf n has: coin*6 n has: flower*2 n at: Aa t right%ff # coins are checked in original state n has: coin*4 n has: flower*3 n at: Aa t right%ssf n has: coin*5 n has: flower*3 n at: Aa """) assert len(expl2) == 6 assert tokensAtStep(expl2, 'coin', 0) == 0 assert tokensAtStep(expl2, 'flower', 0) == 0 assert base.combinedDecisionSet(expl2.getSituation(0).state) == set() assert tokensAtStep(expl2, 'coin', 1) == 5 assert tokensAtStep(expl2, 'flower', 1) == 0 assert base.combinedDecisionSet(expl2.getSituation(1).state) == {0} assert tokensAtStep(expl2, 'coin', 2) == 7 assert tokensAtStep(expl2, 'flower', 2) == 1 assert base.combinedDecisionSet(expl2.getSituation(2).state) == {0} assert tokensAtStep(expl2, 'coin', 3) == 6 assert tokensAtStep(expl2, 'flower', 3) == 2 assert base.combinedDecisionSet(expl2.getSituation(3).state) == {0} assert tokensAtStep(expl2, 'coin', 4) == 4 assert tokensAtStep(expl2, 'flower', 4) == 3 assert base.combinedDecisionSet(expl2.getSituation(4).state) == {0} assert tokensAtStep(expl2, 'coin', 5) == 5 assert tokensAtStep(expl2, 'flower', 5) == 3 assert base.combinedDecisionSet(expl2.getSituation(5).state) == {0} warnings.filterwarnings("default") def test_journal_reversions() -> None: """ Some tests of the state-reversion mechanisms. """ warnings.filterwarnings("error") expl = journal.convertJournal("""\ S A A save slot0 x right B left A save slot1 x right C left At gain coin*1 revert slot0 n at: A n has: coin*0 w revert slot1 n at: B n has: coin*0 w ? destinations t right # graph still unreverted n at: C n has:coin*1 revert slot0 all-positions primary n at: A n has: coin*1 w revert slot1 c-main-tokens n at: A n has: coin*0 x up aboveA down n at: aboveA revert slot0 all-positions primary graph n at: A x up aboveA2 down n at: aboveA2 """) warnings.filterwarnings("default") assert len(expl) == 15 preRevertGraph = expl.getSituation(-3).graph assert len(preRevertGraph) == 4 assert preRevertGraph.destinationsFrom('A') == {"right": 1, "up": 3} revertedGraph = expl.getSituation(-2).graph assert len(revertedGraph) == 2 assert revertedGraph.destinationsFrom('A') == {"up": 4} def test_journal_macros() -> None: """ Some tests of macros. """ warnings.filterwarnings("error") expl = journal.convertJournal("""\ = boss who level [ oa fight{who} gt bossFight e [ { <{level}>sum(combat, {who}){ gain defeated{who}; deactivate }{ goto endings//death } } ] ] = failBossRevert who [ ta fight{who}%f R ] = beatBoss who [ ta fight{who}%s ] S start # step 1 @ endings//death @ @ A save x right bossRoom left # step 2 > boss Boss -1 e gain orb*1 n has: orb*0 n has: coin*0 > failBossRevert Boss # steps 3+4 n at: start n has: orb*0 n has: coin*0 t right # step 5 n at: bossRoom > failBossRevert Boss # step 6+7 n at: start n has: orb*0 t right # step 8 n has: orb*0 n has: coin*0 > beatBoss Boss # step 9 At gain coin*1 n has: orb*1 n has: coin*1 ? steps p endings//death # step 10 R # step 11 n at: start n has: orb*0 n has: coin*0 ? steps t right # step 12 n has: orb*0 n has: coin*0 > beatBoss Boss # step 13 n has: orb*1 n has: coin*1 n can: defeatedBoss """) warnings.filterwarnings("default") assert len(expl) == 14 # Most assertions are in the journal itself here def test_applyAfterwards() -> None: """ Tests using 'At' to note non-obvious transition effects. """ warnings.filterwarnings("error") expl = journal.convertJournal("""\ = get what [ a get At gain {what} ] S start n has: coin*0 oa grab e gain coin*1 n has: coin*0 n has: chip*0 ta grab n has: coin*1 n has: chip*0 At gain chip*1 n has: coin*1 n has: chip*1 ta grab n has: coin*2 n has: chip*2 > get coin*2 n has: coin*4 ta get n has: coin*6 oa charged e gain coin*1 =3 ta charged n has: coin*7 ta charged n has: coin*8 ta charged n has: coin*9 ta charged n has: coin*9 # charges used up a sudden At gain chip*3 ,1 =2 n has: chip*2 # delay prevents effect from happening on first transition ta sudden n has: chip*5 # Now have used 1/2 charges ta sudden n has: chip*8 # Now have used 2/2 charges ta sudden n has: chip*8 # charges used up """) warnings.filterwarnings("default") assert expl.tokenCountNow('coin') == 9 assert expl.tokenCountNow('chip') == 8 def test_requireAfterwards() -> None: """ Tests checking of 'q' entries after steps """ warnings.filterwarnings("error") with pytest.raises(journal.LocatedJournalParseError): expl = journal.convertJournal("""\ S start x right one left q requirement """) # This shouldn't raise expl = journal.convertJournal("""\ S start A gain requirement x right one left q requirement """) # This also shouldn't raise expl = journal.convertJournal("""\ S start x right one left qr requirement """) # This also shouldn't raise expl = journal.convertJournal("""\ S start x right one left w q requirement """) warnings.filterwarnings("default") def test_warpLimitations() -> None: """ Tests warping with or without new destinations. """ # No error when expecting new decision expl = journal.convertJournal("""\ S start pd new n at: new """) # Error due to new decision with pytest.raises(journal.LocatedJournalParseError): expl = journal.convertJournal("""\ S start p new """) # Warping to a decision in another zone expl = journal.convertJournal("""\ S Zone1::start x right Zone2::first left p start n at: Zone1::start """) # Error due to zone part with pytest.raises(journal.LocatedJournalParseError): expl = journal.convertJournal("""\ S Zone1::start x right Zone2::first left p Zone2::start """) # No error if 'd' target specifier is present expl = journal.convertJournal("""\ S Zone1::start x right Zone2::first left pd Zone2::start n at: Zone2::start """) # Warp exploration statuses expl = journal.convertJournal("""\ S Zone1::start pd one @ pd two @ @ """) assert expl.getExplorationStatus("start") == "explored" assert expl.getExplorationStatus("one") == "exploring" assert expl.getExplorationStatus("two") == "noticed" # TODO: More specific case coverage tests... def test_exploreRestrictions() -> None: """ Tests old/new explore restrictions. """ with pytest.raises(journal.LocatedJournalParseError): journal.convertJournal("""\ S start xt right one left """) # xt requires an existing transition with pytest.raises(journal.LocatedJournalParseError): journal.convertJournal("""\ S start o right x right one left """) # x requires transition is novel expl = journal.convertJournal("""\ S start o right xt right one left """) # xt with existing transition g = expl.getSituation().graph assert g.destinationsFrom('start') == {'right': 1} expl = journal.convertJournal("""\ S start x right one left """) # x with novel transition g = expl.getSituation().graph assert g.destinationsFrom('start') == {'right': 1} with pytest.warns(core.TransitionCollisionWarning): expl = journal.convertJournal("""\ P softerChecks on S start o right x right one left """, enhanceWarnings=False) # x with duplicate transition g = expl.getSituation().graph assert g.destinationsFrom('start') == {'right': 1} with pytest.warns(core.MissingTransitionWarning): expl = journal.convertJournal("""\ P softerChecks on S start xt right one left """, enhanceWarnings=False) # x with missing transition g = expl.getSituation().graph assert g.destinationsFrom('start') == {'right': 1} def test_traceInference() -> None: """ Tests new trace inference stuff. """ expl = journal.convertJournal("""\ S start x right room left x up attic down t ? room t ? start n at: start """) assert expl.primaryDecision(4) == 1 assert expl.primaryDecision(5) == 0 with pytest.raises(journal.JournalParseError): expl = journal.convertJournal("""\ S start x right room left t ? """) with pytest.raises(journal.JournalParseError): expl = journal.convertJournal("""\ S start x right room left x up attic down t ? start """) with pytest.raises(journal.JournalParseError): expl = journal.convertJournal("""\ S start x right room left x up attic down o down2 room up2 t ? room """) with pytest.raises(journal.JournalParseError): expl = journal.convertJournal("""\ S start x right room left x up attic down t down start """) with pytest.warns(journal.JournalParseWarning): expl = journal.convertJournal("""\ P softerChecks on S start x right room left x up attic down t down start """, enhanceWarnings=False) with pytest.raises(journal.LocatedJournalParseError): expl = journal.convertJournal("""\ S start x right room left x up attic down t down unknown """) expl = journal.convertJournal("""\ S House::main x right rightSide left x up Attic::main down t ? House::rightSide t left House::main n at: House::main """)