exploration.tests.test_analysis

  • Authors: Nissi, Jada, Rachel, Kaitlyn, Kitty, Peter Mawhorter
  • Consulted: Peter Mawhorter
  • Date: 2022-12-5
  • Purpose: Tests for exploration analysis.
  1"""
  2- Authors: Nissi, Jada, Rachel, Kaitlyn, Kitty, Peter Mawhorter
  3- Consulted: Peter Mawhorter
  4- Date: 2022-12-5
  5- Purpose: Tests for exploration analysis.
  6"""
  7
  8from .. import analysis, journal, core, base
  9
 10#Part of Rachel's journal:
 11JOURNAL = """
 12S Start_room::Start
 13  zz Starting_region
 14  A gain attack
 15  o right
 16  o left
 17xt left coin_room sr
 18  o up above_coin_room fall
 19    q _wall_kick_jumps  # TODO: '?' syntax
 20t sr
 21xt right platforms rp
 22  o up
 23  o right
 24xt up dangerplat ud
 25"""
 26
 27BABY_JOURNAL = """
 28S Start
 29  A gain jump
 30  A gain attack
 31  n button check
 32  zz Wilds
 33  o up
 34    q _flight
 35  o left
 36xt left left_nook right
 37  a geo_rock
 38    At gain geo*15
 39    At deactivate
 40  o up
 41    q _tall_narrow
 42t right
 43  o right
 44    q attack
 45"""
 46
 47BABY_JOURNAL_2 = """
 48S Start
 49  A gain jump
 50  A gain attack
 51  n button check
 52  zz Wilds
 53  o up
 54    q _flight
 55  o left
 56xt left left_nook right
 57"""
 58
 59REVISITS_JOURNAL = """
 60S A
 61x right B left
 62x down C up
 63x left D right           # 4 decisions ABCD
 64r up A down              # revisit #1 for A
 65t right                  # revisit #1 for B
 66r down_left D up_right   # revisit #1 for D
 67t right                  # revisit #1 for C
 68t up                     # revisit #2 for A
 69t left                   # revisit #2 for B
 70t down                   # revisit #2 for C
 71
 72# now we have: 2 revisits each for A, B, and C, and 1 revisit for D
 73"""
 74
 75ACTIONS_JOURNAL = """
 76S A
 77  oa alpha
 78x right B left
 79  oa beta
 80t left
 81  ta alpha
 82  oa gamma
 83x down C up
 84  a delta
 85  a epsilon
 86x right D left
 87w
 88"""
 89
 90
 91def test_CreateExploration() -> None:
 92    """
 93    Simple test to make sure we can create an exploration object for
 94    other tests in this file.
 95    """
 96    ex = journal.convertJournal(JOURNAL)
 97    assert len(ex) == 6
 98    assert ex.getActiveDecisions(0) == set()
 99    assert ex.getActiveDecisions(1) == {0}
