diff --git a/fehnt/core.py b/fehnt/core.py
index 157f216..be7324e 100644
--- a/fehnt/core.py
+++ b/fehnt/core.py
@@ -64,7 +64,7 @@ class OutcomeCalculator:
                 alt_session.prob(self.event_details)
                 / session.prob(self.event_details)
             )
-            self.states[StateStruct(event, alt_session)] += prob * alt_subprob
+            self.push_state(event, alt_session, prob * alt_subprob)
 
             prob *= (1-alt_subprob)
 
@@ -82,15 +82,15 @@ class OutcomeCalculator:
             self.push_outcome(event, prob)
             return
 
-        self.states[StateStruct(event, SessionState(
+        self.push_state(event, SessionState(
             prob_tier=event.dry_streak // SUMMONS_PER_SESSION,
             stone_summons=sf.Series(0, index=tuple(Colors)),
             stone_presences=sf.Series(True, index=tuple(Colors)),
-        ))] += prob
+        ), prob)
 
     def branch_event(self, event, session, prob, stone_choice):
         """Split session into all potential following sub-sessions."""
-        orb_count = event.orb_count - stone_cost(session.stone_summons.sum()-1)
+        orbs_spent = event.orbs_spent + stone_cost(session.stone_summons.sum()-1)
 
         choice_starpool_probs = (self.event_details
                                  .pool_probs(session.prob_tier)
@@ -126,24 +126,34 @@ class OutcomeCalculator:
                          (event.targets_pulled, 1-prob_success))
 
             for targets_pulled, subsubprob in pulls:
-                new_event = EventState(orb_count, dry_streak, targets_pulled)
+                new_event = EventState(orbs_spent, dry_streak, targets_pulled)
+                new_prob = total_prob * subsubprob
 
-                self.states[StateStruct(new_event, session)] += (
-                    total_prob * subsubprob
-                )
+                self.push_state(new_event, session, new_prob)
+
+    def push_state(self, event, session, prob):
+        self.states[StateStruct(event, session)] += prob
+        self.push_outcome(event, prob)
+
+    def pull_outcome(self, event, prob):
+        """Remove a given probabilistic outcome to the recorded results."""
+        result = ResultState(event.orbs_spent, event.targets_pulled)
+        self.outcomes[result] -= prob
+        if self.outcomes[result] == Fraction:
+            del self.outcomes[result]
 
     def push_outcome(self, event, prob):
         """Add a given probabilistic outcome to the recorded results."""
-        result = ResultState(event.orb_count, event.targets_pulled)
+        result = ResultState(event.orbs_spent, event.targets_pulled)
         self.outcomes[result] += prob
 
-    def __call__(self, no_of_orbs):
-        """Calculate the summoning probabilities."""
+    def __iter__(self):
+        """Iterate the summoning probabilities at every orb milestone."""
         self.states = DefaultSortedDict()
-        self.outcomes = defaultdict(Fraction, [])
+        self.outcomes = DefaultSortedDict()
 
         self.init_new_session(
-            EventState(no_of_orbs, 0, 0*self.summoner.targets), Fraction(1)
+            EventState(0, 0, 0*self.summoner.targets), Fraction(1)
         )
 
         def iter_states():
@@ -154,17 +164,23 @@ class OutcomeCalculator:
         for i, ((event, session), prob) in enumerate(iter_states()):
             self.callback("  state no.:", i)
             self.callback("  no. of states in queue:", len(self.states))
-            self.callback('  orbs left:', event.orb_count)
+            self.callback("  no. of outcomes:", len(self.outcomes))
+            self.callback('  orbs spent:', event.orbs_spent)
 
             if session.stone_summons.sum() == SUMMONS_PER_SESSION:
                 self.callback('completed summoning session')
+                self.pull_outcome(event, prob)
                 self.init_new_session(event, prob)
                 continue
 
-            if event.orb_count < stone_cost(session.stone_summons.sum()):
-                self.callback('out of orbs')
-                self.push_outcome(event, prob)
-                continue
+            if self.outcomes and (
+                event.orbs_spent + stone_cost(session.stone_summons.sum())
+                > self.outcomes.peekitem(-1)[0].orbs_spent
+            ):
+                yield (self.outcomes.peekitem(-1)[0].orbs_spent, self.outcomes)
+
+            # Needs to pull *after* yielding outcomes
+            self.pull_outcome(event, prob)
 
             stone_choice_sequence = self.summoner.stone_choice_sequence(
                 event.targets_pulled,
@@ -177,7 +193,7 @@ class OutcomeCalculator:
                 event, session, prob, stone_choice_sequence
             )
 
-        return self.outcomes
+            assert sum(self.outcomes.values()) == 1
 
 
 def condense_results(results):
diff --git a/fehnt/core_utils.py b/fehnt/core_utils.py
index a52860c..18967b8 100644
--- a/fehnt/core_utils.py
+++ b/fehnt/core_utils.py
@@ -5,7 +5,7 @@ import static_frame as sf
 from fehnt.core_defs import Colors, stone_cost, SUMMONS_PER_SESSION
 
 
