1
Fork 0
mirror of https://github.com/Steffo99/patched-porobot.git synced 2024-12-23 01:54:22 +00:00

Batch of changes for v0.8.0 (#2)

See changelog for more details.
This commit is contained in:
Steffo 2023-01-30 18:33:50 +01:00 committed by GitHub
parent c0ed6189e5
commit e91e89cab1
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 399 additions and 89 deletions

2
Cargo.lock generated
View file

@ -1039,7 +1039,7 @@ dependencies = [
[[package]] [[package]]
name = "patched_porobot" name = "patched_porobot"
version = "0.7.1" version = "0.8.0"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"glob", "glob",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "patched_porobot" name = "patched_porobot"
version = "0.7.1" version = "0.8.0"
authors = ["Stefano Pigozzi <me@steffo.eu>"] authors = ["Stefano Pigozzi <me@steffo.eu>"]
edition = "2021" edition = "2021"
description = "Legends of Runeterra card database utilities and bots" description = "Legends of Runeterra card database utilities and bots"
@ -23,9 +23,9 @@ regex = { version = "1.6.0" }
lazy_static = { version = "1.4.0" } lazy_static = { version = "1.4.0" }
data-encoding = { version = "2.3.2" } data-encoding = { version = "2.3.2" }
varint-rs = { version = "2.2.0" } varint-rs = { version = "2.2.0" }
glob = { version = "0.3.0" }
# exec # exec
pretty_env_logger = { version = "0.4.0", optional = true } pretty_env_logger = { version = "0.4.0", optional = true }
glob = { version = "0.3.0", optional = true }
# data # data
serde = { version = "1.0.140", features = ["derive"] } serde = { version = "1.0.140", features = ["derive"] }
serde_json = { version = "1.0.82" } serde_json = { version = "1.0.82" }
@ -43,7 +43,7 @@ rand = { version = "0.8.5", optional = true }
[features] [features]
# data = [] # Always included # data = [] # Always included
exec = ["pretty_env_logger", "glob"] exec = ["pretty_env_logger"]
search = ["tantivy"] search = ["tantivy"]
telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5", "rand"] telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5", "rand"]
discord = ["exec", "search"] discord = ["exec", "search"]

View file

@ -46,7 +46,7 @@ impl CoreBundle {
let locale = metadata.locale().ok_or(LoadingError::GettingLocale)?; let locale = metadata.locale().ok_or(LoadingError::GettingLocale)?;
let globals_path = &bundle_path let globals_path = &bundle_path
.join(&locale) .join(locale)
.join("data") .join("data")
.join(format!("globals-{}.json", &locale)); .join(format!("globals-{}.json", &locale));
@ -59,3 +59,19 @@ impl CoreBundle {
}) })
} }
} }
/// Create [`globals::LocalizedGlobalsIndexes`] from the core bundle in the current working directory.
///
/// This function tries to load data from the first directory matching the [glob] `./data/core-*`.
pub fn create_globalindexes_from_wd() -> globals::LocalizedGlobalsIndexes {
let path = glob::glob("./data/core-*")
.expect("glob to be a valid glob")
.filter_map(Some)
.find_map(Result::ok)
.expect("a valid core bundle to exist");
let core = CoreBundle::load(&path)
.expect("to be able to load `core-en_us` bundle");
globals::LocalizedGlobalsIndexes::from(core.globals)
}

View file