100
101    assert bool(ex.getSituation(1).graph) is True
102
103
104def test_countActionsAtDecision() -> None:
105    """
106    Test to make sure the number of actions in the whole graph
107    at each step is accurate.
108    """
109    ex = journal.convertJournal(BABY_JOURNAL)
110    assert [
111        analysis.totalActions(ex, i)
112        for i in range(len(ex))
113    ] == [0, 0, 1, 1, 1]
114
115    now = ex.getSituation()
116    graph = now.graph
117    startID = graph.resolveDecision("Start")
118    nookID = graph.resolveDecision("left_nook")
119    assert analysis.actionCount(ex, len(ex) - 1, startID) == 0
120    assert analysis.actionCount(ex, len(ex) - 1, nookID) == 1
121
122    ex2 = journal.convertJournal(ACTIONS_JOURNAL)
123    assert [
124        analysis.totalActions(ex2, i)
125        for i in range(len(ex2))
126    ] == [0, 1, 2, 2, 3, 4, 5, 5, 5, 5]
127    assert [
128        analysis.meanActions(ex2, i)
129        for i in range(len(ex2))
130    ] == [0.0, 1/2, 2/2, 2/2, 3/3, 4/3, 5/3, 5/4, 5/4, 5/4]
131
132    for step in range(len(ex2)):
133        assert (
134            analysis.totalActions(ex2, step) /
135            analysis.totalDecisionsSoFar(ex2, step)
136         == analysis.meanActions(ex2, step)
137        )
138
139
140def test_count_actions() -> None:
141    """
142    Tests the 'actionCount' metric and its total/mean combiner values.
143    """
144    ex = journal.convertJournal(BABY_JOURNAL)
145    ex2 = journal.convertJournal(ACTIONS_JOURNAL)
146
147    assert analysis.actionCount(ex, 1, 0) == 0
148    assert analysis.actionCount(ex, 2, 1) == 0
149
150
151def test_describeProgress() -> None:
152    """
153    Tests the `describeProgress` function.
154    """
155    e1 = journal.convertJournal(BABY_JOURNAL)
156    e2 = journal.convertJournal(BABY_JOURNAL_2)
157    e3 = journal.convertJournal(REVISITS_JOURNAL)
158
159    description = analysis.describeProgress(e1)
160    assert description == """\
161Start of the exploration
162Start exploring domain main at 0 (Start)
163  Gained capability 'attack'
164  Gained capability 'jump'
165At decision 0 (Start)
166  In region Wilds
167  There are transitions:
168    left to unconfirmed
169    up to unconfirmed; requires _flight
170  1 note(s) at this step
171Explore left from decision 0 (Start) to 2 (now Wilds::left_nook)
172At decision 2 (left_nook)
173  There are transitions:
174    right to 0 (Start)
175  There are actions:
176    geo_rock
177Do action geo_rock
178  Gained 15 geo(s)
179Take right from decision 2 (left_nook) to 0 (Start)
180At decision 0 (Start)
181  There are transitions:
182    left to 2 (left_nook)
183    right to unconfirmed; requires attack
184    up to unconfirmed; requires _flight
185Waiting for another action...
186End of the exploration.
187"""
188
189    description2 = analysis.describeProgress(e2)
190    assert description2 == """\
191Start of the exploration
192Start exploring domain main at 0 (Start)
193  Gained capability 'attack'
194  Gained capability 'jump'
195At decision 0 (Start)
196  In region Wilds
197  There are transitions:
198    left to unconfirmed
199    up to unconfirmed; requires _flight
200  1 note(s) at this step
201Explore left from decision 0 (Start) to 2 (now Wilds::left_nook)
202At decision 2 (left_nook)
203  There are transitions:
204    right to 0 (Start)
205Waiting for another action...
206End of the exploration.
207"""
208
209    description3 = analysis.describeProgress(e3)
210    assert description3 == """\
211Start of the exploration
212Start exploring domain main at 0 (A)
213At decision 0 (A)
214  There are transitions:
215    right to unconfirmed
216Explore right from decision 0 (A) to 1 (now B)
217At decision 1 (B)
218  There are transitions:
219    down to unconfirmed
220    left to 0 (A)
221Explore down from decision 1 (B) to 2 (now C)
222At decision 2 (C)
223  There are transitions:
224    left to unconfirmed
225    up to 1 (B)
226Explore left from decision 2 (C) to 3 (now D)
227At decision 3 (D)
228  There are transitions:
229    right to 2 (C)
230    up to 0 (A)
231Take up from decision 3 (D) to 0 (A)
232At decision 0 (A)
233  There are transitions:
234    down to 3 (D)
235    right to 1 (B)
236Take right from decision 0 (A) to 1 (B)
237At decision 1 (B)
238  There are transitions:
239    down to 2 (C)
240    down_left to 3 (D)
241    left to 0 (A)
242Take down_left from decision 1 (B) to 3 (D)
243At decision 3 (D)
244  There are transitions:
245    right to 2 (C)
246    up to 0 (A)
247    up_right to 1 (B)
248Take right from decision 3 (D) to 2 (C)
249At decision 2 (C)
250  There are transitions:
251    left to 3 (D)
252    up to 1 (B)
253Take up from decision 2 (C) to 1 (B)
254At decision 1 (B)
255  There are transitions:
256    down to 2 (C)
257    down_left to 3 (D)
258    left to 0 (A)
259Take left from decision 1 (B) to 0 (A)
260At decision 0 (A)
261  There are transitions:
262    down to 3 (D)
263    right to 1 (B)
264Take down from decision 0 (A) to 3 (D)
265At decision 3 (D)
266  There are transitions:
267    right to 2 (C)
268    up to 0 (A)
269    up_right to 1 (B)
270Waiting for another action...
271End of the exploration.
272"""
273
274
275def test_unexploredBranches() -> None:
276    """
277    Tests the `unexploredBranches` and related count functions.
278    """
279    ex = journal.convertJournal(JOURNAL)
280    assert analysis.unexploredBranches(ex.getSituation(0).graph) == []
281    g1 = ex.getSituation(1).graph
282    assert g1.destinationsFrom(0) == {
283        'right': 1,
284        'left': 2
285    }
286    assert g1.nameFor(1) == '_u.0'
287    assert g1.nameFor(2) == '_u.1'
288    assert analysis.unexploredBranches(ex.getSituation(1).graph) == [
289        (0, 'right'),
290        (0, 'left'),
291    ]
292    assert analysis.unexploredBranches(ex.getSituation(2).graph) == [
293        (0, 'right'),
294        (2, 'up'),
295    ]
296    assert analysis.unexploredBranches(ex.getSituation(3).graph) == [
297        (0, 'right'),
298        (2, 'up'),
299    ]
300    g4 = ex.getSituation(4).graph
301    assert g4.namesListing(set(g4.nodes)) == """\
302  0 (Start_room::Start)
303  1 (Start_room::platforms)
304  2 (Start_room::coin_room)
305  3 (Start_room::above_coin_room)
306  4 (_u.3)
307  5 (_u.4)
308"""
309    assert analysis.unexploredBranches(ex.getSituation(4).graph) == [
310        (1, 'up'),
311        (1, 'right'),
312        (2, 'up'),
313    ]
314    assert analysis.unexploredBranches(ex.getSituation(5).graph) == [
315        (1, 'right'),
316        (2, 'up'),
317    ]
318    allPerStep = [
319        analysis.unexploredBranchCount(ex, step)
320        for step in range(len(ex))
321    ]
322    traversablePerStep = [
323        analysis.traversableUnexploredCount(ex, step)
324        for step in range(len(ex))
325    ]
326    assert allPerStep == [0, 2, 2, 2, 3, 2]
327    assert traversablePerStep == [0, 2, 1, 1, 2, 1]
328    # TODO
329
330
331def test_countBranches() -> None:
332    """
333    Tests the `countBranches` function.
334    """
335    ex = journal.convertJournal(BABY_JOURNAL)
336    ex2 = journal.convertJournal(BABY_JOURNAL_2)
337
338    # Note: as of v0.6, we can index an exploration to get a Situation,
339    # and most analysis functions want Situations as input
340
341    first = ex[0]
342    second = ex[1]
343    third = ex[2]
344    fourth = ex[3]
345    fifth = ex[4]
346
347    ex2first = ex2[0]
348    ex2second = ex2[1]
349    ex2third = ex2[2]
350
351    assert first.graph.namesListing(set(first.graph.nodes)) == """\
352  0 (Start)
353"""
354    assert analysis.meanBranches(ex, 0) == 0
355    assert analysis.meanBranches(ex, 1) == 2
356    assert analysis.meanBranches(ex, 2) == 1.5
357    assert analysis.meanBranches(ex, 3) == 2
358    assert analysis.meanBranches(ex, 4) == 2.5
359
360    startID = fifth.graph.resolveDecision("Start")
361    nookID = fifth.graph.resolveDecision("left_nook")
362    assert analysis.branches(ex, 1, startID) == 2
363    assert analysis.branches(ex, 2, startID) == 2
364    assert analysis.branches(ex, 2, nookID) == 1
365    assert analysis.branches(ex, 4, startID) == 3
366    assert analysis.branches(ex, 4, nookID) == 2
367
368    assert analysis.meanBranches(ex2, 0) == 0
369    assert analysis.meanBranches(ex2, 1) == 2
370    assert analysis.meanBranches(ex2, 2) == 1.5
371
372
373def test_revisits() -> None:
374    """
375    Tests the `arrivals`, `revisits`, and related total/mean/median
376    functions.
377    """
378
379    babyExp = journal.convertJournal(BABY_JOURNAL)
380    graph = babyExp.getSituation().graph
381    startID = graph.resolveDecision("Start")
382    nookID = graph.resolveDecision("left_nook")
383    assert analysis.stepsVisited(babyExp) == {
384        startID: [1, 4],
385        nookID: [2, 3]
386    }
387    assert analysis.arrivals(babyExp, startID) == 2
388    assert analysis.revisits(babyExp, startID) == 1
389    assert analysis.arrivals(babyExp, nookID) == 1
390    assert analysis.revisits(babyExp, nookID) == 0
391    assert analysis.arrivals(babyExp, 28309823) == 0
392    assert analysis.revisits(babyExp, 28309823) == 0
393
394    fullExp = journal.convertJournal(JOURNAL)
395    graph = fullExp.getSituation().graph
396    startID = graph.resolveDecision("Start")
397    coinRoomID = graph.resolveDecision("coin_room")
398    platformsID = graph.resolveDecision("platforms")
399    dangerplatID = graph.resolveDecision("dangerplat")
400    assert analysis.revisits(fullExp, startID) == 1
401    assert analysis.revisits(fullExp, coinRoomID) == 0
402    assert analysis.revisits(fullExp, platformsID) == 0
403    assert analysis.revisits(fullExp, dangerplatID) == 0
404
405    revExp = journal.convertJournal(REVISITS_JOURNAL)
406    graph = revExp.getSituation().graph
407    aID = graph.resolveDecision('A')
408    bID = graph.resolveDecision('B')
409    cID = graph.resolveDecision('C')
410    dID = graph.resolveDecision('D')
411    assert analysis.revisits(revExp, aID) == 2
412    assert analysis.revisits(revExp, bID) == 2
413    assert analysis.revisits(revExp, cID) == 1
414    assert analysis.revisits(revExp, dID) == 2
415
416    assert analysis.totalRevisits(babyExp) == 1
417    assert analysis.meanRevisits(babyExp) == 1/5
418    assert analysis.medianRevisits(babyExp) == 0
419    assert analysis.totalRevisits(fullExp) == 1
420    assert analysis.meanRevisits(fullExp) == 1/6
421    assert analysis.medianRevisits(fullExp) == 0
422    assert analysis.totalRevisits(revExp) == 7
423    assert analysis.meanRevisits(revExp) == 7/4
424    assert analysis.medianRevisits(revExp) == 2.0
425
426    blankExp = core.DiscreteExploration()
427    assert analysis.totalRevisits(blankExp) == 0
428    assert analysis.meanRevisits(blankExp) is None
429    assert analysis.medianRevisits(blankExp) is None
430
431    soloExp = core.DiscreteExploration()
432    soloExp.start("start")
433    assert analysis.totalRevisits(soloExp) == 0
434    assert analysis.meanRevisits(soloExp) == 0
435    assert analysis.medianRevisits(soloExp) == 0
436
437    doubleExp = core.DiscreteExploration()
438    doubleExp.start("start")
439    doubleExp.observe("start", "up")
440    doubleExp.explore("up", "above", "down")
441    assert analysis.totalRevisits(doubleExp) == 0
442    assert analysis.meanRevisits(doubleExp) == 0
443    assert analysis.medianRevisits(doubleExp) == 0
444
445
446def test_makeFractionCombiner() -> None:
447    """
448    Tests `makeFractionCombiner`.
449    """
450    c = analysis.makeFractionCombiner(lambda k, x: x > 2, lambda k, x: x < 0)
451    d = {
452        'a': 1,
453        'b': -2,
454        'c': 5,
455        'd': 3,
456        'e': 2.1
457    }
458    assert c(d) == 0.75  # type:ignore
459    c = analysis.makeFractionCombiner(lambda k, x: x < 0, lambda k, x: x > 0)
460    assert c(d) == 1.0  # type:ignore
461    c = analysis.makeFractionCombiner(lambda k, x: x < 0)
462    assert c(d) == 0.2  # type:ignore
463    c = analysis.makeFractionCombiner(lambda k, x: x > 2)
464    assert c(d) == 0.6  # type:ignore
465    c = analysis.makeFractionCombiner(lambda k, x: x % 3 == 7)
466    assert c(d) == 0.0  # type:ignore
467
468def test_cycleBasis() -> None:
469    """
470    Tests `cycleBasis`.
471    """
472    # Test using a custom several-cycle decision graph:
473    g = core.DecisionGraph()
474    g.addDecision('A')  # 0
475    g.addDecision('B')  # 1
476    g.addDecision('C')  # 2
477    g.addDecision('D')  # 3
478    g.addDecision('E')  # 4
479    g.addDecision('F')  # 5
480    g.addDecision('G')  # 6
481    g.addDecision('H')  # 7
482    g.addDecision('I')  # 8
483    assert analysis.cycleBasis(g) == []
484    g.addTransition('A', 'left', 'B', 'right')
485    g.addTransition('B', 'down', 'C', 'upRight')
486    assert analysis.cycleBasis(g) == []
487    g.addTransition('A', 'down', 'C', 'upLeft')  # triangle
488    assert analysis.cycleBasis(g) == [[0, 1, 2]]
489    g.addTransition('C', 'down', 'D', 'up')
490    g.addTransition('D', 'down', 'E', 'up')  # D is in-between
491    assert analysis.cycleBasis(g) == [[0, 1, 2]]
492    g.addTransition('E', 'downLeft', 'F', 'up')
493    g.addTransition('F', 'right', 'G', 'left')
494    g.addTransition('G', 'right', 'H', 'left')
495    assert analysis.cycleBasis(g) == [[0, 1, 2]]
496    g.addTransition('H', 'up', 'E', 'downRight')  # 4-node cycle
497    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
498        frozenset({0, 1, 2}),
499        frozenset({4, 5, 6, 7})
500    }
501    g.addTransition('G', 'down', 'I', 'up')  # branches off
502    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
503        frozenset({0, 1, 2}),
504        frozenset({4, 5, 6, 7})
505    }
506    g.setTransitionRequirement('C', 'down', base.ReqCapability('fly'))
507    g.setTransitionRequirement('E', 'up', base.ReqCapability('fly'))
508    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
509        frozenset({0, 1, 2}),
510        frozenset({4, 5, 6, 7})
511    }
512
513    # Test using snippet from problematic journal
514    cotmStart = """\
515# Setup
516P softerChecks on
517
518= enemy which common level rare rareLevel [
519  gd enemy:{which}
520  oa farm{which}{common}
521    e { <{level}>luck{ gain {common}*1 }{ } }
522  oa farm{which}{rare}
523    e { <{rareLevel}>luck{ gain {rare}*1 }{ } }
524]
525
526= farm from what [
527  ta farm{from}{what}%s
528]
529
530= cardEnemy which card level otherDrop otherLevel [
531  gd enemy:{which}
532  oa farmCard{which}{card}
533    e { <{level}>luck{ gain {card}; deactivate }{ } }
534  oa farm{which}{otherDrop}
535    e { <{otherLevel}>luck{ gain {otherDrop}*1 }{ } }
536]
537
538= gotCard from which [
539  ta farmCard{from}{which}%s
540]
541
542= subweapon which [
543  oa take{which}
544    e { lose Dagger; lose HolyWater; lose Boomerang; lose Axe; gain {which} }
545]
546
547= getItem what [
548  a get{what}
549    At { gain {what}*1; deactivate }
550]
551
552# Start
553S ConfrontationSite::main
554  zz Catacombs
555  o left
556  o down
557  m door:sealed
558  o right
559    qb door:unsealed
560  @
561    xt down RubbleTowerShaft::shaft up
562      gd suspended
563  @ @
564  a meetDracula
565    gt triggered
566    At follow down
567
568n at: RubbleTowerShaft::shaft
569  o fall bottom flyUp
570    qr ?flight
571  !a fallDown
572    q !?flight
573    gt triggered
574    At follow fall
575  n at: RubbleTowerShaft::bottom
576
577x down RubbleTowerBase::top up
578  > cardEnemy SkeletonBomber Salamander 1 Potion 6
579    n [ Real chances are 20% / 1%; this gives 25% / 0.78% ]
580  > gotCard SkeletonBomber Salamander
581    n [ Literally first enemy I touched; super lucky! ]
582x down mid up
583  > cardEnemy SkeletonBomber Salamander 1 Potion 6
584  o leftCrack
585    q ?airSlide
586x down bottom up
587  > cardEnemy SkeletonBomber Salamander 1 Potion 6
588
589x right CatacombsEntrance::top left
590  > cardEnemy SkeletonBomber Salamander 1 Potion 6
591  o upLeft
592    q ?higherJump
593  o upRight
594    q ?higherJump
595  > enemy Bat WristBand 4 Heart 7
596    n [ Real 2.5% / 0.5%; these are 3.125% / 0.39% ]
597    """
598    exp = journal.convertJournal(cotmStart)
599    assert analysis.cycleBasis(exp.getSituation().graph) == []
JOURNAL = "\nS Start_room::Start\n zz Starting_region\n A gain attack\n o right\n o left\nxt left coin_room sr\n o up above_coin_room fall\n q _wall_kick_jumps # TODO: '?' syntax\nt sr\nxt right platforms rp\n o up\n o right\nxt up dangerplat ud\n"
BABY_JOURNAL = '\nS Start\n A gain jump\n A gain attack\n n button check\n zz Wilds\n o up\n q _flight\n o left\nxt left left_nook right\n a geo_rock\n At gain geo*15\n At deactivate\n o up\n q _tall_narrow\nt right\n o right\n q attack\n'
BABY_JOURNAL_2 = '\nS Start\n A gain jump\n A gain attack\n n button check\n zz Wilds\n o up\n q _flight\n o left\nxt left left_nook right\n'
REVISITS_JOURNAL = '\nS A\nx right B left\nx down C up\nx left D right # 4 decisions ABCD\nr up A down # revisit #1 for A\nt right # revisit #1 for B\nr down_left D up_right # revisit #1 for D\nt right # revisit #1 for C\nt up # revisit #2 for A\nt left # revisit #2 for B\nt down # revisit #2 for C\n\n# now we have: 2 revisits each for A, B, and C, and 1 revisit for D\n'
ACTIONS_JOURNAL = '\nS A\n oa alpha\nx right B left\n oa beta\nt left\n ta alpha\n oa gamma\nx down C up\n a delta\n a epsilon\nx right D left\nw\n'
def test_CreateExploration() -> None:
 92def test_CreateExploration() -> None:
 93    """
 94    Simple test to make sure we can create an exploration object for
 95    other tests in this file.
 96    """
 97    ex = journal.convertJournal(JOURNAL)
 98    assert len(ex) == 6
 99    assert ex.getActiveDecisions(0) == set()
