Repository: marcincichocki/breach-protocol-autosolver
Pull request #293 (2022-10-09 20:51:27)

defer sequences


Patch:
diff --git a/src/common/util.ts b/src/common/util.ts
index 41ff46e..896ce80 100644
--- a/src/common/util.ts
+++ b/src/common/util.ts
@@ -28,22 +28,6 @@ export function uniqueWith<T, R>(fn: (obj: T) => R) {
   };
 }
 
-export function groupBy<T>(data: T[], callback: (item: T) => any) {
-  const group: Record<string, T[]> = {};
-
-  for (const item of data) {
-    const key = callback(item);
-
-    if (!group[key]) {
-      group[key] = [item];
-    } else {
-      group[key].push(item);
-    }
-  }
-
-  return group;
-}
-
 // Simple memo, only use with primitives
 export function memoize<R, T extends (...args: any[]) => R>(fn: T): T {
   const cache = new Map<string, R>();
diff --git a/src/core/bp-registry/sequences.json b/src/core/bp-registry/sequences.json
index 766936d..6db05cd 100644
--- a/src/core/bp-registry/sequences.json
+++ b/src/core/bp-registry/sequences.json
@@ -29,6 +29,7 @@
       "sequences": [
         ["7A", "7A", "BD", "BD"],
         ["7A", "BD", "BD", "7A", "7A"],
+        ["BD", "BD", "7A", "7A"],
         ["7A", "7A"],
         ["7A", "BD", "BD"],
         ["BD", "BD"]
@@ -36,6 +37,7 @@
       "indexes": [
         [0, 1, 2],
         [0, 1, 2],
+        [1, 2],
         [2],
         [0, 1],
         [1]
diff --git a/src/core/common.ts b/src/core/common.ts
index 8b4769e..befae52 100644
--- a/src/core/common.ts
+++ b/src/core/common.ts
@@ -1,4 +1,4 @@
-import { getClosest, uniqueBy } from '@/common';
+import { getClosest } from '@/common';
 import * as daemons from './daemons';
 import {
   BreachProtocolBufferSizeFragmentResult,
@@ -8,7 +8,6 @@ import {
   BreachProtocolTypesFragmentResult,
   FragmentId,
 } from './ocr';
-import { Daemon, Sequence } from './sequence';
 
 export const BUFFER_SIZE_MIN = 4;
 export const BUFFER_SIZE_MAX = 9;
@@ -86,28 +85,6 @@ export function generateSquareMap<T>(
   return new Map<string, T>(squares.map((s, i) => [s, cb(s, i)]));
 }
 
-/**
- * Filter list of {@link Sequence} by buffer length.
- *
- * ```ts
- * [1, 2, 3]
- *   .map(x => new Sequence(x));
- *   .filter(byBufferSize(4))
- * ```
- *
- * @param bufferSize Length of buffer.
- */
-export function byBufferSize(bufferSize: BufferSize) {
-  return (s: Sequence) => s.value.length <= bufferSize;
-}
-
-/**
- * Get unique list of {@link Sequence} or {@link Daemon} by their value.
- */
-export function byUniqueValue() {
-  return uniqueBy<Sequence | Daemon>('tValue');
-}
-
 export interface BreachProtocolRawData {
   [FragmentId.Grid]: GridRawData;
   [FragmentId.Daemons]: DaemonsRawData;
diff --git a/src/core/compare-strategy.ts b/src/core/compare-strategy.ts
deleted file mode 100644
index a9cf4c9..0000000
--- a/src/core/compare-strategy.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { Sequence } from './sequence';
-
-/**
- * Determines value of a {@link Sequence}.
- *
- * Usage:
- *
- * ```ts
- *
- * declare const sequences: Sequence[];
- * declare const strategy: SequenceCompareStrategy;
- *
- * // sequences are sorted according to chosen strategy
- * sequences.sort((s1, s2) => strategy.apply(s1, s2));
- *
- * ```
- */
-export interface SequenceCompareStrategy {
-  /** Compare sequences according to strategy. */
-  apply(s1: Sequence, s2: Sequence): number;
-}
-
-/**
- * Default compare strategy, higher index is better.
- *
- * Indexes are counted from the top to bottom, without daemon solved by
- * "Head Start" perk.
- *
- * Example #1:
- *
- * ```text
- *   1C 1C       <- index: 0
- *   BD BD 1C    <- index: 1
- *   FF BD BD    <- index: 2
- * ```
- *
- * Example #2:
- *
- * ```text
- * ~~BD BD~~     <- index: none, alredy solved by "Head Start" perk
- *   7A 7A BD    <- index: 0
- *   BD 7A 1C    <- index: 1
- *   BD BD       <- index: 2
- *   1C 1C       <- index: 3
- * ```
- *
- * */
-export class IndexSequenceCompareStrategy implements SequenceCompareStrategy {
-  protected byStrength(s1: Sequence, s2: Sequence) {
-    return s2.strength - s1.strength;
-  }
-
-  protected byLength(s1: Sequence, s2: Sequence) {
-    return s1.length - s2.length;
-  }
-
-  apply(s1: Sequence, s2: Sequence) {
-    return this.byStrength(s1, s2) || this.byLength(s1, s2);
-  }
-}
-
-/**
- * Strategy which focuses on a daemon with given index.
- * If it does not exist in a sequence, index strategy is used as a fallback.
- */
-export class FocusDaemonSequenceCompareStrategy
-  extends IndexSequenceCompareStrategy
-  implements SequenceCompareStrategy
-{
-  constructor(private readonly focusedDaemonIndex: number) {
-    super();
-  }
-
-  private hasFocusedDaemon(s: Sequence) {
-    return s.indexes.includes(this.focusedDaemonIndex);
-  }
-
-  private byDaemonIndex(s1: Sequence, s2: Sequence) {
-    const a = this.hasFocusedDaemon(s1);
-    const b = this.hasFocusedDaemon(s2);
-
-    return a === b ? 0 : a ? -1 : 1;
-  }
-
-  override apply(s1: Sequence, s2: Sequence) {
-    return this.byDaemonIndex(s1, s2) || super.apply(s1, s2);
-  }
-}
diff --git a/src/core/index.ts b/src/core/index.ts
index b7c6dc9..d1627b5 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,6 +1,13 @@
 export * from './common';
-export * from './compare-strategy';
 export * from './daemons-i18n';
-export * from './game';
 export * from './ocr';
-export * from './sequence';
+export {
+  BreachProtocol,
+  BreachProtocolOptions,
+  BreachProtocolResultJSON,
+  BreachProtocolStrategy,
+} from './solver/breach-protocol';
+export { FocusHierarchyProvider } from './solver/hierarchy/focus-hierarchy-provider';
+export { HierarchyProvider } from './solver/hierarchy/hierarchy-provider';
+export { IndexHierarchyProvider } from './solver/hierarchy/index-hierarchy-provider';
+export { SequenceJSON } from './solver/sequence';
diff --git a/src/core/sequence.ts b/src/core/sequence.ts
deleted file mode 100644
index b583ab8..0000000
--- a/src/core/sequence.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import {
-  groupBy,
-  memoize,
-  permute,
-  Serializable,
-  uniqueBy,
-  uniqueWith,
-} from '@/common';
-import {
-  BreachProtocolRawData,
-  byBufferSize,
-  byUniqueValue,
-  DaemonRawData,
-  DaemonsRawData,
-  fromHex,
-  HexCode,
-  toHex,
-} from './common';
-import {
-  IndexSequenceCompareStrategy,
-  SequenceCompareStrategy,
-} from './compare-strategy';
-import { FragmentId } from './ocr';
-
-export interface RawSequence {
-  value: string[];
-  parts: string[][];
-}
-
-export class Daemon {
-  readonly tValue = this.value.map(fromHex).join('');
-
-  readonly length = this.value.length;
-
-  get isChild() {
-    return !!this.parent;
-  }
-
-  get isParent() {
-    return !!this.children.length;
-  }
-
-  private parent: Daemon = null;
-
-  private children: Daemon[] = [];
-
-  constructor(
-    public readonly value: DaemonRawData,
-    public readonly index: number,
-    id?: string
-  ) {}
-
-  addChild(child: Daemon) {
-    this.children = [child, ...this.children];
-  }
-
-  setParent(parent: Daemon) {
-    this.parent = parent;
-  }
-
-  getParts() {
-    return this.children.length ? [this, ...this.children] : [this];
-  }
-}
-
-export function findOverlap(s1: string, s2: string) {
-  const l = s1.length;
-  let i = Math.min(s1.length, s2.length);
-
-  while (--i) {
-    const a = s1.substring(l - i, l);
-    const b = s2.substring(0, i);
-
-    if (a === b) {
-      return s1 + s2.substring(i);
-    }
-  }
-
-  return s1 + s2;
-}
-
-export const memoizedFindOverlap = memoize(findOverlap);
-
-// interface will lose its index signature.
-export type SequenceJSON = {
-  value: HexCode[];
-  parts: number[];
-};
-
-export class Sequence implements Serializable {
-  readonly tValue = this.value.map(fromHex).join('');
-
-  readonly length = this.value.length;
-
-  readonly indexes = this.parts.map((d) => d.index);
-
-  /** List of indexes on which it is safe to break sequence. */
-  readonly breaks = this.getSequenceBreaks();
-
-  /** Strength is calculated by index of daemon. */
-  readonly strength = this.parts
-    .map((d) => 2 * d.index + 1)
-    .reduce((a, b) => a + b, 0);
-
-  constructor(public value: HexCode[], public readonly parts: Daemon[]) {}
-
-  toJSON(): SequenceJSON {
-    return {
-      value: this.value,
-      parts: this.parts.map((p) => p.index),
-    };
-  }
-
-  static fromPermutation(permutation: Daemon[]) {
-    let { tValue } = permutation[0];
-
-    for (let i = 1; i < permutation.length; i++) {
-      tValue = memoizedFindOverlap(tValue, permutation[i].tValue);
-    }
-
-    const value = tValue.split('').map(toHex);
-    const parts = permutation
-      .flatMap((d) => d.getParts())
-      .filter(uniqueBy('index'));
-
-    return new Sequence(value, parts);
-  }
-
-  // Sequence break is an index which doesn't have daemon attached.
-  // For example daemons: FF 7A and BD BD can create sequence FF 7A BD BD.
-  // At index 0 and 2 next daemon is not yet started and can be delayed if
-  // buffer allows it.
-  // It's not possible to break sequence on overlap. For example daemons:
-  // FF 7A and 7A BD BD can not be broken on index 2 because they share
-  // beginning and the end.
-  private getSequenceBreaks() {
-    return this.parts
-      .map((d) => {
-        const start = this.tValue.indexOf(d.tValue);
-        const end = start + d.length - 1;
-
-        return [start, end];
-      })
-      .filter(([s1], i, array) => {
-        const prev = array[i - 1];
-        const e2 = prev ? prev[1] : -1;
-
-        return s1 > e2;
-      })
-      .map(([start]) => start);
-  }
-}
-
-function getPermutationId(p: Daemon[]) {
-  return p.map((d) => d.tValue).join();
-}
-
-export function parseDaemons(
-  daemons: DaemonsRawData
-): [regular: Daemon[], children: Daemon[]] {
-  const baseDaemons = daemons.map((d, i) => new Daemon(d, i));
-
-  for (let i = 0; i < baseDaemons.length; i++) {
-    const d1 = baseDaemons[i];
-
-    for (let j = 0; j < baseDaemons.length; j++) {
-      if (i === j) {
-        continue;
-      }
-
-      const d2 = baseDaemons[j];
-
-      // Prevent marking child as a parent.
-      if (d1.tValue.includes(d2.tValue) && !d1.isChild) {
-        d1.addChild(d2);
-        d2.setParent(d1);
-      }
-    }
-  }
-
-  // These sequences are created out of daemons that overlap completely.
-  const childDaemons = baseDaemons.filter((d) => d.isChild);
-  const regularDaemons = baseDaemons.filter((d) => !d.isChild);
-
-  return [regularDaemons, childDaemons];
-}
-
-function getBestSequenceFromGroup(group: Sequence[]) {
-  const strengths = group.map((s) => s.strength);
-  const maxStrength = Math.max(...strengths);
-
-  return group.find((s) => s.strength === maxStrength);
-}
-
-export function generateSequences(
-  { daemons, bufferSize }: Omit<BreachProtocolRawData, FragmentId.Grid>,
-  strategy: SequenceCompareStrategy = new IndexSequenceCompareStrategy()
-) {
-  const [regularDaemons, childDaemons] = parseDaemons(daemons);
-  const childSequences = childDaemons
-    .filter(byUniqueValue())
-    .map((d) => Sequence.fromPermutation([d]));
-  const regularSequences = permute(regularDaemons)
-    .flatMap((p) => p.map((d, i) => p.slice(0, i + 1)))
-    .filter(uniqueWith(getPermutationId))
-    .map((p) => Sequence.fromPermutation(p));
-  const sequences = regularSequences
-    .concat(childSequences)
-    .filter(byBufferSize(bufferSize));
-  const group = groupBy(sequences, (s) => s.tValue);
-
-  return Object.keys(group)
-    .map((k) => getBestSequenceFromGroup(group[k]))
-    .sort((s1, s2) => strategy.apply(s1, s2));
-}
diff --git a/src/core/solver/breach-protocol.spec.ts b/src/core/solver/breach-protocol.spec.ts
new file mode 100644
index 0000000..a913ff6
--- /dev/null
+++ b/src/core/solver/breach-protocol.spec.ts
@@ -0,0 +1,207 @@
+import registry from '../bp-registry/registry.json';
+import data from '../bp-registry/test-data.json';
+import {
+  BreachProtocolRawData,
+  BufferSize,
+  DaemonRawData,
+  DaemonsRawData,
+  GridRawData,
+} from '../common';
+import {
+  BreachProtocol,
+  BreachProtocolOptions,
+  BreachProtocolResult,
+  BreachProtocolStrategy,
+} from './breach-protocol';
+import { Daemon } from './daemon';
+import { IndexHierarchyProvider } from './hierarchy/index-hierarchy-provider';
+import { Sequence } from './sequence';
+
+const registryBreachProtocols = [
+  ...registry.custom,
+  ...registry['1024x768'],
+  ...registry['1920x1080'],
+  ...registry['2560x1440'],
+  ...registry['3440x1440'],
+  ...registry['3840x2160'],
+] as BreachProtocolRawData[];
+
+const testData = (data as BreachProtocolRawData[]).concat(
+  registryBreachProtocols
+);
+
+describe('Breach protocol solve', () => {
+  const hierarchyProvider = new IndexHierarchyProvider();
+  const options: BreachProtocolOptions = { strategy: 'bfs', hierarchyProvider };
+
+  it('should resolve 3 base cases', () => {
+    // prettier-ignore
+    const grid: GridRawData = [
+      '55', '55', '1C', 
+      '55', '1C', 'E9',
+      '55', '55', '1C'
+    ]
+    const bufferSize = 5;
+    const g1 = new BreachProtocol({ grid, bufferSize, daemons: [] }, options);
+    const results = [
+      // case 1) all symbols are accessible from the start.
+      ['55', '55', 'E9'],
+      // case 2) one or more symbols are not in chain, but fallback values
+      // contain starting value of sequence.
+      ['55', 'E9', '1C'],
+      // case 3) no starting value at all in one or more units. Sequence
+      // will start from the start every time that happens until we run
+      // out of buffer.
+      ['E9', '55', '55'],
+    ].map((s: DaemonRawData) =>
+      g1.solveForSequence(new Sequence(s, Daemon.parse([s])))
+    );
+
+    results.forEach((result) => {
+      expect(result.path.length).toBeLessThanOrEqual(bufferSize);
+      expectResolvedSequenceToContainDaemons(result);
+    });
+  });
+
+  it.each(['bfs', 'dfs'] as BreachProtocolStrategy[])(
+    'should find best sequences and solve breach protocols from raw data',
+    (strategy) => {
+      testData.forEach((rawData) => {
+        const game = new BreachProtocol(rawData, {
+          strategy,
+          hierarchyProvider,
+        });
+        const result = game.solve();
+
+        expect(result.path.length).toBeLessThanOrEqual(rawData.bufferSize);
+        expectResolvedSequenceToContainDaemons(result);
+      });
+    }
+  );
+
+  it('should slice path if it contains accidental daemons', () => {
+    // prettier-ignore
+    const grid: GridRawData = [
+      'FF', '1C', 'FF', 'FF',
+      '1C', '55', 'FF', 'FF',
+      '55', 'FF', 'FF', 'FF',
+      'FF', 'FF', 'FF', 'FF',
+    ]
+    const daemons: DaemonsRawData = [
+      ['55', '1C'],
+      ['1C', '55'],
+    ];
+    const bufferSize: BufferSize = 7;
+    const s1 = new Sequence(['55', '1C', '55'], Daemon.parse(daemons));
+    const result = new BreachProtocol(
+      { grid, daemons, bufferSize },
+      options
+    ).solveForSequence(s1);
+
+    expect(result.path.length).toBeLessThan(result.rawPath.length);
+    expect(result.path).not.toEqual(result.rawPath);
+    expectResolvedSequenceToContainDaemons(result);
+  });
+
+  describe('forceful exit', () => {
+    // prettier-ignore
+    const grid: GridRawData = [
+      '55', '1C', '1C', 'E9', '55',
+      'BD', 'BD', 'BD', '1C', '55',
+      '55', '1C', '55', '55', '1C',
+      '55', '55', 'BD', 'E9', 'E9',
+      '55', '1C', '55', '1C', 'E9',
+    ]
+
+    it('should not use force exit when BP exits automatically', () => {
+      const bufferSize = 4;
+      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C']];
+      const s1 = new Sequence(['55', 'BD', 'BD', '1C'], Daemon.parse(daemons));
+      const result = new BreachProtocol(
+        {
+          grid,
+          bufferSize,
+          daemons,
+        },
+        options
+      ).solveForSequence(s1);
+
+      expect(result.exitStrategy).toEqual({
+        willExit: true,
+        shouldForceClose: false,
+      });
+    });
+
+    it('should exit when BP is completed', () => {
+      const bufferSize = 5;
+      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C']];
+      const s1 = new Sequence(['55', 'BD', 'BD', '1C'], Daemon.parse(daemons));
+      const result = new BreachProtocol(
+        {
+          grid,
+          bufferSize,
+          daemons,
+        },
+        options
+      ).solveForSequence(s1);
+
+      expect(result.exitStrategy).toEqual({
+        willExit: false,
+        shouldForceClose: false,
+      });
+    });
+
+    it('should force exit if leftover daemon fit buffer', () => {
+      const bufferSize = 5;
+      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C'], ['7A']];
+      const s1 = new Sequence(
+        ['55', 'BD', 'BD', '1C'],
+        Daemon.parse(daemons).filter(({ index }) => index !== 2)
+      );
+      const result = new BreachProtocol(
+        {
+          grid,
+          bufferSize,
+          daemons,
+        },
+        options
+      ).solveForSequence(s1);
+
+      expect(result.exitStrategy).toEqual({
+        willExit: false,
+        shouldForceClose: true,
+      });
+    });
+
+    it('should force exit if leftover daemon overlap with sequence and fit buffer', () => {
+      const bufferSize = 5;
+      const daemons: DaemonsRawData = [
+        ['55', 'BD', 'BD', '1C'],
+        ['1C', '7A'],
+      ];
+      const s1 = new Sequence(
+        ['55', 'BD', 'BD', '1C'],
+        Daemon.parse(daemons).filter(({ index }) => index !== 1)
+      );
+      const result = new BreachProtocol(
+        {
+          grid,
+          bufferSize,
+          daemons,
+        },
+        options
+      ).solveForSequence(s1);
+
+      expect(result.exitStrategy).toEqual({
+        willExit: false,
+        shouldForceClose: true,
+      });
+    });
+  });
+});
+
+function expectResolvedSequenceToContainDaemons(result: BreachProtocolResult) {
+  result.sequence.parts.forEach((d) => {
+    expect(result.resolvedSequence.tValue).toContain(d.tValue);
+  });
+}
diff --git a/src/core/game.ts b/src/core/solver/breach-protocol.ts
similarity index 88%
rename from src/core/game.ts
rename to src/core/solver/breach-protocol.ts
index 20baaa9..1e7dbbf 100644
--- a/src/core/game.ts
+++ b/src/core/solver/breach-protocol.ts
@@ -10,15 +10,12 @@ import {
   getUnits,
   ROWS,
   toHex,
-} from './common';
-import { SequenceCompareStrategy } from './compare-strategy';
-import {
-  Daemon,
-  generateSequences,
-  memoizedFindOverlap,
-  Sequence,
-  SequenceJSON,
-} from './sequence';
+} from '../common';
+import { Daemon } from './daemon';
+import { HierarchyProvider } from './hierarchy/hierarchy-provider';
+import { memoizedFindOverlap } from './overlap';
+import { Sequence, SequenceJSON } from './sequence';
+import { SequenceFactory } from './sequence-factory';
 
 export type BreachProtocolResultJSON = {
   path: string[];
@@ -61,9 +58,9 @@ export class BreachProtocolResult implements Serializable {
     const willExit = this.path.length === bufferSize;
 
     // Get daemons that were not used in resolved sequence.
-    // There is no point of finding shorthest daemon,
+    // There is no point of finding shortest daemon,
     // since in very rare cases longer daemon could create
-    // better sequence than its shorther peers(bigger overlap).
+    // better sequence than its shorter peers(bigger overlap).
     const shouldForceClose = daemons
       .filter((d, i) => !this.sequence.indexes.includes(i))
       .some((d) => {
@@ -125,7 +122,7 @@ export class BreachProtocolResult implements Serializable {
 
 export interface BreachProtocolOptions {
   strategy: BreachProtocolStrategy;
-  compare?: SequenceCompareStrategy;
+  hierarchyProvider: HierarchyProvider<BreachProtocolRawData>;
 }
 
 export type BreachProtocolStrategy = 'dfs' | 'bfs';
@@ -157,10 +154,9 @@ export class BreachProtocol {
     fromHex(this.rawData.grid[i])
   );
 
-  public readonly sequences = generateSequences(
-    this.rawData,
-    this.options?.compare
-  );
+  private readonly factory = new SequenceFactory(this.rawData, {
+    hierarchy: this.options.hierarchyProvider.provide(this.rawData),
+  });
 
   constructor(
     public readonly rawData: BreachProtocolRawData,
@@ -174,29 +170,22 @@ export class BreachProtocol {
   }
 
   /** Solve grid with every sequence. */
-  solveAll(sequences: Sequence[] = this.sequences) {
-    return sequences.map((s) => this.solveForSequence(s));
+  solveAll() {
+    return Array.from(this.factory.getSequences()).map((s) =>
+      this.solveForSequence(s)
+    );
   }
 
-  /**
-   * Try to solve current grid with provided sequences or
-   * sequences produced from daemons.
-   *
-   * @param sequences List of sequences to try.
-   */
-  solve(sequences: Sequence[] = this.sequences) {
-    if (!sequences.length) {
-      return null;
-    }
-
-    let result: BreachProtocolResult;
-    let i = 0;
+  solve() {
+    for (const sequence of this.factory.getSequences()) {
+      const result = this.solveForSequence(sequence);
 
-    do {
-      result = this.solveForSequence(sequences[i]);
-    } while (!result && ++i < sequences.length);
+      if (result) {
+        return result;
+      }
+    }
 
-    return result;
+    return null;
   }
 
   private getInitialQueue(sequence: string) {
@@ -230,12 +219,12 @@ export class BreachProtocol {
       return tail;
     }
 
-    // Start from the begining without first value if it matches.
+    // Start from the beginning without first value if it matches.
     if (value === tValue[0]) {
       return tValue.slice(1);
     }
 
-    // Worst case, nothing matches, start from the begining.
+    // Worst case, nothing matches, start from the beginning.
     return tValue;
   }
 
diff --git a/src/core/solver/combination-store.ts b/src/core/solver/combination-store.ts
new file mode 100644
index 0000000..b078bb5
--- /dev/null
+++ b/src/core/solver/combination-store.ts
@@ -0,0 +1,53 @@
+/** Stores combinations of arbitrary data in a space efficient manner. */
+export class CombinationStore {
+  private readonly store = this.createStore();
+
+  /** Returns value of combination which includes every element. */
+  get max() {
+    return Array.from(this.store.keys()).reduce((a, b) => a + b, 0);
+  }
+
+  constructor(hierarchy: number[]);
+  constructor(size: number);
+  constructor(private readonly hierarchyOrSize: number | number[]) {}
+
+  /** Returns indexes of combination for given strength. */
+  getCombination(strength: number) {
+    if (strength > this.max || strength < 0) {
+      throw new Error(`There is no combination of strength ${strength}.`);
+    }
+
+    return this.reduceNumberToPowersOfTwo(strength).map((power) =>
+      this.store.get(power)
+    );
+  }
+
+  private createHierarchy() {
+    if (typeof this.hierarchyOrSize === 'number') {
+      return Array.from({ length: this.hierarchyOrSize }, (_, i) => i);
+    }
+
+    return this.hierarchyOrSize;
+  }
+
+  private createStore() {
+    const entires = this.createHierarchy()
+      .map((value, index) => ({ value, index }))
+      .sort(({ value: a }, { value: b }) => a - b)
+      .map(({ index }, k) => [Math.pow(2, k), index] as const);
+
+    return new Map(entires);
+  }
+
+  private reduceNumberToPowersOfTwo(n: number) {
+    const powers = [];
+
+    while (n) {
+      const power = 1 << (31 - Math.clz32(n));
+      powers.unshift(power);
+      n -= power;
+    }
+
+    return powers;
+  }
+}
diff --git a/src/core/solver/daemon.spec.ts b/src/core/solver/daemon.spec.ts
new file mode 100644
index 0000000..88c6916
--- /dev/null
+++ b/src/core/solver/daemon.spec.ts
@@ -0,0 +1,53 @@
+import { Daemon } from './daemon';
+
+describe('Daemon parse', () => {
+  it('should not create any relations', () => {
+    const daemons = Daemon.parse([
+      ['1C', '1C'],
+      ['55', '7A'],
+    ]);
+    const [p1, p2] = daemons.map((d) => d.getParts());
+
+    expect(daemons.every((d) => !d.isChild)).toBeTruthy();
+    expect(daemons.every((d) => !d.isParent)).toBeTruthy();
+    expect(p1.length).toBe(1);
+    expect(p2.length).toBe(1);
+    expect(p1[0]).toEqual(daemons[0]);
+    expect(p2[0]).toEqual(daemons[1]);
+  });
+
+  it('should mark one daemon as a child', () => {
+    const [d1, d2] = Daemon.parse([
+      ['1C', '55', 'BD'],
+      ['55', 'BD'],
+    ]);
+    expect(d1.isParent).toBeTruthy();
+    expect(d1.getParts()[1]).toEqual(d2);
+    expect(d2.isChild).toBeTruthy();
+  });
+
+  it('should mark daemons as children of multiple parents', () => {
+    const [d1, d2, d3, d4, d5] = Daemon.parse([
+      ['55', '55', '55'], // Parent of d2, d5
+      ['55'], // Child of d1, d4, d5
+      ['BD', 'FF'], // Child of d4
+      ['BD', 'FF', '55'], // Parent of d2, d3
+      ['55', '55'], // Child of d1
+    ]);
+
+    expect(d1.isParent).toBe(true);
+    expect(d1.getParts()).toEqual([d1, d2, d5]);
+
+    expect(d2.isChild).toBe(true);
+    expect(d2.isParent).toBe(false);
+
+    expect(d3.isChild).toBe(true);
+    expect(d3.isParent).toBe(false);
+
+    expect(d4.isParent).toBe(true);
+    expect(d4.getParts()).toEqual([d4, d2, d3]);
+
+    expect(d5.isChild).toBe(true);
+    expect(d5.isParent).toBe(false);
+  });
+});
diff --git a/src/core/solver/daemon.ts b/src/core/solver/daemon.ts
new file mode 100644
index 0000000..801f27f
--- /dev/null
+++ b/src/core/solver/daemon.ts
@@ -0,0 +1,68 @@
+import {
+  BreachProtocolRawData,
+  DaemonRawData,
+  DaemonsRawData,
+} from '../common';
+import { HexCodeSequence } from './hex-code-sequence';
+
+export class Daemon extends HexCodeSequence<DaemonRawData> {
+  /** Whether this daemon is a child of another daemon. */
+  get isChild() {
+    return this.parents.size > 0;
+  }
+
+  /** Whether this daemon is a parent of another daemon. */
+  get isParent() {
+    return this.children.size > 0;
+  }
+
+  private readonly parents = new Set<Daemon>();
+  private readonly children = new Set<Daemon>();
+
+  constructor(value: DaemonRawData, public readonly index: number) {
+    super(value);
+  }
+
+  static parse(daemons: DaemonsRawData): Daemon[];
+  static parse(rawData: BreachProtocolRawData): Daemon[];
+  static parse(daemonsOrRawData: DaemonsRawData | BreachProtocolRawData) {
+    const rawData = Array.isArray(daemonsOrRawData)
+      ? daemonsOrRawData
+      : daemonsOrRawData.daemons;
+    const daemons = rawData.map((d, i) => new Daemon(d, i));
+
+    for (const d1 of daemons) {
+      for (const d2 of daemons) {
+        if (d1 === d2) {
+          continue;
+        }
+
+        if (d1.tValue.includes(d2.tValue) && !d1.isChild) {
+          d1.addChild(d2);
+        }
+      }
+    }
+
+    return daemons;
+  }
+
+  /** Checks whether this daemon has given child. */
+  has(child: Daemon) {
+    return this.children.has(child);
+  }
+
+  /** Adds child for this daemon. */
+  addChild(child: Daemon) {
+    if (this.isChild) {
+      throw new Error('Child daemon cannot have children.');
+    }
+
+    child.parents.add(this);
+    this.children.add(child);
+  }
+
+  /** Returns all daemons that are part of this daemon. */
+  getParts() {
+    return this.children.size ? [this, ...this.children] : [this];
+  }
+}
diff --git a/src/core/solver/hex-code-sequence.ts b/src/core/solver/hex-code-sequence.ts
new file mode 100644
index 0000000..f610445
--- /dev/null
+++ b/src/core/solver/hex-code-sequence.ts
@@ -0,0 +1,11 @@
+import { fromHex, HexCode } from '../common';
+
+export abstract class HexCodeSequence<T extends HexCode[]> {
+  /** Transformed value each hex code is a one character. */
+  readonly tValue = this.value.map(fromHex).join('');
+
+  /** Length of hex codes. */
+  readonly length = this.value.length;
+
+  constructor(public readonly value: T) {}
+}
diff --git a/src/core/solver/hierarchy/focus-hierarchy-provider.ts b/src/core/solver/hierarchy/focus-hierarchy-provider.ts
new file mode 100644
index 0000000..120642b
--- /dev/null
+++ b/src/core/solver/hierarchy/focus-hierarchy-provider.ts
@@ -0,0 +1,23 @@
+import { BreachProtocolRawData } from '@/core/common';
+import { HierarchyProvider } from './hierarchy-provider';
+import { IndexHierarchyProvider } from './index-hierarchy-provider';
+
+/** Focuses one daemon, and applies index to others. */
+export class FocusHierarchyProvider
+  extends IndexHierarchyProvider
+  implements HierarchyProvider<BreachProtocolRawData>
+{
+  constructor(private readonly focusedDaemonIndex: number) {
+    super();
+  }
+
+  override provide(rawData: BreachProtocolRawData) {
+    const hierarchy = super.provide(rawData);
+
+    if (this.focusedDaemonIndex in hierarchy) {
+      hierarchy[this.focusedDaemonIndex] = Number.POSITIVE_INFINITY;
+    }
+
+    return hierarchy;
+  }
+}
diff --git a/src/core/solver/hierarchy/hierarchy-provider.ts b/src/core/solver/hierarchy/hierarchy-provider.ts
new file mode 100644
index 0000000..6136a72
--- /dev/null
+++ b/src/core/solver/hierarchy/hierarchy-provider.ts
@@ -0,0 +1,5 @@
+/** Provides order table for some data. */
+export interface HierarchyProvider<T> {
+  /** Returns list of number which indicates which elements will be prioritized(higher is better). */
+  provide(data: T): number[];
+}
diff --git a/src/core/solver/hierarchy/index-hierarchy-provider.ts b/src/core/solver/hierarchy/index-hierarchy-provider.ts
new file mode 100644
index 0000000..f05c274
--- /dev/null
+++ b/src/core/solver/hierarchy/index-hierarchy-provider.ts
@@ -0,0 +1,11 @@
+import { BreachProtocolRawData } from '@/core/common';
+import { HierarchyProvider } from './hierarchy-provider';
+
+/** Marks each daemon with its index. */
+export class IndexHierarchyProvider
+  implements HierarchyProvider<BreachProtocolRawData>
+{
+  provide({ daemons }: BreachProtocolRawData) {
+    return daemons.map((_, index) => index);
+  }
+}
diff --git a/src/core/solver/overlap.spec.ts b/src/core/solver/overlap.spec.ts
new file mode 100644
index 0000000..53f923a
--- /dev/null
+++ b/src/core/solver/overlap.spec.ts
@@ -0,0 +1,18 @@
+import { findOverlap } from './overlap';
+
+describe('String overlap', () => {
+  const entries = [
+    { s1: '121', s2: '345', expected: '121345' }, // 1) no overlap
+    { s1: '222', s2: '2211', expected: '22211' }, // 2) two overlaps in start dir
+    { s1: '2211', s2: '111', expected: '22111' }, // 3) two overlaps in end dir
+    { s1: '123', s2: '345', expected: '12345' }, // 4) standard same start end
+    { s1: '543', s2: '321', expected: '54321' }, // 5) standard same end start
+  ];
+
+  it.each(entries)(
+    'should find correct overlap for $s1 and $s2',
+    ({ s1, s2, expected }) => {
+      expect(findOverlap(s1, s2)).toEqual(expected);
+    }
+  );
+});
diff --git a/src/core/solver/overlap.ts b/src/core/solver/overlap.ts
new file mode 100644
index 0000000..0275054
--- /dev/null
+++ b/src/core/solver/overlap.ts
@@ -0,0 +1,19 @@
+import { memoize } from '@/common';
+
+export function findOverlap(s1: string, s2: string) {
+  const l = s1.length;
+  let i = Math.min(s1.length, s2.length);
+
+  while (--i) {
+    const a = s1.substring(l - i, l);
+    const b = s2.substring(0, i);
+
+    if (a === b) {
+      return s1 + s2.substring(i);
+    }
+  }
+
+  return s1 + s2;
+}
+
+export const memoizedFindOverlap = memoize(findOverlap);
diff --git a/src/core/solver/sequence-factory.spec.ts b/src/core/solver/sequence-factory.spec.ts
new file mode 100644
index 0000000..2098aa0
--- /dev/null
+++ b/src/core/solver/sequence-factory.spec.ts
@@ -0,0 +1,86 @@
+import { BreachProtocolRawData, DaemonsRawData } from '../common';
+import { Sequence } from './sequence';
+import entries from '../bp-registry/sequences.json';
+import { IndexHierarchyProvider } from './hierarchy/index-hierarchy-provider';
+import { SequenceFactory } from './sequence-factory';
+import { FocusHierarchyProvider } from './hierarchy/focus-hierarchy-provider';
+
+interface SequenceEntry {
+  description: string;
+  rawData: BreachProtocolRawData;
+  expected: {
+    indexes: number[][];
+    sequences: DaemonsRawData;
+  };
+}
+
+describe('Sequence factory', () => {
+  describe('Index hierarchy', () => {
+    it.each(entries as SequenceEntry[])(
+      'should work with $description',
+      ({ rawData, expected }) => {
+        const provider = new IndexHierarchyProvider();
+        const factory = new SequenceFactory(rawData, {
+          hierarchy: provider.provide(rawData),
+        });
+
+        const sequences = Array.from(factory.getSequences());
+        expectSequencesToContainDaemons(sequences, expected.indexes);
+        expectSequencesToEqual(sequences, expected.sequences);
+      }
+    );
+  });
+
+  describe('Focus hierarchy', () => {
+    it('should return sequences sorted by selected daemon', () => {
+      const provider = new FocusHierarchyProvider(0);
+      const rawData: BreachProtocolRawData = {
+        grid: [],
+        bufferSize: 5,
+        daemons: [
+          ['1C', '1C'],
+          ['55', '7A', 'BD'],
+          ['1C', '7A', '7A'],
+        ],
+      };
+      const factory = new SequenceFactory(rawData, {
+        hierarchy: provider.provide(rawData),
+      });
+
+      const sequences = Array.from(factory.getSequences());
+      expectSequencesToContainDaemons(sequences, [
+        [0, 2],
+        [0, 2],
+        [0, 1],
+        [0, 1],
+        [0],
+        [2],
+        [1],
+      ]);
+      expectSequencesToEqual(sequences, [
+        ['1C', '1C', '7A', '7A'],
+        ['1C', '7A', '7A', '1C', '1C'],
+        ['55', '7A', 'BD', '1C', '1C'],
+        ['1C', '1C', '55', '7A', 'BD'],
+        ['1C', '1C'],
+        ['1C', '7A', '7A'],
+        ['55', '7A', 'BD'],
+      ]);
+    });
+  });
+});
+
+function expectSequencesToEqual(sequences: Sequence[], values: DaemonsRawData) {
+  sequences.forEach((s, i) => expect(s.value).toEqual(values[i]));
+}
+
+function expectSequencesToContainDaemons(
+  sequences: Sequence[],
+  indexes: number[][]
+) {
+  sequences.forEach((s, i) => expectSequenceToContainDaemons(s, indexes[i]));
+}
+
+function expectSequenceToContainDaemons(sequence: Sequence, indexes: number[]) {
+  expect(sequence.indexes.sort()).toEqual(indexes.sort());
+}
diff --git a/src/core/solver/sequence-factory.ts b/src/core/solver/sequence-factory.ts
new file mode 100644
index 0000000..d62bbf4
--- /dev/null
+++ b/src/core/solver/sequence-factory.ts
@@ -0,0 +1,113 @@
+import { uniqueBy } from '@/common';
+import { BreachProtocolRawData, toHex } from '../common';
+import { CombinationStore } from './combination-store';
+import { Daemon } from './daemon';
+import { memoizedFindOverlap } from './overlap';
+import { Sequence } from './sequence';
+
+interface SequenceFactoryOptions {
+  /** Hierarchy of daemons that will be used to sort sequences. */
+  hierarchy: number[];
+
+  /**
+   * Determines if sequences are emitted immediately, or wait for their
+   * permutation cycle to complete. If that is the case, before emitting they
+   * will be sorted from the shortest sequence to the longest.
+   */
+  immediate?: boolean;
+}
+
+/** Creates sequences lazily from raw data, according to given hierarchy. */
+export class SequenceFactory {
+  private readonly daemons = Daemon.parse(this.rawData.daemons);
+  private readonly registry = new Set<string>();
+  private readonly store = new CombinationStore(this.options.hierarchy);
+
+  constructor(
+    private readonly rawData: BreachProtocolRawData,
+    private readonly options: SequenceFactoryOptions
+  ) {}
+
+  *getSequences() {
+    this.registry.clear();
+
+    let { max } = this.store;
+
+    while (max--) {
+      const sequences: Sequence[] = [];
+      const combination = this.store.getCombination(max + 1);
+      const daemons = combination
+        .map((index) => this.daemons[index])
+        .filter((d1, _, daemons) => {
+          if (d1.isChild) {
+            return !daemons.some((d2) => d2.has(d1));
+          }
+
+          return true;
+        });
+
+      permutations: for (const permutation of this.permutate(daemons)) {
+        let { tValue } = permutation[0];
+
+        for (let i = 1; i < permutation.length; i++) {
+          tValue = memoizedFindOverlap(tValue, permutation[i].tValue);
+
+          if (tValue.length > this.rawData.bufferSize) {
+            continue permutations;
+          }
+        }
+
+        if (this.registry.has(tValue)) {
+          continue;
+        }
+
+        this.registry.add(tValue);
+
+        const value = tValue.split('').map(toHex);
+        const parts = permutation
+          .flatMap((d) => d.getParts())
+          .filter(uniqueBy('index'));
+        const sequence = new Sequence(value, parts);
+
+        if (this.options.immediate) {
+          yield sequence;
+        } else {
+          sequences.push(sequence);
+        }
+      }
+
+      if (!this.options.immediate) {
+        yield* sequences.sort(this.byLength);
+      }
+    }
+  }
+
+  private *permutate<T>(combination: T[]) {
+    const { length } = combination;
+    const c = Array.from<number>({ length }).fill(0);
+    let i = 1;
+    let k;
+    let p;
+
+    yield combination.slice();
+
+    while (i < length) {
+      if (c[i] < i) {
+        k = i % 2 && c[i];
+        p = combination[i];
+        combination[i] = combination[k];
+        combination[k] = p;
+        ++c[i];
+        i = 1;
+
+        yield combination.slice();
+      } else {
+        c[i] = 0;
+        ++i;
+      }
+    }
+  }
+
+  private readonly byLength = (s1: Sequence, s2: Sequence) =>
+    s1.length - s2.length;
+}
diff --git a/src/core/solver/sequence.spec.ts b/src/core/solver/sequence.spec.ts
new file mode 100644
index 0000000..943428e
--- /dev/null
+++ b/src/core/solver/sequence.spec.ts
@@ -0,0 +1,47 @@
+import { DaemonsRawData } from '../common';
+import { Daemon } from './daemon';
+import { Sequence } from './sequence';
+
+describe('Sequence', () => {
+  it('should have breaks between daemons if there is no overlap', () => {
+    const rawData: DaemonsRawData = [
+      ['1C', '1C'],
+      ['55', '7A'],
+    ];
+    const sequence = new Sequence(
+      ['1C', '1C', '55', '7A'],
+      Daemon.parse(rawData)
+    );
+
+    expect(sequence.breaks).toEqual([0, 2]);
+  });
+
+  it('should not have break at overlap', () => {
+    const rawData: DaemonsRawData = [
+      ['1C', '1C'],
+      ['1C', '7A'],
+      ['BD', 'BD'],
+    ];
+    const sequence = new Sequence(
+      ['1C', '1C', '7A', 'BD', 'BD'],
+      Daemon.parse(rawData)
+    );
+
+    expect(sequence.breaks).toEqual([0, 3]);
+  });
+
+  it('should have only one break at the start if there is a full overlap', () => {
+    const rawData: DaemonsRawData = [
+      ['1C', '1C'],
+      ['1C', '1C', '7A'],
+    ];
+    const daemons = Daemon.parse(rawData);
+    const sequence = new Sequence(
+      ['1C', '1C', '7A'],
+      daemons.filter(({ index }) => index === 1)
+    );
+
+    expect(daemons[0].isChild).toBe(true);
+    expect(sequence.breaks).toEqual([0]);
+  });
+});
diff --git a/src/core/solver/sequence.ts b/src/core/solver/sequence.ts
new file mode 100644
index 0000000..50e63a6
--- /dev/null
+++ b/src/core/solver/sequence.ts
@@ -0,0 +1,55 @@
+import { Serializable } from '@/common';
+import { HexCode } from '../common';
+import { Daemon } from './daemon';
+import { HexCodeSequence } from './hex-code-sequence';
+
+// interface will lose its index signature.
+export type SequenceJSON = {
+  value: HexCode[];
+  parts: number[];
+};
+
+export class Sequence
+  extends HexCodeSequence<HexCode[]>
+  implements Serializable
+{
+  readonly indexes = this.parts.map((d) => d.index);
+
+  /** List of indexes on which it is safe to break sequence. */
+  readonly breaks = this.getSequenceBreaks();
+
+  constructor(value: HexCode[], public readonly parts: Daemon[]) {
+    super(value);
+  }
+
+  toJSON(): SequenceJSON {
+    return {
+      value: this.value,
+      parts: this.parts.map((p) => p.index),
+    };
+  }
+
+  // Sequence break is an index which doesn't have daemon attached.
+  // For example daemons: FF 7A and BD BD can create sequence FF 7A BD BD.
+  // At index 0 and 2 next daemon is not yet started and can be delayed if
+  // buffer allows it.
+  // It's not possible to break sequence on overlap. For example daemons:
+  // FF 7A and 7A BD BD can not be broken on index 2 because they share
+  // beginning and the end.
+  private getSequenceBreaks() {
+    return this.parts
+      .map((d) => {
+        const start = this.tValue.indexOf(d.tValue);
+        const end = start + d.length - 1;
+
+        return [start, end];
+      })
+      .filter(([s1], i, array) => {
+        const prev = array[i - 1];
+        const e2 = prev ? prev[1] : -1;
+
+        return s1 > e2;
+      })
+      .map(([start]) => start);
+  }
+}
diff --git a/src/electron/common/common.ts b/src/electron/common/common.ts
index d162917..bc1e86c 100644
--- a/src/electron/common/common.ts
+++ b/src/electron/common/common.ts
@@ -10,7 +10,6 @@ import type {
   BreachProtocolOptions,
   BreachProtocolResultJSON,
   FragmentId,
-  SequenceJSON,
 } from '@/core';
 import type { ScreenshotDisplayOutput } from 'screenshot-desktop';
 import type { SoundPlayerConfig } from '../worker/sound-player';
@@ -45,7 +44,6 @@ export interface HistoryEntry {
   fileName: string;
   settings: AppSettings;
   fragments: BreachProtocolFragmentResults;
-  sequences: SequenceJSON[];
   result: BreachProtocolResultJSON;
 }
 
diff --git a/src/electron/worker/autosolver.ts b/src/electron/worker/autosolver.ts
index 4a55ba4..3e036b3 100644
--- a/src/electron/worker/autosolver.ts
+++ b/src/electron/worker/autosolver.ts
@@ -10,11 +10,11 @@ import { WasmBreachProtocolRecognizer } from '@/common/node/recognizer-wasm';
 import {
   BreachProtocol,
   breachProtocolOCR,
+  BreachProtocolRawData,
   BreachProtocolRecognitionResult,
   BreachProtocolResultJSON,
   BufferSize,
-  FragmentId,
-  SequenceCompareStrategy,
+  HierarchyProvider,
 } from '@/core';
 import {
   AnalysisInput,
@@ -52,13 +52,12 @@ export class BreachProtocolAutosolver {
     private readonly settings: AppSettings,
     private readonly robot: BreachProtocolRobot,
     private readonly player: BreachProtocolSoundPlayer,
-    private readonly compareStrategy: SequenceCompareStrategy
+    private readonly hierarchyProvider: HierarchyProvider<BreachProtocolRawData>
   ) {}
 
   toHistoryEntry(): HistoryEntry {
     return {
       ...this.getBaseState(),
-      ...this.getFragmentsValidState(),
       ...this.getSolutionFoundState(),
     };
   }
@@ -109,7 +108,7 @@ export class BreachProtocolAutosolver {
 
     this.game = new BreachProtocol(rawData, {
       strategy: this.settings.strategy,
-      compare: this.compareStrategy,
+      hierarchyProvider: this.hierarchyProvider,
     });
 
     this.progress.add(BreachProtocolSolveProgress.FragmentsValid);
@@ -208,14 +207,6 @@ export class BreachProtocolAutosolver {
     };
   }
 
-  private getFragmentsValidState() {
-    const bool = this.progress.has(BreachProtocolSolveProgress.FragmentsValid);
-
-    return {
-      sequences: bool ? this.game.sequences.map((s) => s.toJSON()) : null,
-    };
-  }
-
   private getSolutionFoundState() {
     const bool = this.progress.has(BreachProtocolSolveProgress.SolutionFound);
 
diff --git a/src/electron/worker/worker.ts b/src/electron/worker/worker.ts
index e9b8359..d328caf 100644
--- a/src/electron/worker/worker.ts
+++ b/src/electron/worker/worker.ts
@@ -11,12 +11,13 @@ import {
   BreachProtocolDaemonsFragment,
   BreachProtocolFragmentOptions,
   BreachProtocolGridFragment,
+  BreachProtocolRawData,
   BreachProtocolResultJSON,
   BreachProtocolTypesFragment,
-  FocusDaemonSequenceCompareStrategy,
-  IndexSequenceCompareStrategy,
-  SequenceCompareStrategy,
+  FocusHierarchyProvider,
+  IndexHierarchyProvider,
 } from '@/core';
+import { HierarchyProvider } from '@/core/solver/hierarchy/hierarchy-provider';
 import {
   Action,
   ActionTypes,
@@ -196,12 +197,14 @@ export class BreachProtocolWorker {
     event.sender.send('main:async-response', res);
   }
 
-  private getAutosolver(compareStrategy?: SequenceCompareStrategy) {
+  private getAutosolver(
+    hierarchyProvider?: HierarchyProvider<BreachProtocolRawData>
+  ) {
     return new BreachProtocolAutosolver(
       this.settings,
       this.getRobot(),
       this.player,
-      compareStrategy ?? this.getCompareStrategy()
+      hierarchyProvider ?? this.getHierarchyProvider()
     );
   }
 
@@ -230,8 +233,8 @@ export class BreachProtocolWorker {
   }
 
   private async onWorkerSolve(e: IpcRendererEvent, index?: number) {
-    const compareStrategy = this.getCompareStrategy(index);
-    const bpa = this.getAutosolver(compareStrategy);
+    const hierarchyProvider = this.getHierarchyProvider(index);
+    const bpa = this.getAutosolver(hierarchyProvider);
     const entry = await bpa.solve();
 
     if (
@@ -259,12 +262,12 @@ export class BreachProtocolWorker {
     ipc.send('main:focus-renderer');
   }
 
-  private getCompareStrategy(index?: number) {
+  private getHierarchyProvider(index?: number) {
     if (index != null) {
-      return new FocusDaemonSequenceCompareStrategy(index);
+      return new FocusHierarchyProvider(index);
     }
 
-    return new IndexSequenceCompareStrategy();
+    return new IndexHierarchyProvider();
   }
 
   private onStateChanged(


Test patch:
diff --git a/src/common/util.test.ts b/src/common/util.test.ts
index 8d5a68b..a4a647a 100644
--- a/src/common/util.test.ts
+++ b/src/common/util.test.ts
@@ -3,7 +3,6 @@ import {
   capitalize,
   chunk,
   getClosest,
-  groupBy,
   memoize,
   unique,
   uniqueBy,
@@ -91,27 +90,6 @@ describe('utils', () => {
     expect(mask.has(4)).toBe(false);
   });
 
-  it('should group elements by value', () => {
-    const data = [
-      { key: 'bug', value: 99 },
-      { key: 'feature', value: 18 },
-      { key: 'bug', value: 7 },
-      { key: 'feature', value: 42 },
-    ];
-    const group = groupBy(data, (item) => item.key);
-
-    expect(group).toEqual({
-      bug: [
-        { key: 'bug', value: 99 },
-        { key: 'bug', value: 7 },
-      ],
-      feature: [
-        { key: 'feature', value: 18 },
-        { key: 'feature', value: 42 },
-      ],
-    });
-  });
-
   it('should capitalize string', () => {
     expect(capitalize('test')).toBe('Test');
   });
diff --git a/src/core/index.test.ts b/src/core/index.test.ts
index 8aaa25f..8d53a25 100644
--- a/src/core/index.test.ts
+++ b/src/core/index.test.ts
@@ -1,37 +1,10 @@
-import registry from './bp-registry/registry.json';
-import data from './bp-registry/test-data.json';
 import {
-  BreachProtocolRawData,
-  BufferSize,
   cross,
-  DaemonRawData,
-  DaemonsRawData,
   generateSquareMap,
   getRegularGap,
   getShortestGap,
   getUnits,
-  GridRawData,
 } from './common';
-import {
-  BreachProtocol,
-  BreachProtocolOptions,
-  BreachProtocolResult,
-  BreachProtocolStrategy,
-} from './game';
-import { Daemon, parseDaemons, Sequence } from './sequence';
-
-const registryBreachProtocols = [
-  ...registry.custom,
-  ...registry['1024x768'],
-  ...registry['1920x1080'],
-  ...registry['2560x1440'],
-  ...registry['3440x1440'],
-  ...registry['3840x2160'],
-] as BreachProtocolRawData[];
-
-const testData = (data as BreachProtocolRawData[]).concat(
-  registryBreachProtocols
-);
 
 describe('utilities', () => {
   it('should combine 2 strings', () => {
@@ -164,172 +137,3 @@ describe('utilities', () => {
     });
   });
 });
-
-describe('Breach protocol solve', () => {
-  const options: BreachProtocolOptions = { strategy: 'bfs' };
-
-  it('should resolve 3 base cases', () => {
-    // prettier-ignore
-    const grid: GridRawData = [
-      '55', '55', '1C', 
-      '55', '1C', 'E9',
-      '55', '55', '1C'
-    ]
-    const bufferSize = 5;
-    const g1 = new BreachProtocol({ grid, bufferSize, daemons: [] }, options);
-    const results = [
-      // case 1) all symbols are accesible from the start.
-      ['55', '55', 'E9'],
-      // case 2) one or more symbols are not in chain, but fallback values
-      // contain starting value of sequence.
-      ['55', 'E9', '1C'],
-      // case 3) no starting value at all in one or more units. Sequence
-      // will start from the start every time that happens until we run
-      // out of buffer.
-      ['E9', '55', '55'],
-    ].map((s: DaemonRawData) =>
-      g1.solve([new Sequence(s, [new Daemon(s, 0)])])
-    );
-
-    results.forEach((result) => {
-      expect(result.path.length).toBeLessThanOrEqual(bufferSize);
-      expectResolvedSequenceToContainDaemons(result);
-    });
-  });
-
-  it.each(['bfs', 'dfs'] as BreachProtocolStrategy[])(
-    'should find best sequences and solve BPs from raw data',
-    (strategy) => {
-      testData.forEach((rawData) => {
-        const game = new BreachProtocol(rawData, { strategy });
-        const result = game.solve();
-
-        expect(result.path.length).toBeLessThanOrEqual(rawData.bufferSize);
-        expectResolvedSequenceToContainDaemons(result);
-      });
-    }
-  );
-
-  it('should slice path if it contains accidental daemons', () => {
-    // prettier-ignore
-    const grid: GridRawData = [
-      'FF', '1C', 'FF', 'FF',
-      '1C', '55', 'FF', 'FF',
-      '55', 'FF', 'FF', 'FF',
-      'FF', 'FF', 'FF', 'FF',
-    ]
-    const daemons: DaemonsRawData = [
-      ['55', '1C'],
-      ['1C', '55'],
-    ];
-    const bufferSize: BufferSize = 7;
-    const result = new BreachProtocol(
-      { grid, daemons, bufferSize },
-      options
-    ).solve();
-
-    expect(result.path.length).toBeLessThan(result.rawPath.length);
-    expect(result.path).not.toEqual(result.rawPath);
-    expectResolvedSequenceToContainDaemons(result);
-  });
-
-  describe('forceful exit', () => {
-    // prettier-ignore
-    const grid: GridRawData = [
-      '55', '1C', '1C', 'E9', '55',
-      'BD', 'BD', 'BD', '1C', '55',
-      '55', '1C', '55', '55', '1C',
-      '55', '55', 'BD', 'E9', 'E9',
-      '55', '1C', '55', '1C', 'E9',
-    ]
-
-    it('should not use force exit when BP exits automatically', () => {
-      const bufferSize = 4;
-      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C']];
-      const [p1] = parseDaemons(daemons);
-      const s1 = Sequence.fromPermutation(p1);
-      const result = new BreachProtocol(
-        {
-          grid,
-          bufferSize,
-          daemons,
-        },
-        options
-      ).solveForSequence(s1);
-
-      expect(result.exitStrategy).toEqual({
-        willExit: true,
-        shouldForceClose: false,
-      });
-    });
-
-    it('should exit when BP is completed', () => {
-      const bufferSize = 5;
-      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C']];
-      const [p1] = parseDaemons(daemons);
-      const s1 = Sequence.fromPermutation(p1);
-      const result = new BreachProtocol(
-        {
-          grid,
-          bufferSize,
-          daemons,
-        },
-        options
-      ).solveForSequence(s1);
-
-      expect(result.exitStrategy).toEqual({
-        willExit: false,
-        shouldForceClose: false,
-      });
-    });
-
-    it('should force exit if leftover damon fit buffer', () => {
-      const bufferSize = 5;
-      const daemons: DaemonsRawData = [['55', 'BD', 'BD'], ['1C'], ['7A']];
-      const [p1] = parseDaemons(daemons);
-      const s1 = Sequence.fromPermutation(p1.slice(0, 2));
-      const result = new BreachProtocol(
-        {
-          grid,
-          bufferSize,
-          daemons,
-        },
-        options
-      ).solveForSequence(s1);
-
-      expect(result.exitStrategy).toEqual({
-        willExit: false,
-        shouldForceClose: true,
-      });
-    });
-
-    it('should force exit if leftover daemon overlap with sequence and fit buffer', () => {
-      const bufferSize = 5;
-      const daemons: DaemonsRawData = [
-        ['55', 'BD', 'BD', '1C'],
-        ['1C', '7A'],
-      ];
-      const [p1] = parseDaemons(daemons);
-      const s1 = Sequence.fromPermutation(p1.slice(0, 1));
-      const result = new BreachProtocol(
-        {
-          grid,
-          bufferSize,
-          daemons,
-        },
-        options
-      ).solveForSequence(s1);
-
-      expect(result.exitStrategy).toEqual({
-        willExit: false,
-        shouldForceClose: true,
-      });
-    });
-  });
-});
-
-function expectResolvedSequenceToContainDaemons(result: BreachProtocolResult) {
-  result.sequence.parts.forEach((d) => {
-    expect(result.resolvedSequence.tValue).toContain(d.tValue);
-  });
-}
diff --git a/src/core/sequence.test.ts b/src/core/sequence.test.ts
deleted file mode 100644
index 3ce17ab..0000000
--- a/src/core/sequence.test.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-import entries from './bp-registry/sequences.json';
-import { BreachProtocolRawData, DaemonsRawData } from './common';
-import { FocusDaemonSequenceCompareStrategy } from './compare-strategy';
-import { FragmentId } from './ocr';
-import {
-  Daemon,
-  findOverlap,
-  generateSequences,
-  parseDaemons,
-  Sequence,
-} from './sequence';
-
-interface SequenceEntry {
-  description: string;
-  rawData: Omit<BreachProtocolRawData, FragmentId.Grid>;
-  expected: {
-    indexes: number[][];
-    sequences: DaemonsRawData;
-  };
-}
-
-describe('sequences', () => {
-  it('should find correct overlaps', () => {
-    const overlaps = [
-      ['121', '345'], // 1) no overlap
-      ['222', '2211'], // 2) two overlaps in start dir
-      ['2211', '111'], // 3) two overlaps in end dir
-      ['123', '345'], // 4) standard same start end
-      ['543', '321'], // 5) standard same end start
-    ].map((s) => findOverlap(s[0], s[1]));
-
-    expect(overlaps[0]).toEqual('121345'); // 1)
-    expect(overlaps[1]).toEqual('22211'); // 2)
-    expect(overlaps[2]).toEqual('22111'); // 3)
-    expect(overlaps[3]).toEqual('12345'); // 4)
-    expect(overlaps[4]).toEqual('54321'); // 5)
-  });
-
-  it('should parse daemons', () => {
-    // no relations
-    const [d1] = parseDaemons([
-      ['1C', '1C'],
-      ['55', '7A'],
-    ]);
-
-    expect(d1.every((d) => !d.isChild)).toBeTruthy();
-    expect(d1.every((d) => !d.isParent)).toBeTruthy();
-
-    const p1 = d1[0].getParts();
-
-    expect(p1.length).toBe(1);
-    expect(p1[0]).toEqual(d1[0]);
-
-    // simple relation
-    const [d2r, d2c] = parseDaemons([
-      ['1C', '55', 'BD'],
-      ['55', 'BD'],
-    ]);
-
-    expect(d2r[0].isParent).toBeTruthy();
-    expect(d2r[0].getParts()[1]).toEqual(d2c[0]);
-    expect(d2c[0].isChild).toBeTruthy();
-
-    // extreme example
-    const [d3r, d3c] = parseDaemons([
-      ['55', '55', '55'],
-      ['55'],
-      ['BD', 'FF'],
-      ['BD', 'FF', '55'],
-      ['55', '55'],
-    ]);
-
-    expect(d3r.every((d) => d.isParent)).toBeTruthy();
-
-    const p3 = d3r[0].getParts();
-
-    expect(p3[1]).toEqual(d3c[2]);
-    expect(p3[2]).toEqual(d3c[0]);
-    expect(p3.length).toBe(3);
-
-    expect(d3c.every((d) => d.isChild)).toBeTruthy();
-  });
-
-  it('should create correct sequence out of permutation', () => {
-    const [p1] = parseDaemons([['BD', '55']]);
-    const s1 = Sequence.fromPermutation(p1);
-
-    expect(s1.length).toBe(2);
-    expect(s1.value).toEqual(p1[0].value);
-    expect(s1.strength).toBe(1);
-    expect(s1.tValue).toBe(p1[0].tValue);
-
-    const [p2] = parseDaemons([
-      ['BD', 'BD'],
-      ['7A', '55'],
-    ]);
-    const s2 = Sequence.fromPermutation(p2);
-
-    expect(s2.length).toBe(4);
-    expect(s2.value).toEqual(p2.flatMap((d) => d.value));
-    expect(s2.strength).toBe(4);
-    expect(s2.tValue).toBe(p2.map((d) => d.tValue).join(''));
-
-    const [p3] = parseDaemons([
-      ['1C', '1C', '1C'],
-      ['1C', '1C'],
-      ['1C', 'BD'],
-    ]);
-    const s3 = Sequence.fromPermutation(p3);
-
-    expect(s3.length).toBe(4);
-    expect(s3.value).toEqual(['1C', '1C', '1C', 'BD']);
-    expect(s3.strength).toBe(9);
-
-    const [p4] = parseDaemons([
-      ['BD', 'BD'],
-      ['55', 'BD', 'BD'],
-      ['BD', 'BD', 'BD'],
-      ['FF', 'BD', 'BD'],
-    ]);
-
-    const s4 = Sequence.fromPermutation(p4);
-
-    expect(s4.length).toBe(7);
-    expect(s4.parts.length).toBe(4);
-  });
-
-  it('should find breaks in the sequence', () => {
-    // No overlaps
-    const s1 = Sequence.fromPermutation([
-      new Daemon(['FF', '7A'], 0),
-      new Daemon(['BD', '7A'], 1),
-      new Daemon(['1C', '1C'], 2),
-    ]);
-
-    expect(s1.breaks).toEqual([0, 2, 4]);
-
-    // Part overlap
-    const s2 = Sequence.fromPermutation([
-      new Daemon(['FF', '7A'], 0),
-      new Daemon(['7A', '7A'], 1),
-      new Daemon(['1C', '1C'], 2),
-    ]);
-
-    expect(s2.breaks).toEqual([0, 3]);
-
-    // Full overlap
-    const child = new Daemon(['FF', '7A'], 0);
-    const parent = new Daemon(['FF', '7A'], 1);
-
-    parent.addChild(child);
-
-    const s3 = Sequence.fromPermutation([parent]);
-
-    expect(s3.breaks).toEqual([0]);
-  });
-
-  describe('generateSequences', () => {
-    describe('index strategy', () => {
-      it.each(entries as SequenceEntry[])(
-        'should work with $description',
-        ({ rawData, expected }) => {
-          const sequences = generateSequences(rawData);
-
-          expectSequencesToContainDaemons(sequences, expected.indexes);
-          expectSequencesToEqual(sequences, expected.sequences);
-        }
-      );
-    });
-
-    describe('focus daemon strategy', () => {
-      it('should return sequences sorted by selected daemon', () => {
-        const strategy = new FocusDaemonSequenceCompareStrategy(0);
-        const rawData: Omit<BreachProtocolRawData, FragmentId.Grid> = {
-          bufferSize: 5,
-          daemons: [
-            ['1C', '1C'],
-            ['55', '7A', 'BD'],
-            ['1C', '7A', '7A'],
-          ],
-        };
-        const sequences = generateSequences(rawData, strategy);
-
-        expectSequencesToContainDaemons(sequences, [
-          [0, 2],
-          [0, 2],
-          [0, 1],
-          [0, 1],
-          [0],
-          [2],
-          [1],
-        ]);
-        expectSequencesToEqual(sequences, [
-          ['1C', '1C', '7A', '7A'],
-          ['1C', '7A', '7A', '1C', '1C'],
-          ['1C', '1C', '55', '7A', 'BD'],
-          ['55', '7A', 'BD', '1C', '1C'],
-          ['1C', '1C'],
-          ['1C', '7A', '7A'],
-          ['55', '7A', 'BD'],
-        ]);
-      });
-    });
-  });
-});
-
-function expectSequencesToEqual(sequences: Sequence[], values: DaemonsRawData) {
-  sequences.forEach((s, i) => expect(s.value).toEqual(values[i]));
-}
-
-function expectSequencesToContainDaemons(
-  sequences: Sequence[],
-  indexes: number[][]
-) {
-  sequences.forEach((s, i) => expectSequenceToContainDaemons(s, indexes[i]));
-}
-
-function expectSequenceToContainDaemons(sequence: Sequence, indexes: number[]) {
-  expect(sequence.indexes.sort()).toEqual(indexes.sort());
-}