@ -2,13 +2,15 @@
use super::format::DeckCodeFormat; use super::format::DeckCodeFormat;
use crate::data::deckcode::version::{DeckCodeVersion, DeckCodeVersioned}; use crate::data::deckcode::version::{DeckCodeVersion, DeckCodeVersioned};
use crate::data::setbundle::card::{Card, CardIndex};
use crate::data::setbundle::code::CardCode; use crate::data::setbundle::code::CardCode;
use crate::data::setbundle::region::CardRegion; use crate::data::setbundle::region::CardRegion;
use crate::data::setbundle::set::CardSet; use crate::data::setbundle::set::CardSet;
use itertools::Itertools; use itertools::Itertools;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read, Write};
use varint_rs::{VarintReader, VarintWriter}; use varint_rs::{VarintReader, VarintWriter};
use crate::data::setbundle::supertype::CardSupertype;
/// A unshuffled Legends of Runeterra card deck. /// A unshuffled Legends of Runeterra card deck.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -408,6 +410,170 @@ impl Deck {
Ok(Self::encode_code(&cursor.into_inner())) Ok(Self::encode_code(&cursor.into_inner()))
} }
/// Get an [`Iterator`] of all Champion [`Card`]s in the deck.
pub fn champions<'a>(&'a self, cards: &'a CardIndex) -> impl Iterator<Item = &'a Card> {
self.contents.keys()
.filter_map(|cc| cc.to_card(cards))
.filter(|c| c.supertype == CardSupertype::Champion)
}
/// Count the number of copies of the given card present in this deck.
///
/// # Example
///
/// ```rust
/// use patched_porobot::deck;
/// use patched_porobot::data::deckcode::deck::Deck;
/// use patched_porobot::data::setbundle::code::CardCode;
///
/// let d: Deck = deck![
/// "01DE002": 40,
/// ];
///
/// let copies = d.copies_of(&CardCode::from("01DE002".to_string()));
/// assert_eq!(copies, 40);
/// ```
pub fn copies_of(&self, code: &CardCode) -> u32 {
self.contents.get(code).map_or(0, ToOwned::to_owned)
}
/// Get the number of cards in the deck.
///
/// In the *Standard* and *Singleton* formats, this is never more than 40.
///
/// # Example
///
/// ```rust
/// use patched_porobot::deck;
/// use patched_porobot::data::deckcode::deck::Deck;
/// use patched_porobot::data::setbundle::card::CardIndex;
/// use patched_porobot::data::setbundle::create_cardindex_from_wd;
///
/// let index: CardIndex = create_cardindex_from_wd();
/// let deck: Deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
/// assert_eq!(d.card_count(&index), 40);
///
pub fn card_count(&self) -> u32 {
self.contents.values().sum()
}
/// Get the number of champion cards in the deck.
///
/// In the *Standard* and *Singleton* format, this is never more than 6.
///
/// # Example
///
/// ```rust
/// use patched_porobot::deck;
/// use patched_porobot::data::deckcode::deck::Deck;
/// use patched_porobot::data::setbundle::card::CardIndex;
/// use patched_porobot::data::setbundle::create_cardindex_from_wd;
///
/// let index: CardIndex = create_cardindex_from_wd();
/// let deck: Deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
/// assert_eq!(d.champions_count(&index), 6);
/// ```
pub fn champions_count(&self, cards: &CardIndex) -> u32 {
self.champions(cards)
.map(|c| &c.code)
.map(|cc| self.copies_of(cc))
.sum()
}
/// Find the first possible set of regions that the [`Deck`] fits in.
///
/// Lower amounts of regions are preferred: the limit will be increased from 1 up to `limit` until a valid solution is found.
///
/// # Warning
///
/// This method traverses the tree of possible region selections until one is found.
///
/// The time required to run this function may grow exponentially with the amount of cards in the deck!
pub fn regions(&self, card_index: &CardIndex, limit: usize) -> Option<HashSet<CardRegion>> {
let cards: Vec<&Card> = self.contents.keys()
.flat_map(|cc| cc.to_card(card_index))
.collect();
for n in 1..=limit {
let result = Self::regions_recursive_first_limit(cards.as_slice(), HashSet::new(), n);
if result.is_some() {
return result;
}
}
None
}
/// Find the first possible set of regions that covers all given [`Card`]s.
///
/// This function is *recursive*: the `cards` parameter holds the [`Card`]s left to process, while the `regions` parameter holds the regions found so far, and the `limit` parameter holds the size of the `regions` set to stop the recursion at.
///
/// # Warning
///
/// This method traverses the tree of possible region selections until one is found.
///
/// The time required to run this function may grow exponentially with the size of `cards`!
fn regions_recursive_first_limit(cards: &[&Card], regions: HashSet<CardRegion>, limit: usize) -> Option<HashSet<CardRegion>> {
match cards.get(0) {
None => Some(regions),
Some(card) => {
card.regions.iter()
.map(|region| {
match region {
CardRegion::Unsupported => {
None
}
CardRegion::Runeterra => {
Self::regions_recursive_first_limit(&cards[1..], regions.clone(), limit)
}
_ => {
let mut regions = regions.clone();
let inserted = regions.insert(*region);
match inserted && regions.len() > limit {
true => None,
false => Self::regions_recursive_first_limit(&cards[1..], regions, limit)
}
}
}
})
.find(Option::is_some)
.unwrap_or(None)
},
}
}
/// Check if the [`Deck`] is legal for play in the *Standard* format.
///
/// # Returns
///
/// - `None` if the deck is not legal for *Standard* play.
/// - `Some(regions)` if the deck is legal for *Standard* play considering the specified region set.
pub fn standard(&self, cards: &CardIndex) -> Option<HashSet<CardRegion>> {
let copies_limit = self.contents.values().all(|n| n <= &3);
let cards_limit = self.card_count() == 40;
let champions_limit = self.champions_count(cards) <= 6;
let regions = self.regions(cards, 2);
match copies_limit && cards_limit && champions_limit {
false => None,
true => regions,
}
}
/// Check if the [`Deck`] is legal for play in the *Singleton* format.
pub fn singleton(&self, cards: &CardIndex) -> Option<HashSet<CardRegion>> {
let copies_limit = self.contents.values().all(|n| n <= &1);
let cards_limit = self.card_count() == 40;
let champions_limit = self.champions_count(cards) <= 6;
let regions = self.regions(cards, 3);
match copies_limit && cards_limit && champions_limit {
false => None,
true => regions,
}
}
} }
/// An error occoured while decoding a [`Deck`] from a code. /// An error occoured while decoding a [`Deck`] from a code.
@ -448,9 +614,26 @@ pub type DeckDecodingResult<T> = Result<T, DeckDecodingError>;
/// The [`Result`] of a [`Deck`] **encoding** operation, for example [`Deck::to_code`]. /// The [`Result`] of a [`Deck`] **encoding** operation, for example [`Deck::to_code`].
pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>; pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>;
/// Macro to build a deck from card code strings and quantities. /// Macro to build a deck.
/// ///
/// # Example /// Useful to quickly build [`Deck`]s from trusted input, such as when creating tests.
///
/// It can build a deck:
///
/// - from a deck code;
/// - from card code strings and quantities.
///
/// # Panics
///
/// If the deck code is not valid.
///
/// # Examples
///
/// ```rust
/// use patched_porobot::deck;
///
/// let _my_deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
/// ```
/// ///
/// ```rust /// ```rust
/// use patched_porobot::deck; /// use patched_porobot::deck;
@ -480,13 +663,17 @@ pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>;
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! deck { macro_rules! deck {
[$($cd:literal: $qty:literal),* $(,)?] => { [ $($cd:literal: $qty:expr),* $(,)? ] => {
$crate::data::deckcode::deck::Deck { $crate::data::deckcode::deck::Deck {
contents: std::collections::HashMap::from([ contents: std::collections::HashMap::from([
$(($crate::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)* $(($crate::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)*
]) ])
} }
} };
( $code:expr ) => {
$crate::data::deckcode::deck::Deck::from_code($code).expect("deck code created with deck!() to be valid")
};
} }
#[rustfmt::skip::macros(test_de_ser, test_ser_de)] #[rustfmt::skip::macros(test_de_ser, test_ser_de)]
@ -653,5 +840,64 @@ mod tests {
"02BW012": 69, "02BW012": 69,
]); ]);
//test_ser_de!(test_ser_de_, deck![]); // test_ser_de!(test_ser_de_, deck![]);
macro_rules! test_legality {
( $id:ident, $deck:expr, $check:path, $assert:expr ) => {
#[test]
fn $id() {
let index = $crate::data::setbundle::create_cardindex_from_wd();
let deck: Deck = $deck;
let result = $check(&deck, &index).is_some();
assert_eq!(result, $assert);
}
}
}
test_legality!(
test_legality_standard_lonelyporo1,
deck!("CEAAAAIBAEAQQ"),
Deck::standard, false
);
test_legality!(
test_legality_standard_twistedshrimp,
deck!("CICACBAFAEBAGBQICABQCBJLF4YQOAQGAQEQYEQUDITAAAIBAMCQO"),
Deck::standard, true
);
test_legality!(
test_legality_standard_poros,
deck!("CQDQCAQBAMAQGAICAECACDYCAECBIFYCAMCBEEYCAUFIYANAAEBQCAIICA2QCAQBAEVTSAA"),
Deck::standard, true
);
test_legality!(
test_legality_standard_sand,
deck!("CMBAGBAHANTXEBQBAUCAOFJGFIYQEAIBAUOQIBAHGM5HM6ICAECAOOYCAECRSGY"),
Deck::standard, true
);
test_legality!(
test_legality_singleton_lonelyporo1,
deck!("CEAAAAIBAEAQQ"),
Deck::singleton, false
);
test_legality!(
test_legality_singleton_twistedshrimp,
deck!("CICACBAFAEBAGBQICABQCBJLF4YQOAQGAQEQYEQUDITAAAIBAMCQO"),
Deck::singleton, false
);
test_legality!(
test_legality_singleton_poros,
deck!("CQDQCAQBAMAQGAICAECACDYCAECBIFYCAMCBEEYCAUFIYANAAEBQCAIICA2QCAQBAEVTSAA"),
Deck::singleton, false
);
test_legality!(
test_legality_singleton_sand,
deck!("CMBAGBAHANTXEBQBAUCAOFJGFIYQEAIBAUOQIBAHGM5HM6ICAECAOOYCAECRSGY"),
Deck::singleton, false
);
test_legality!(
test_legality_singleton_paltri,
deck!("CQAAADABAICACAIFBLAACAIFAEHQCBQBEQBAGBADAQBAIAIKBUBAKBAWDUBQIBACA4GAMAIBAMCAYHJBGADAMBAOCQKRMKBLA4AQIAQ3D4QSIKZYBACAODJ3JRIW3AABQIAYUAI"),
Deck::singleton, true
);
} }