100    assert ex.getActiveDecisions(1) == {0}
101
102    assert bool(ex.getSituation(1).graph) is True

Simple test to make sure we can create an exploration object for other tests in this file.

def test_countActionsAtDecision() -> None:
105def test_countActionsAtDecision() -> None:
106    """
107    Test to make sure the number of actions in the whole graph
108    at each step is accurate.
109    """
110    ex = journal.convertJournal(BABY_JOURNAL)
111    assert [
112        analysis.totalActions(ex, i)
113        for i in range(len(ex))
114    ] == [0, 0, 1, 1, 1]
115
116    now = ex.getSituation()
117    graph = now.graph
118    startID = graph.resolveDecision("Start")
119    nookID = graph.resolveDecision("left_nook")
120    assert analysis.actionCount(ex, len(ex) - 1, startID) == 0
121    assert analysis.actionCount(ex, len(ex) - 1, nookID) == 1
122
123    ex2 = journal.convertJournal(ACTIONS_JOURNAL)
124    assert [
125        analysis.totalActions(ex2, i)
126        for i in range(len(ex2))
127    ] == [0, 1, 2, 2, 3, 4, 5, 5, 5, 5]
128    assert [
129        analysis.meanActions(ex2, i)
130        for i in range(len(ex2))
131    ] == [0.0, 1/2, 2/2, 2/2, 3/3, 4/3, 5/3, 5/4, 5/4, 5/4]
132
133    for step in range(len(ex2)):
134        assert (
135            analysis.totalActions(ex2, step) /
136            analysis.totalDecisionsSoFar(ex2, step)
137         == analysis.meanActions(ex2, step)
138        )

