Last active 1744251009

cwars.py Raw
1import random
2import argparse
3import math
4import sys
5
6class 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
264if __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)'}")