exploration.tests.test_journal
Authors: Peter Mawhorter Consulted: Date: 2022-9-30 Purpose: Tests for journal functionality.
1""" 2Authors: Peter Mawhorter 3Consulted: 4Date: 2022-9-30 5Purpose: Tests for journal functionality. 6""" 7 8import json 9import warnings 10 11import pytest 12 13from .. import core, journal, base 14 15 16def listingAtStep(expl: core.DiscreteExploration, step: int = -1) -> str: 17 """ 18 Calls `namesListing` for all decisions in the graph at the specified 19 step of the given exploration. 20 """ 21 graph = expl.getSituation(step).graph 22 return graph.namesListing(graph) 23 24 25def test_simpleConversion() -> None: 26 """ 27 A simple test of journal conversion. 28 """ 29 simple = journal.convertJournal("""\ 30S First_Room 31x Exit Second_Room Entrance 32gt tag 33 34o Onwards 35E END 36""") 37 assert len(simple) == 4 38 39 finalGraph = simple.getSituation().graph 40 41 assert len(finalGraph) == 4 42 assert finalGraph.namesListing(finalGraph) == """\ 43 0 (First_Room) 44 1 (Second_Room) 45 2 (_u.1) 46 3 (endings//END) 47""" 48 49 assert finalGraph.destinationsFrom("First_Room") == {'Exit': 1} 50 assert finalGraph.destinationsFrom("Second_Room") == { 51 'Entrance': 0, 52 'Onwards': 2 53 } 54 assert finalGraph.destinationsFrom("END") == {} 55 assert json.dumps( 56 finalGraph.textMapObj(), 57 indent=4 58 ) == """\ 59{ 60 "0::Exit": { 61 "1::Entrance": "0", 62 "1::Onwards": { 63 "2::return": "1" 64 } 65 } 66}""" 67 # TODO: We'd like to include the ending here! 68 69# TODO: Specific case coverage tests... 70 71 72def test_exploring_zones() -> None: 73 """ 74 A test for exploration with zones. 75 """ 76 expl = journal.convertJournal("""\ 77S zone::room # decision 0 78x ahead room2 back # decision 1 79o hatch basement ladder # decision 2 80x ahead zone2::room3 back # decision 3 81x ahead room4 back # decision 4 82t back 83t back 84x hatch 85""") 86 # TODO: Add test here for returning to unvisited decision not placed 87 # in zone 88 assert expl.getActiveDecisions() == {2} 89 now = expl.getSituation() 90 assert now.graph.getDecision('basement') == 2 91 assert now.graph.getDecision( 92 base.DecisionSpecifier(domain=None, zone='zone', name='basement') 93 ) == 2 94 assert now.graph.getDecision( 95 base.DecisionSpecifier(domain=None, zone='zone2', name='basement') 96 ) is None 97 assert now.graph.getDecision( 98 base.DecisionSpecifier(domain=None, zone='zone2', name='room3') 99 ) == 3 100 assert now.graph.namesListing(now.graph) == """\ 101 0 (zone::room) 102 1 (zone::room2) 103 2 (zone::basement) 104 3 (zone2::room3) 105 4 (zone2::room4) 106""" 107 assert now.graph.zoneParents(0) == {'zone'} 108 assert now.graph.zoneParents(2) == {'zone'} 109 assert now.graph.zoneParents(3) == {'zone2'} 110 assert now.graph.zoneParents(4) == {'zone2'} 111 assert now.graph.zoneParents(1) == {'zone'} 112 113 114def test_zoneNaming() -> None: 115 """ 116 Tests some situations with zone propagation in journals. 117 """ 118 expl = journal.convertJournal("""\ 119S West::start 120x right second left 121 o down third up 122x right East::fourth left 123t left 124x down # third should be in zone 'West' 125 o left New::fifth right 126x left # fifth should remain in zone 'New' 127""") 128 g1 = expl.getSituation(1).graph 129 assert g1.namesListing(g1) == """\ 130 0 (West::start) 131 1 (_u.0) 132""" 133 134 g2 = expl.getSituation(2).graph 135 assert g2.namesListing(g2) == """\ 136 0 (West::start) 137 1 (West::second) 138 2 (West::third) 139 3 (_u.2) 140""" 141 assert g2.zoneParents(0) == {'West'} 142 assert g2.zoneParents(1) == {'West'} 143 assert g2.zoneParents(2) == {'West'} 144 assert g2.zoneParents(3) == set() 145 146 g3 = expl.getSituation(3).graph 147 assert g3.namesListing(g3) == """\ 148 0 (West::start) 149 1 (West::second) 150 2 (West::third) 151 3 (East::fourth) 152""" 153 assert g3.zoneParents(0) == {'West'} 154 assert g3.zoneParents(1) == {'West'} 155 assert g3.zoneParents(2) == {'West'} 156 assert g3.zoneParents(3) == {'East'} 157 158 g4 = expl.getSituation(4).graph 159 assert g4.namesListing(g4) == """\ 160 0 (West::start) 161 1 (West::second) 162 2 (West::third) 163 3 (East::fourth) 164""" 165 assert g4.zoneParents(0) == {'West'} 166 assert g4.zoneParents(1) == {'West'} 167 assert g3.zoneParents(2) == {'West'} 168 assert g4.zoneParents(3) == {'East'} 169 170 g5 = expl.getSituation(5).graph 171 assert g5.namesListing(g5) == """\ 172 0 (West::start) 173 1 (West::second) 174 2 (West::third) 175 3 (East::fourth) 176 4 (New::fifth) 177""" 178 assert g5.zoneParents(0) == {'West'} 179 assert g5.zoneParents(1) == {'West'} 180 assert g5.zoneParents(2) == {'West'} 181 assert g5.zoneParents(3) == {'East'} 182 assert g5.zoneParents(4) == {'New'} 183 184 g6 = expl.getSituation(6).graph 185 assert g6.namesListing(g6) == """\ 186 0 (West::start) 187 1 (West::second) 188 2 (West::third) 189 3 (East::fourth) 190 4 (New::fifth) 191""" 192 assert g6.zoneParents(0) == {'West'} 193 assert g6.zoneParents(1) == {'West'} 194 assert g6.zoneParents(2) == {'West'} 195 assert g6.zoneParents(3) == {'East'} 196 assert g6.zoneParents(4) == {'New'} 197 198 199def test_startOfCotM() -> None: 200 """ 201 A test for converting part of a journal from Castlevania: Circle of 202 the Moon. This journal covers just the first few rooms of the game. 203 """ 204 # Note that we're expecting a `TransitionBlockedWarning` because the 205 # requirement 'q distance' does not match the power gained 'At gain 206 # dash_boots'... 207 # TODO: Add an equivalence now that we support those! 208 with pytest.warns(core.TransitionBlockedWarning): 209 expl = journal.convertJournal("""\ 210# Setup 211# ----- 212# A save-point alias that creates, explores and tags a new decision but 213# then returns to where we just were. 214= savePoint [ 215 x {_1} save{_2} return 216 g save 217 t return 218] 219= dagger [ 220 a candle{_} 221 At gain dagger 222 At lose axe 223 At lose holy_water 224 At lose cross 225] 226= axe [ 227 a candle{_} 228 At lose dagger 229 At gain axe 230 At lose holy_water 231 At lose cross 232] 233= holy_water [ 234 a candle{_} 235 At lose dagger 236 At lose axe 237 At gain holy_water 238 At lose cross 239] 240= cross [ 241 a candle{_} 242 At lose dagger 243 At lose axe 244 At lose holy_water 245 At gain cross 246] 247 248# Journal 249# ------- 250S Confrontation_Site::main # Stars the exploration, and applies a zone 251 zz Catacombs # Creates and applies a level-1 zone above the existing zone 252 o Top_Left # Observes a transition at the current decision 253x Bottom Rubble_Tower_Base::top Top # Explores a transition (newly created) 254 # The 'x' entry above notes the transition taken, the room name on 255 # the other end, the transition name for the reciprocal, and the 256 # new level-0 zone for the destination (could be omitted if it's in 257 # the same level-0 zone). 258 gt forced # Tags the transition we just explored 259 260 o Crack # We see a crack in the wall as we go down 261 q ?short_and_high # But we can't get through it right now 262 # Spoilers: we'll never be able to get through it from this side 263 n Fought -Skeleton_Bomber- # A note, applied to the exploration step 264 A gain Salamander # An applied effect: gaining a power 265 # Note: We're not modelling random drops but could with a challenge... 266 n Literally first enemy I touched! # Another note 267x Bottom_Right Catacombs_Entrance::top Top_Left # Another exploration 268 o up 269 n Can't jump up here yet 270 q height 271 o top_stairs mid top_stairs # The stairs to the middle level 272 # Here we define the room name on the other side and the reciprocal 273 # transition name. 274x First_Right Catacombs_Treasury::left Left # But first a detour 275 276 o box # Can't get through this box yet 277 qb heavy # Requirement applies in both directions 278t Left # This 't' retraces a known transition at the current position 279 280x top_stairs # Here the room and reciprocal names have already been 281 # defined, and the zone is the same 282 283 o bottom_stairs bottom bottom_stairs 284 A gain Mercury 285 n Bone head along the way 286 287> savePoint 288x bottom_stairs 289 > dagger 290 o Bot_Left 291 o across_bottom 292x Bot_Left Blockwall_Cave::bottom Right 293 294 o ledge 295 q height 296t Right 297x across_bottom bottom_right back_across_bottom 298 o Bot_Right 299 # TODO: Is the zone necessary here? Feels like it shouldn't be 300 u Catacombs_Entrance::bottom 301 # Nothing super interesting across the bottom of the room, so we might 302 # as well treat the entire bottom as one decision. 303x Bot_Right Green_Gates::entrance Left 304 305 n Fought -Earth_Demon- 306 o jump 307 q distance 308 a get_dash_boots 309 At gain dash_boots 310x jump upper left 311 o right_stairs_down right_side 312 o climb 313x climb attic fall 314 v fall_left entrance fly_up 315 qr flight 316 n at: Green_Gates::attic 317 o fall_right right_side fly_up 318 qr flight 319 320x Upper_Right Catacombs_Treasury_2::entrance Left 321 o block 322 qb crumble 323 324t Left 325 n at: Green_Gates::attic 326x fall_right 327 n at: Green_Gates::right_side 328x Lower_Right Zombie_Corridor::main Left 329 330 n [ 331 Fought -Poison_Worm- combo'd with -Zombie- and -Skeleton-. This 332 makes for an interesting room. 333 ] 334 # This annotation spans multiple lines of text 335x Right Pinkstone_Tower::bottom_left Left_Mid 336 337 # And that's the end of this fragment for testing purposes! 338""") 339 assert len(expl) == 23 340 341 firstSituation = expl.getSituation(0) 342 firstDecisions = expl.getActiveDecisions(0) 343 secondSituation = expl.getSituation(1) 344 secondDecisions = expl.getActiveDecisions(1) 345 sixthSituation = expl.getSituation(5) 346 sixthDecisions = expl.getActiveDecisions(5) 347 finalSituation = expl.getSituation() 348 finalDecisions = expl.getActiveDecisions() 349 350 assert len(firstSituation.graph) == 1 351 assert firstDecisions == set() 352 353 assert len(secondSituation.graph) == 3 # room + 2 unknowns 354 assert listingAtStep(expl, 1) == """\ 355 0 (Confrontation_Site::main) 356 1 (_u.0) 357 2 (_u.1) 358""" 359 assert secondDecisions == {0} 360 361 g6 = sixthSituation.graph 362 assert len(g6) == 9 363 assert listingAtStep(expl, 5) == """\ 364 0 (Confrontation_Site::main) 365 1 (_u.0) 366 2 (Rubble_Tower_Base::top) 367 3 (_u.2) 368 4 (Catacombs_Entrance::top) 369 5 (_u.4) 370 6 (Catacombs_Entrance::mid) 371 7 (Catacombs_Treasury::left) 372 8 (_u.7) 373""" # 'mid' has not been explored yet so isn't in a zone 374 gZones = [g6.zoneParents(d) for d in sorted(g6)] 375 assert gZones == [ 376 {'Confrontation_Site'}, 377 set(), 378 {'Rubble_Tower_Base'}, 379 set(), 380 {'Catacombs_Entrance'}, 381 set(), 382 {'Catacombs_Entrance'}, 383 {'Catacombs_Treasury'}, 384 set() 385 ] 386 assert g6.destination('_u.0', 'return') == 0 387 assert g6.destination('_u.2', 'return') == 2 388 assert g6.destination('_u.4', 'return') == 4 389 assert g6.destination('_u.7', 'return') == 7 390 assert sixthDecisions == {4} 391 392 gL = finalSituation.graph 393 assert len(gL) == 21 394 assert listingAtStep(expl, -1) == """\ 395 0 (Confrontation_Site::main) 396 1 (_u.0) 397 2 (Rubble_Tower_Base::top) 398 3 (_u.2) 399 4 (Catacombs_Entrance::top) 400 5 (_u.4) 401 6 (Catacombs_Entrance::mid) 402 7 (Catacombs_Treasury::left) 403 8 (_u.7) 404 9 (Catacombs_Entrance::bottom) 405 10 (Catacombs_Entrance::save_1) 406 11 (Blockwall_Cave::bottom) 407 13 (_u.12) 408 14 (Green_Gates::entrance) 409 15 (Green_Gates::upper) 410 16 (Green_Gates::right_side) 411 17 (Green_Gates::attic) 412 20 (Catacombs_Treasury_2::entrance) 413 21 (_u.20) 414 22 (Zombie_Corridor::main) 415 23 (Pinkstone_Tower::bottom_left) 416""" 417 assert finalDecisions == {23} 418 419 420def _test_CotMToCerberus() -> None: 421 """ 422 A test for converting part of a journal from Castlevania: Circle of 423 the Moon. This journal covers an experienced re-playthrough up to 424 the first boss (Cerberus) and boss reward (double-jump). 425 426 TODO: This test is not finished yet! 427 """ 428 expl = journal.convertJournal("""\ 429# Setup 430# ----- 431P on # Turns on zone-prefixing (we're using level-0 zones for rooms) 432# A save-point alias that creates, explores and tags a new decision but 433# then returns to where we just were. 434= savePoint [ 435 x {_1} save{_2} return 436 g save 437 t return 438] 439= dagger [ 440 a candle{_} 441 At gain dagger 442 At lose axe 443 At lose holy_water 444 At lose cross 445] 446= axe [ 447 a candle{_} 448 At lose dagger 449 At gain axe 450 At lose holy_water 451 At lose cross 452] 453= holy_water [ 454 a candle{_} 455 At lose dagger 456 At lose axe 457 At gain holy_water 458 At lose cross 459] 460= cross [ 461 a candle{_} 462 At lose dagger 463 At lose axe 464 At lose holy_water 465 At gain cross 466] 467 468# Journal 469# ------- 470S main Confrontation_Site # Stars the exploration, and applies a zone 471zz Catacombs # Creates and applies a level-1 zone above the existing zone 472o Top_Left # Observes a transition at the current decision 473x Bottom top Top Rubble_Tower_Base # Explores a transition (newly created) 474 # The 'x' entry above notes the transition taken, the room name on 475 # the other end, the transition name for the reciprocal, and the 476 # new level-0 zone for the destination (could be omitted if it's in 477 # the same level-0 zone). 478 gt forced # Tags the transition we just explored 479 480o Crack # We see a crack in the wall as we go down 481 q short_and_high # But we can't get through it right now 482 # Spoilers: we'll never be able to get through it from this side 483 n Fought -Skeleton_Bomber- # A note, applied to the exploration step 484 A gain Salamander # An applied effect: gaining a power 485 # Note we're not modelling random drops here but we could with 486 # challenges 487 n Literally first enemy I touched! # Another note 488x Bottom_Right Catacombs_Entrance::top Top_Left # Another exploration 489 490o up 491 n Can't jump up here yet 492 q height 493o top_stairs mid top_stairs # The stairs to the middle level 494 # Here we define the room name on the other side and the reciprocal 495 # transition name. 496x First_Right Catacombs_Treasury::left Left # But first a detour 497 498o box # Can't get through this box yet 499 qb heavy # Requirement applies in both directions 500t Left # This 't' retraces a known transition at the current position 501 502x top_stairs # Here the room and reciprocal names have already been 503 # defined, and the zone is the same 504 o bottom_stairs bottom bottom_stairs 505 A gain Mercury 506 n Bone head along the way 507 508> savePoint 509x bottom_stairs 510 > dagger 511 o Bot_Left 512 o across_bottom 513x Bot_Left Blockwall_Cave::bottom Right 514 515 o ledge 516 q height 517t Right 518x across_bottom bottom_right back_across_bottom 519 o Bot_Right 520 u Catacombs_Entrance::bottom 521 # Nothing super interesting across the bottom of the room, so we might 522 # as well treat the entire bottom as one decision. 523x Bot_Right entrance Left Green_Gates 524 n Fought -Earth_Demon- 525 o jump 526 q distance 527 a get_dash_boots 528 At gain dash_boots 529x jump upper left 530 o right_stairs_down right_side 531 o climb 532x climb attic fall 533 v fall_left entrance fly_up 534 qr flight 535 o fall_right right_side fly_up 536 qr flight 537 538x Upper_Right Catacombs_Treasury_2::entrance Left 539 o block 540 qb crumble 541 542t Left 543x fall_right 544x Lower_Right main Left Zombie_Corridor 545 546 n [ 547 Fought -Poison_Worm- combo'd with -Zombie- and -Skeleton-. This 548 makes for an interesting room. 549 ] 550 # This annotation spans multiple lines of text 551x Right bottom_left Left_Mid Pinkstone_Tower 552 553# TODO: HERE 554 555[Pinkstone_Tower] 556< Left_Mid 557? down 558? up 559? across 560>< Left_Crumble [. <mp>] 561@ down 562> Bottom_Right 563 564[Green_Shaft_Cave] 565< Left 566# -Earth_Demon- 567. <ht> 568x< up (tall_and_narrow) 569> Left 570 571[Pinkstone_Tower] 572< Bottom_Right 573- across 574> Mid_Right 575 576[Squeeze_Under] 577< Right 578. <hp> 579> Right 580 581[Pinkstone_Tower] 582< Mid_Right 583- up 584? up2 585> Upper_Left 586 587[Pinkstone_Connector_Crack] 588< Right 589x< crack (short_and_high) 590> Right 591 592[Pinkstone_Tower] 593< Upper_Left 594- up2 595? across2 596? up3 597>< Top_Left [. <mp>] 598- up3 599? Top 600- across2 601>< Top_Right [save] 602> Top 603 604[FlameMouth_Chamber] 605< Bottom 606? right 607> Left 608 609[Pinkstone_Tower_2] 610< Entrance_Crack 611. <axe> 612? Right 613>< Left [. <hp>] 614> Right 615 616[Pinkstone_Connector_Crack] 617< Left 618-> crack 619> Right 620 621[Pinkstone_Tower] 622< Upper_Left 623> Top 624 625[FlameMouth_Chamber] 626< Bottom 627- right 628? up 629- right2 630. <mp> 631x< ledge (height) 632- up 633> Top_Left 634 635[Muddy_Pinkstone] 636< Right 637? down 638>< Top_Left [save] 639- down 640> Bottom_Left 641 642[Greenstone_Double_Shaft] 643< Top_Right 644# -Gremlin- 645>< Mid_Right [. <ht>] 646? up 647> Bottom_Left 648 649[Mummies_Hallway] 650< Right 651# Mummy 652. <holy water> 653x< ledge (height) 654> Right 655 656[Greenstone_Double_Shaft] 657< Bottom_Left 658- up 659?b Top_Left 660> Mid_Left 661 662[Hopper_Treasury] 663< Right 664# -Hopper- 665. <mp> 666> Right 667 668[Greenstone_Double_Shaft] 669< Mid_Left 670> Top_Left {boss} 671 672[Cerberus] 673< Right {boss} 674# -Cerberus- 675>< Left [. <double> (/height|distance)] 676> Right 677""") 678 # TODO: Fix this up? Or swap for another example? 679 assert len(expl) == 3 680 681 firstSituation = expl.getSituation(0) 682 firstDecisions = expl.getActiveDecisions(0) 683 sixthSituation = expl.getSituation(5) 684 sixthDecisions = expl.getActiveDecisions(5) 685 finalSituation = expl.getSituation() 686 finalDecisions = expl.getActiveDecisions() 687 688 assert len(firstSituation.graph) == 1 689 assert firstDecisions == {0} 690 691 assert len(sixthSituation.graph) == 10 692 assert sixthDecisions == {3} 693 694 assert len(finalSituation.graph) == 100 695 assert finalDecisions == {41} 696 697 698def test_entry_types() -> None: 699 pf = journal.JournalParseFormat() 700 for ebits, expect in [ 701 (["o", "action"], ("observe", 'active', None, ["action"])), 702 (["observe", "action"], ("observe", 'active', None, ["action"])), 703 (["oa", "action"], ("observe", 'active', "actionPart", ["action"])), 704 (["ot", "action"], 705 ("observe", 'active', "transitionPart", ["action"])), 706 (["observe@a", "action"], 707 ("observe", 'active', "actionPart", ["action"])), 708 (["observe@action", "action"], 709 ("observe", 'active', "actionPart", ["action"])), 710 (["observe@actionPart", "action"], 711 ("observe", 'active', "actionPart", ["action"])), 712 (["imposed/observe@actionPart", "action"], 713 ("observe", 'imposed', "actionPart", ["action"])), 714 (["!oa", "action"], 715 ("observe", 'unintended', "actionPart", ["action"])), 716 ]: 717 assert pf.determineEntryType(ebits) == expect, ebits 718 719 for broken in ( 720 ["obse"], 721 ["obse", "action"], 722 ["observe@madeUp"], 723 ["oq"], 724 ): 725 with pytest.raises(journal.JournalParseError): 726 assert pf.determineEntryType(broken) is None, broken 727 728 729def test_long_entry_types() -> None: 730 """ 731 Tests the use of long entry types. 732 """ 733 expl = journal.convertJournal("""\ 734START start 735zone Start 736zone@zone Region 737observe@action action 738observe door 739explore door Room1::room1 door 740tag@both blue 741""") 742 expl2 = journal.convertJournal("""\ 743S start 744z Start 745zz Region 746oa action 747o door 748x door Room1::room1 door 749gb blue 750""") 751 assert expl == expl2 752 753 754def test_revisions() -> None: 755 """ 756 Tests revision-type entries. 757 """ 758 expl = journal.convertJournal("""\ 759S A 760 o left 761 o right 762 extinguish left 763x right B left 764 unify A 765x down C up 766 complicate up D down2 up2 767t up 768x right E left 769""") 770 assert len(expl) == 6 771 772 # Ignoring empty starting graph 773 first = expl.getSituation(1).graph 774 assert expl.getActiveDecisions(1) == {0} 775 assert listingAtStep(expl, 1) == """\ 776 0 (A) 777 2 (_u.1) 778""" 779 assert first.getDestination('A', 'left') is None 780 assert first.getDestination('A', 'right') is not None 781 assert not first.isConfirmed(first.destination('A', 'right')) 782 783 second = expl.getSituation(2).graph 784 assert listingAtStep(expl, 2) == """\ 785 0 (A) 786 3 (_u.2) 787""" 788 assert expl.getActiveDecisions(2) == {0} 789 assert 'B' not in [second.nameFor(d) for d in second] 790 assert second.getDestination('A', 'right') == 0 791 assert second.getDestination('A', 'left') == 0 792 assert not second.isConfirmed(second.destination('A', 'down')) 793 794 third = expl.getSituation(3).graph 795 assert listingAtStep(expl, 3) == """\ 796 0 (A) 797 3 (C) 798 4 (D) 799""" 800 assert expl.getActiveDecisions(3) == {3} 801 assert third.getDestination('A', 'down') == 4 802 assert third.getDestination('D', 'up2') == 0 803 assert third.getDestination('C', 'up') == 4 804 assert third.getDestination('D', 'down2') == 3 805 806 fourth = expl.getSituation(4).graph 807 assert listingAtStep(expl, 4) == """\ 808 0 (A) 809 3 (C) 810 4 (D) 811 5 (_u.3) 812""" 813 assert expl.getActiveDecisions(4) == {4} 814 assert fourth.getDestination('A', 'down') == 4 815 assert fourth.getDestination('D', 'up2') == 0 816 assert fourth.getDestination('C', 'up') == 4 817 assert fourth.getDestination('D', 'down2') == 3 818 assert not fourth.isConfirmed(fourth.destination('D', 'right')) 819 820 fifth = expl.getSituation(5).graph 821 assert listingAtStep(expl, 5) == """\ 822 0 (A) 823 3 (C) 824 4 (D) 825 5 (E) 826""" 827 assert expl.getActiveDecisions(5) == {5} 828 assert fifth.getDestination('A', 'down') == 4 829 assert fifth.getDestination('D', 'up2') == 0 830 assert fifth.getDestination('C', 'up') == 4 831 assert fifth.getDestination('D', 'down2') == 3 832 assert fifth.getDestination('D', 'right') == 5 833 834 835def test_journal_outcomes() -> None: 836 """ 837 Tests challenge outcome mechanisms. 838 """ 839 warnings.filterwarnings("error") 840 expl = journal.convertJournal("""\ 841S A 842 A gain coin*5 843 o right 844 e [ 845 {<0>luck>{gain coin*1}{lose coin*1}} 846 ] # gain outcome is selected 847x right B left 848 n has: coin*6 # gain outcome triggers by default 849 er [ 850 {<0>luck{gain coin*2}>{lose coin*2}} 851 ] # lose outcome is selected 852t left 853 n has: coin*4 # lose outcome triggered by default 854t right%f # explicit outcome overrules 855 n has: coin*3 856t left%s # explicit again 857 n has: coin*5 858t right # remembers most-recent outcomes 859 n has: coin*4 860t left%f # specified again 861 n has: coin*2 862t right%s 863 n has: coin*3 864t left # remembers most-recent 865 n has: coin*1 866""") 867 # (should not warn about mismatched 'has:' notes) 868 869 assert len(expl) == 10 870 871 def tokensAtStep(e, t, n): 872 return base.combinedTokenCount(e.getSituation(n).state, t) 873 874 assert tokensAtStep(expl, 'coin', 0) == 0 875 assert tokensAtStep(expl, 'coin', 1) == 5 876 assert tokensAtStep(expl, 'coin', 2) == 6 877 assert tokensAtStep(expl, 'coin', 3) == 4 878 assert tokensAtStep(expl, 'coin', 4) == 3 879 assert tokensAtStep(expl, 'coin', 5) == 5 880 assert tokensAtStep(expl, 'coin', 6) == 4 881 assert tokensAtStep(expl, 'coin', 7) == 2 882 assert tokensAtStep(expl, 'coin', 8) == 3 883 assert tokensAtStep(expl, 'coin', 9) == 1 884 885 expl2 = journal.convertJournal("""\ 886S A 887 A gain coin*5 888 n has: coin*5 889 n has: flower*0 890 o right B left 891 e [ 892 { 893 <0>luck{<0>luck{gain coin*1}{lose coin*1}; bounce} 894 {<0>luck{gain coin*2}{lose coin*2}; follow left}; 895 ??(coin*5){gain flower*1}{<0>luck{gain flower*1}{}} 896 } 897 ] # no pre-selected outcomes 898x right%fs 899 # effectively bounces each time so we can just keep taking it 900 n has: coin*7 901 n has: flower*1 902 n at: A 903t right%sf 904 n has: coin*6 905 n has: flower*2 906 n at: A 907t right%ff # coins are checked in original state 908 n has: coin*4 909 n has: flower*3 910 n at: A 911t right%ssf 912 n has: coin*5 913 n has: flower*3 914 n at: A 915""") 916 assert len(expl2) == 6 917 918 assert tokensAtStep(expl2, 'coin', 0) == 0 919 assert tokensAtStep(expl2, 'flower', 0) == 0 920 assert base.combinedDecisionSet(expl2.getSituation(0).state) == set() 921 assert tokensAtStep(expl2, 'coin', 1) == 5 922 assert tokensAtStep(expl2, 'flower', 1) == 0 923 assert base.combinedDecisionSet(expl2.getSituation(1).state) == {0} 924 assert tokensAtStep(expl2, 'coin', 2) == 7 925 assert tokensAtStep(expl2, 'flower', 2) == 1 926 assert base.combinedDecisionSet(expl2.getSituation(2).state) == {0} 927 assert tokensAtStep(expl2, 'coin', 3) == 6 928 assert tokensAtStep(expl2, 'flower', 3) == 2 929 assert base.combinedDecisionSet(expl2.getSituation(3).state) == {0} 930 assert tokensAtStep(expl2, 'coin', 4) == 4 931 assert tokensAtStep(expl2, 'flower', 4) == 3 932 assert base.combinedDecisionSet(expl2.getSituation(4).state) == {0} 933 assert tokensAtStep(expl2, 'coin', 5) == 5 934 assert tokensAtStep(expl2, 'flower', 5) == 3 935 assert base.combinedDecisionSet(expl2.getSituation(5).state) == {0} 936 937 warnings.filterwarnings("default") 938 939 940def test_journal_reversions() -> None: 941 """ 942 Some tests of the state-reversion mechanisms. 943 """ 944 warnings.filterwarnings("error") 945 expl = journal.convertJournal("""\ 946S A 947 A save slot0 948x right B left 949 A save slot1 950x right C left 951 At gain coin*1 952revert slot0 953 n at: A 954 n has: coin*0 955w 956revert slot1 957 n at: B 958 n has: coin*0 959w 960? destinations 961t right # graph still unreverted 962 n at: C 963 n has:coin*1 964revert slot0 all-positions primary 965 n at: A 966 n has: coin*1 967w 968revert slot1 c-main-tokens 969 n at: A 970 n has: coin*0 971x up aboveA down 972 n at: aboveA 973revert slot0 all-positions primary graph 974 n at: A 975x up aboveA2 down 976 n at: aboveA2 977""") 978 warnings.filterwarnings("default") 979 assert len(expl) == 15 980 preRevertGraph = expl.getSituation(-3).graph 981 assert len(preRevertGraph) == 4 982 assert preRevertGraph.destinationsFrom('A') == {"right": 1, "up": 3} 983 revertedGraph = expl.getSituation(-2).graph 984 assert len(revertedGraph) == 2 985 assert revertedGraph.destinationsFrom('A') == {"up": 4} 986 987 988def test_journal_macros() -> None: 989 """ 990 Some tests of macros. 991 """ 992 warnings.filterwarnings("error") 993 expl = journal.convertJournal("""\ 994= boss who level [ 995 oa fight{who} 996 gt bossFight 997 e [ 998 { 999 <{level}>sum(combat, {who}){ 1000 gain defeated{who}; 1001 deactivate 1002 }{ 1003 goto endings//death 1004 } 1005 } 1006 ] 1007] 1008 1009 1010= failBossRevert who [ 1011 ta fight{who}%f 1012 R 1013] 1014 1015= beatBoss who [ 1016 ta fight{who}%s 1017] 1018 1019S start # step 1 1020@ endings//death 1021@ @ 1022A save 1023 1024x right bossRoom left # step 2 1025> boss Boss -1 1026 e gain orb*1 1027 n has: orb*0 1028 n has: coin*0 1029 1030> failBossRevert Boss # steps 3+4 1031 n at: start 1032 n has: orb*0 1033 n has: coin*0 1034 1035t right # step 5 1036 n at: bossRoom 1037 1038> failBossRevert Boss # step 6+7 1039 n at: start 1040 n has: orb*0 1041 1042t right # step 8 1043 n has: orb*0 1044 n has: coin*0 1045 > beatBoss Boss # step 9 1046 At gain coin*1 1047 n has: orb*1 1048 n has: coin*1 1049 1050? steps 1051 1052p endings//death # step 10 1053R # step 11 1054 n at: start 1055 n has: orb*0 1056 n has: coin*0 1057 1058? steps 1059 1060t right # step 12 1061 n has: orb*0 1062 n has: coin*0 1063 > beatBoss Boss # step 13 1064 n has: orb*1 1065 n has: coin*1 1066 n can: defeatedBoss 1067""") 1068 warnings.filterwarnings("default") 1069 assert len(expl) == 14 1070 # Most assertions are in the journal itself here 1071 1072 1073def test_applyAfterwards() -> None: 1074 """ 1075 Tests using 'At' to note non-obvious transition effects. 1076 """ 1077 warnings.filterwarnings("error") 1078 expl = journal.convertJournal("""\ 1079= get what [ 1080 a get 1081 At gain {what} 1082] 1083 1084S start 1085 n has: coin*0 1086 oa grab 1087 e gain coin*1 1088 n has: coin*0 1089 n has: chip*0 1090 ta grab 1091 n has: coin*1 1092 n has: chip*0 1093 At gain chip*1 1094 n has: coin*1 1095 n has: chip*1 1096 ta grab 1097 n has: coin*2 1098 n has: chip*2 1099 > get coin*2 1100 n has: coin*4 1101 ta get 1102 n has: coin*6 1103""") 1104 warnings.filterwarnings("default") 1105 assert expl.tokenCountNow('coin') == 6 1106 assert expl.tokenCountNow('chip') == 2 1107 1108# TODO: More specific case coverage tests...
17def listingAtStep(expl: core.DiscreteExploration, step: int = -1) -> str: 18 """ 19 Calls `namesListing` for all decisions in the graph at the specified 20 step of the given exploration. 21 """ 22 graph = expl.getSituation(step).graph 23 return graph.namesListing(graph)
Calls namesListing
for all decisions in the graph at the specified
step of the given exploration.
def
test_simpleConversion() -> None:
26def test_simpleConversion() -> None: 27 """ 28 A simple test of journal conversion. 29 """ 30 simple = journal.convertJournal("""\ 31S First_Room 32x Exit Second_Room Entrance 33gt tag 34 35o Onwards 36E END 37""") 38 assert len(simple) == 4 39 40 finalGraph = simple.getSituation().graph 41 42 assert len(finalGraph) == 4 43 assert finalGraph.namesListing(finalGraph) == """\ 44 0 (First_Room) 45 1 (Second_Room) 46 2 (_u.1) 47 3 (endings//END) 48""" 49 50 assert finalGraph.destinationsFrom("First_Room") == {'Exit': 1} 51 assert finalGraph.destinationsFrom("Second_Room") == { 52 'Entrance': 0, 53 'Onwards': 2 54 } 55 assert finalGraph.destinationsFrom("END") == {} 56 assert json.dumps( 57 finalGraph.textMapObj(), 58 indent=4 59 ) == """\ 60{ 61 "0::Exit": { 62 "1::Entrance": "0", 63 "1::Onwards": { 64 "2::return": "1" 65 } 66 } 67}""" 68 # TODO: We'd like to include the ending here!
A simple test of journal conversion.
def
test_exploring_zones() -> None:
73def test_exploring_zones() -> None: 74 """ 75 A test for exploration with zones. 76 """ 77 expl = journal.convertJournal("""\ 78S zone::room # decision 0 79x ahead room2 back # decision 1 80o hatch basement ladder # decision 2 81x ahead zone2::room3 back # decision 3 82x ahead room4 back # decision 4 83t back 84t back 85x hatch 86""") 87 # TODO: Add test here for returning to unvisited decision not placed 88 # in zone 89 assert expl.getActiveDecisions() == {2} 90 now = expl.getSituation() 91 assert now.graph.getDecision('basement') == 2 92 assert now.graph.getDecision( 93 base.DecisionSpecifier(domain=None, zone='zone', name='basement') 94 ) == 2 95 assert now.graph.getDecision( 96 base.DecisionSpecifier(domain=None, zone='zone2', name='basement') 97 ) is None 98 assert now.graph.getDecision( 99 base.DecisionSpecifier(domain=None, zone='zone2', name='room3') 100 ) == 3 101 assert now.graph.namesListing(now.graph) == """\ 102 0 (zone::room) 103 1 (zone::room2) 104 2 (zone::basement) 105 3 (zone2::room3) 106 4 (zone2::room4) 107""" 108 assert now.graph.zoneParents(0) == {'zone'} 109 assert now.graph.zoneParents(2) == {'zone'} 110 assert now.graph.zoneParents(3) == {'zone2'} 111 assert now.graph.zoneParents(4) == {'zone2'} 112 assert now.graph.zoneParents(1) == {'zone'}
A test for exploration with zones.
def
test_zoneNaming() -> None:
115def test_zoneNaming() -> None: 116 """ 117 Tests some situations with zone propagation in journals. 118 """ 119 expl = journal.convertJournal("""\ 120S West::start 121x right second left 122 o down third up 123x right East::fourth left 124t left 125x down # third should be in zone 'West' 126 o left New::fifth right 127x left # fifth should remain in zone 'New' 128""") 129 g1 = expl.getSituation(1).graph 130 assert g1.namesListing(g1) == """\ 131 0 (West::start) 132 1 (_u.0) 133""" 134 135 g2 = expl.getSituation(2).graph 136 assert g2.namesListing(g2) == """\ 137 0 (West::start) 138 1 (West::second) 139 2 (West::third) 140 3 (_u.2) 141""" 142 assert g2.zoneParents(0) == {'West'} 143 assert g2.zoneParents(1) == {'West'} 144 assert g2.zoneParents(2) == {'West'} 145 assert g2.zoneParents(3) == set() 146 147 g3 = expl.getSituation(3).graph 148 assert g3.namesListing(g3) == """\ 149 0 (West::start) 150 1 (West::second) 151 2 (West::third) 152 3 (East::fourth) 153""" 154 assert g3.zoneParents(0) == {'West'} 155 assert g3.zoneParents(1) == {'West'} 156 assert g3.zoneParents(2) == {'West'} 157 assert g3.zoneParents(3) == {'East'} 158 159 g4 = expl.getSituation(4).graph 160 assert g4.namesListing(g4) == """\ 161 0 (West::start) 162 1 (West::second) 163 2 (West::third) 164 3 (East::fourth) 165""" 166 assert g4.zoneParents(0) == {'West'} 167 assert g4.zoneParents(1) == {'West'} 168 assert g3.zoneParents(2) == {'West'} 169 assert g4.zoneParents(3) == {'East'} 170 171 g5 = expl.getSituation(5).graph 172 assert g5.namesListing(g5) == """\ 173 0 (West::start) 174 1 (West::second) 175 2 (West::third) 176 3 (East::fourth) 177 4 (New::fifth) 178""" 179 assert g5.zoneParents(0) == {'West'} 180 assert g5.zoneParents(1) == {'West'} 181 assert g5.zoneParents(2) == {'West'} 182 assert g5.zoneParents(3) == {'East'} 183 assert g5.zoneParents(4) == {'New'} 184 185 g6 = expl.getSituation(6).graph 186 assert g6.namesListing(g6) == """\ 187 0 (West::start) 188 1 (West::second) 189 2 (West::third) 190 3 (East::fourth) 191 4 (New::fifth) 192""" 193 assert g6.zoneParents(0) == {'West'} 194 assert g6.zoneParents(1) == {'West'} 195 assert g6.zoneParents(2) == {'West'} 196 assert g6.zoneParents(3) == {'East'} 197 assert g6.zoneParents(4) == {'New'}
Tests some situations with zone propagation in journals.
def
test_startOfCotM() -> None:
200def test_startOfCotM() -> None: 201 """ 202 A test for converting part of a journal from Castlevania: Circle of 203 the Moon. This journal covers just the first few rooms of the game. 204 """ 205 # Note that we're expecting a `TransitionBlockedWarning` because the 206 # requirement 'q distance' does not match the power gained 'At gain 207 # dash_boots'... 208 # TODO: Add an equivalence now that we support those! 209 with pytest.warns(core.TransitionBlockedWarning): 210 expl = journal.convertJournal("""\ 211# Setup 212# ----- 213# A save-point alias that creates, explores and tags a new decision but 214# then returns to where we just were. 215= savePoint [ 216 x {_1} save{_2} return 217 g save 218 t return 219] 220= dagger [ 221 a candle{_} 222 At gain dagger 223 At lose axe 224 At lose holy_water 225 At lose cross 226] 227= axe [ 228 a candle{_} 229 At lose dagger 230 At gain axe 231 At lose holy_water 232 At lose cross 233] 234= holy_water [ 235 a candle{_} 236 At lose dagger 237 At lose axe 238 At gain holy_water 239 At lose cross 240] 241= cross [ 242 a candle{_} 243 At lose dagger 244 At lose axe 245 At lose holy_water 246 At gain cross 247] 248 249# Journal 250# ------- 251S Confrontation_Site::main # Stars the exploration, and applies a zone 252 zz Catacombs # Creates and applies a level-1 zone above the existing zone 253 o Top_Left # Observes a transition at the current decision 254x Bottom Rubble_Tower_Base::top Top # Explores a transition (newly created) 255 # The 'x' entry above notes the transition taken, the room name on 256 # the other end, the transition name for the reciprocal, and the 257 # new level-0 zone for the destination (could be omitted if it's in 258 # the same level-0 zone). 259 gt forced # Tags the transition we just explored 260 261 o Crack # We see a crack in the wall as we go down 262 q ?short_and_high # But we can't get through it right now 263 # Spoilers: we'll never be able to get through it from this side 264 n Fought -Skeleton_Bomber- # A note, applied to the exploration step 265 A gain Salamander # An applied effect: gaining a power 266 # Note: We're not modelling random drops but could with a challenge... 267 n Literally first enemy I touched! # Another note 268x Bottom_Right Catacombs_Entrance::top Top_Left # Another exploration 269 o up 270 n Can't jump up here yet 271 q height 272 o top_stairs mid top_stairs # The stairs to the middle level 273 # Here we define the room name on the other side and the reciprocal 274 # transition name. 275x First_Right Catacombs_Treasury::left Left # But first a detour 276 277 o box # Can't get through this box yet 278 qb heavy # Requirement applies in both directions 279t Left # This 't' retraces a known transition at the current position 280 281x top_stairs # Here the room and reciprocal names have already been 282 # defined, and the zone is the same 283 284 o bottom_stairs bottom bottom_stairs 285 A gain Mercury 286 n Bone head along the way 287 288> savePoint 289x bottom_stairs 290 > dagger 291 o Bot_Left 292 o across_bottom 293x Bot_Left Blockwall_Cave::bottom Right 294 295 o ledge 296 q height 297t Right 298x across_bottom bottom_right back_across_bottom 299 o Bot_Right 300 # TODO: Is the zone necessary here? Feels like it shouldn't be 301 u Catacombs_Entrance::bottom 302 # Nothing super interesting across the bottom of the room, so we might 303 # as well treat the entire bottom as one decision. 304x Bot_Right Green_Gates::entrance Left 305 306 n Fought -Earth_Demon- 307 o jump 308 q distance 309 a get_dash_boots 310 At gain dash_boots 311x jump upper left 312 o right_stairs_down right_side 313 o climb 314x climb attic fall 315 v fall_left entrance fly_up 316 qr flight 317 n at: Green_Gates::attic 318 o fall_right right_side fly_up 319 qr flight 320 321x Upper_Right Catacombs_Treasury_2::entrance Left 322 o block 323 qb crumble 324 325t Left 326 n at: Green_Gates::attic 327x fall_right 328 n at: Green_Gates::right_side 329x Lower_Right Zombie_Corridor::main Left 330 331 n [ 332 Fought -Poison_Worm- combo'd with -Zombie- and -Skeleton-. This 333 makes for an interesting room. 334 ] 335 # This annotation spans multiple lines of text 336x Right Pinkstone_Tower::bottom_left Left_Mid 337 338 # And that's the end of this fragment for testing purposes! 339""") 340 assert len(expl) == 23 341 342 firstSituation = expl.getSituation(0) 343 firstDecisions = expl.getActiveDecisions(0) 344 secondSituation = expl.getSituation(1) 345 secondDecisions = expl.getActiveDecisions(1) 346 sixthSituation = expl.getSituation(5) 347 sixthDecisions = expl.getActiveDecisions(5) 348 finalSituation = expl.getSituation() 349 finalDecisions = expl.getActiveDecisions() 350 351 assert len(firstSituation.graph) == 1 352 assert firstDecisions == set() 353 354 assert len(secondSituation.graph) == 3 # room + 2 unknowns 355 assert listingAtStep(expl, 1) == """\ 356 0 (Confrontation_Site::main) 357 1 (_u.0) 358 2 (_u.1) 359""" 360 assert secondDecisions == {0} 361 362 g6 = sixthSituation.graph 363 assert len(g6) == 9 364 assert listingAtStep(expl, 5) == """\ 365 0 (Confrontation_Site::main) 366 1 (_u.0) 367 2 (Rubble_Tower_Base::top) 368 3 (_u.2) 369 4 (Catacombs_Entrance::top) 370 5 (_u.4) 371 6 (Catacombs_Entrance::mid) 372 7 (Catacombs_Treasury::left) 373 8 (_u.7) 374""" # 'mid' has not been explored yet so isn't in a zone 375 gZones = [g6.zoneParents(d) for d in sorted(g6)] 376 assert gZones == [ 377 {'Confrontation_Site'}, 378 set(), 379 {'Rubble_Tower_Base'}, 380 set(), 381 {'Catacombs_Entrance'}, 382 set(), 383 {'Catacombs_Entrance'}, 384 {'Catacombs_Treasury'}, 385 set() 386 ] 387 assert g6.destination('_u.0', 'return') == 0 388 assert g6.destination('_u.2', 'return') == 2 389 assert g6.destination('_u.4', 'return') == 4 390 assert g6.destination('_u.7', 'return') == 7 391 assert sixthDecisions == {4} 392 393 gL = finalSituation.graph 394 assert len(gL) == 21 395 assert listingAtStep(expl, -1) == """\ 396 0 (Confrontation_Site::main) 397 1 (_u.0) 398 2 (Rubble_Tower_Base::top) 399 3 (_u.2) 400 4 (Catacombs_Entrance::top) 401 5 (_u.4) 402 6 (Catacombs_Entrance::mid) 403 7 (Catacombs_Treasury::left) 404 8 (_u.7) 405 9 (Catacombs_Entrance::bottom) 406 10 (Catacombs_Entrance::save_1) 407 11 (Blockwall_Cave::bottom) 408 13 (_u.12) 409 14 (Green_Gates::entrance) 410 15 (Green_Gates::upper) 411 16 (Green_Gates::right_side) 412 17 (Green_Gates::attic) 413 20 (Catacombs_Treasury_2::entrance) 414 21 (_u.20) 415 22 (Zombie_Corridor::main) 416 23 (Pinkstone_Tower::bottom_left) 417""" 418 assert finalDecisions == {23}
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.
def
test_entry_types() -> None:
699def test_entry_types() -> None: 700 pf = journal.JournalParseFormat() 701 for ebits, expect in [ 702 (["o", "action"], ("observe", 'active', None, ["action"])), 703 (["observe", "action"], ("observe", 'active', None, ["action"])), 704 (["oa", "action"], ("observe", 'active', "actionPart", ["action"])), 705 (["ot", "action"], 706 ("observe", 'active', "transitionPart", ["action"])), 707 (["observe@a", "action"], 708 ("observe", 'active', "actionPart", ["action"])), 709 (["observe@action", "action"], 710 ("observe", 'active', "actionPart", ["action"])), 711 (["observe@actionPart", "action"], 712 ("observe", 'active', "actionPart", ["action"])), 713 (["imposed/observe@actionPart", "action"], 714 ("observe", 'imposed', "actionPart", ["action"])), 715 (["!oa", "action"], 716 ("observe", 'unintended', "actionPart", ["action"])), 717 ]: 718 assert pf.determineEntryType(ebits) == expect, ebits 719 720 for broken in ( 721 ["obse"], 722 ["obse", "action"], 723 ["observe@madeUp"], 724 ["oq"], 725 ): 726 with pytest.raises(journal.JournalParseError): 727 assert pf.determineEntryType(broken) is None, broken
def
test_long_entry_types() -> None:
730def test_long_entry_types() -> None: 731 """ 732 Tests the use of long entry types. 733 """ 734 expl = journal.convertJournal("""\ 735START start 736zone Start 737zone@zone Region 738observe@action action 739observe door 740explore door Room1::room1 door 741tag@both blue 742""") 743 expl2 = journal.convertJournal("""\ 744S start 745z Start 746zz Region 747oa action 748o door 749x door Room1::room1 door 750gb blue 751""") 752 assert expl == expl2
Tests the use of long entry types.
def
test_revisions() -> None:
755def test_revisions() -> None: 756 """ 757 Tests revision-type entries. 758 """ 759 expl = journal.convertJournal("""\ 760S A 761 o left 762 o right 763 extinguish left 764x right B left 765 unify A 766x down C up 767 complicate up D down2 up2 768t up 769x right E left 770""") 771 assert len(expl) == 6 772 773 # Ignoring empty starting graph 774 first = expl.getSituation(1).graph 775 assert expl.getActiveDecisions(1) == {0} 776 assert listingAtStep(expl, 1) == """\ 777 0 (A) 778 2 (_u.1) 779""" 780 assert first.getDestination('A', 'left') is None 781 assert first.getDestination('A', 'right') is not None 782 assert not first.isConfirmed(first.destination('A', 'right')) 783 784 second = expl.getSituation(2).graph 785 assert listingAtStep(expl, 2) == """\ 786 0 (A) 787 3 (_u.2) 788""" 789 assert expl.getActiveDecisions(2) == {0} 790 assert 'B' not in [second.nameFor(d) for d in second] 791 assert second.getDestination('A', 'right') == 0 792 assert second.getDestination('A', 'left') == 0 793 assert not second.isConfirmed(second.destination('A', 'down')) 794 795 third = expl.getSituation(3).graph 796 assert listingAtStep(expl, 3) == """\ 797 0 (A) 798 3 (C) 799 4 (D) 800""" 801 assert expl.getActiveDecisions(3) == {3} 802 assert third.getDestination('A', 'down') == 4 803 assert third.getDestination('D', 'up2') == 0 804 assert third.getDestination('C', 'up') == 4 805 assert third.getDestination('D', 'down2') == 3 806 807 fourth = expl.getSituation(4).graph 808 assert listingAtStep(expl, 4) == """\ 809 0 (A) 810 3 (C) 811 4 (D) 812 5 (_u.3) 813""" 814 assert expl.getActiveDecisions(4) == {4} 815 assert fourth.getDestination('A', 'down') == 4 816 assert fourth.getDestination('D', 'up2') == 0 817 assert fourth.getDestination('C', 'up') == 4 818 assert fourth.getDestination('D', 'down2') == 3 819 assert not fourth.isConfirmed(fourth.destination('D', 'right')) 820 821 fifth = expl.getSituation(5).graph 822 assert listingAtStep(expl, 5) == """\ 823 0 (A) 824 3 (C) 825 4 (D) 826 5 (E) 827""" 828 assert expl.getActiveDecisions(5) == {5} 829 assert fifth.getDestination('A', 'down') == 4 830 assert fifth.getDestination('D', 'up2') == 0 831 assert fifth.getDestination('C', 'up') == 4 832 assert fifth.getDestination('D', 'down2') == 3 833 assert fifth.getDestination('D', 'right') == 5
Tests revision-type entries.
def
test_journal_outcomes() -> None:
836def test_journal_outcomes() -> None: 837 """ 838 Tests challenge outcome mechanisms. 839 """ 840 warnings.filterwarnings("error") 841 expl = journal.convertJournal("""\ 842S A 843 A gain coin*5 844 o right 845 e [ 846 {<0>luck>{gain coin*1}{lose coin*1}} 847 ] # gain outcome is selected 848x right B left 849 n has: coin*6 # gain outcome triggers by default 850 er [ 851 {<0>luck{gain coin*2}>{lose coin*2}} 852 ] # lose outcome is selected 853t left 854 n has: coin*4 # lose outcome triggered by default 855t right%f # explicit outcome overrules 856 n has: coin*3 857t left%s # explicit again 858 n has: coin*5 859t right # remembers most-recent outcomes 860 n has: coin*4 861t left%f # specified again 862 n has: coin*2 863t right%s 864 n has: coin*3 865t left # remembers most-recent 866 n has: coin*1 867""") 868 # (should not warn about mismatched 'has:' notes) 869 870 assert len(expl) == 10 871 872 def tokensAtStep(e, t, n): 873 return base.combinedTokenCount(e.getSituation(n).state, t) 874 875 assert tokensAtStep(expl, 'coin', 0) == 0 876 assert tokensAtStep(expl, 'coin', 1) == 5 877 assert tokensAtStep(expl, 'coin', 2) == 6 878 assert tokensAtStep(expl, 'coin', 3) == 4 879 assert tokensAtStep(expl, 'coin', 4) == 3 880 assert tokensAtStep(expl, 'coin', 5) == 5 881 assert tokensAtStep(expl, 'coin', 6) == 4 882 assert tokensAtStep(expl, 'coin', 7) == 2 883 assert tokensAtStep(expl, 'coin', 8) == 3 884 assert tokensAtStep(expl, 'coin', 9) == 1 885 886 expl2 = journal.convertJournal("""\ 887S A 888 A gain coin*5 889 n has: coin*5 890 n has: flower*0 891 o right B left 892 e [ 893 { 894 <0>luck{<0>luck{gain coin*1}{lose coin*1}; bounce} 895 {<0>luck{gain coin*2}{lose coin*2}; follow left}; 896 ??(coin*5){gain flower*1}{<0>luck{gain flower*1}{}} 897 } 898 ] # no pre-selected outcomes 899x right%fs 900 # effectively bounces each time so we can just keep taking it 901 n has: coin*7 902 n has: flower*1 903 n at: A 904t right%sf 905 n has: coin*6 906 n has: flower*2 907 n at: A 908t right%ff # coins are checked in original state 909 n has: coin*4 910 n has: flower*3 911 n at: A 912t right%ssf 913 n has: coin*5 914 n has: flower*3 915 n at: A 916""") 917 assert len(expl2) == 6 918 919 assert tokensAtStep(expl2, 'coin', 0) == 0 920 assert tokensAtStep(expl2, 'flower', 0) == 0 921 assert base.combinedDecisionSet(expl2.getSituation(0).state) == set() 922 assert tokensAtStep(expl2, 'coin', 1) == 5 923 assert tokensAtStep(expl2, 'flower', 1) == 0 924 assert base.combinedDecisionSet(expl2.getSituation(1).state) == {0} 925 assert tokensAtStep(expl2, 'coin', 2) == 7 926 assert tokensAtStep(expl2, 'flower', 2) == 1 927 assert base.combinedDecisionSet(expl2.getSituation(2).state) == {0} 928 assert tokensAtStep(expl2, 'coin', 3) == 6 929 assert tokensAtStep(expl2, 'flower', 3) == 2 930 assert base.combinedDecisionSet(expl2.getSituation(3).state) == {0} 931 assert tokensAtStep(expl2, 'coin', 4) == 4 932 assert tokensAtStep(expl2, 'flower', 4) == 3 933 assert base.combinedDecisionSet(expl2.getSituation(4).state) == {0} 934 assert tokensAtStep(expl2, 'coin', 5) == 5 935 assert tokensAtStep(expl2, 'flower', 5) == 3 936 assert base.combinedDecisionSet(expl2.getSituation(5).state) == {0} 937 938 warnings.filterwarnings("default")
Tests challenge outcome mechanisms.
def
test_journal_reversions() -> None:
941def test_journal_reversions() -> None: 942 """ 943 Some tests of the state-reversion mechanisms. 944 """ 945 warnings.filterwarnings("error") 946 expl = journal.convertJournal("""\ 947S A 948 A save slot0 949x right B left 950 A save slot1 951x right C left 952 At gain coin*1 953revert slot0 954 n at: A 955 n has: coin*0 956w 957revert slot1 958 n at: B 959 n has: coin*0 960w 961? destinations 962t right # graph still unreverted 963 n at: C 964 n has:coin*1 965revert slot0 all-positions primary 966 n at: A 967 n has: coin*1 968w 969revert slot1 c-main-tokens 970 n at: A 971 n has: coin*0 972x up aboveA down 973 n at: aboveA 974revert slot0 all-positions primary graph 975 n at: A 976x up aboveA2 down 977 n at: aboveA2 978""") 979 warnings.filterwarnings("default") 980 assert len(expl) == 15 981 preRevertGraph = expl.getSituation(-3).graph 982 assert len(preRevertGraph) == 4 983 assert preRevertGraph.destinationsFrom('A') == {"right": 1, "up": 3} 984 revertedGraph = expl.getSituation(-2).graph 985 assert len(revertedGraph) == 2 986 assert revertedGraph.destinationsFrom('A') == {"up": 4}
Some tests of the state-reversion mechanisms.
def
test_journal_macros() -> None:
989def test_journal_macros() -> None: 990 """ 991 Some tests of macros. 992 """ 993 warnings.filterwarnings("error") 994 expl = journal.convertJournal("""\ 995= boss who level [ 996 oa fight{who} 997 gt bossFight 998 e [ 999 { 1000 <{level}>sum(combat, {who}){ 1001 gain defeated{who}; 1002 deactivate 1003 }{ 1004 goto endings//death 1005 } 1006 } 1007 ] 1008] 1009 1010 1011= failBossRevert who [ 1012 ta fight{who}%f 1013 R 1014] 1015 1016= beatBoss who [ 1017 ta fight{who}%s 1018] 1019 1020S start # step 1 1021@ endings//death 1022@ @ 1023A save 1024 1025x right bossRoom left # step 2 1026> boss Boss -1 1027 e gain orb*1 1028 n has: orb*0 1029 n has: coin*0 1030 1031> failBossRevert Boss # steps 3+4 1032 n at: start 1033 n has: orb*0 1034 n has: coin*0 1035 1036t right # step 5 1037 n at: bossRoom 1038 1039> failBossRevert Boss # step 6+7 1040 n at: start 1041 n has: orb*0 1042 1043t right # step 8 1044 n has: orb*0 1045 n has: coin*0 1046 > beatBoss Boss # step 9 1047 At gain coin*1 1048 n has: orb*1 1049 n has: coin*1 1050 1051? steps 1052 1053p endings//death # step 10 1054R # step 11 1055 n at: start 1056 n has: orb*0 1057 n has: coin*0 1058 1059? steps 1060 1061t right # step 12 1062 n has: orb*0 1063 n has: coin*0 1064 > beatBoss Boss # step 13 1065 n has: orb*1 1066 n has: coin*1 1067 n can: defeatedBoss 1068""") 1069 warnings.filterwarnings("default") 1070 assert len(expl) == 14 1071 # Most assertions are in the journal itself here
Some tests of macros.
def
test_applyAfterwards() -> None:
1074def test_applyAfterwards() -> None: 1075 """ 1076 Tests using 'At' to note non-obvious transition effects. 1077 """ 1078 warnings.filterwarnings("error") 1079 expl = journal.convertJournal("""\ 1080= get what [ 1081 a get 1082 At gain {what} 1083] 1084 1085S start 1086 n has: coin*0 1087 oa grab 1088 e gain coin*1 1089 n has: coin*0 1090 n has: chip*0 1091 ta grab 1092 n has: coin*1 1093 n has: chip*0 1094 At gain chip*1 1095 n has: coin*1 1096 n has: chip*1 1097 ta grab 1098 n has: coin*2 1099 n has: chip*2 1100 > get coin*2 1101 n has: coin*4 1102 ta get 1103 n has: coin*6 1104""") 1105 warnings.filterwarnings("default") 1106 assert expl.tokenCountNow('coin') == 6 1107 assert expl.tokenCountNow('chip') == 2
Tests using 'At' to note non-obvious transition effects.