Test to make sure the number of actions in the whole graph at each step is accurate.

def test_count_actions() -> None:
141def test_count_actions() -> None:
142    """
143    Tests the 'actionCount' metric and its total/mean combiner values.
144    """
145    ex = journal.convertJournal(BABY_JOURNAL)
146    ex2 = journal.convertJournal(ACTIONS_JOURNAL)
147
148    assert analysis.actionCount(ex, 1, 0) == 0
149    assert analysis.actionCount(ex, 2, 1) == 0

Tests the 'actionCount' metric and its total/mean combiner values.

def test_describeProgress() -> None:
152def test_describeProgress() -> None:
153    """
154    Tests the `describeProgress` function.
155    """
156    e1 = journal.convertJournal(BABY_JOURNAL)
157    e2 = journal.convertJournal(BABY_JOURNAL_2)
158    e3 = journal.convertJournal(REVISITS_JOURNAL)
159
160    description = analysis.describeProgress(e1)
161    assert description == """\
162Start of the exploration
163Start exploring domain main at 0 (Start)
164  Gained capability 'attack'
165  Gained capability 'jump'
166At decision 0 (Start)
167  In region Wilds
168  There are transitions:
169    left to unconfirmed
170    up to unconfirmed; requires _flight
171  1 note(s) at this step
172Explore left from decision 0 (Start) to 2 (now Wilds::left_nook)
173At decision 2 (left_nook)
174  There are transitions:
175    right to 0 (Start)
176  There are actions:
177    geo_rock
178Do action geo_rock
179  Gained 15 geo(s)
180Take right from decision 2 (left_nook) to 0 (Start)
181At decision 0 (Start)
182  There are transitions:
183    left to 2 (left_nook)
184    right to unconfirmed; requires attack
185    up to unconfirmed; requires _flight
186Waiting for another action...
187End of the exploration.
188"""
189
190    description2 = analysis.describeProgress(e2)
191    assert description2 == """\
192Start of the exploration
193Start exploring domain main at 0 (Start)
194  Gained capability 'attack'
195  Gained capability 'jump'
196At decision 0 (Start)
197  In region Wilds
198  There are transitions:
199    left to unconfirmed
200    up to unconfirmed; requires _flight
201  1 note(s) at this step
202Explore left from decision 0 (Start) to 2 (now Wilds::left_nook)
203At decision 2 (left_nook)
204  There are transitions:
205    right to 0 (Start)
206Waiting for another action...
207End of the exploration.
208"""
209
210    description3 = analysis.describeProgress(e3)
211    assert description3 == """\
212Start of the exploration
213Start exploring domain main at 0 (A)
214At decision 0 (A)
215  There are transitions:
216    right to unconfirmed
217Explore right from decision 0 (A) to 1 (now B)
218At decision 1 (B)
219  There are transitions:
220    down to unconfirmed
221    left to 0 (A)
222Explore down from decision 1 (B) to 2 (now C)
223At decision 2 (C)
224  There are transitions:
225    left to unconfirmed
226    up to 1 (B)
227Explore left from decision 2 (C) to 3 (now D)
228At decision 3 (D)
229  There are transitions:
230    right to 2 (C)
231    up to 0 (A)
232Take up from decision 3 (D) to 0 (A)
233At decision 0 (A)
234  There are transitions:
235    down to 3 (D)
236    right to 1 (B)
237Take right from decision 0 (A) to 1 (B)
238At decision 1 (B)
239  There are transitions:
240    down to 2 (C)
241    down_left to 3 (D)
242    left to 0 (A)
243Take down_left from decision 1 (B) to 3 (D)
244At decision 3 (D)
245  There are transitions:
246    right to 2 (C)
247    up to 0 (A)
248    up_right to 1 (B)
249Take right from decision 3 (D) to 2 (C)
250At decision 2 (C)
251  There are transitions:
252    left to 3 (D)
253    up to 1 (B)
254Take up from decision 2 (C) to 1 (B)
255At decision 1 (B)
256  There are transitions:
257    down to 2 (C)
258    down_left to 3 (D)
259    left to 0 (A)
260Take left from decision 1 (B) to 0 (A)
261At decision 0 (A)
262  There are transitions:
263    down to 3 (D)
264    right to 1 (B)
265Take down from decision 0 (A) to 3 (D)
266At decision 3 (D)
267  There are transitions:
268    right to 2 (C)
269    up to 0 (A)
270    up_right to 1 (B)
271Waiting for another action...
272End of the exploration.
273"""

