yorgei22 hat die Gist bearbeitet . Zu Änderung gehen
1 file changed, 285 insertions
cwars.py(Datei erstellt)
@@ -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)'}") |
Neuer
Älter