View file

@ -77,9 +77,6 @@ impl DeckCodeVersioned for CardRegion {
CardRegion::BandleCity => Some(DeckCodeVersion::V4), CardRegion::BandleCity => Some(DeckCodeVersion::V4),
CardRegion::Runeterra => Some(DeckCodeVersion::V5), CardRegion::Runeterra => Some(DeckCodeVersion::V5),
CardRegion::Jhin => Some(DeckCodeVersion::V5),
CardRegion::Evelynn => Some(DeckCodeVersion::V5),
CardRegion::Bard => Some(DeckCodeVersion::V5),
_ => None, _ => None,
} }

View file

@ -2,7 +2,7 @@
use crate::data::setbundle::card::{Card, CardIndex}; use crate::data::setbundle::card::{Card, CardIndex};
/// The internal code of a [Card](super::card::Card). /// The internal code of a [`Card`].
/// ///
/// It is a ASCII string composed of the following segments: /// It is a ASCII string composed of the following segments:
/// - `0..2`: set; /// - `0..2`: set;
@ -98,3 +98,10 @@ impl From<String> for CardCode {
CardCode { full } CardCode { full }
} }
} }
/// Extract the card code from a [`Card`].
impl From<Card> for CardCode {
fn from(c: Card) -> Self {
c.code
}
}