Tests the describeProgress function.

def test_unexploredBranches() -> None:
276def test_unexploredBranches() -> None:
277    """
278    Tests the `unexploredBranches` and related count functions.
279    """
280    ex = journal.convertJournal(JOURNAL)
281    assert analysis.unexploredBranches(ex.getSituation(0).graph) == []
282    g1 = ex.getSituation(1).graph
283    assert g1.destinationsFrom(0) == {
284        'right': 1,
285        'left': 2
286    }
287    assert g1.nameFor(1) == '_u.0'
288    assert g1.nameFor(2) == '_u.1'
289    assert analysis.unexploredBranches(ex.getSituation(1).graph) == [
290        (0, 'right'),
291        (0, 'left'),
292    ]
293    assert analysis.unexploredBranches(ex.getSituation(2).graph) == [
294        (0, 'right'),
295        (2, 'up'),
296    ]
297    assert analysis.unexploredBranches(ex.getSituation(3).graph) == [
298        (0, 'right'),
299        (2, 'up'),
300    ]
301    g4 = ex.getSituation(4).graph
302    assert g4.namesListing(set(g4.nodes)) == """\
303  0 (Start_room::Start)
304  1 (Start_room::platforms)
305  2 (Start_room::coin_room)
306  3 (Start_room::above_coin_room)
307  4 (_u.3)
308  5 (_u.4)
309"""
310    assert analysis.unexploredBranches(ex.getSituation(4).graph) == [
311        (1, 'up'),
312        (1, 'right'),
313        (2, 'up'),
314    ]
315    assert analysis.unexploredBranches(ex.getSituation(5).graph) == [
316        (1, 'right'),
317        (2, 'up'),
318    ]
319    allPerStep = [
320        analysis.unexploredBranchCount(ex, step)
321        for step in range(len(ex))
322    ]
323    traversablePerStep = [
324        analysis.traversableUnexploredCount(ex, step)
325        for step in range(len(ex))
326    ]
327    assert allPerStep == [0, 2, 2, 2, 3, 2]
328    assert traversablePerStep == [0, 2, 1, 1, 2, 1]
329    # TODO

