1use crate::connection::Connection;
8use crate::error::{MtgjsonError, Result};
9use rand::{Rng, RngExt};
10use std::collections::HashMap;
11
12pub struct BoosterSimulator<'a> {
18 conn: &'a Connection,
19}
20
21impl<'a> BoosterSimulator<'a> {
22 pub fn new(conn: &'a Connection) -> Self {
24 Self { conn }
25 }
26
27 pub fn available_types(&self, set_code: &str) -> Result<Vec<String>> {
31 self.conn.ensure_views(&["set_booster_content_weights"])?;
32
33 let upper = set_code.to_uppercase();
34 let sql = r#"
35 SELECT DISTINCT "boosterName"
36 FROM set_booster_content_weights
37 WHERE "setCode" = ?
38 ORDER BY "boosterName"
39 "#;
40
41 let rows = self.conn.execute(sql, &[upper.clone()])?;
42
43 let types: Vec<String> = rows
44 .into_iter()
45 .filter_map(|r| {
46 r.get("boosterName")
47 .and_then(|v| v.as_str())
48 .map(|s| s.to_string())
49 })
50 .collect();
51
52 Ok(types)
53 }
54
55 pub fn open_pack(
61 &self,
62 set_code: &str,
63 booster_type: &str,
64 ) -> Result<Vec<serde_json::Value>> {
65 let upper = set_code.to_uppercase();
66
67 let pack_templates = self.get_pack_templates(&upper, booster_type)?;
69 if pack_templates.is_empty() {
70 return Err(MtgjsonError::NotFound(format!(
71 "No booster configuration found for set '{}' type '{}'",
72 set_code, booster_type
73 )));
74 }
75
76 let template = pick_pack(&pack_templates);
78
79 let mut all_uuids: Vec<String> = Vec::new();
81
82 if let Some(sheets) = template.get("sheets") {
83 if let Some(sheets_map) = sheets.as_object() {
84 for (sheet_name, pick_count_val) in sheets_map {
85 let pick_count = pick_count_val.as_u64().unwrap_or(0) as usize;
86 if pick_count == 0 {
87 continue;
88 }
89
90 let sheet = self.get_sheet_data(&upper, booster_type, sheet_name)?;
92 if let Some(ref sheet_data) = sheet {
93 let uuids = pick_from_sheet(sheet_data, pick_count);
94 all_uuids.extend(uuids);
95 }
96 }
97 }
98 }
99
100 if all_uuids.is_empty() {
102 return Ok(Vec::new());
103 }
104
105 self.fetch_cards_by_uuids(&all_uuids)
106 }
107
108 pub fn open_box(
112 &self,
113 set_code: &str,
114 booster_type: &str,
115 packs: usize,
116 ) -> Result<Vec<Vec<serde_json::Value>>> {
117 let mut box_contents = Vec::with_capacity(packs);
118 for _ in 0..packs {
119 let pack = self.open_pack(set_code, booster_type)?;
120 box_contents.push(pack);
121 }
122 Ok(box_contents)
123 }
124
125 pub fn sheet_contents(
129 &self,
130 set_code: &str,
131 booster_type: &str,
132 sheet_name: &str,
133 ) -> Result<Option<HashMap<String, i64>>> {
134 self.conn.ensure_views(&["set_booster_sheet_cards"])?;
135
136 let upper = set_code.to_uppercase();
137 let sql = r#"
138 SELECT "cardUuid", "cardWeight"
139 FROM set_booster_sheet_cards
140 WHERE "setCode" = ?
141 AND "boosterName" = ?
142 AND "sheetName" = ?
143 "#;
144
145 let rows = self.conn.execute(sql, &[upper.clone(), booster_type.to_string(), sheet_name.to_string()])?;
146
147 if rows.is_empty() {
148 return Ok(None);
149 }
150
151 let mut contents: HashMap<String, i64> = HashMap::new();
152 for row in rows {
153 let uuid = row
154 .get("cardUuid")
155 .and_then(|v| v.as_str())
156 .unwrap_or("")
157 .to_string();
158 let weight = row
159 .get("cardWeight")
160 .and_then(|v| v.as_i64())
161 .unwrap_or(1);
162
163 if !uuid.is_empty() {
164 contents.insert(uuid, weight);
165 }
166 }
167
168 Ok(Some(contents))
169 }
170
171 fn get_pack_templates(
177 &self,
178 set_code: &str,
179 booster_type: &str,
180 ) -> Result<Vec<serde_json::Value>> {
181 self.conn.ensure_views(&[
182 "set_booster_content_weights",
183 "set_booster_contents",
184 ])?;
185
186 let weight_sql = r#"
188 SELECT "boosterIndex", "boosterWeight"
189 FROM set_booster_content_weights
190 WHERE "setCode" = ?
191 AND "boosterName" = ?
192 ORDER BY "boosterIndex"
193 "#;
194
195 let weight_rows =
196 self.conn.execute(weight_sql, &[set_code.to_string(), booster_type.to_string()])?;
197
198 if weight_rows.is_empty() {
199 return Ok(Vec::new());
200 }
201
202 let contents_sql = r#"
204 SELECT "boosterIndex", "sheetName", "sheetPicks"
205 FROM set_booster_contents
206 WHERE "setCode" = ?
207 AND "boosterName" = ?
208 ORDER BY "boosterIndex", "sheetName"
209 "#;
210
211 let contents_rows =
212 self.conn.execute(contents_sql, &[set_code.to_string(), booster_type.to_string()])?;
213
214 let mut contents_map: HashMap<i64, serde_json::Map<String, serde_json::Value>> =
216 HashMap::new();
217 for row in &contents_rows {
218 let idx = row
219 .get("boosterIndex")
220 .and_then(|v| v.as_i64())
221 .unwrap_or(0);
222 let sheet_name = row
223 .get("sheetName")
224 .and_then(|v| v.as_str())
225 .unwrap_or("")
226 .to_string();
227 let picks = row
228 .get("sheetPicks")
229 .and_then(|v| v.as_i64())
230 .unwrap_or(1);
231
232 contents_map
233 .entry(idx)
234 .or_default()
235 .insert(sheet_name, serde_json::Value::Number(picks.into()));
236 }
237
238 let mut templates: Vec<serde_json::Value> = Vec::new();
240 for row in &weight_rows {
241 let idx = row
242 .get("boosterIndex")
243 .and_then(|v| v.as_i64())
244 .unwrap_or(0);
245 let weight = row
246 .get("boosterWeight")
247 .and_then(|v| v.as_i64())
248 .unwrap_or(1);
249
250 let sheets = contents_map
251 .get(&idx)
252 .cloned()
253 .unwrap_or_default();
254
255 templates.push(serde_json::json!({
256 "weight": weight,
257 "sheets": sheets,
258 }));
259 }
260
261 Ok(templates)
262 }
263
264 fn get_sheet_data(
267 &self,
268 set_code: &str,
269 booster_type: &str,
270 sheet_name: &str,
271 ) -> Result<Option<serde_json::Value>> {
272 self.conn.ensure_views(&[
273 "set_booster_sheet_cards",
274 "set_booster_sheets",
275 ])?;
276
277 let props_sql = r#"
279 SELECT "sheetHasBalanceColors", "sheetIsFoil", "sheetIsFixed",
280 "sheetAllowDuplicates", "totalWeight"
281 FROM set_booster_sheets
282 WHERE "setCode" = ?
283 AND "boosterName" = ?
284 AND "sheetName" = ?
285 LIMIT 1
286 "#;
287
288 let props_rows =
289 self.conn.execute(props_sql, &[set_code.to_string(), booster_type.to_string(), sheet_name.to_string()])?;
290
291 let allow_duplicates = props_rows
292 .first()
293 .and_then(|r| r.get("sheetAllowDuplicates"))
294 .and_then(|v| v.as_bool())
295 .unwrap_or(false);
296
297 let total_weight = props_rows
298 .first()
299 .and_then(|r| r.get("totalWeight"))
300 .and_then(|v| v.as_i64())
301 .unwrap_or(0);
302
303 let cards_sql = r#"
305 SELECT "cardUuid", "cardWeight"
306 FROM set_booster_sheet_cards
307 WHERE "setCode" = ?
308 AND "boosterName" = ?
309 AND "sheetName" = ?
310 "#;
311
312 let card_rows =
313 self.conn.execute(cards_sql, &[set_code.to_string(), booster_type.to_string(), sheet_name.to_string()])?;
314
315 if card_rows.is_empty() {
316 return Ok(None);
317 }
318
319 let mut cards = serde_json::Map::new();
320 for row in &card_rows {
321 let uuid = row
322 .get("cardUuid")
323 .and_then(|v| v.as_str())
324 .unwrap_or("")
325 .to_string();
326 let weight = row
327 .get("cardWeight")
328 .and_then(|v| v.as_i64())
329 .unwrap_or(1);
330
331 if !uuid.is_empty() {
332 cards.insert(uuid, serde_json::Value::Number(weight.into()));
333 }
334 }
335
336 Ok(Some(serde_json::json!({
337 "allowDuplicates": allow_duplicates,
338 "totalWeight": total_weight,
339 "cards": cards,
340 })))
341 }
342
343 fn fetch_cards_by_uuids(&self, uuids: &[String]) -> Result<Vec<serde_json::Value>> {
345 if uuids.is_empty() {
346 return Ok(Vec::new());
347 }
348
349 self.conn.ensure_views(&["cards"])?;
350
351 let placeholders: Vec<&str> = uuids.iter().map(|_| "?").collect();
353
354 let sql = format!(
355 "SELECT * FROM cards WHERE uuid IN ({})",
356 placeholders.join(", ")
357 );
358
359 let rows = self.conn.execute(&sql, uuids)?;
360
361 let mut card_map: HashMap<String, serde_json::Value> = HashMap::new();
363 for row in rows {
364 if let Some(uuid) = row.get("uuid").and_then(|v| v.as_str()) {
365 let val = serde_json::to_value(&row).unwrap_or(serde_json::Value::Null);
366 card_map.insert(uuid.to_string(), val);
367 }
368 }
369
370 let mut result = Vec::with_capacity(uuids.len());
372 for uuid in uuids {
373 if let Some(card) = card_map.get(uuid) {
374 result.push(card.clone());
375 }
376 }
377
378 Ok(result)
379 }
380}
381
382fn pick_pack(boosters: &[serde_json::Value]) -> &serde_json::Value {
391 let mut rng = rand::rng();
392
393 let total_weight: i64 = boosters
394 .iter()
395 .map(|b| b.get("weight").and_then(|w| w.as_i64()).unwrap_or(1))
396 .sum();
397
398 if total_weight <= 0 {
399 return &boosters[rng.random_range(0..boosters.len())];
400 }
401
402 let mut roll = rng.random_range(0..total_weight);
403
404 for booster in boosters {
405 let w = booster
406 .get("weight")
407 .and_then(|w| w.as_i64())
408 .unwrap_or(1);
409 roll -= w;
410 if roll < 0 {
411 return booster;
412 }
413 }
414
415 boosters.last().unwrap()
417}
418
419fn pick_from_sheet(sheet: &serde_json::Value, count: usize) -> Vec<String> {
425 let mut rng = rand::rng();
426
427 let allow_duplicates = sheet
428 .get("allowDuplicates")
429 .and_then(|v| v.as_bool())
430 .unwrap_or(false);
431
432 let cards = match sheet.get("cards").and_then(|c| c.as_object()) {
433 Some(c) => c,
434 None => return Vec::new(),
435 };
436
437 if cards.is_empty() {
438 return Vec::new();
439 }
440
441 let mut uuids: Vec<String> = Vec::with_capacity(cards.len());
443 let mut weights: Vec<i64> = Vec::with_capacity(cards.len());
444
445 for (uuid, weight_val) in cards {
446 uuids.push(uuid.clone());
447 weights.push(weight_val.as_i64().unwrap_or(1));
448 }
449
450 if allow_duplicates {
451 weighted_choices_with_replacement(&uuids, &weights, count, &mut rng)
453 } else {
454 weighted_choices_without_replacement(&uuids, &weights, count, &mut rng)
456 }
457}
458
459fn weighted_choices_with_replacement(
461 uuids: &[String],
462 weights: &[i64],
463 count: usize,
464 rng: &mut impl Rng,
465) -> Vec<String> {
466 let total_weight: i64 = weights.iter().sum();
467 if total_weight <= 0 {
468 return Vec::new();
469 }
470
471 let mut results = Vec::with_capacity(count);
472 for _ in 0..count {
473 let mut roll = rng.random_range(0..total_weight);
474 for (i, &w) in weights.iter().enumerate() {
475 roll -= w;
476 if roll < 0 {
477 results.push(uuids[i].clone());
478 break;
479 }
480 }
481 }
482 results
483}
484
485fn weighted_choices_without_replacement(
487 uuids: &[String],
488 weights: &[i64],
489 count: usize,
490 rng: &mut impl Rng,
491) -> Vec<String> {
492 let actual_count = count.min(uuids.len());
493 let mut remaining_uuids: Vec<String> = uuids.to_vec();
494 let mut remaining_weights: Vec<i64> = weights.to_vec();
495 let mut results = Vec::with_capacity(actual_count);
496
497 for _ in 0..actual_count {
498 if remaining_uuids.is_empty() {
499 break;
500 }
501
502 let total_weight: i64 = remaining_weights.iter().sum();
503 if total_weight <= 0 {
504 break;
505 }
506
507 let mut roll = rng.random_range(0..total_weight);
508 let mut picked_idx = remaining_uuids.len() - 1;
509
510 for (i, &w) in remaining_weights.iter().enumerate() {
511 roll -= w;
512 if roll < 0 {
513 picked_idx = i;
514 break;
515 }
516 }
517
518 results.push(remaining_uuids.remove(picked_idx));
519 remaining_weights.remove(picked_idx);
520 }
521
522 results
523}