View file

@ -6,7 +6,7 @@
use super::anybundle::metadata::BundleMetadata; use super::anybundle::metadata::BundleMetadata;
use crate::data::anybundle::outcomes::{LoadingError, LoadingResult}; use crate::data::anybundle::outcomes::{LoadingError, LoadingResult};
use std::fs::File; use std::fs::File;
use std::path::Path; use std::path::{Path, PathBuf};
pub mod art; pub mod art;
pub mod card; pub mod card;
@ -50,7 +50,7 @@ impl SetBundle {
let mut json_filename = name.to_os_string(); let mut json_filename = name.to_os_string();
json_filename.push(".json"); json_filename.push(".json");
&bundle_path.join(&locale).join("data").join(&json_filename) &bundle_path.join(locale).join("data").join(&json_filename)
}; };
let name = name let name = name
@ -70,3 +70,37 @@ impl SetBundle {
}) })
} }
} }
/// Create a [`card::CardIndex`] from set bundles in the given paths.
///
/// # Panics
///
/// If any of the required files cannot be loaded (see [`SetBundle::load`]).
pub fn create_cardindex_from_paths(paths: impl Iterator<Item = PathBuf>) -> card::CardIndex {
let mut index = card::CardIndex::new();
for path in paths {
let set = SetBundle::load(&path).expect("to be able to load SetBundle");
for card in set.cards {
index.insert(card.code.clone(), card);
}
};
index
}
/// Create a [`card::CardIndex`] from set bundles in the current working directory.
///
/// This function tries to load data from any directory matching the [glob] `./data/set*-*`.
///
/// # Panics
///
/// See [`create_cardindex_from_paths`].
pub fn create_cardindex_from_wd() -> card::CardIndex {
let paths = glob::glob("./data/set*-*")
.expect("glob to be a valid glob")
.filter_map(Some)
.filter_map(Result::ok);
create_cardindex_from_paths(paths)
}