Tests the unexploredBranches and related count functions.

def test_countBranches() -> None:
332def test_countBranches() -> None:
333    """
334    Tests the `countBranches` function.
335    """
336    ex = journal.convertJournal(BABY_JOURNAL)
337    ex2 = journal.convertJournal(BABY_JOURNAL_2)
338
339    # Note: as of v0.6, we can index an exploration to get a Situation,
340    # and most analysis functions want Situations as input
341
342    first = ex[0]
343    second = ex[1]
344    third = ex[2]
345    fourth = ex[3]
346    fifth = ex[4]
347
348    ex2first = ex2[0]
349    ex2second = ex2[1]
350    ex2third = ex2[2]
351
352    assert first.graph.namesListing(set(first.graph.nodes)) == """\
353  0 (Start)
354"""
355    assert analysis.meanBranches(ex, 0) == 0
356    assert analysis.meanBranches(ex, 1) == 2
357    assert analysis.meanBranches(ex, 2) == 1.5
358    assert analysis.meanBranches(ex, 3) == 2
359    assert analysis.meanBranches(ex, 4) == 2.5
360
361    startID = fifth.graph.resolveDecision("Start")
362    nookID = fifth.graph.resolveDecision("left_nook")
363    assert analysis.branches(ex, 1, startID) == 2
364    assert analysis.branches(ex, 2, startID) == 2
365    assert analysis.branches(ex, 2, nookID) == 1
366    assert analysis.branches(ex, 4, startID) == 3
367    assert analysis.branches(ex, 4, nookID) == 2
368
369    assert analysis.meanBranches(ex2, 0) == 0
370    assert analysis.meanBranches(ex2, 1) == 2
371    assert analysis.meanBranches(ex2, 2) == 1.5

