type GoldAdvantageTag = 'ahead' | 'behind' | 'even' type PlatformCounts = { euw: number eun: number na: number kr: number } type ItemTree = { data: number | undefined count: number children: Array // Gold advantage tracking boughtWhen: { aheadCount: number behindCount: number evenCount: number meanGold: number } // Platform tracking platformCount: PlatformCounts } function initPlatformCounts(): PlatformCounts { return { euw: 0, eun: 0, na: 0, kr: 0 } } function treeInit(): ItemTree { return { data: undefined, count: 0, children: [], boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }, platformCount: initPlatformCounts() } } /* * Merge a node with an item tree */ function nodeMerge(itemtree: ItemTree, node: ItemTree) { const item = node.data const count = node.count let next: ItemTree | null = null // Try to find an existing node in this tree level with same item for (const child of itemtree.children) { if (child.data == item) { child.count += 1 child.boughtWhen.aheadCount += node.boughtWhen.aheadCount child.boughtWhen.evenCount += node.boughtWhen.evenCount child.boughtWhen.behindCount += node.boughtWhen.behindCount // Merge platform counts child.platformCount.euw += node.platformCount.euw child.platformCount.eun += node.platformCount.eun child.platformCount.na += node.platformCount.na child.platformCount.kr += node.platformCount.kr next = child break } } // If not found, add item node at this level if (next == null && item !== undefined) { next = { data: item, count: count, children: [], boughtWhen: { ...node.boughtWhen }, platformCount: { ...node.platformCount } } itemtree.children.push(next) } return next! } /* * Merge a full build path with an existing item tree */ function treeMerge( itemtree: ItemTree, items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }> ) { let current = itemtree for (const item of items) { const platformKey = item.platform ? item.platform.toLowerCase() : null current = nodeMerge(current, { data: item.itemId, count: 1, boughtWhen: { aheadCount: item.goldAdvantage == 'ahead' ? 1 : 0, evenCount: item.goldAdvantage == 'even' ? 1 : 0, behindCount: item.goldAdvantage == 'behind' ? 1 : 0, meanGold: 0 }, children: [], platformCount: { euw: platformKey === 'euw1' ? 1 : 0, eun: platformKey === 'eun1' ? 1 : 0, na: platformKey === 'na1' ? 1 : 0, kr: platformKey === 'kr' ? 1 : 0 } }) } } function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPerc: number) { // Remove branches that are above threshold count while (itemtree.children.length > thresholdCount) { const leastUsedBranch = itemtree.children.reduce( (a, b) => (Math.min(a.count, b.count) == a.count ? a : b), { data: undefined, count: +Infinity, children: [], boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }, platformCount: initPlatformCounts() } ) itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1) } // Remove branches that are of too low usage const toRemove: Array = [] for (const child of itemtree.children) { if (child.count / itemtree.count < thresholdPerc) { toRemove.push(child) } } for (const tr of toRemove) { itemtree.children.splice(itemtree.children.indexOf(tr), 1) } itemtree.children.map(x => treeCutBranches(x, thresholdCount, thresholdPerc)) } function treeSort(itemtree: ItemTree) { itemtree.children.sort((a, b) => b.count - a.count) for (const item of itemtree.children) { treeSort(item) } } /* * Deep clone an ItemTree */ function treeClone(tree: ItemTree): ItemTree { return { data: tree.data, count: tree.count, children: tree.children.map(child => treeClone(child)), boughtWhen: { aheadCount: tree.boughtWhen.aheadCount, behindCount: tree.boughtWhen.behindCount, evenCount: tree.boughtWhen.evenCount, meanGold: tree.boughtWhen.meanGold }, platformCount: { euw: tree.platformCount.euw, eun: tree.platformCount.eun, na: tree.platformCount.na, kr: tree.platformCount.kr } } } /* * Merge two ItemTrees into one */ function treeMergeTree(t1: ItemTree, t2: ItemTree): ItemTree { // Merge counts for the root t1.count += t2.count // Merge platform counts t1.platformCount.euw += t2.platformCount.euw t1.platformCount.eun += t2.platformCount.eun t1.platformCount.na += t2.platformCount.na t1.platformCount.kr += t2.platformCount.kr // Merge boughtWhen t1.boughtWhen.aheadCount += t2.boughtWhen.aheadCount t1.boughtWhen.evenCount += t2.boughtWhen.evenCount t1.boughtWhen.behindCount += t2.boughtWhen.behindCount // Merge children from t2 into t1 for (const child2 of t2.children) { // Find matching child in t1 (same data value) const matchingChild = t1.children.find(child1 => child1.data === child2.data) if (matchingChild) { // Recursively merge matching children treeMergeTree(matchingChild, child2) } else { // Add a deep copy of child2 to t1 t1.children.push(treeClone(child2)) } } return t1 } /* * Flatten an ItemTree into a Set of item numbers */ function treeToSet(itemtree: ItemTree): Set { const items: Set = new Set() function traverse(node: ItemTree) { if (node.data !== undefined) { items.add(node.data) } for (const child of node.children) { traverse(child) } } traverse(itemtree) return items } /* * Calculate similarity between two trees as item sets. * Returns a number between 0 and 1, where 1 means identical and 0 means completely different. * Uses Jaccard similarity: |A ∩ B| / |A ∪ B| * Sets included in one another will have similarity close to 1. */ function areTreeSimilars(t1: ItemTree, t2: ItemTree): number { const set1 = treeToSet(t1) const set2 = treeToSet(t2) // Handle empty sets if (set1.size === 0 && set2.size === 0) { return 1.0 } // Calculate intersection const intersection = new Set() for (const item of Array.from(set1)) { if (set2.has(item)) { intersection.add(item) } } // Calculate union const union = new Set() for (const item of Array.from(set1)) { union.add(item) } for (const item of Array.from(set2)) { union.add(item) } // Jaccard similarity: |intersection| / |union| const similarity = intersection.size / Math.min(set1.size, set2.size) // Ensure result is between 0 and 1 return Math.max(0, Math.min(1, similarity)) } export { ItemTree, PlatformCounts, GoldAdvantageTag, treeMerge, treeInit, treeCutBranches, treeSort, treeMergeTree, areTreeSimilars }