-EventState = namedtuple('EventState', 'orb_count dry_streak targets_pulled')
+EventState = namedtuple('EventState', 'orbs_spent dry_streak targets_pulled')
 
 
 class SessionState(
@@ -28,7 +28,6 @@ class SessionState(
         ].sum()
 
 
-
 class StateStruct(namedtuple('_', 'event session')):
     """
     Represents a unique state in summoning.
@@ -49,7 +48,7 @@ class StateStruct(namedtuple('_', 'event session')):
             next_stone_cost = stone_cost(self.session.stone_summons.sum())
 
         return (
-            self.event.orb_count - next_stone_cost,
+            -(self.event.orbs_spent + next_stone_cost),
             self.session.stone_presences.sum(),
         )
 
@@ -58,7 +57,7 @@ class StateStruct(namedtuple('_', 'event session')):
         return self._obj_func() < other._obj_func()
 
 
-ResultState = namedtuple('ResultState', 'orb_count targets_pulled')
+ResultState = namedtuple('ResultState', 'orbs_spent targets_pulled')
 
 
 def nCkarray(*k_values):
diff --git a/fehnt/runner.py b/fehnt/runner.py
index fd15112..ec5ece0 100644
--- a/fehnt/runner.py
+++ b/fehnt/runner.py
@@ -32,9 +32,12 @@ def run():
         (StarPools.x5_STAR_FOCUS, Colors.RED, 1),
     )
 
-    outcome_probs = OutcomeCalculator(
+    outcomes = OutcomeCalculator(
         event_details=EventDetails.make_standard(pool_counts),
         summoner=ColorHuntSummoner(target_pool_counts),
-    )(no_of_orbs=10)
+    )
+    for orbs_spent, outcome_probs in outcomes:
+        if orbs_spent == 10:
+            break
     for state, prob in condense_results(outcome_probs):
         print("{}: {:%}".format(state, float(prob)))

reverse orb iteration
Currently the program starts with a known orb count and iterates through states of *remaining orbs* down to zero.

The code can be rewritten to:
- iterate over orbs *spent*; start from 0 and iterate *upwards*
- aggregate results (%-chance of x targets pulled, (for finite targets) avg. orbs spent) on every orbs-spent milestone and yield them 
diff --git a/tests/test_integration.py b/tests/test_integration.py
index c1b6b5d..7d727fe 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -34,31 +34,34 @@ def test_run():
         (StarPools.x5_STAR_FOCUS, Colors.RED, 1),
     )
 
-    outcome_probs = OutcomeCalculator(
+    outcomes = OutcomeCalculator(
         event_details=EventDetails.make_standard(pool_counts),
         summoner=ColorHuntSummoner(target_pool_counts),
-    )(no_of_orbs=10)
+    )
+    for orbs_spent, outcome_probs in outcomes:
+        if orbs_spent == 10:
+            break
 
     assert outcome_probs[5, make_pool_counts(
         (StarPools.x5_STAR_FOCUS, Colors.RED, 1)
     )] == Fraction(53331461890648098721, 2529964800000000000000)
 
-    assert outcome_probs[1, make_pool_counts(
+    assert outcome_probs[9, make_pool_counts(
         (StarPools.x5_STAR_FOCUS, Colors.RED, 1)
     )] == Fraction(1803428037031254581, 158122800000000000000)
 
-    assert outcome_probs[1, make_pool_counts(
+    assert outcome_probs[9, make_pool_counts(
         (StarPools.x5_STAR_FOCUS, Colors.RED, 0)
     )] == Fraction(8492342626380177821929, 19567696500000000000000)
 
-    assert outcome_probs[0, make_pool_counts(
+    assert outcome_probs[10, make_pool_counts(
         (StarPools.x5_STAR_FOCUS, Colors.RED, 1)
     )] == Fraction(
         35632905572024260525584546495453381641543009,
         3168357335173324800000000000000000000000000000
     )
 
-    assert outcome_probs[0, make_pool_counts(
+    assert outcome_probs[10, make_pool_counts(
         (StarPools.x5_STAR_FOCUS, Colors.RED, 0)
     )] == Fraction(
         1654738849167994100960528289363746618358456991,

pip install -e .
parse_log_pytest
numpy>=1.16.0 sortedcontainers
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup==1.2.2
-e git+https://github.com/CrepeGoat/FEHnt.git@8b5692806a6ef0af0a4c90856ca42324f52d3f06#egg=Fehnt
iniconfig==2.1.0
numpy @ file:///croot/numpy_and_numpy_base_1736283260865/work/dist/numpy-2.0.2-cp39-cp39-linux_x86_64.whl#sha256=3387e3e62932fa288bc18e8f445ce19e998b418a65ed2064dd40a054f976a6c7
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
sortedcontainers @ file:///tmp/build/80754af9/sortedcontainers_1623949099177/work
tomli==2.2.1

name: FEHnt
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - blas=1.0=openblas
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgfortran-ng=11.2.0=h00389a5_1
  - libgfortran5=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libopenblas=0.3.21=h043d6bf_0
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - numpy=2.0.2=py39heeff2f4_0
  - numpy-base=2.0.2=py39h8a23956_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=72.1.0=py39h06a4308_0
  - sortedcontainers=2.4.0=pyhd3eb1b0_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/FEHnt

tests/test_integration.py::test_run
fehnt/runner.py fehnt/core.py fehnt/core_utils.py
fehnt/runner.py fehnt/core.py fehnt/core_utils.py