Tests the countBranches function.

def test_revisits() -> None:
374def test_revisits() -> None:
375    """
376    Tests the `arrivals`, `revisits`, and related total/mean/median
377    functions.
378    """
379
380    babyExp = journal.convertJournal(BABY_JOURNAL)
381    graph = babyExp.getSituation().graph
382    startID = graph.resolveDecision("Start")
383    nookID = graph.resolveDecision("left_nook")
384    assert analysis.stepsVisited(babyExp) == {
385        startID: [1, 4],
386        nookID: [2, 3]
387    }
388    assert analysis.arrivals(babyExp, startID) == 2
389    assert analysis.revisits(babyExp, startID) == 1
390    assert analysis.arrivals(babyExp, nookID) == 1
391    assert analysis.revisits(babyExp, nookID) == 0
392    assert analysis.arrivals(babyExp, 28309823) == 0
393    assert analysis.revisits(babyExp, 28309823) == 0
394
395    fullExp = journal.convertJournal(JOURNAL)
396    graph = fullExp.getSituation().graph
397    startID = graph.resolveDecision("Start")
398    coinRoomID = graph.resolveDecision("coin_room")
399    platformsID = graph.resolveDecision("platforms")
400    dangerplatID = graph.resolveDecision("dangerplat")
401    assert analysis.revisits(fullExp, startID) == 1
402    assert analysis.revisits(fullExp, coinRoomID) == 0
403    assert analysis.revisits(fullExp, platformsID) == 0
404    assert analysis.revisits(fullExp, dangerplatID) == 0
405
406    revExp = journal.convertJournal(REVISITS_JOURNAL)
407    graph = revExp.getSituation().graph
408    aID = graph.resolveDecision('A')
409    bID = graph.resolveDecision('B')
410    cID = graph.resolveDecision('C')
411    dID = graph.resolveDecision('D')
412    assert analysis.revisits(revExp, aID) == 2
413    assert analysis.revisits(revExp, bID) == 2
414    assert analysis.revisits(revExp, cID) == 1
415    assert analysis.revisits(revExp, dID) == 2
416
417    assert analysis.totalRevisits(babyExp) == 1
418    assert analysis.meanRevisits(babyExp) == 1/5
419    assert analysis.medianRevisits(babyExp) == 0
420    assert analysis.totalRevisits(fullExp) == 1
421    assert analysis.meanRevisits(fullExp) == 1/6
422    assert analysis.medianRevisits(fullExp) == 0
423    assert analysis.totalRevisits(revExp) == 7
424    assert analysis.meanRevisits(revExp) == 7/4
425    assert analysis.medianRevisits(revExp) == 2.0
426
427    blankExp = core.DiscreteExploration()
428    assert analysis.totalRevisits(blankExp) == 0
429    assert analysis.meanRevisits(blankExp) is None
430    assert analysis.medianRevisits(blankExp) is None
431
432    soloExp = core.DiscreteExploration()
433    soloExp.start("start")
434    assert analysis.totalRevisits(soloExp) == 0
435    assert analysis.meanRevisits(soloExp) == 0
436    assert analysis.medianRevisits(soloExp) == 0
437
438    doubleExp = core.DiscreteExploration()
439    doubleExp.start("start")
440    doubleExp.observe("start", "up")
441    doubleExp.explore("up", "above", "down")
442    assert analysis.totalRevisits(doubleExp) == 0
443    assert analysis.meanRevisits(doubleExp) == 0
444    assert analysis.medianRevisits(doubleExp) == 0

Tests the arrivals, revisits, and related total/mean/median functions.

def test_makeFractionCombiner() -> None:
447def test_makeFractionCombiner() -> None:
448    """
449    Tests `makeFractionCombiner`.
450    """
451    c = analysis.makeFractionCombiner(lambda k, x: x > 2, lambda k, x: x < 0)
452    d = {
453        'a': 1,
454        'b': -2,
455        'c': 5,
456        'd': 3,
457        'e': 2.1
458    }
459    assert c(d) == 0.75  # type:ignore
460    c = analysis.makeFractionCombiner(lambda k, x: x < 0, lambda k, x: x > 0)
461    assert c(d) == 1.0  # type:ignore
462    c = analysis.makeFractionCombiner(lambda k, x: x < 0)
463    assert c(d) == 0.2  # type:ignore
464    c = analysis.makeFractionCombiner(lambda k, x: x > 2)
465    assert c(d) == 0.6  # type:ignore
466    c = analysis.makeFractionCombiner(lambda k, x: x % 3 == 7)
467    assert c(d) == 0.0  # type:ignore

