fifa17 store: real 6-pack economy + full-DB pool; drop extPrice
- store_catalog: replace the invented catalogue with the real always-available FUT17 regular packs (Bronze/Prem Bronze/Silver/Prem Silver/Gold/Prem Gold) at real prices + tier composition; PackDef now carries per-tier quantities. - pack_body: drop extPrice (its mtx side-effect switched on the broken "or %1s" FIFA-Points tile line; plan-2026-08-05-store-subsystem.md section 3.4). - pack_content: tier-aware generator draws each pack bronze/silver/gold composition with special_chance bias + empty-tier fallback. - host: CoreAccess::all_definitions (GET /cards); build_content_pool draws the FULL card universe via non-minting catalog lookup, owned-inventory fallback. - economy_differential: store ops reclassified DIFFERENT-BY-DESIGN (Rust is the authoritative store; Python oracle stays the untouched rollback baseline). - fixtures/tests updated to the real catalogue. Odds are DESIGNED placeholders (FUT17 pack probabilities were never published); club items remain excluded (cardtype-9 mapping unknown). Full regression green; real prices + tier-correct draws verified server-side on staging.
This commit is contained in:
@@ -244,7 +244,11 @@ fn pack_open_body(pid: u64, pack: &PackDef) -> WireResponse {
|
||||
"firstPartyStoreId": 0,
|
||||
"groupName": "fifa17",
|
||||
"productId": pid.to_string(),
|
||||
"purchasePackType": if pack.gold { "GOLD" } else { "BRONZE" },
|
||||
"purchasePackType": match pack.category {
|
||||
"gold" => "GOLD",
|
||||
"silver" => "SILVER",
|
||||
_ => "BRONZE",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -695,7 +699,7 @@ mod tests {
|
||||
// ── Store BUY ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn buy_pack1_debits_mints_and_reveals_five_cards() {
|
||||
fn buy_pack1_debits_mints_and_reveals_twelve_cards() {
|
||||
let econ = RecEcon::new(10_000);
|
||||
let pool = pool();
|
||||
let assets = FakeAssets::for_pool(&pool);
|
||||
@@ -705,23 +709,23 @@ mod tests {
|
||||
assert_eq!(resp.status, 200);
|
||||
let b: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||
let cpr = &b["createPackResponse"];
|
||||
assert_eq!(cpr["numberItems"], 5); // pack 1 count
|
||||
assert_eq!(cpr["itemList"].as_array().unwrap().len(), 5);
|
||||
assert_eq!(cpr["numberItems"], 12); // pack 1 count (10 bronze + 2 silver)
|
||||
assert_eq!(cpr["itemList"].as_array().unwrap().len(), 12);
|
||||
assert_eq!(cpr["purchasedPackId"], 1);
|
||||
assert_eq!(cpr["duplicateItemIdList"], json!([]));
|
||||
// Exactly one atomic debit of the pack price (400) minting 5 items.
|
||||
// Exactly one atomic debit of the pack price (400) minting 12 items.
|
||||
assert_eq!(econ.coins(), 10_000 - 400);
|
||||
let purchased = econ.purchased.lock();
|
||||
assert_eq!(purchased.len(), 1);
|
||||
assert_eq!(purchased[0].0, 400);
|
||||
assert_eq!(purchased[0].1.len(), 5);
|
||||
assert_eq!(purchased[0].1.len(), 12);
|
||||
for g in &purchased[0].1 {
|
||||
assert!(pool.iter().any(|c| c.card_id == g.card_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_pack5_debits_gold_price_and_mints_seven() {
|
||||
fn buy_pack5_debits_gold_price_and_mints_twelve() {
|
||||
let econ = RecEcon::new(10_000);
|
||||
let pool = pool();
|
||||
let assets = FakeAssets::for_pool(&pool);
|
||||
@@ -729,7 +733,7 @@ mod tests {
|
||||
let deps = store_deps(&econ, &assets, &ent, &pool);
|
||||
let resp = handle_store_buy(&body(json!({ "packId": 5 })), &deps, &mut rng(2));
|
||||
let b: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||
assert_eq!(b["createPackResponse"]["numberItems"], 7); // pack 5 count
|
||||
assert_eq!(b["createPackResponse"]["numberItems"], 12); // pack 5 count (10 gold + 2 silver)
|
||||
assert_eq!(econ.coins(), 10_000 - 5000);
|
||||
}
|
||||
|
||||
@@ -860,7 +864,7 @@ mod tests {
|
||||
assert_eq!(b["firstPartyStoreId"], 0);
|
||||
assert_eq!(b["purchasePackType"], "GOLD"); // pack 5 is gold
|
||||
assert_eq!(econ.coins(), 10_000 - 5000);
|
||||
assert_eq!(econ.purchased.lock()[0].1.len(), 7);
|
||||
assert_eq!(econ.purchased.lock()[0].1.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -663,6 +663,17 @@ pub trait CoreAccess: Send + Sync {
|
||||
Ok(self.query_owned(&[])?.items)
|
||||
}
|
||||
|
||||
/// Every card DEFINITION in Core content (`GET /cards`) — the full card
|
||||
/// universe a pack can draw from, independent of ownership. Same shape as
|
||||
/// owned items (rating/position/nation/league/club/attributes) so
|
||||
/// [`build_content_pool`] treats definitions and owned items uniformly.
|
||||
/// Default: unimplemented (callers fall back to owned inventory).
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
Err(CoreError::Parse(
|
||||
"Core content enumeration is not implemented".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Read the active squad + its opaque extension for `namespace`.
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError>;
|
||||
|
||||
@@ -746,6 +757,22 @@ impl CoreAccess for HttpCoreClient {
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_page(&v)
|
||||
}
|
||||
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let url = format!("{}/cards", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(CoreError::Status(status));
|
||||
}
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_definitions(&v)
|
||||
}
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError> {
|
||||
let url = format!("{}/squad/ext", self.base_url);
|
||||
let resp = self
|
||||
@@ -1361,6 +1388,38 @@ fn core_item_from_json(e: &Value) -> Option<CoreOwnedItem> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse Core's `/cards` response `{ "cards": [CardDefinition...], ... }` into the
|
||||
/// same [`CoreOwnedItem`] shape as owned items (`owned_card_id` empty — a
|
||||
/// definition is not an instance), for full-universe pool building.
|
||||
pub fn parse_core_definitions(v: &Value) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let arr = v
|
||||
.get("cards")
|
||||
.and_then(|c| c.as_array())
|
||||
.ok_or_else(|| CoreError::Parse("missing `cards` array".into()))?;
|
||||
Ok(arr.iter().filter_map(core_item_from_definition).collect())
|
||||
}
|
||||
|
||||
fn core_item_from_definition(card: &Value) -> Option<CoreOwnedItem> {
|
||||
let attr = |k: &str| card.get(k).and_then(|v| v.as_i64()).unwrap_or(0) as u8;
|
||||
Some(CoreOwnedItem {
|
||||
owned_card_id: String::new(),
|
||||
card_id: card.get("id")?.as_str()?.to_string(),
|
||||
rating: card.get("overall").and_then(|v| v.as_i64()).unwrap_or(0) as u8,
|
||||
position: card.get("position")?.as_str()?.to_string(),
|
||||
nation: card.get("nation")?.as_str()?.to_string(),
|
||||
league: card.get("league")?.as_str()?.to_string(),
|
||||
club: card.get("club")?.as_str()?.to_string(),
|
||||
attributes: [
|
||||
attr("pace"),
|
||||
attr("shooting"),
|
||||
attr("passing"),
|
||||
attr("dribbling"),
|
||||
attr("defending"),
|
||||
attr("physical"),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────── Item identity resolver ───────────────────────
|
||||
|
||||
/// The single production [`ItemIdentityResolver`]: it composes the two distinct
|
||||
@@ -1419,6 +1478,15 @@ impl Fifa17IdentityResolver {
|
||||
.map(|c| c.rareflag)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Non-minting definition identity from the catalog: `Some((rareflag, kind))`
|
||||
/// if the card resolves, else `None`. Used to build the pack pool over ALL
|
||||
/// content definitions WITHOUT allocating a wire id per definition (that would
|
||||
/// pollute the identity store); a real wire id is minted only when a pack draw
|
||||
/// actually mints the card.
|
||||
pub fn definition_identity(&self, card_id: &str) -> Option<(i64, ContentKind)> {
|
||||
self.catalog.lookup(card_id).map(|c| (c.rareflag, c.kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemIdentityResolver for Fifa17IdentityResolver {
|
||||
@@ -2372,30 +2440,40 @@ pub struct EconomyServices {
|
||||
pub sold_experiment: crate::sold_experiment::SoldExperiment,
|
||||
}
|
||||
|
||||
/// Build the pack-content candidate pool from Core's current content, evidenced
|
||||
/// by the owned inventory: every distinct owned card definition that resolves to
|
||||
/// a real FIFA asset id is a candidate (`gold` = rating ≥ 75; `special` from the
|
||||
/// catalog `rareflag > 1`). This is the resolvable FIFA∩Core card universe — the
|
||||
/// cards a pack can award and the shared shaper can render. An empty pool (no
|
||||
/// content, or Core unreachable) is fail-closed by construction: the generator
|
||||
/// returns no cards, so the Store neither mints nor debits.
|
||||
/// Build the pack-content candidate pool. Prefers the FULL card universe (Core
|
||||
/// content, `GET /cards` via [`CoreAccess::all_definitions`]) so a pack can award
|
||||
/// any card in the game, not only cards the profile already owns; falls back to
|
||||
/// owned inventory when content enumeration is unavailable (older Core / tests).
|
||||
/// Each candidate resolves in the FIFA catalog (unmapped or non-player cards are
|
||||
/// dropped, never faked); `gold` = rating ≥ 75, `special` = catalog `rareflag > 1`.
|
||||
/// An empty pool (no content, or Core unreachable) is fail-closed by construction:
|
||||
/// the generator returns no cards, so the Store neither mints nor debits.
|
||||
pub fn build_content_pool(
|
||||
core: &dyn CoreAccess,
|
||||
resolver: &Fifa17IdentityResolver,
|
||||
) -> Vec<GeneratedCandidate> {
|
||||
let owned = match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
// Full universe first; owned inventory only as a degrade path.
|
||||
let items = match core.all_definitions() {
|
||||
Ok(v) if !v.is_empty() => v,
|
||||
_ => match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
};
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pool = Vec::new();
|
||||
for item in &owned {
|
||||
for item in &items {
|
||||
if !seen.insert(item.card_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = resolver.resolve(item) else {
|
||||
// Non-minting catalog lookup: drop unmapped cards and non-player content
|
||||
// (consumables/staff never enter the player-card pack pool).
|
||||
let Some((rareflag, kind)) = resolver.definition_identity(&item.card_id) else {
|
||||
continue;
|
||||
};
|
||||
if kind != ContentKind::Player {
|
||||
continue;
|
||||
}
|
||||
pool.push(GeneratedCandidate {
|
||||
card_id: item.card_id.clone(),
|
||||
rating: item.rating,
|
||||
@@ -2405,7 +2483,7 @@ pub fn build_content_pool(
|
||||
club: item.club.clone(),
|
||||
attributes: item.attributes,
|
||||
gold: item.rating >= 75,
|
||||
special: id.rareflag > 1,
|
||||
special: rareflag > 1,
|
||||
});
|
||||
}
|
||||
pool
|
||||
|
||||
@@ -311,7 +311,7 @@ fn case_a_two_buys(h: &Harness) -> String {
|
||||
refs, 1,
|
||||
"exactly one BUY refused 461 (statuses {statuses:?})"
|
||||
);
|
||||
// The winner minted 5 cards; the loser minted nothing.
|
||||
// The winner minted 12 cards (real Bronze Pack); the loser minted nothing.
|
||||
for r in &rs {
|
||||
if r.status == 200 {
|
||||
assert_eq!(
|
||||
@@ -319,7 +319,7 @@ fn case_a_two_buys(h: &Harness) -> String {
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
5
|
||||
12
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@
|
||||
//! | userMassInfo economy | PARITY | `userInfo.currencies[coins].funds == credits coins`; both |
|
||||
//! | | | carry `unopenedPacks.recoveredPacks==1`. Coins consistent |
|
||||
//! | | | across the credits & massinfo surfaces on each side. |
|
||||
//! | purchasegroup pack70 | PARITY | owned pack present -> id set {1,5,6,7,70}, NO 65534 sentinel. |
|
||||
//! | purchasegroup sentinel | PARITY | empty My Packs + unverified session -> {1,5,6,7,65534}, |
|
||||
//! | | | sentinel `state:"active"`. |
|
||||
//! | purchasegroup clean-v1 | PARITY | empty My Packs + verified capability session -> {1,5,6,7}, |
|
||||
//! | purchasegroup pack70 | DIFFERENT-BY-DESIGN| STORE DIVERGED: Rust serves the real 6-pack catalogue |
|
||||
//! | | | {1,2,3,4,5,6,70}; oracle (rollback) keeps {1,5,6,7,70}. |
|
||||
//! | purchasegroup sentinel | DIFFERENT-BY-DESIGN| empty My Packs + unverified -> rust {1..6,65534} vs oracle |
|
||||
//! | | | {1,5,6,7,65534}; sentinel `state:"active"` on both. |
|
||||
//! | purchasegroup clean-v1 | DIFFERENT-BY-DESIGN| empty + verified capability -> rust {1..6}, oracle {1,5,6,7};|
|
||||
//! | | | sentinel stripped. Rust drives the REAL `SessionStore` state |
|
||||
//! | | | machine (register_capability+open_session+freeze) exactly as |
|
||||
//! | | | the oracle's launcher/auth handshake does. |
|
||||
//! | Store BUY (pack 1) | PARITY | 200; `createPackResponse{itemList(5),numberItems:5, |
|
||||
//! | | | purchasedPackId,duplicateItemIdList}`; coin delta -400. |
|
||||
//! | | | machine (register_capability+open_session+freeze). |
|
||||
//! | Store BUY (pack 1) | DIFFERENT-BY-DESIGN| 200; rust real Bronze Pack -> itemList(12),numberItems:12; |
|
||||
//! | | | oracle placeholder -> 5; both debit 400 + purchasedPackId 1. |
|
||||
//! | POST /purchased open70 | PARITY | 200; envelope `{packId:70,firstPartyStoreId,productId:"70", |
|
||||
//! | | | purchasePackType:"GOLD"}`; coin delta 0; entitlement -1. |
|
||||
//! | GET /purchased reveal | PARITY | Durable single-profile purchased pile on BOTH (Rust reveal |
|
||||
@@ -550,29 +550,43 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
None,
|
||||
)
|
||||
.1;
|
||||
// STORE DIVERGED: Rust is the authoritative store owner and serves the real
|
||||
// 6-pack regular catalogue (ids 1..6 + owned 70). The Python oracle (rollback
|
||||
// baseline, never modified) still serves the old placeholder set {1,5,6,7,70},
|
||||
// so this is Rust-authoritative, NOT oracle parity.
|
||||
assert_eq!(
|
||||
pack_ids(&opg),
|
||||
vec![1, 5, 6, 7, 70],
|
||||
"oracle owned pack70 id set"
|
||||
"oracle (rollback) placeholder id set"
|
||||
);
|
||||
assert_eq!(
|
||||
pack_ids(&rpg),
|
||||
vec![1, 5, 6, 7, 70],
|
||||
"rust owned pack70 id set"
|
||||
vec![1, 2, 3, 4, 5, 6, 70],
|
||||
"rust authoritative real-catalogue id set"
|
||||
);
|
||||
for b in [&opg, &rpg] {
|
||||
assert!(
|
||||
!pack_ids(b).contains(&SENTINEL_PACK_ID),
|
||||
"no 65534 sentinel while a pack is owned"
|
||||
);
|
||||
// packType parity per id (BRONZE for 1, GOLD for the rest).
|
||||
for p in b["purchase"].as_array().unwrap() {
|
||||
let id = p["id"].as_u64().unwrap();
|
||||
let want = if id == 1 { "BRONZE" } else { "GOLD" };
|
||||
assert_eq!(p["packType"], want, "packType parity for pack {id}");
|
||||
}
|
||||
}
|
||||
matrix.push(("purchasegroup pack70", "PARITY"));
|
||||
// packType by side: oracle BRONZE for 1 else GOLD; rust by real category
|
||||
// (1,2 bronze / 3,4 silver / 5,6,70 gold).
|
||||
for p in opg["purchase"].as_array().unwrap() {
|
||||
let id = p["id"].as_u64().unwrap();
|
||||
let want = if id == 1 { "BRONZE" } else { "GOLD" };
|
||||
assert_eq!(p["packType"], want, "oracle packType for pack {id}");
|
||||
}
|
||||
for p in rpg["purchase"].as_array().unwrap() {
|
||||
let id = p["id"].as_u64().unwrap();
|
||||
let want = match id {
|
||||
1 | 2 => "BRONZE",
|
||||
3 | 4 => "SILVER",
|
||||
_ => "GOLD",
|
||||
};
|
||||
assert_eq!(p["packType"], want, "rust packType for pack {id}");
|
||||
}
|
||||
matrix.push(("purchasegroup pack70", "DIFFERENT-BY-DESIGN"));
|
||||
|
||||
// ── OP 4: Store BUY (pack 1 Bronze, price 400, 5 cards) ────────────────
|
||||
let o_bal0 = oracle.coins();
|
||||
@@ -600,11 +614,22 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
.as_array()
|
||||
.expect("rust itemList")
|
||||
.clone();
|
||||
assert_eq!(o_items.len(), 5, "oracle pack1 -> 5 cards");
|
||||
assert_eq!(r_items.len(), 5, "rust pack1 -> 5 cards");
|
||||
// STORE DIVERGED: rust serves the real Bronze Pack (10+2 = 12 cards); the
|
||||
// oracle placeholder awards 5. Both debit the same 400-coin price.
|
||||
assert_eq!(o_items.len(), 5, "oracle (rollback) pack1 -> 5 cards");
|
||||
assert_eq!(r_items.len(), 12, "rust pack1 real Bronze Pack -> 12 cards");
|
||||
assert_eq!(
|
||||
o_buy["createPackResponse"]["numberItems"].as_i64().unwrap(),
|
||||
5,
|
||||
"oracle numberItems==5"
|
||||
);
|
||||
assert_eq!(
|
||||
r_buy["createPackResponse"]["numberItems"].as_i64().unwrap(),
|
||||
12,
|
||||
"rust numberItems==12"
|
||||
);
|
||||
for b in [&o_buy, &r_buy] {
|
||||
let cpr = &b["createPackResponse"];
|
||||
assert_eq!(cpr["numberItems"].as_i64().unwrap(), 5, "numberItems==5");
|
||||
assert_eq!(
|
||||
cpr["purchasedPackId"].as_i64().unwrap(),
|
||||
1,
|
||||
@@ -619,9 +644,9 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
assert_eq!(
|
||||
client.balance().unwrap() - r_bal0,
|
||||
-400,
|
||||
"rust BUY debits 400"
|
||||
"rust BUY debits 400 (price parity)"
|
||||
);
|
||||
matrix.push(("Store BUY (pack1)", "PARITY"));
|
||||
matrix.push(("Store BUY (pack1)", "DIFFERENT-BY-DESIGN"));
|
||||
// Minted wire ids for the item ops that follow (both sides put them in the
|
||||
// pending purchased pile).
|
||||
let o_wire: Vec<i64> = o_items.iter().map(|i| i["id"].as_i64().unwrap()).collect();
|
||||
@@ -1176,8 +1201,8 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
);
|
||||
assert_eq!(
|
||||
pack_ids(&r_pg_s),
|
||||
vec![1, 5, 6, 7, SENTINEL_PACK_ID],
|
||||
"rust empty (unknown SID) -> sentinel"
|
||||
vec![1, 2, 3, 4, 5, 6, SENTINEL_PACK_ID],
|
||||
"rust empty (unknown SID) -> real catalogue + sentinel"
|
||||
);
|
||||
for b in [&o_pg_s, &r_pg_s] {
|
||||
let s = b["purchase"]
|
||||
@@ -1188,7 +1213,7 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
.unwrap();
|
||||
assert_eq!(s["state"], "active", "sentinel state active");
|
||||
}
|
||||
matrix.push(("purchasegroup sentinel", "PARITY"));
|
||||
matrix.push(("purchasegroup sentinel", "DIFFERENT-BY-DESIGN"));
|
||||
|
||||
// ── OP 3c: purchasegroup — clean-v1 (empty + verified capability session) ─
|
||||
// Oracle: register the launcher capability, open a session (auth), present the
|
||||
@@ -1240,11 +1265,11 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
let r_pg_c: Value = serde_json::from_slice(&r_pg_c_resp.body).unwrap();
|
||||
assert_eq!(
|
||||
pack_ids(&r_pg_c),
|
||||
vec![1, 5, 6, 7],
|
||||
"rust clean-v1 strips the sentinel"
|
||||
vec![1, 2, 3, 4, 5, 6],
|
||||
"rust clean-v1 real catalogue, sentinel stripped"
|
||||
);
|
||||
assert!(!pack_ids(&r_pg_c).contains(&SENTINEL_PACK_ID));
|
||||
matrix.push(("purchasegroup clean-v1", "PARITY"));
|
||||
matrix.push(("purchasegroup clean-v1", "DIFFERENT-BY-DESIGN"));
|
||||
|
||||
// ── Emit the matrix for the run log. ────────────────────────────────────
|
||||
eprintln!("\n===== economy_differential PARITY / DIFFERENT-BY-DESIGN matrix =====");
|
||||
|
||||
@@ -373,8 +373,8 @@ fn economy_sequence(base: &str, dir: &std::path::Path) -> SeqResult {
|
||||
.as_array()
|
||||
.expect("itemList")
|
||||
.clone();
|
||||
assert_eq!(items.len(), 5, "pack 1 awards 5 cards");
|
||||
assert_eq!(bv["createPackResponse"]["numberItems"], 5);
|
||||
assert_eq!(items.len(), 12, "pack 1 (real Bronze Pack) awards 12 cards");
|
||||
assert_eq!(bv["createPackResponse"]["numberItems"], 12);
|
||||
assert!(
|
||||
items[0]["id"].as_i64().unwrap() >= 100_000_000,
|
||||
"minted wire id above the FIFA floor"
|
||||
|
||||
Reference in New Issue
Block a user