diff --git a/Cargo.toml b/Cargo.toml index df4ba34..e5b2e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,11 @@ polars = { version = "0.39.2", default-features = false, features = [ "lazy", "concat_str", "strings", + "regex", "csv", "json", + "dtype-struct", + "serde", ] } [target.'cfg(engine)'.dev-dependencies] diff --git a/src/data/card.rs b/src/data/card.rs index 3276afe..08dac82 100644 --- a/src/data/card.rs +++ b/src/data/card.rs @@ -1,20 +1,83 @@ +use core::panic; use once_cell::sync::Lazy; use polars::prelude::*; use serde::{Deserialize, Serialize}; use std::io::Cursor; -use std::{collections::HashMap, hash::Hash}; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, +}; #[cfg(engine)] use std::fs; #[cfg(engine)] use std::path::Path; -enum CartType { - NormalMonster, - EffectMonster, - SpellCard, - TrapCard, - Unknown { name: String }, +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum MonsterCardType { + Regular, + Normal, + Xyz, + Ritual, + Fusion, + Synchro, + Link, + Token, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum MonsterAttribute { + Dark, + Divine, + Earth, + Fire, + Light, + Water, + Wind, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum SpellType { + Normal, + Continuous, + Equip, + QuickPlay, + Field, + Ritual, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum TrapType { + Normal, + Continuous, + Counter, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum CardTypeInfo { + Monster { + level: u32, // level/rank/link rating + atk: u32, + def: Option, + pendulum_scale: Option, + attribute: MonsterAttribute, + monster_type: String, + monster_card_type: MonsterCardType, + tuner: bool, + pendulum: bool, + // abilities + flip: bool, + spirit: bool, + toon: bool, + union: bool, + gemini: bool, + }, + Spell { + spell_type: SpellType, + }, + Trap { + trap_type: TrapType, + }, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -33,8 +96,10 @@ pub struct ArchetypeInfo { pub struct CardInfo { pub id: u32, pub name: String, - pub card_sets: Vec, + pub desc: String, + pub card_type_info: CardTypeInfo, pub archetype: Option, + pub card_sets: Vec, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -43,42 +108,327 @@ pub struct CardInstance { set_id: String, } +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct CardTable { pub cards: HashMap, pub sets: HashMap, pub archetypes: HashMap, + pub monster_types: HashSet, pub df: DataFrame, } impl CardTable { + pub fn df_to_card_info_test(df: DataFrame) -> Vec { + let id_idx = df.get_column_index("id").unwrap(); + let name_idx = df.get_column_index("name").unwrap(); + let desc_idx = df.get_column_index("desc").unwrap(); + let arch_idx = df.get_column_index("archetype").unwrap(); + let type_idx = df.get_column_index("type").unwrap(); + let spell_type_idx = df.get_column_index("spell_type").unwrap(); + let trap_type_idx = df.get_column_index("trap_type").unwrap(); + let level_idx = df.get_column_index("level").unwrap(); + let atk_idx = df.get_column_index("atk").unwrap(); + let def_idx = df.get_column_index("def").unwrap(); + let pendulum_scale_idx = df.get_column_index("scale").unwrap(); + let attribute_idx = df.get_column_index("attribute").unwrap(); + let monster_type_idx = df.get_column_index("monster_type").unwrap(); + let monster_card_type_idx = df.get_column_index("monster_card_type").unwrap(); + let tuner_idx = df.get_column_index("tuner").unwrap(); + let pendulum_idx = df.get_column_index("pendulum").unwrap(); + let flip_idx = df.get_column_index("flip").unwrap(); + let spirit_idx = df.get_column_index("spirit").unwrap(); + let toon_idx = df.get_column_index("toon").unwrap(); + let union_idx = df.get_column_index("union").unwrap(); + let gemini_idx = df.get_column_index("gemini").unwrap(); + + let all_cards: Vec = df + .into_struct("Structs") + .iter() + .map(|row| { + let card_type = row[type_idx].get_str().unwrap(); + + let info = CardInfo { + id: row[id_idx].try_extract().unwrap(), + name: row[name_idx].get_str().unwrap().to_string(), + desc: row[desc_idx].get_str().unwrap().to_string(), + + card_type_info: if card_type.contains("Spell") { + CardTypeInfo::Spell { + spell_type: match row[spell_type_idx].get_str().unwrap() { + "Continuous" => SpellType::Continuous, + "Quick-Play" => SpellType::QuickPlay, + "Equip" => SpellType::Equip, + "Normal" => SpellType::Normal, + "Field" => SpellType::Field, + "Ritual" => SpellType::Ritual, + unknown => panic!("Unknown spell type {}", unknown), + }, + } + } else if card_type.contains("Trap") { + CardTypeInfo::Trap { + trap_type: match row[trap_type_idx].get_str().unwrap() { + "Continuous" => TrapType::Continuous, + "Counter" => TrapType::Counter, + "Normal" => TrapType::Normal, + unknown => panic!("Unknown trap type {}", unknown), + }, + } + } else { + CardTypeInfo::Monster { + level: row[level_idx].try_extract().unwrap(), + atk: row[atk_idx].try_extract().unwrap(), + def: row[def_idx].try_extract().ok(), + pendulum_scale: row[pendulum_scale_idx].try_extract().ok(), + attribute: match row[attribute_idx].get_str().unwrap() { + "DARK" => MonsterAttribute::Dark, + "DIVINE" => MonsterAttribute::Divine, + "EARTH" => MonsterAttribute::Earth, + "FIRE" => MonsterAttribute::Fire, + "LIGHT" => MonsterAttribute::Light, + "WATER" => MonsterAttribute::Water, + "WIND" => MonsterAttribute::Wind, + unknown => panic!("Unknown attribute {}", unknown), + }, + monster_type: row[monster_type_idx] + .get_str() + .unwrap_or("Token") + .to_string(), + monster_card_type: match row[monster_card_type_idx].get_str().unwrap() { + "Fusion" => MonsterCardType::Fusion, + "Link" => MonsterCardType::Link, + "Normal" => MonsterCardType::Normal, + "Regular" => MonsterCardType::Regular, + "Ritual" => MonsterCardType::Ritual, + "Synchro" => MonsterCardType::Synchro, + "Token" => MonsterCardType::Token, + "XYZ" => MonsterCardType::Xyz, + unknown => panic!("Unknown monster card type {}", unknown), + }, + tuner: match row[tuner_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + pendulum: match row[pendulum_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + flip: match row[flip_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + spirit: match row[spirit_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + toon: match row[toon_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + union: match row[union_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + gemini: match row[gemini_idx] { + AnyValue::Boolean(val) => val, + _ => panic!("Expected bool"), + }, + } + }, + + archetype: match row[arch_idx].get_str() { + Some(arch) => Some(String::from(arch)), + None => None, + }, + card_sets: vec![], + }; + info + }) + .collect(); + all_cards + } + #[cfg(engine)] pub fn new_from_server_json(path: &Path) -> Self { // First load json into initial dataframe + // // Override types to be correct + // let raw_df_schema = Schema::from_iter(vec![ + // Field::new("id", DataType::UInt64), + // ]); // TODO list all required files - let raw_df = JsonReader::new(std::fs::File::open("./data/cardinfo.json").unwrap()) + // Convert JSON to dataframe + + use core::arch; + + use axum::handler::HandlerWithoutStateExt; + let df = JsonReader::new(std::fs::File::open(path).unwrap()) .finish() .unwrap(); - let raw_df = raw_df + // Cast to inner "data" dictionary + let df = df .lazy() .select(&[col("data")]) .explode(vec!["data"]) .unnest(vec!["data"]) .collect() .unwrap(); - log::info!("{:?}", &raw_df); + // Cast headers dtypes to be correct + let df = df + .lazy() + .with_columns([ + col("id").cast(DataType::UInt32), + col("atk").cast(DataType::UInt32), + col("def").cast(DataType::UInt32), + col("level").cast(DataType::UInt32), + col("linkval").cast(DataType::UInt32), + col("scale").cast(DataType::UInt32), + ]) + // Merge linkval and level columns + .with_columns([col("level").fill_null(col("linkval"))]) + // Create separate columns for monster/spell/trap for readability + .with_columns([ + when(col("type").str().contains(lit("Monster"), false)) + .then(col("race")) + .otherwise(lit(NULL)) + .alias("monster_type"), + when(col("type").str().contains(lit("Spell"), false)) + .then(col("race")) + .otherwise(lit(NULL)) + .alias("spell_type"), + when(col("type").str().contains(lit("Trap"), false)) + .then(col("race")) + .otherwise(lit(NULL)) + .alias("trap_type"), + ]) + // Create separate columns for monster type attributes + .with_columns( + [when(col("type").str().contains(lit("XYZ.*Monster"), false)) + .then(lit("XYZ")) + .when(col("type").str().contains(lit("Ritual.*Monster"), false)) + .then(lit("Ritual")) + .when(col("type").str().contains(lit("Fusion.*Monster"), false)) + .then(lit("Fusion")) + .when(col("type").str().contains(lit("Synchro.*Monster"), false)) + .then(lit("Synchro")) + .when(col("type").str().contains(lit("Link.*Monster"), false)) + .then(lit("Link")) + .when(col("type").str().contains(lit("Normal.*Monster"), false)) + .then(lit("Normal")) + .when(col("type").str().contains(lit("Token"), false)) + .then(lit("Token")) + // default monster case + .when(col("type").str().contains(lit("Monster"), false)) + .then(lit("Regular")) + .otherwise(lit(NULL)) + .alias("monster_card_type")], + ) + .with_columns([ + when(col("type").str().contains(lit("Pendulum.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("pendulum"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Tuner.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("tuner"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Flip.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("flip"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Spirit.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("spirit"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Toon.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("toon"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Union.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("union"), + ]) + .with_columns([ + when(col("type").str().contains(lit("Gemini.*Monster"), false)) + .then(lit(true)) + // default monster case + .when(col("type").str().contains(lit("Monster|Token"), false)) + .then(lit(false)) + .otherwise(lit(NULL)) + .alias("gemini"), + ]) + .select([col("*").exclude([ + "monster_desc", + "pend_desc", + "frameType", + "ygoprodeck_url", + "linkval", + "race" + ])]) + // Remove link markers, unless it's needed later + .select([col("*").exclude(["linkmarkers"])]) + // TODO add banlist support + .select([col("*").exclude(["banlist_info"])]) + // TODO readd + .select([col("*").exclude([ + "card_sets", + "card_images", + "card_prices", + ])]) + // Filter out "Skill Card" + .filter(col("type").str().contains(lit("Skill Card"), false).not()) + // Filters for testing + // .filter(col("type").str().contains(lit("Monster"), false)) + // .filter(col("type").str().contains(lit("Token"), false)) + // Final dataframe + .collect() + .unwrap(); - let id_col = UInt32Chunked::new("id_row", &[1]).into_series(); + log::info!("{:?}", &df); - let cards = HashMap::new(); + let all_cards = Self::df_to_card_info_test(df.clone()); + + let mut cards = HashMap::new(); let sets = HashMap::new(); let archetypes = HashMap::new(); + let monster_types = HashSet::new(); - let df = DataFrame::new(vec![id_col]).unwrap(); + for card in all_cards.iter() { + cards.insert(card.id, card.clone()); + } Self { cards, sets, archetypes, + monster_types, df, } } @@ -90,12 +440,14 @@ impl CardTable { let cards = HashMap::new(); let sets = HashMap::new(); let archetypes = HashMap::new(); + let monster_types = HashSet::new(); let df = DataFrame::new(vec![id_col]).unwrap(); Self { cards, sets, archetypes, + monster_types, df, } } diff --git a/src/data/store.rs b/src/data/store.rs index 71000be..6af3b40 100644 --- a/src/data/store.rs +++ b/src/data/store.rs @@ -4,30 +4,20 @@ use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::{fs, path::Path, sync::Mutex}; +use super::card::CardTable; + #[derive(Serialize, Deserialize, Clone)] pub struct Store { - // pub matches: PoolMatchList, + pub card_table: CardTable, } impl Store { fn new() -> Store { - fs::create_dir_all("data").unwrap(); - match Path::new("data/store.json").exists() { - false => Store { - // matches: PoolMatchList::new(), - }, - true => { - let contents = fs::read_to_string("data/store.json").unwrap(); - serde_json::from_str(&contents).unwrap() - } - } + // fs::create_dir_all("data").unwrap(); + let card_table = CardTable::new_from_server_json(Path::new("./data/cardinfo.json")); + Store { card_table} } // TODO -> Store data - #[allow(dead_code)] - pub fn write(&self) { - let contents = serde_json::to_string(&self).unwrap(); - fs::write("data/store.json", contents).unwrap(); - } } pub static DATA: Lazy> = Lazy::new(|| Mutex::new(Store::new())); diff --git a/src/main.rs b/src/main.rs index 806d3dc..b862d4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,9 +42,6 @@ pub async fn dflt_server GlobalStateCreator { @@ -25,7 +27,11 @@ pub fn get_global_state_creator() -> GlobalStateCreator { #[engine_only_fn] fn get_state() -> AppState { - AppState {} + let card_table = thread::spawn(move || DATA.lock().unwrap().deref().card_table.clone()) + .join() + .unwrap(); + + AppState { card_table } } #[engine_only_fn]