View file

@ -32,22 +32,15 @@ pub enum CardRegion {
/// Runeterra. /// Runeterra.
Runeterra, Runeterra,
/// Origin: The Virtuoso.
Jhin,
/// Origin: Agony's Embrace.
Evelynn,
/// Origin: The Wandering Caretaker.
Bard,
/// Unsupported region. /// Unsupported region.
#[serde(other)] #[serde(other)]
Unsupported, Unsupported,
} }
impl CardRegion { impl CardRegion {
/// Get the [LocalizedCardRegion] associated with this [CardRegion]. /// Get the [`LocalizedCardRegion`] associated with this [`CardRegion`].
/// ///
/// Returns [Option::None] if no matching [LocalizedCardRegion] was found, for example for [CardRegion::Unsupported] regions. /// Returns [`None`] if no matching [`LocalizedCardRegion`] was found, for example for [`CardRegion::Unsupported`] regions.
/// ///
/// Equivalent to calling [LocalizedCardRegionIndex::get]. /// Equivalent to calling [LocalizedCardRegionIndex::get].
pub fn localized<'hm>( pub fn localized<'hm>(
@ -79,7 +72,7 @@ impl CardRegion {
/// Get the short code of this [`CardRegion`]. /// Get the short code of this [`CardRegion`].
/// ///
/// If the region has no short code, it will return [`Option::None`]. /// If the region has no short code, it will return [`None`].
pub fn to_code(&self) -> Option<String> { pub fn to_code(&self) -> Option<String> {
match self { match self {
Self::Demacia => Some("DE".to_string()), Self::Demacia => Some("DE".to_string()),
@ -122,7 +115,7 @@ impl From<u32> for CardRegion {
/// Get the internal id of this [`CardRegion`]. /// Get the internal id of this [`CardRegion`].
/// ///
/// If the region has no internal id, it will return [`Result::Err`]. /// If the region has no internal id, it will return [`Err`].
impl TryFrom<CardRegion> for u32 { impl TryFrom<CardRegion> for u32 {
type Error = (); type Error = ();
@ -169,8 +162,5 @@ mod tests {
test_deserialization!(deserialize_piltoverzaun, r#""PiltoverZaun""#, CardRegion::PiltoverZaun); test_deserialization!(deserialize_piltoverzaun, r#""PiltoverZaun""#, CardRegion::PiltoverZaun);
test_deserialization!(deserialize_bandlecity, r#""BandleCity""#, CardRegion::BandleCity); test_deserialization!(deserialize_bandlecity, r#""BandleCity""#, CardRegion::BandleCity);
test_deserialization!(deserialize_runeterra, r#""Runeterra""#, CardRegion::Runeterra); test_deserialization!(deserialize_runeterra, r#""Runeterra""#, CardRegion::Runeterra);
test_deserialization!(deserialize_jhin, r#""Jhin""#, CardRegion::Jhin);
test_deserialization!(deserialize_evelynn, r#""Evelynn""#, CardRegion::Evelynn);
test_deserialization!(deserialize_bard, r#""Bard""#, CardRegion::Bard);
test_deserialization!(deserialize_fallback, r#""Xyzzy""#, CardRegion::Unsupported); test_deserialization!(deserialize_fallback, r#""Xyzzy""#, CardRegion::Unsupported);
} }

View file

@ -2,7 +2,7 @@
use crate::data::corebundle::speed::{LocalizedSpellSpeed, LocalizedSpellSpeedIndex}; use crate::data::corebundle::speed::{LocalizedSpellSpeed, LocalizedSpellSpeedIndex};
/// A possible [Spell](super::type::CardType::Spell) speed. /// A possible [`Spell`](super::r#type::CardType::Spell) speed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum SpellSpeed { pub enum SpellSpeed {
/// Non-spell cards have this speed. /// Non-spell cards have this speed.
@ -17,11 +17,11 @@ pub enum SpellSpeed {
} }
impl SpellSpeed { impl SpellSpeed {
/// Get the [LocalizedSpellSpeed] associated with this [SpellSpeed]. /// Get the [`LocalizedSpellSpeed`] associated with this [`SpellSpeed`].
/// ///
/// Returns [Option::None] if no matching [LocalizedSpellSpeed] was found, for example spell speeds missing from the index. /// Returns [`None`] if no matching [`LocalizedSpellSpeed`] was found, for example spell speeds missing from the index.
/// ///
/// Equivalent to calling [LocalizedSpellSpeedIndex::get]. /// Equivalent to calling [`LocalizedSpellSpeedIndex::get`].
pub fn localized<'hm>( pub fn localized<'hm>(
&self, &self,
hm: &'hm LocalizedSpellSpeedIndex, hm: &'hm LocalizedSpellSpeedIndex,

View file

@ -2,6 +2,8 @@
//! //!
//! [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style //! [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
use std::cmp::Ordering::{Equal, Greater, Less};
use std::collections::HashSet;
use crate::data::corebundle::globals::LocalizedGlobalsIndexes; use crate::data::corebundle::globals::LocalizedGlobalsIndexes;
use crate::data::corebundle::keyword::LocalizedCardKeywordIndex; use crate::data::corebundle::keyword::LocalizedCardKeywordIndex;
use crate::data::corebundle::region::LocalizedCardRegionIndex; use crate::data::corebundle::region::LocalizedCardRegionIndex;
@ -161,33 +163,79 @@ fn display_levelup(levelup: &String) -> String {
/// ///
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style /// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&str>) -> String { pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&str>) -> String {
// TODO: optimize this
let cards = deck let cards = deck
.contents .contents
.keys() .keys()
.sorted_by(|a, b| { .map(|k| (
let card_a = index.get(a).expect("card to exist in the index"); index.get(k),
let card_b = index.get(b).expect("card to exist in the index"); deck.contents.get(k).expect("CardCode from Deck to have a quantity"),
))
.sorted_by(|(opt_a, _), (opt_b, _)| {
if opt_a.is_none() && opt_b.is_none() {
return Equal;
}
if opt_b.is_none() {
return Greater;
}
else if opt_a.is_none() {
return Less;
}
let card_a = opt_a.expect("opt_a to be Some");
let card_b = opt_b.expect("opt_b to be Some");
card_a card_a
.cost .cost
.cmp(&card_b.cost) .cmp(&card_b.cost)
.then(card_a.name.cmp(&card_b.name)) .then(card_a.name.cmp(&card_b.name))
}) })
.map(|k| { .map(|(card, quantity)| {
let card = index.get(k).expect("card to exist in the index"); let name = match card {
let quantity = deck.contents.get(k).unwrap(); None => "<i>Unknown Card</i>".to_string(),
Some(card) => match card.supertype {
CardSupertype::Champion => format!("<u>{}</u>", escape(&card.name)),
_ => escape(&card.name),
}
};
if card.supertype == CardSupertype::Champion { format!("<b>{}×</b> {}", &quantity, &name)
format!("<b>{}×</b> <u>{}</u>", &quantity, &card.name)
} else {
format!("<b>{}×</b> {}", &quantity, &card.name)
}
}) })
.join("\n"); .join("\n");
let mut tags: Vec<&'static str> = vec![];
let regions = if let Some(regions) = deck.standard(&index) {
tags.push("#Standard");
regions
} else if let Some(regions) = deck.singleton(&index) {
tags.push("#Singleton");
regions
} else {
HashSet::new()
};
for region in regions {
tags.push(match region {
CardRegion::Noxus => "#Noxus",
CardRegion::Demacia => "#Demacia",
CardRegion::Freljord => "#Freljord",
CardRegion::ShadowIsles => "#ShadowIsles",
CardRegion::Targon => "#Targon",
CardRegion::Ionia => "#Ionia",
CardRegion::Bilgewater => "#Bilgewater",
CardRegion::Shurima => "#Shurima",
CardRegion::PiltoverZaun => "#PiltoverZaun",
CardRegion::BandleCity => "#BandleCity",
CardRegion::Runeterra => "#Runeterra",
CardRegion::Unsupported => "<i>Unknown</i>",
})
}
let tags = tags.join(", ");
let tags = if tags.len() > 0 { format!("{}\n", &tags) } else { "".to_string() };
match name { match name {
Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n\n{}", &name, &code, &cards), Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n{}\n{}", &name, &code, &tags, &cards),
None => format!("<code>{}</code>\n\n{}", &code, &cards), None => format!("<code>{}</code>\n{}\n{}", &code, &tags, &cards),
} }
} }

View file

@ -55,6 +55,8 @@ pub fn deck_to_inlinequeryresult(
.to_code(DeckCodeFormat::F1) .to_code(DeckCodeFormat::F1)
.expect("serialized deck to deserialize properly"); .expect("serialized deck to deserialize properly");
let message_text = display_deck(index, deck, &code, &name);
InlineQueryResult::Article(InlineQueryResultArticle { InlineQueryResult::Article(InlineQueryResultArticle {
id: format!("{}:{:x}", &crystal, md5::compute(&code)), id: format!("{}:{:x}", &crystal, md5::compute(&code)),
title: match &name { title: match &name {

View file

@ -1,14 +1,10 @@
//! This module defines the [`main`] function for `patched_porobot_telegram`. //! This module defines the [`main`] function for `patched_porobot_telegram`.
use crate::data::corebundle::globals::LocalizedGlobalsIndexes; use crate::data::corebundle::create_globalindexes_from_wd;
use crate::data::corebundle::CoreBundle; use crate::data::setbundle::create_cardindex_from_wd;
use crate::data::setbundle::card::{Card, CardIndex};
use crate::data::setbundle::SetBundle;
use crate::search::cardsearch::CardSearchEngine; use crate::search::cardsearch::CardSearchEngine;
use crate::telegram::handler::{inline_query_handler, message_handler}; use crate::telegram::handler::{inline_query_handler, message_handler};
use glob::glob;
use log::*; use log::*;
use std::path::PathBuf;
use rand::Rng; use rand::Rng;
use teloxide::prelude::*; use teloxide::prelude::*;
@ -17,43 +13,17 @@ pub async fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
debug!("Logger initialized successfully!"); debug!("Logger initialized successfully!");
debug!("Loading core bundle..."); debug!("Creating LocalizedGlobalIndexes...");
let core = CoreBundle::load(&*PathBuf::from("./data/core-en_us")) let globals = create_globalindexes_from_wd();
.expect("to be able to load `core-en_us` bundle"); debug!("Created LocalizedGlobalIndexes!");
debug!("Loaded core bundle successfully!");
debug!("Loading set bundles..."); debug!("Creating CardIndex...");
let setpaths = glob("./data/set*-*") let cards = create_cardindex_from_wd();
.expect("setglob to be a valid glob") debug!("Created CardIndex!");
.into_iter()
.filter(|sp| sp.is_ok())
.map(|sp| sp.unwrap());
let mut cards: Vec<Card> = vec![];
for setpath in setpaths {
debug!("Loading {:?}...", &setpath);
let set = SetBundle::load(&setpath).expect(&*format!(
"to be able to load {:?} as a set bundle",
&setpath
));
let mut setcards = set.cards;
cards.append(&mut setcards);
}
debug!("Loaded {} cards!", &cards.len());
debug!("Indexing globals..."); debug!("Creating CardSearchEngine...");
let globals = LocalizedGlobalsIndexes::from(core.globals);
debug!("Indexed globals!");
let mut index = CardIndex::new();
for card in cards {
index.insert(card.code.clone(), card);
}
let cards = index;
debug!("Indexed cards!");
debug!("Creating search engine...");
let engine = CardSearchEngine::new(globals, cards); let engine = CardSearchEngine::new(globals, cards);
debug!("Created search engine!"); debug!("Created CardSearchEngine!");
debug!("Creating Telegram bot with parameters from the environment..."); debug!("Creating Telegram bot with parameters from the environment...");
let bot = Bot::from_env(); let bot = Bot::from_env();