Tests makeFractionCombiner.

def test_cycleBasis() -> None:
469def test_cycleBasis() -> None:
470    """
471    Tests `cycleBasis`.
472    """
473    # Test using a custom several-cycle decision graph:
474    g = core.DecisionGraph()
475    g.addDecision('A')  # 0
476    g.addDecision('B')  # 1
477    g.addDecision('C')  # 2
478    g.addDecision('D')  # 3
479    g.addDecision('E')  # 4
480    g.addDecision('F')  # 5
481    g.addDecision('G')  # 6
482    g.addDecision('H')  # 7
483    g.addDecision('I')  # 8
484    assert analysis.cycleBasis(g) == []
485    g.addTransition('A', 'left', 'B', 'right')
486    g.addTransition('B', 'down', 'C', 'upRight')
487    assert analysis.cycleBasis(g) == []
488    g.addTransition('A', 'down', 'C', 'upLeft')  # triangle
489    assert analysis.cycleBasis(g) == [[0, 1, 2]]
490    g.addTransition('C', 'down', 'D', 'up')
491    g.addTransition('D', 'down', 'E', 'up')  # D is in-between
492    assert analysis.cycleBasis(g) == [[0, 1, 2]]
493    g.addTransition('E', 'downLeft', 'F', 'up')
494    g.addTransition('F', 'right', 'G', 'left')
495    g.addTransition('G', 'right', 'H', 'left')
496    assert analysis.cycleBasis(g) == [[0, 1, 2]]
497    g.addTransition('H', 'up', 'E', 'downRight')  # 4-node cycle
498    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
499        frozenset({0, 1, 2}),
500        frozenset({4, 5, 6, 7})
501    }
502    g.addTransition('G', 'down', 'I', 'up')  # branches off
503    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
504        frozenset({0, 1, 2}),
505        frozenset({4, 5, 6, 7})
506    }
507    g.setTransitionRequirement('C', 'down', base.ReqCapability('fly'))
508    g.setTransitionRequirement('E', 'up', base.ReqCapability('fly'))
509    assert {frozenset(c) for c in analysis.cycleBasis(g)} == {
510        frozenset({0, 1, 2}),
511        frozenset({4, 5, 6, 7})
512    }
513
514    # Test using snippet from problematic journal
515    cotmStart = """\
516# Setup
517P softerChecks on
518
519= enemy which common level rare rareLevel [
520  gd enemy:{which}
521  oa farm{which}{common}
522    e { <{level}>luck{ gain {common}*1 }{ } }
523  oa farm{which}{rare}
524    e { <{rareLevel}>luck{ gain {rare}*1 }{ } }
525]
526
527= farm from what [
528  ta farm{from}{what}%s
529]
530
531= cardEnemy which card level otherDrop otherLevel [
532  gd enemy:{which}
533  oa farmCard{which}{card}
534    e { <{level}>luck{ gain {card}; deactivate }{ } }
535  oa farm{which}{otherDrop}
536    e { <{otherLevel}>luck{ gain {otherDrop}*1 }{ } }
537]
538
539= gotCard from which [
540  ta farmCard{from}{which}%s
541]
542
543= subweapon which [
544  oa take{which}
545    e { lose Dagger; lose HolyWater; lose Boomerang; lose Axe; gain {which} }
546]
547
548= getItem what [
549  a get{what}
550    At { gain {what}*1; deactivate }
551]
552
553# Start
554S ConfrontationSite::main
555  zz Catacombs
556  o left
557  o down
558  m door:sealed
559  o right
560    qb door:unsealed
561  @
562    xt down RubbleTowerShaft::shaft up
563      gd suspended
564  @ @
565  a meetDracula
566    gt triggered
567    At follow down
568
569n at: RubbleTowerShaft::shaft
570  o fall bottom flyUp
571    qr ?flight
572  !a fallDown
573    q !?flight
574    gt triggered
575    At follow fall
576  n at: RubbleTowerShaft::bottom
577
578x down RubbleTowerBase::top up
579  > cardEnemy SkeletonBomber Salamander 1 Potion 6
580    n [ Real chances are 20% / 1%; this gives 25% / 0.78% ]
581  > gotCard SkeletonBomber Salamander
582    n [ Literally first enemy I touched; super lucky! ]
583x down mid up
584  > cardEnemy SkeletonBomber Salamander 1 Potion 6
585  o leftCrack
586    q ?airSlide
587x down bottom up
588  > cardEnemy SkeletonBomber Salamander 1 Potion 6
589
590x right CatacombsEntrance::top left
591  > cardEnemy SkeletonBomber Salamander 1 Potion 6
592  o upLeft
593    q ?higherJump
594  o upRight
595    q ?higherJump
596  > enemy Bat WristBand 4 Heart 7
597    n [ Real 2.5% / 0.5%; these are 3.125% / 0.39% ]
598    """
599    exp = journal.convertJournal(cotmStart)
600    assert analysis.cycleBasis(exp.getSituation().graph) == []

Tests cycleBasis.