import random import argparse import math import sys class SupplyCrateSimulator: def __init__(self, use_random_values=False): self.use_random_values = use_random_values self.blighted_supplies = [ {"name": "Blighted anglerfish", "quantity_range": (25, 35), "rarity": "3 × ~1/11", "price_range": (13375, 18725)}, {"name": "Blighted manta ray", "quantity_range": (15, 25), "rarity": "3 × ~1/11", "price_range": (15904, 24850)}, {"name": "Blighted karambwan", "quantity_range": (35, 45), "rarity": "3 × ~1/11", "price_range": (8015, 10076)}, {"name": "Blighted super restore(4)", "quantity_range": (4, 4), "rarity": "3 × ~1/11", "price_range": (20752, 20752)}, {"name": "Blighted ancient ice sack", "quantity_range": (10, 15), "rarity": "3 × ~1/11", "price_range": (7510, 11265)}, {"name": "Blighted vengeance sack", "quantity_range": (20, 30), "rarity": "3 × ~1/11", "price_range": (5140, 7710)} ] self.other = [ {"name": "Rune javelin", "quantity_range": (100, 120), "rarity": "3 × ~1/11", "price_range": (24000, 28800)}, {"name": "Rune arrow", "quantity_range": (175, 225), "rarity": "3 × ~1/11", "price_range": (42240, 54000)}, {"name": "Castle wars bolts", "quantity_range": (75, 105), "rarity": "3 × ~1/11", "price_range": (0, 0)}, {"name": "Castle wars arrow", "quantity_range": (75, 105), "rarity": "3 × ~1/11", "price_range": (0, 0)}, {"name": "Castle wars ticket", "quantity_range": (2, 2), "rarity": "3 × ~1/11", "price_range": (0, 0)} ] self.all_loot = self.blighted_supplies + self.other self.rolls_per_crate = 3 self.tickets_per_game = 2.75 if not self.all_loot: raise ValueError("Loot table cannot be empty.") self.item_probability = 1 / len(self.all_loot) for item in self.all_loot: min_qty, max_qty = item["quantity_range"] item["avg_quantity"] = (min_qty + max_qty) / 2 min_price, max_price = item["price_range"] item["avg_total_price"] = (min_price + max_price) / 2 if item["avg_quantity"] > 0: item["avg_unit_price"] = item["avg_total_price"] / item["avg_quantity"] else: item["avg_unit_price"] = 0 self.ticket_item_data = next((item for item in self.all_loot if item["name"] == "Castle wars ticket"), None) def get_item(self): return random.choice(self.all_loot) def get_quantity(self, item): if self.use_random_values: min_qty, max_qty = item["quantity_range"] return random.randint(min_qty, max_qty) else: return item["avg_quantity"] def get_value(self, item, quantity): avg_unit_price = item.get("avg_unit_price", 0) return quantity * avg_unit_price def open_crate(self): loot = [] total_value = 0.0 if self.use_random_values: for _ in range(self.rolls_per_crate): item = self.get_item() quantity = self.get_quantity(item) value = self.get_value(item, quantity) loot.append({"name": item["name"], "quantity": quantity, "value": value}) total_value += value else: expected_drops_per_item_per_crate = self.rolls_per_crate * self.item_probability for item in self.all_loot: avg_quantity_per_drop = item["avg_quantity"] expected_quantity_this_crate = expected_drops_per_item_per_crate * avg_quantity_per_drop value_this_crate = self.get_value(item, expected_quantity_this_crate) loot.append({"name": item["name"], "quantity": expected_quantity_this_crate, "value": value_this_crate}) total_value += value_this_crate return loot, total_value def _get_crates_for_game(self, crate_accumulator): if self.use_random_values: base_crates = math.floor(self.tickets_per_game) fraction = self.tickets_per_game - base_crates extra_crate = 1 if random.random() < fraction else 0 crates_to_open = base_crates + extra_crate return crates_to_open, crate_accumulator else: crate_accumulator += self.tickets_per_game crates_to_open = math.floor(crate_accumulator) crate_accumulator = max(0.0, crate_accumulator - crates_to_open) return int(crates_to_open), crate_accumulator def open_multiple_crates(self, num_games): all_loot_details = [] total_value = 0.0 actual_crates_opened = 0 crate_accumulator = 0.0 for _ in range(num_games): crates_this_game, crate_accumulator = self._get_crates_for_game(crate_accumulator) for _ in range(crates_this_game): crate_loot, crate_value = self.open_crate() all_loot_details.extend(crate_loot) total_value += crate_value actual_crates_opened += 1 return all_loot_details, total_value, actual_crates_opened def simulate_until_tickets(self, target_tickets): """ Simulate until a specific number of tickets are acquired. Stops when actual crates opened + actual tickets looted >= target, for BOTH random and expected modes. """ item_quantities = {item["name"]: 0.0 for item in self.all_loot} total_value = 0.0 games_played = 0 actual_crates_opened = 0 crate_accumulator = 0.0 # stop_condition_value = 0.0 # The value checked against target_tickets # Safety check for invalid target if target_tickets <= 0: print("Warning: Target tickets must be positive. Setting to 1.", file=sys.stderr) target_tickets = 1 while True: # Check condition *before* playing the next game to handle edge case of target=0 or already met # Use the values from the *end* of the previous iteration tickets_looted = item_quantities.get("Castle wars ticket", 0.0) current_total_tickets = actual_crates_opened + tickets_looted if current_total_tickets >= target_tickets: break # Target met or exceeded # Play a game games_played += 1 crates_this_game, crate_accumulator = self._get_crates_for_game(crate_accumulator) # Open crates for this game for _ in range(crates_this_game): # Increment crates *before* checking loot, as opening costs the ticket actual_crates_opened += 1 crate_loot, crate_value = self.open_crate() total_value += crate_value # Accumulate loot for item in crate_loot: item_quantities[item["name"]] += item["quantity"] # # Update the value used for the next iteration's check # stop_condition_value = actual_crates_opened + item_quantities.get("Castle wars ticket", 0.0) # # Safety breaks # # Check if progress is stalled (e.g., if tickets_per_game was 0 somehow) # if games_played > 5 and stop_condition_value <= 1e-9: # print(f"Warning: No tickets seem to be accumulating after {games_played} games. Check loot tables/logic. Breaking.", file=sys.stderr) # break # Note: stop_condition_value might be slightly higher than target_tickets final_stop_value = actual_crates_opened + item_quantities.get("Castle wars ticket", 0.0) return item_quantities, total_value, games_played, final_stop_value, actual_crates_opened def analyze_drop_rates(self, num_games): all_loot_details, total_value_simulated, actual_crates_opened = self.open_multiple_crates(num_games) item_quantities_simulated = {item["name"]: 0.0 for item in self.all_loot} for loot_item in all_loot_details: item_quantities_simulated[loot_item["name"]] += loot_item["quantity"] expected_total_crates = num_games * self.tickets_per_game expected_total_rolls = expected_total_crates * self.rolls_per_crate print(f"\n--- Analysis Over {num_games:,} Games ---") print(f"Mode: {'Randomized Simulation' if self.use_random_values else 'Expected Value Simulation (Deterministic)'}") print(f"Expected Tickets & Crates: {expected_total_crates:,.0f} ({expected_total_rolls:,.0f} rolls)") print(f"Actual Crates Opened in Simulation: {actual_crates_opened:,}") print(f"Approx Hours to Complete: {num_games * 0.416:,.2f}") headers = ["Item Name", "Simulated Qty", "Expected Qty", "Simulated Gold", "Expected Gold"] all_data = [] total_simulated_gold = 0.0 total_expected_gold = 0.0 total_simulated_items = 0.0 total_expected_items = 0.0 item_lookup = {item['name']: item for item in self.all_loot} for item in self.all_loot: name = item["name"] simulated_quantity = item_quantities_simulated.get(name, 0.0) simulated_gold = self.get_value(item, simulated_quantity) expected_drops_of_item = expected_total_rolls * self.item_probability expected_quantity = expected_drops_of_item * item["avg_quantity"] expected_gold = self.get_value(item, expected_quantity) total_simulated_items += simulated_quantity total_expected_items += expected_quantity total_simulated_gold += simulated_gold total_expected_gold += expected_gold all_data.append([name, f"{simulated_quantity:,.0f}", f"{expected_quantity:,.0f}", f"{simulated_gold:,.0f}", f"{expected_gold:,.0f}"]) col_widths = [len(h) for h in headers] for row in all_data: for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) col_widths = [w + 2 for w in col_widths] header_line = "| " + " | ".join(f"{h:<{col_widths[i]}}" for i, h in enumerate(headers)) + " |" separator = "-" * len(header_line) print(separator); print(header_line); print(separator) for row in all_data: line = "| " + f"{row[0]:<{col_widths[0]}} | " + "".join([f"{cell:>{col_widths[i]}} | " for i, cell in enumerate(row[1:], 1)]) print(line) print(separator) totals_line = "| " + f"{'TOTAL':<{col_widths[0]}} | " + f"{total_simulated_items:>{col_widths[1]}.2f} | " + f"{total_expected_items:>{col_widths[2]}.2f} | " + f"{total_simulated_gold:>{col_widths[3]},.0f} | " + f"{total_expected_gold:>{col_widths[4]},.0f} | " print(totals_line); print(separator) sim_tickets_looted = item_quantities_simulated.get("Castle wars ticket", 0.0) exp_tickets_total_yield = num_games * self.tickets_per_game + sim_tickets_looted print(f"Simulated Tickets Looted: {sim_tickets_looted:,.0f}") print(f"Total Expected Tickets : {exp_tickets_total_yield:,.0f}") def display_simulation_results(self, item_quantities, total_value, games_played, final_total_tickets, actual_crates_opened): """Display results in a formatted table from ticket-focused simulation.""" print("\n--- Simulation Results (Target Tickets) ---") print(f"Mode: {'Randomized Simulation' if self.use_random_values else 'Expected Value Simulation (Deterministic)'}") print(f"Target Tickets Set: {args.tickets:,}") print(f"Total Tickets: {final_total_tickets:,.2f}") print(f"Games Played: {games_played:,}") print(f"Approx. Hours Needed: {games_played * 0.416:,.2f}") print(f"Crates Opened / Tickets from playing: {actual_crates_opened:,}") simulated_tickets_looted = item_quantities.get("Castle wars ticket", 0.0) print(f"Tickets from crates: {simulated_tickets_looted:,.0f}") print(f"Total GP: {total_value:,.0f} gp") if games_played > 0: print(f"GP per Game: {total_value / games_played:,.0f} gp") if final_total_tickets > 0: print(f"GP per crate: {total_value / actual_crates_opened:,.0f} gp") print(f"GP per hour: {total_value/(games_played*0.416):,.0f} gp") # Prepare Table Data headers = ["Item Name", "Total Qty", "GP"] all_data = [] total_items_received = 0.0 item_lookup = {item['name']: item for item in self.all_loot} sorted_item_names = sorted(item_quantities.keys()) for name in sorted_item_names: total_quantity = item_quantities.get(name, 0.0) item_data = item_lookup.get(name) if not item_data: continue item_gold = self.get_value(item_data, total_quantity) total_items_received += total_quantity all_data.append([name, f"{total_quantity:,.0f}", f"{item_gold:,.0f}"]) # Format and Print Table if not all_data: print("\nNo significant items received."); return col_widths = [len(h) for h in headers] for row in all_data: for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) col_widths = [w + 2 for w in col_widths] header_line = "| " + " | ".join(f"{h:<{col_widths[i]}}" for i, h in enumerate(headers)) + " |" separator = "-" * len(header_line) print(separator); print(header_line); print(separator) for row in all_data: line = "| " + f"{row[0]:<{col_widths[0]}} | " + "".join([f"{cell:>{col_widths[i]}} | " for i, cell in enumerate(row[1:], 1)]) print(line) print(separator) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Simulate opening Castle Wars supply crates.') parser.add_argument('--games', '-g', type=int, default=1000, help='Number of games to simulate for drop rate analysis (default: 1000)') parser.add_argument('--tickets', '-t', type=int, default=None, help='Target number of tickets to acquire (stop condition: crates opened + tickets looted >= target).') parser.add_argument('--random', '-r', action='store_true', help='Use random quantities and item rolls. Default is deterministic expected values.') args = parser.parse_args() simulator = SupplyCrateSimulator(use_random_values=args.random) if args.tickets is not None: # print(f"Simulating until (crates opened + tickets looted) reaches target {args.tickets:,}...") item_quantities, total_value, games_played, final_total_tickets, actual_crates = simulator.simulate_until_tickets(args.tickets) simulator.display_simulation_results(item_quantities, total_value, games_played, final_total_tickets, actual_crates) else: print(f"Analyzing drop rates over {args.games:,} games...") simulator.analyze_drop_rates(args.games) print(f"\nSimulation finished. Mode used: {'Random' if args.random else 'Expected Values (Deterministic)'}")