cwars.py
· 15 KiB · Python
Raw
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)'}")
1 | import random |
2 | import argparse |
3 | import math |
4 | import sys |
5 | |
6 | class SupplyCrateSimulator: |
7 | def __init__(self, use_random_values=False): |
8 | self.use_random_values = use_random_values |
9 | self.blighted_supplies = [ |
10 | {"name": "Blighted anglerfish", "quantity_range": (25, 35), "rarity": "3 × ~1/11", "price_range": (13375, 18725)}, |
11 | {"name": "Blighted manta ray", "quantity_range": (15, 25), "rarity": "3 × ~1/11", "price_range": (15904, 24850)}, |
12 | {"name": "Blighted karambwan", "quantity_range": (35, 45), "rarity": "3 × ~1/11", "price_range": (8015, 10076)}, |
13 | {"name": "Blighted super restore(4)", "quantity_range": (4, 4), "rarity": "3 × ~1/11", "price_range": (20752, 20752)}, |
14 | {"name": "Blighted ancient ice sack", "quantity_range": (10, 15), "rarity": "3 × ~1/11", "price_range": (7510, 11265)}, |
15 | {"name": "Blighted vengeance sack", "quantity_range": (20, 30), "rarity": "3 × ~1/11", "price_range": (5140, 7710)} |
16 | ] |
17 | self.other = [ |
18 | {"name": "Rune javelin", "quantity_range": (100, 120), "rarity": "3 × ~1/11", "price_range": (24000, 28800)}, |
19 | {"name": "Rune arrow", "quantity_range": (175, 225), "rarity": "3 × ~1/11", "price_range": (42240, 54000)}, |
20 | {"name": "Castle wars bolts", "quantity_range": (75, 105), "rarity": "3 × ~1/11", "price_range": (0, 0)}, |
21 | {"name": "Castle wars arrow", "quantity_range": (75, 105), "rarity": "3 × ~1/11", "price_range": (0, 0)}, |
22 | {"name": "Castle wars ticket", "quantity_range": (2, 2), "rarity": "3 × ~1/11", "price_range": (0, 0)} |
23 | ] |
24 | self.all_loot = self.blighted_supplies + self.other |
25 | self.rolls_per_crate = 3 |
26 | self.tickets_per_game = 2.75 |
27 | if not self.all_loot: |
28 | raise ValueError("Loot table cannot be empty.") |
29 | self.item_probability = 1 / len(self.all_loot) |
30 | for item in self.all_loot: |
31 | min_qty, max_qty = item["quantity_range"] |
32 | item["avg_quantity"] = (min_qty + max_qty) / 2 |
33 | min_price, max_price = item["price_range"] |
34 | item["avg_total_price"] = (min_price + max_price) / 2 |
35 | if item["avg_quantity"] > 0: |
36 | item["avg_unit_price"] = item["avg_total_price"] / item["avg_quantity"] |
37 | else: |
38 | item["avg_unit_price"] = 0 |
39 | self.ticket_item_data = next((item for item in self.all_loot if item["name"] == "Castle wars ticket"), None) |
40 | |
41 | |
42 | def get_item(self): |
43 | return random.choice(self.all_loot) |
44 | |
45 | def get_quantity(self, item): |
46 | if self.use_random_values: |
47 | min_qty, max_qty = item["quantity_range"] |
48 | return random.randint(min_qty, max_qty) |
49 | else: |
50 | return item["avg_quantity"] |
51 | |
52 | def get_value(self, item, quantity): |
53 | avg_unit_price = item.get("avg_unit_price", 0) |
54 | return quantity * avg_unit_price |
55 | |
56 | def open_crate(self): |
57 | loot = [] |
58 | total_value = 0.0 |
59 | if self.use_random_values: |
60 | for _ in range(self.rolls_per_crate): |
61 | item = self.get_item() |
62 | quantity = self.get_quantity(item) |
63 | value = self.get_value(item, quantity) |
64 | loot.append({"name": item["name"], "quantity": quantity, "value": value}) |
65 | total_value += value |
66 | else: |
67 | expected_drops_per_item_per_crate = self.rolls_per_crate * self.item_probability |
68 | for item in self.all_loot: |
69 | avg_quantity_per_drop = item["avg_quantity"] |
70 | expected_quantity_this_crate = expected_drops_per_item_per_crate * avg_quantity_per_drop |
71 | value_this_crate = self.get_value(item, expected_quantity_this_crate) |
72 | loot.append({"name": item["name"], "quantity": expected_quantity_this_crate, "value": value_this_crate}) |
73 | total_value += value_this_crate |
74 | return loot, total_value |
75 | |
76 | def _get_crates_for_game(self, crate_accumulator): |
77 | if self.use_random_values: |
78 | base_crates = math.floor(self.tickets_per_game) |
79 | fraction = self.tickets_per_game - base_crates |
80 | extra_crate = 1 if random.random() < fraction else 0 |
81 | crates_to_open = base_crates + extra_crate |
82 | return crates_to_open, crate_accumulator |
83 | else: |
84 | crate_accumulator += self.tickets_per_game |
85 | crates_to_open = math.floor(crate_accumulator) |
86 | crate_accumulator = max(0.0, crate_accumulator - crates_to_open) |
87 | return int(crates_to_open), crate_accumulator |
88 | |
89 | def open_multiple_crates(self, num_games): |
90 | all_loot_details = [] |
91 | total_value = 0.0 |
92 | actual_crates_opened = 0 |
93 | crate_accumulator = 0.0 |
94 | for _ in range(num_games): |
95 | crates_this_game, crate_accumulator = self._get_crates_for_game(crate_accumulator) |
96 | for _ in range(crates_this_game): |
97 | crate_loot, crate_value = self.open_crate() |
98 | all_loot_details.extend(crate_loot) |
99 | total_value += crate_value |
100 | actual_crates_opened += 1 |
101 | return all_loot_details, total_value, actual_crates_opened |
102 | |
103 | def simulate_until_tickets(self, target_tickets): |
104 | """ |
105 | Simulate until a specific number of tickets are acquired. |
106 | Stops when actual crates opened + actual tickets looted >= target, |
107 | for BOTH random and expected modes. |
108 | """ |
109 | item_quantities = {item["name"]: 0.0 for item in self.all_loot} |
110 | total_value = 0.0 |
111 | games_played = 0 |
112 | actual_crates_opened = 0 |
113 | crate_accumulator = 0.0 |
114 | # stop_condition_value = 0.0 # The value checked against target_tickets |
115 | |
116 | # Safety check for invalid target |
117 | if target_tickets <= 0: |
118 | print("Warning: Target tickets must be positive. Setting to 1.", file=sys.stderr) |
119 | target_tickets = 1 |
120 | |
121 | while True: |
122 | # Check condition *before* playing the next game to handle edge case of target=0 or already met |
123 | # Use the values from the *end* of the previous iteration |
124 | tickets_looted = item_quantities.get("Castle wars ticket", 0.0) |
125 | current_total_tickets = actual_crates_opened + tickets_looted |
126 | if current_total_tickets >= target_tickets: |
127 | break # Target met or exceeded |
128 | |
129 | # Play a game |
130 | games_played += 1 |
131 | crates_this_game, crate_accumulator = self._get_crates_for_game(crate_accumulator) |
132 | |
133 | # Open crates for this game |
134 | for _ in range(crates_this_game): |
135 | # Increment crates *before* checking loot, as opening costs the ticket |
136 | actual_crates_opened += 1 |
137 | crate_loot, crate_value = self.open_crate() |
138 | total_value += crate_value |
139 | # Accumulate loot |
140 | for item in crate_loot: |
141 | item_quantities[item["name"]] += item["quantity"] |
142 | |
143 | # # Update the value used for the next iteration's check |
144 | # stop_condition_value = actual_crates_opened + item_quantities.get("Castle wars ticket", 0.0) |
145 | |
146 | # # Safety breaks |
147 | # # Check if progress is stalled (e.g., if tickets_per_game was 0 somehow) |
148 | # if games_played > 5 and stop_condition_value <= 1e-9: |
149 | # print(f"Warning: No tickets seem to be accumulating after {games_played} games. Check loot tables/logic. Breaking.", file=sys.stderr) |
150 | # break |
151 | |
152 | # Note: stop_condition_value might be slightly higher than target_tickets |
153 | final_stop_value = actual_crates_opened + item_quantities.get("Castle wars ticket", 0.0) |
154 | return item_quantities, total_value, games_played, final_stop_value, actual_crates_opened |
155 | |
156 | def analyze_drop_rates(self, num_games): |
157 | all_loot_details, total_value_simulated, actual_crates_opened = self.open_multiple_crates(num_games) |
158 | item_quantities_simulated = {item["name"]: 0.0 for item in self.all_loot} |
159 | for loot_item in all_loot_details: |
160 | item_quantities_simulated[loot_item["name"]] += loot_item["quantity"] |
161 | |
162 | expected_total_crates = num_games * self.tickets_per_game |
163 | expected_total_rolls = expected_total_crates * self.rolls_per_crate |
164 | |
165 | print(f"\n--- Analysis Over {num_games:,} Games ---") |
166 | print(f"Mode: {'Randomized Simulation' if self.use_random_values else 'Expected Value Simulation (Deterministic)'}") |
167 | print(f"Expected Tickets & Crates: {expected_total_crates:,.0f} ({expected_total_rolls:,.0f} rolls)") |
168 | print(f"Actual Crates Opened in Simulation: {actual_crates_opened:,}") |
169 | print(f"Approx Hours to Complete: {num_games * 0.416:,.2f}") |
170 | |
171 | headers = ["Item Name", "Simulated Qty", "Expected Qty", "Simulated Gold", "Expected Gold"] |
172 | all_data = [] |
173 | total_simulated_gold = 0.0 |
174 | total_expected_gold = 0.0 |
175 | total_simulated_items = 0.0 |
176 | total_expected_items = 0.0 |
177 | item_lookup = {item['name']: item for item in self.all_loot} |
178 | |
179 | for item in self.all_loot: |
180 | name = item["name"] |
181 | simulated_quantity = item_quantities_simulated.get(name, 0.0) |
182 | simulated_gold = self.get_value(item, simulated_quantity) |
183 | expected_drops_of_item = expected_total_rolls * self.item_probability |
184 | expected_quantity = expected_drops_of_item * item["avg_quantity"] |
185 | expected_gold = self.get_value(item, expected_quantity) |
186 | total_simulated_items += simulated_quantity |
187 | total_expected_items += expected_quantity |
188 | total_simulated_gold += simulated_gold |
189 | total_expected_gold += expected_gold |
190 | all_data.append([name, f"{simulated_quantity:,.0f}", f"{expected_quantity:,.0f}", f"{simulated_gold:,.0f}", f"{expected_gold:,.0f}"]) |
191 | |
192 | col_widths = [len(h) for h in headers] |
193 | for row in all_data: |
194 | for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) |
195 | col_widths = [w + 2 for w in col_widths] |
196 | header_line = "| " + " | ".join(f"{h:<{col_widths[i]}}" for i, h in enumerate(headers)) + " |" |
197 | separator = "-" * len(header_line) |
198 | print(separator); print(header_line); print(separator) |
199 | for row in all_data: |
200 | line = "| " + f"{row[0]:<{col_widths[0]}} | " + "".join([f"{cell:>{col_widths[i]}} | " for i, cell in enumerate(row[1:], 1)]) |
201 | print(line) |
202 | print(separator) |
203 | 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} | " |
204 | print(totals_line); print(separator) |
205 | |
206 | sim_tickets_looted = item_quantities_simulated.get("Castle wars ticket", 0.0) |
207 | exp_tickets_total_yield = num_games * self.tickets_per_game + sim_tickets_looted |
208 | print(f"Simulated Tickets Looted: {sim_tickets_looted:,.0f}") |
209 | print(f"Total Expected Tickets : {exp_tickets_total_yield:,.0f}") |
210 | |
211 | |
212 | def display_simulation_results(self, item_quantities, total_value, games_played, final_total_tickets, actual_crates_opened): |
213 | """Display results in a formatted table from ticket-focused simulation.""" |
214 | print("\n--- Simulation Results (Target Tickets) ---") |
215 | print(f"Mode: {'Randomized Simulation' if self.use_random_values else 'Expected Value Simulation (Deterministic)'}") |
216 | print(f"Target Tickets Set: {args.tickets:,}") |
217 | print(f"Total Tickets: {final_total_tickets:,.2f}") |
218 | print(f"Games Played: {games_played:,}") |
219 | print(f"Approx. Hours Needed: {games_played * 0.416:,.2f}") |
220 | print(f"Crates Opened / Tickets from playing: {actual_crates_opened:,}") |
221 | |
222 | simulated_tickets_looted = item_quantities.get("Castle wars ticket", 0.0) |
223 | print(f"Tickets from crates: {simulated_tickets_looted:,.0f}") |
224 | |
225 | print(f"Total GP: {total_value:,.0f} gp") |
226 | if games_played > 0: |
227 | print(f"GP per Game: {total_value / games_played:,.0f} gp") |
228 | if final_total_tickets > 0: |
229 | print(f"GP per crate: {total_value / actual_crates_opened:,.0f} gp") |
230 | print(f"GP per hour: {total_value/(games_played*0.416):,.0f} gp") |
231 | |
232 | |
233 | # Prepare Table Data |
234 | headers = ["Item Name", "Total Qty", "GP"] |
235 | all_data = [] |
236 | total_items_received = 0.0 |
237 | item_lookup = {item['name']: item for item in self.all_loot} |
238 | sorted_item_names = sorted(item_quantities.keys()) |
239 | |
240 | for name in sorted_item_names: |
241 | total_quantity = item_quantities.get(name, 0.0) |
242 | item_data = item_lookup.get(name) |
243 | if not item_data: continue |
244 | item_gold = self.get_value(item_data, total_quantity) |
245 | total_items_received += total_quantity |
246 | all_data.append([name, f"{total_quantity:,.0f}", f"{item_gold:,.0f}"]) |
247 | |
248 | # Format and Print Table |
249 | if not all_data: print("\nNo significant items received."); return |
250 | col_widths = [len(h) for h in headers] |
251 | for row in all_data: |
252 | for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) |
253 | col_widths = [w + 2 for w in col_widths] |
254 | header_line = "| " + " | ".join(f"{h:<{col_widths[i]}}" for i, h in enumerate(headers)) + " |" |
255 | separator = "-" * len(header_line) |
256 | print(separator); print(header_line); print(separator) |
257 | for row in all_data: |
258 | line = "| " + f"{row[0]:<{col_widths[0]}} | " + "".join([f"{cell:>{col_widths[i]}} | " for i, cell in enumerate(row[1:], 1)]) |
259 | print(line) |
260 | print(separator) |
261 | |
262 | |
263 | |
264 | if __name__ == "__main__": |
265 | parser = argparse.ArgumentParser(description='Simulate opening Castle Wars supply crates.') |
266 | parser.add_argument('--games', '-g', type=int, default=1000, |
267 | help='Number of games to simulate for drop rate analysis (default: 1000)') |
268 | parser.add_argument('--tickets', '-t', type=int, default=None, |
269 | help='Target number of tickets to acquire (stop condition: crates opened + tickets looted >= target).') |
270 | parser.add_argument('--random', '-r', action='store_true', |
271 | help='Use random quantities and item rolls. Default is deterministic expected values.') |
272 | |
273 | args = parser.parse_args() |
274 | |
275 | simulator = SupplyCrateSimulator(use_random_values=args.random) |
276 | |
277 | if args.tickets is not None: |
278 | # print(f"Simulating until (crates opened + tickets looted) reaches target {args.tickets:,}...") |
279 | item_quantities, total_value, games_played, final_total_tickets, actual_crates = simulator.simulate_until_tickets(args.tickets) |
280 | simulator.display_simulation_results(item_quantities, total_value, games_played, final_total_tickets, actual_crates) |
281 | else: |
282 | print(f"Analyzing drop rates over {args.games:,} games...") |
283 | simulator.analyze_drop_rates(args.games) |
284 | |
285 | print(f"\nSimulation finished. Mode used: {'Random' if args.random else 'Expected Values (Deterministic)'}") |