Last active 1744251009

yorgei22 revised this gist 1744251008. Go to revision

1 file changed, 285 insertions

cwars.py(file created)

@@ -0,0 +1,285 @@
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)'}")
Newer Older