class_name SimulatorDirect extends Control # DIRECT SIMULATOR - Uses real TickProcess with isolated state # Zero code duplication via dependency injection # Load the actual game resources var unlock_collection: UnlockDataCollection = load("res://resources/UnlockData.tres") # Results tracking var all_results: Array[Dictionary] = [] var results_mutex: Mutex = Mutex.new() # Chunk-based caching for intermediate simulation states var simulation_cache: Dictionary = {} var cache_mutex: Mutex = Mutex.new() # Global unlock template cache (build once, clone per simulation) # Pre-calculated lookup tables (built once from UnlockDataResource, then used statically) var precalc_costs: Dictionary = {} # {unlock_id: [cost_at_rank_0, cost_at_rank_1, ...]} var precalc_modifiers: Dictionary = {} # {unlock_id: {rank: {"modifier_name": value}}} var precalc_base_modifiers: Dictionary = {} # {unlock_id: base_modifiers_dict} var precalc_max_ranks: Dictionary = {} # {unlock_id: max_rank} var precalc_is_scaling: Dictionary = {} # {unlock_id: is_scaling} var precalc_unlock_names: Dictionary = {} # {unlock_id: name} var precalc_initialized: bool = false var precalc_mutex: Mutex = Mutex.new() # Manual thread pool var num_threads: int = 14 var threads: Array[Thread] = [] var task_queue: Array[Dictionary] = [] var queue_mutex: Mutex = Mutex.new() var completed_count: int = 0 var completed_mutex: Mutex = Mutex.new() var threads_done: bool = false var start_time: int = 0 var total_combinations: int = 0 var last_progress_time: int = 0 var monitoring_active: bool = false var cache_hits: int = 0 var cache_misses: int = 0 # UI References @onready var status_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/StatusLabel @onready var progress_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/ProgressLabel @onready var progress_bar = $MarginContainer/VBoxContainer/StatusPanel/VBox/ProgressBar @onready var rate_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/RateLabel @onready var eta_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/ETALabel @onready var cache_hits_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheHitsLabel @onready var cache_misses_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheMissesLabel @onready var cache_rate_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheRateLabel @onready var cache_size_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheSizeLabel @onready var results_label = $MarginContainer/VBoxContainer/ResultsPanel/VBox/ScrollContainer/ResultsLabel func _ready(): GameManager.tick.stop() print("=== DIRECT Unlock Simulator Started ===") print("Using real TickProcess with isolated state via dependency injection") var cpu_count = OS.get_processor_count() print("CPU cores detected: %d" % cpu_count) print("Creating %d worker threads" % num_threads) # Update UI status_label.text = "Status: Starting direct simulation..." results_label.text = "[b]DIRECT Unlock Simulator Started[/b]\n\nCPU cores: %d\nWorker threads: %d\n\nGenerating combinations..." % [cpu_count, num_threads] run_comprehensive_test() func _process(_delta): if monitoring_active: var current_time = Time.get_ticks_msec() if current_time - last_progress_time >= 1000: last_progress_time = current_time update_progress() # ============================================================================= # ISOLATED GAME STATE CLASS # ============================================================================= class IsolatedGameState: """Isolated game state using pre-calculated lookup tables (NO object creation!)""" # Reference to pre-calculated tables (shared, read-only) var costs: Dictionary # {unlock_id: [cost_array]} var modifiers: Dictionary # {unlock_id: {rank: mods_dict}} var max_ranks: Dictionary # {unlock_id: max_rank} var is_scaling: Dictionary # {unlock_id: bool} var base_mods: Dictionary # {unlock_id: base_modifiers} # Local inventory state (not Inventory singleton) var currency: float = 0.0 var wood: float = 0.0 var stock: float = 0.0 # Cached modifiers (not Unlocks.current_modifiers) var sale_price_mod: float = 1.0 var efficiency_mod: float = 1.0 var wood_per_click_mod: float = 1.0 var purchase_rate_mod: float = 1.0 var autowood_mod: float = 0.0 var multicraft_rank: int = 0 var wholesale_unlocked: bool = false # Simulation state var ticks: int = 0 var current_ranks: Dictionary = {} # Cache FakeUnlock objects to avoid repeated creation (OPTIMIZATION) var fake_unlock_cache: Dictionary = {} # {unlock_id: FakeUnlock} func _init(precalc_costs: Dictionary, precalc_modifiers: Dictionary, precalc_max_ranks: Dictionary, precalc_is_scaling: Dictionary, precalc_base_mods: Dictionary): """Initialize with references to pre-calculated lookup tables""" costs = precalc_costs modifiers = precalc_modifiers max_ranks = precalc_max_ranks is_scaling = precalc_is_scaling base_mods = precalc_base_mods # ============================================================================= # UNLOCKS SINGLETON INTERFACE REPLACEMENT # ============================================================================= func get_unlock_by_id(unlock_id: int): """Replacement for Unlocks.get_unlock_by_id() - returns cached fake unlock""" if not current_ranks.has(unlock_id): return null # Reuse cached FakeUnlock object (just update its state) var fake = fake_unlock_cache.get(unlock_id) if fake == null: fake = FakeUnlock.new() fake.unlock_id = unlock_id fake.state = self fake_unlock_cache[unlock_id] = fake # Update state from current_ranks fake.current_rank = current_ranks.get(unlock_id, 0) fake.is_unlocked = fake.current_rank > 0 return fake # Nested class for fake unlock objects (uses parent's lookup tables) class FakeUnlock: var unlock_id: int var current_rank: int var is_unlocked: bool var state: IsolatedGameState func get_next_cost() -> int: var cost_array = state.costs.get(unlock_id, []) if current_rank < cost_array.size(): return cost_array[current_rank] return 999999999 func get_current_modifiers() -> Dictionary: if not is_unlocked or current_rank == 0: return {} var rank_mods = state.modifiers.get(unlock_id, {}) return rank_mods.get(current_rank, {}) func get_modifiers_at_rank(rank: int) -> Dictionary: """Get modifiers at a specific rank (for calculating deltas)""" if rank == 0: return {} var rank_mods = state.modifiers.get(unlock_id, {}) return rank_mods.get(rank, {}) func can_rank_up() -> bool: var max_rank = state.max_ranks.get(unlock_id, -1) if max_rank > 0 and current_rank >= max_rank: return false return true func unlock() -> bool: if not can_rank_up(): return false current_rank += 1 is_unlocked = true state.current_ranks[unlock_id] = current_rank return true func get_modifier_value(modifier_key: String) -> float: """Replacement for Unlocks.get_modifier_value()""" match modifier_key: "sale_price_modifier": return sale_price_mod "efficiency_modifier": return efficiency_mod "wood_per_click_modifier": return wood_per_click_mod "purchase_rate_modifier": return purchase_rate_mod "autowood_modifier": return autowood_mod _: return 1.0 func get_wood_per_click() -> float: """Replacement for Unlocks.get_wood_per_click()""" return Global.wood_per_click * wood_per_click_mod func get_items_produced_per_tick() -> float: """Replacement for Unlocks.get_items_produced_per_tick()""" return Global.cost_per_whittle * efficiency_mod func get_sale_price_per_item() -> float: """Replacement for Unlocks.get_sale_price_per_item()""" return Global.base_sale_price * sale_price_mod # ============================================================================= # INVENTORY SINGLETON INTERFACE REPLACEMENT # ============================================================================= func add_wood(amount: float): """Replacement for Inventory.add_wood()""" wood += amount func get_wood() -> float: """Replacement for Inventory.get_wood()""" return wood func spend_wood(amount: float) -> bool: """Replacement for Inventory.spend_wood()""" if wood >= amount: wood -= amount return true return false func add_stock(amount: float): """Replacement for Inventory.add_stock()""" stock += amount func get_stock() -> float: """Replacement for Inventory.get_stock()""" return stock func spend_stock(amount: float) -> bool: """Replacement for Inventory.spend_stock()""" if stock >= amount: stock -= amount return true return false func add_currency(amount: float): """Replacement for Inventory.add_currency()""" currency += amount func spend_currency(amount: float) -> bool: """Replacement for Inventory.spend_currency()""" if currency >= amount: currency -= amount return true return false # ============================================================================= # UNLOCK PURCHASE AND MODIFIER MANAGEMENT # ============================================================================= func purchase_unlock(unlock_id: int) -> bool: """Purchase an unlock and update modifiers""" var unlock = get_unlock_by_id(unlock_id) if not unlock or not unlock.can_rank_up(): return false var cost = unlock.get_next_cost() if not spend_currency(cost): return false var prev_rank = unlock.current_rank unlock.unlock() current_ranks[unlock_id] = unlock.current_rank # Update modifier cache update_modifiers_for_unlock(unlock, prev_rank) # Update special flags if unlock_id == Global.wholesale_unlock_id: wholesale_unlocked = true if unlock_id == Global.multicraft_unlock_id: multicraft_rank = unlock.current_rank if unlock_id == Global.autowood_unlock_id: # Autowood is additive, not multiplicative autowood_mod = calculate_autowood_modifier() return true func update_modifiers_for_unlock(unlock, prev_rank: int): """Update cached modifiers when unlock rank changes""" var old_mods = unlock.get_modifiers_at_rank(prev_rank) var new_mods = unlock.get_current_modifiers() # Calculate ratio for multiplicative modifiers for key in new_mods.keys(): var old_val = old_mods.get(key, 1.0) var new_val = new_mods[key] var ratio = new_val / old_val if old_val != 0 else new_val match key: "sale_price_modifier": sale_price_mod *= ratio "efficiency_modifier": efficiency_mod *= ratio "wood_per_click_modifier": wood_per_click_mod *= ratio "purchase_rate_modifier": purchase_rate_mod *= ratio func calculate_autowood_modifier() -> float: """Calculate autowood modifier from scratch (additive)""" var total = 0.0 for unlock_id in current_ranks.keys(): var rank = current_ranks[unlock_id] if rank > 0: var rank_mods = modifiers.get(unlock_id, {}) var mods = rank_mods.get(rank, {}) if mods.has("autowood_modifier"): total += mods["autowood_modifier"] return total # ============================================================================= # INLINED TICK LOGIC (optimized for performance, matches TickProcess exactly) # ============================================================================= func execute_tick(): """Execute one game tick - INLINED for performance (no TickProcess overhead)""" # 1. Generate wood from autowood if autowood_mod > 0.0: var wood_to_gather = max(get_wood_per_click() * autowood_mod, 1.0) wood += wood_to_gather # 2. Whittle wood into stock if wood >= 1: # Base whittling action var items_produced_per_tick = get_items_produced_per_tick() var wood_needed = ceil(items_produced_per_tick) var wood_to_whittle = min(wood, wood_needed) var items_produced = wood_to_whittle wood -= wood_to_whittle stock += items_produced # Multicraft additional whittles for i in range(multicraft_rank): if wood >= 1: wood_needed = ceil(items_produced_per_tick) wood_to_whittle = min(wood, wood_needed) items_produced = wood_to_whittle wood -= wood_to_whittle stock += items_produced else: break # 3. Sell stock for currency if stock > 0: var price_per_item = get_sale_price_per_item() # 3a. Wholesale selling (if unlocked) if wholesale_unlocked: while stock >= Global.wholesale_bundle_size: stock -= Global.wholesale_bundle_size currency += Global.wholesale_bundle_size * price_per_item * Global.wholesale_discount_multiplier # 3b. Regular selling if stock > 0: var purchase_rate = Global.base_purchase_rate * purchase_rate_mod var max_stock_to_sell = floor(purchase_rate) var actual_stock_to_sell = min(stock, max(1.0, max_stock_to_sell)) stock -= actual_stock_to_sell currency += actual_stock_to_sell * price_per_item ticks += 1 # ============================================================================= # RESET FOR REUSE # ============================================================================= func reset_for_new_simulation(): """Reset state for a new simulation (OPTIMIZATION: reuse same IsolatedGameState)""" # Reset inventory currency = 0.0 wood = 0.0 stock = 0.0 # Reset modifiers sale_price_mod = 1.0 efficiency_mod = 1.0 wood_per_click_mod = 1.0 purchase_rate_mod = 1.0 autowood_mod = 0.0 multicraft_rank = 0 wholesale_unlocked = false # Reset simulation state ticks = 0 current_ranks.clear() # ============================================================================= # SNAPSHOT AND RESTORE FOR CACHING # ============================================================================= func snapshot_for_cache() -> Dictionary: """Create a snapshot for cache storage""" return { "ticks": ticks, "currency": currency, "stock": stock, "wood": wood, "current_ranks": current_ranks.duplicate(), "modifiers": { "sale_price_mod": sale_price_mod, "efficiency_mod": efficiency_mod, "wood_per_click_mod": wood_per_click_mod, "purchase_rate_mod": purchase_rate_mod, "autowood_mod": autowood_mod, "multicraft_rank": multicraft_rank, "wholesale_unlocked": wholesale_unlocked } } func restore_from_cache(snapshot: Dictionary): """Restore state from a cache snapshot""" ticks = snapshot.ticks currency = snapshot.currency stock = snapshot.stock wood = snapshot.wood # Restore modifiers var mods = snapshot.modifiers sale_price_mod = mods.sale_price_mod efficiency_mod = mods.efficiency_mod wood_per_click_mod = mods.wood_per_click_mod purchase_rate_mod = mods.purchase_rate_mod autowood_mod = mods.autowood_mod multicraft_rank = mods.multicraft_rank wholesale_unlocked = mods.wholesale_unlocked # Restore unlock ranks current_ranks = snapshot.current_ranks.duplicate() for unlock_id in current_ranks.keys(): var unlock = get_unlock_by_id(unlock_id) if unlock: var target_rank = current_ranks[unlock_id] unlock.current_rank = target_rank unlock.is_unlocked = target_rank > 0 # ============================================================================= # TEMPLATE MANAGEMENT # ============================================================================= func build_precalculated_tables(): """Pre-calculate all costs and modifiers from real UnlockDataResource objects""" precalc_mutex.lock() if precalc_initialized: precalc_mutex.unlock() return print("Pre-calculating unlock tables from UnlockDataResource...") var start_time = Time.get_ticks_msec() for unlock in unlock_collection.unlocks: var unlock_id = unlock.unlock_id precalc_unlock_names[unlock_id] = unlock.unlock_name precalc_is_scaling[unlock_id] = unlock.is_scaling precalc_max_ranks[unlock_id] = unlock.max_rank if unlock.is_scaling else 1 precalc_base_modifiers[unlock_id] = unlock.base_modifiers.duplicate(true) # Calculate cost ladder for all possible ranks var cost_array = [] var max_rank_to_calc = unlock.max_rank if (unlock.is_scaling and unlock.max_rank > 0) else (100 if unlock.is_scaling else 1) for rank in range(max_rank_to_calc + 1): unlock.current_rank = rank cost_array.append(unlock.get_next_cost()) precalc_costs[unlock_id] = cost_array # Calculate modifier values for all possible ranks var mods_by_rank = {} for rank in range(max_rank_to_calc + 1): unlock.current_rank = rank unlock.is_unlocked = rank > 0 mods_by_rank[rank] = unlock.get_current_modifiers() precalc_modifiers[unlock_id] = mods_by_rank # Reset unlock state unlock.current_rank = 0 unlock.is_unlocked = false precalc_initialized = true var elapsed = Time.get_ticks_msec() - start_time print("Pre-calculation complete in %d ms for %d unlocks" % [elapsed, precalc_costs.size()]) precalc_mutex.unlock() func create_isolated_state() -> IsolatedGameState: """Create a new isolated game state using pre-calculated tables""" if not precalc_initialized: build_precalculated_tables() return IsolatedGameState.new(precalc_costs, precalc_modifiers, precalc_max_ranks, precalc_is_scaling, precalc_base_modifiers) # ============================================================================= # CACHE SYSTEM (same as sim_cached.gd) # ============================================================================= func get_cache_key(current_ranks: Dictionary) -> String: """Generate a cache key from current unlock ranks""" var sorted_keys = current_ranks.keys() sorted_keys.sort() var key_parts = [] for k in sorted_keys: key_parts.append(str(k) + ":" + str(current_ranks[k])) return ",".join(key_parts) func try_load_best_prefix_from_cache(rank_targets: Dictionary) -> Variant: """Balanced cache lookup - fast with good coverage""" cache_mutex.lock() # Try exact match first var full_key = get_cache_key(rank_targets) if simulation_cache.has(full_key): cache_hits += 1 var result = simulation_cache[full_key] cache_mutex.unlock() return result # Sort unlock IDs for consistent ordering var unlock_ids = rank_targets.keys() unlock_ids.sort() var num_unlocks = unlock_ids.size() var best_match = null var best_rank_sum = 0 # Try progressively shorter prefixes for prefix_len in range(num_unlocks - 1, 0, -1): var subset = {} for i in range(prefix_len): subset[unlock_ids[i]] = rank_targets[unlock_ids[i]] var key = get_cache_key(subset) if simulation_cache.has(key): var cached_entry = simulation_cache[key] var rank_sum = 0 for r in cached_entry.current_ranks.values(): rank_sum += r if rank_sum > best_rank_sum: best_match = cached_entry best_rank_sum = rank_sum if prefix_len >= num_unlocks - 2: break if best_match != null: cache_hits += 1 else: cache_misses += 1 cache_mutex.unlock() return best_match func should_cache_state(current_ranks: Dictionary, targets_remaining: int) -> bool: """Decide if this state is worth caching""" if targets_remaining == 0: return false var total_ranks = 0 var active_unlocks = 0 for rank in current_ranks.values(): if rank > 0: total_ranks += rank active_unlocks += 1 return (active_unlocks >= 2) or (total_ranks >= 2) # ============================================================================= # MAIN SIMULATION FUNCTION # ============================================================================= func simulate_rank_combination_direct( rank_targets: Dictionary, max_ticks: int, track_purchases: bool = false, _unused_tick_process = null, # Kept for API compatibility but not used _unused_state = null # Kept for API compatibility but not used ) -> Dictionary: """Pure simulation using isolated state with inlined tick logic""" # Always create fresh isolated state to avoid thread conflicts var state = create_isolated_state() # Initialize targets var targets_remaining = 0 var active_unlock_ids: Array = [] for unlock_id in rank_targets.keys(): state.current_ranks[unlock_id] = 0 targets_remaining += rank_targets[unlock_id] active_unlock_ids.append(unlock_id) # Purchase tracking var purchases: Array[Dictionary] = [] # Try cache restoration var cached_state = null if not track_purchases: cached_state = try_load_best_prefix_from_cache(rank_targets) if cached_state != null: state.restore_from_cache(cached_state) # Recalculate remaining targets targets_remaining = 0 active_unlock_ids.clear() for unlock_id in rank_targets.keys(): var remaining = rank_targets[unlock_id] - state.current_ranks.get(unlock_id, 0) if remaining > 0: targets_remaining += remaining active_unlock_ids.append(unlock_id) # Pre-calculate next costs directly from lookup table var next_costs: Array[float] = [] next_costs.resize(active_unlock_ids.size()) for i in range(active_unlock_ids.size()): var unlock_id = active_unlock_ids[i] var current_rank = state.current_ranks.get(unlock_id, 0) var cost_array = state.costs.get(unlock_id, []) next_costs[i] = cost_array[current_rank] if current_rank < cost_array.size() else 999999999 # Main simulation loop while state.ticks < max_ticks: # Find cheapest affordable unlock var cheapest_index = -1 var cheapest_cost = INF var cheapest_unlock_id = -1 if targets_remaining > 0: for i in range(active_unlock_ids.size()): if next_costs[i] < cheapest_cost and state.currency >= next_costs[i]: cheapest_cost = next_costs[i] cheapest_unlock_id = active_unlock_ids[i] cheapest_index = i # Exit early if all targets met and goal reached if cheapest_index == -1 and targets_remaining == 0: if state.currency >= 1000000.0: break # Skip ahead to 1M var currency_needed = 1000000.0 - state.currency var price_per_item = state.get_sale_price_per_item() var items_per_tick = max(1.0, floor( Global.base_purchase_rate * state.get_modifier_value("purchase_rate_modifier") )) var revenue_per_tick = items_per_tick * price_per_item if revenue_per_tick > 0: var ticks_needed = int(ceil(currency_needed / revenue_per_tick)) state.ticks += ticks_needed state.currency += revenue_per_tick * ticks_needed break # Purchase unlock if affordable if cheapest_index != -1: state.purchase_unlock(cheapest_unlock_id) targets_remaining -= 1 # Track purchase if enabled if track_purchases: var current_rank = state.current_ranks[cheapest_unlock_id] purchases.append({ "unlock_id": cheapest_unlock_id, "unlock_name": get_unlock_name_by_id(cheapest_unlock_id), "rank": current_rank, "cost": cheapest_cost, "tick": state.ticks, "currency_after": state.currency }) # Update next cost or remove from active list if state.current_ranks[cheapest_unlock_id] >= rank_targets[cheapest_unlock_id]: # Target reached var last_idx = active_unlock_ids.size() - 1 if cheapest_index != last_idx: active_unlock_ids[cheapest_index] = active_unlock_ids[last_idx] next_costs[cheapest_index] = next_costs[last_idx] active_unlock_ids.resize(last_idx) next_costs.resize(last_idx) else: # Update cost for next rank directly from lookup table var current_rank = state.current_ranks[cheapest_unlock_id] var cost_array = state.costs.get(cheapest_unlock_id, []) next_costs[cheapest_index] = cost_array[current_rank] if current_rank < cost_array.size() else 999999999 # Cache this state if valuable if should_cache_state(state.current_ranks, targets_remaining): var cache_key = get_cache_key(state.current_ranks) cache_mutex.lock() if not simulation_cache.has(cache_key): simulation_cache[cache_key] = state.snapshot_for_cache() cache_mutex.unlock() # Simulate manual clicking to bootstrap economy (matches sim_cached.gd logic) # Manual clicks based on tick range (pre-calculate to avoid repeated conditions) var manual_clicks: float = 1.0 if state.ticks < 120 else (0.5 if state.ticks < 300 else (0.25 if (state.ticks < 600 and state.autowood_mod < 0.2) else 0.0)) if manual_clicks > 0.0: var wood_from_clicks = manual_clicks * state.get_wood_per_click() state.add_wood(wood_from_clicks) # Execute one tick using inlined logic (optimized for performance) state.execute_tick() # Build result var success = state.currency >= 1000000.0 var result = { "rank_targets": rank_targets, "success": success, "ticks": state.ticks if success else -1, "final_currency": state.currency, "time_formatted": format_time(state.ticks) if success else "Failed" } if track_purchases: result["purchases"] = purchases return result # ============================================================================= # HELPER FUNCTIONS # ============================================================================= func format_time(ticks: int) -> String: var seconds = ticks var minutes = seconds / 60 var hours = minutes / 60 if hours > 0: return "%dh %dm %ds" % [hours, minutes % 60, seconds % 60] elif minutes > 0: return "%dm %ds" % [minutes, seconds % 60] else: return "%ds" % seconds func generate_all_combinations(unlimited_scaling_cap: int = 5) -> Array[Dictionary]: """Generate combinations for ALL unlocks dynamically""" var combinations: Array[Dictionary] = [] var unlock_constraints = [] for unlock in unlock_collection.unlocks: var max_rank: int if unlock.max_rank > 0: max_rank = unlock.max_rank elif not unlock.is_scaling: max_rank = 1 else: max_rank = unlimited_scaling_cap unlock_constraints.append({ "id": unlock.unlock_id, "name": unlock.unlock_name, "max_rank": max_rank }) print("\n=== Generating Combinations ===") print("Reading from resource file: %d unlocks" % unlock_constraints.size()) for c in unlock_constraints: print(" - %s (ID %d): 0-%d ranks" % [c.name, c.id, c.max_rank]) _generate_combinations_recursive(unlock_constraints, 0, {}, combinations) print("Generated %d total combinations" % combinations.size()) return combinations func _generate_combinations_recursive(constraints: Array, index: int, current: Dictionary, output: Array): """Recursively generate all valid combinations""" if index >= constraints.size(): if current.size() > 0: output.append(current.duplicate()) return var constraint = constraints[index] for rank in range(constraint.max_rank + 1): if rank > 0: current[constraint.id] = rank _generate_combinations_recursive(constraints, index + 1, current, output) if rank > 0: current.erase(constraint.id) func get_unlock_name_by_id(unlock_id: int) -> String: """Helper function to get unlock name by ID""" if not precalc_initialized: build_precalculated_tables() return precalc_unlock_names.get(unlock_id, "Unknown") # ============================================================================= # THREADING AND PROGRESS # ============================================================================= func worker_thread(thread_id: int): """Worker thread function""" var local_results: Array[Dictionary] = [] var batch_size: int = 10 while true: var task_data = null queue_mutex.lock() if task_queue.size() > 0: task_data = task_queue.pop_front() queue_mutex.unlock() if task_data == null: if local_results.size() > 0: results_mutex.lock() all_results.append_array(local_results) results_mutex.unlock() completed_mutex.lock() completed_count += local_results.size() completed_mutex.unlock() break var result = simulate_rank_combination_direct(task_data.combo, 1000000, false, null, null) local_results.append(result) if local_results.size() >= batch_size: results_mutex.lock() all_results.append_array(local_results) results_mutex.unlock() completed_mutex.lock() completed_count += local_results.size() completed_mutex.unlock() local_results.clear() func update_progress(): """Update progress display""" var current_count = 0 completed_mutex.lock() current_count = completed_count completed_mutex.unlock() if current_count >= total_combinations: monitoring_active = false finish_processing() return var percent = float(current_count) / total_combinations * 100.0 var elapsed = (Time.get_ticks_msec() - start_time) / 1000.0 var rate = current_count / elapsed if elapsed > 0 else 0 var eta_seconds = (total_combinations - current_count) / rate if rate > 0 else 0 var total_cache_checks = cache_hits + cache_misses var cache_hit_rate = (float(cache_hits) / total_cache_checks * 100.0) if total_cache_checks > 0 else 0.0 var eta_str = "" if eta_seconds > 0: var eta_minutes = int(eta_seconds) / 60 var eta_secs = int(eta_seconds) % 60 if eta_minutes > 0: eta_str = "%dm %ds" % [eta_minutes, eta_secs] else: eta_str = "%ds" % eta_secs else: eta_str = "calculating..." print("Progress: %.1f%% (%d/%d) - %.1f combos/sec - Cache: %.1f%% hits - ETA: %s" % [ percent, current_count, total_combinations, rate, cache_hit_rate, eta_str ]) status_label.text = "Status: Running simulation..." progress_label.text = "Progress: %.1f%% (%d/%d)" % [percent, current_count, total_combinations] progress_bar.value = percent / 100.0 rate_label.text = "Speed: %.1f combos/sec" % rate eta_label.text = "ETA: %s" % eta_str cache_hits_label.text = "Cache Hits: %d" % cache_hits cache_misses_label.text = "Cache Misses: %d" % cache_misses cache_rate_label.text = "Hit Rate: %.1f%%" % cache_hit_rate cache_mutex.lock() cache_size_label.text = "Cache Entries: %d" % simulation_cache.size() cache_mutex.unlock() func finish_processing(): """Called when all processing is complete""" print("\nAll combinations complete! Waiting for threads to finish...") for thread in threads: thread.wait_to_finish() threads.clear() threads_done = true print("All threads finished. Processing results...") var total_time = (Time.get_ticks_msec() - start_time) / 1000.0 results_mutex.lock() var actual_results = all_results.size() results_mutex.unlock() if actual_results != total_combinations: print("WARNING: Result count mismatch! Expected %d, got %d" % [total_combinations, actual_results]) print("\n=== RESULTS ===") print("Total time: %.1f seconds" % total_time) print("Total combinations tested: %d" % actual_results) var total_cache_checks = cache_hits + cache_misses var cache_hit_rate = (float(cache_hits) / total_cache_checks * 100.0) if total_cache_checks > 0 else 0.0 cache_mutex.lock() var cache_size = simulation_cache.size() cache_mutex.unlock() print("\n=== CACHE STATISTICS ===") print("Cache hits: %d" % cache_hits) print("Cache misses: %d" % cache_misses) print("Hit rate: %.1f%%" % cache_hit_rate) print("Cache entries stored: %d" % cache_size) var successful = all_results.filter(func(r): return r.success) print("Successful strategies: %d" % successful.size()) status_label.text = "Status: Complete!" progress_label.text = "Progress: 100%% (%d/%d)" % [all_results.size(), total_combinations] progress_bar.value = 1.0 eta_label.text = "Total Time: %.1f seconds" % total_time var results_text = "[b]SIMULATION COMPLETE[/b]\n\n" results_text += "[color=green]Total time: %.1f seconds[/color]\n" % total_time results_text += "Combinations tested: %d\n" % all_results.size() results_text += "Successful strategies: %d\n\n" % successful.size() results_text += "[b]Cache Performance:[/b]\n" results_text += " Hits: %d\n" % cache_hits results_text += " Misses: %d\n" % cache_misses results_text += " [color=cyan]Hit Rate: %.1f%%[/color]\n" % cache_hit_rate results_text += " Entries: %d\n\n" % cache_size if successful.size() > 0: successful.sort_custom(func(a, b): return a.ticks < b.ticks) print("\n=== RE-SIMULATING TOP 10 WITH PURCHASE TRACKING ===") var top_10_detailed: Array = [] for i in range(min(10, successful.size())): var result = successful[i] print("Re-simulating #%d..." % (i + 1)) var detailed_result = simulate_rank_combination_direct(result.rank_targets, 1000000, true) top_10_detailed.append(detailed_result) print("\n=== TOP 10 FASTEST STRATEGIES (WITH PURCHASE TIMELINE) ===") results_text += "[b]TOP 10 FASTEST STRATEGIES:[/b]\n\n" for i in range(top_10_detailed.size()): var result = top_10_detailed[i] print("\n#%d: %s (%d ticks)" % [i + 1, result.time_formatted, result.ticks]) var rank_display = [] for unlock_id in result.rank_targets.keys(): var unlock_name = get_unlock_name_by_id(unlock_id) var ranks = result.rank_targets[unlock_id] rank_display.append("%s: %d" % [unlock_name, ranks]) print("Target Ranks: %s" % ", ".join(rank_display)) results_text += "[color=yellow]#%d: %s (%d ticks)[/color]\n" % [i + 1, result.time_formatted, result.ticks] results_text += " Ranks: %s\n" % ", ".join(rank_display) results_text += " Currency: %.0f\n" % result.final_currency if result.has("purchases") and result.purchases.size() > 0: print("\nPurchase Timeline:") results_text += " [b]Purchase Timeline:[/b]\n" for purchase in result.purchases: var time_str = format_time(purchase.tick) print(" %s: %s Rank %d - Cost: %d¥ @ %s" % [ time_str, purchase.unlock_name, purchase.rank, purchase.cost, time_str ]) results_text += " • %s [color=cyan]%s Rank %d[/color] - %d¥ @ %s\n" % [ format_time(purchase.tick), purchase.unlock_name, purchase.rank, purchase.cost, time_str ] results_text += "\n" else: print("\nNo successful strategies found!") results_text += "[color=red]No successful strategies found![/color]\n" results_label.text = results_text func run_comprehensive_test(): """Test all combinations dynamically generated from resource file""" print("\n=== Available Unlocks ===") for unlock in unlock_collection.unlocks: var max_rank_str = str(unlock.max_rank) if unlock.max_rank > 0 else "unlimited" print("ID: %d | %s | Base Cost: %d | Scaling: %s | Max Rank: %s" % [ unlock.unlock_id, unlock.unlock_name, unlock.base_cost, "Yes" if unlock.is_scaling else "No", max_rank_str ]) print(" Modifiers: ", unlock.base_modifiers) print("\n=== Global Constants ===") print("Base Sale Price: %s" % Global.base_sale_price) print("Base Purchase Rate: %s" % Global.base_purchase_rate) print("Cost Per Whittle: %s" % Global.cost_per_whittle) # Build pre-calculated tables build_precalculated_tables() # Generate combinations var unlimited_cap = 5 print("\n=== Generation Settings ===") print("Unlimited scaling cap: %d ranks" % unlimited_cap) var combinations = generate_all_combinations(unlimited_cap) total_combinations = combinations.size() print("\n=== Testing %d Combinations ===" % total_combinations) # Fill task queue task_queue.clear() for combo in combinations: task_queue.append({"combo": combo}) # Reset counters completed_count = 0 all_results.clear() threads_done = false start_time = Time.get_ticks_msec() last_progress_time = start_time monitoring_active = true # Create and start threads print("Starting %d worker threads..." % num_threads) for i in range(num_threads): var thread = Thread.new() thread.start(worker_thread.bind(i)) threads.append(thread) print("All threads started, processing...") func _exit_tree(): monitoring_active = false for thread in threads: if thread.is_alive(): thread.wait_to_finish()