5JA3AGWJK3LA7SJVA5OAZVPFKC2FAHTHEUERPGCOTYD46FJED3DAC GBKSX54G7KJDXU6JT7JYETXW2B35AH3ZSALHUJBNAXLERT5EINNAC 3BIBMJZKSWLUE7XT2A72R44XBF2LXT7WDROOARNRCYJKUIIHFH3AC R5QFN2QJ6JSKLPEOOQSB6PN2HQLY462KJP22BTDOD4WOVN3F3OIAC RJD6EE7LQXHBPRKCWPZTQ3DNJGZDTPGOT5UXWUHPUPFFHWWLLTVQC RWPG6Y4O4K3E4RA6PTWJIBFOCMT3WTU5QU3K4K3ADRXZCWNVNS4QC XAQZ43H4FBQD7XW6Z4ZMNZPPFZMQRQYYRN5UNELYANKU5JTDOVJAC BK46HLA7J4NM4MPVJHJSJFTXUGKNBMS3R45Y6SA2K5LPQRGWX2WAC 62BXKX6RMC7TXVHKGAYUCKNS7FK2TXGBJEAFQPCQZPIA4Y46IKKAC WBBGVTIE2LOWQOAMAHVKGA3BPHCFPUU6DLJ5OJRKIVA3KJEIKGPAC JURX6MDTJTFW5XML6S32NNK3MQN4FLI65KYJV434SVSXY2OV7NYQC VEMD6INZYVV4Z2TDL5HNQSR5CWZGIRMIMGK24OFOHGFQQH3AAB6QC PPIFV6PIB7UBR2HUAKNAVQHZJCUDILNGAY7YQMPSR633V5WRKYUQC //! Manages turns and phases#[derive(Debug, Default)]pub struct TurnState {current_phase: Phase,/// The game starts when this is 1.current_turn: usize,}impl TurnState {pub fn phase(&self) -> Phase {self.current_phase}}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]pub enum Phase {#[default]Ready,Standby,Draw,PrecombatMain,Battle(BattleStep),PostcombatMain,End,}impl Phase {/// Does this phase default to a closed GES?pub fn is_closed(self) -> bool {matches!(self,Phase::Ready | Phase::Battle(BattleStep::ResolveDamage))}}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum BattleStep {BeginBattle,DeclareAttackers,MakeAttacks,ResolveDamage,EndBattle,}
pub mod ability;pub mod card;pub mod color;pub mod turn;use std::collections::{HashMap, VecDeque};use std::fmt;use ulid::Ulid;pub use self::{ability::Stack,card::CardInstance,color::{Color, ColorBalance},turn::TurnState,};#[derive(Debug, Clone)]pub struct PlayerInitDescriptor {balance: ColorBalance,deck: Vec<CardInstance>,guardians: Vec<CardInstance>,}impl From<(ColorBalance, Vec<CardInstance>, Vec<CardInstance>)> for PlayerInitDescriptor {fn from((balance, deck, guardians): (ColorBalance, Vec<CardInstance>, Vec<CardInstance>),) -> Self {Self {deck,guardians,balance,}}}/// Represents the data of a player#[derive(Default, Debug, Clone)]pub struct Player {id: Ulid,/// Is this player still in the game?is_active: bool,// TODO instead of a simple usize, allow for earmarking for certain purposespub mana_pool: HashMap<Color, usize>,pub balance: ColorBalance,pub field: Field,}impl Player {pub fn id(&self) -> Ulid {self.id}}/// Wrapper that definitely refers to a hecs entity that *is* a card instance#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct CardInstanced(hecs::Entity);impl CardInstanced {}/// Represents all the zones and places belonging to a player#[derive(Debug, Default, Clone)]pub struct Field {magister_place: Option<CardInstanced>,aide_places: [Option<CardInstanced>; 2],fragment_places: [Option<CardInstanced>; 6],// zones have no suffixdeck: VecDeque<CardInstanced>,guardian_deck: VecDeque<CardInstanced>,hand: VecDeque<CardInstanced>,graveyard: VecDeque<CardInstanced>,exile: VecDeque<CardInstanced>,}#[derive(Default)]pub struct Game {world: hecs::World,players: Vec<Player>,stack: Stack,turn: TurnState,}#[derive(thiserror::Error, Debug)]pub enum GameCreationError {}impl Game {pub fn new<I>(players: I) -> Result<Self, GameCreationError>whereI: IntoIterator<Item = PlayerInitDescriptor>,{let mut game = Self::default();for (idx, player_desc) in players.into_iter().enumerate() {_ = idx;_ = player_desc;// Validate that player_dec is validgame.players.push(Player {id: Ulid::new(),is_active: true,mana_pool: HashMap::new(),balance: player_desc.balance,field: Field {deck: vec![].into(),guardian_deck: vec![].into(),..Default::default()},})}Ok(game)}pub fn players(&self) -> &[Player] {&self.players}pub fn players_mut(&mut self) -> &mut [Player] {&mut self.players}}impl fmt::Debug for Game {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {f.debug_struct("Game").field("players", &self.players).field("stack", &self.stack).field("turn", &self.turn).finish_non_exhaustive()}}
use rand::{distributions::WeightedIndex, Rng};/// Represents the colors of the game#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]pub enum Color {/// Gold (D)Divine,/// Blue (R)Revelation,/// White (A)Grace,/// Green (G)Growth,/// Red (U)Crusade,/// Black (S)Suffering,/// Gray/Colorless (M)Mundane,}impl Color {/// Array of all colors in color wheel orderpub const fn all() -> [Color; 7] {use Color::*;[Mundane, Divine, Revelation, Grace, Growth, Crusade, Suffering,]}/// Array of all factions in color wheel orderpub const fn all_factions() -> [Color; 6] {use Color::*;[Divine, Revelation, Grace, Growth, Crusade, Suffering]}/// Get the color that opposes this one.pub fn opposes(self) -> Self {match self {Self::Mundane => Self::Mundane,faction => {pub(crate) const ALL_FACTIONS: &[Color] = &Color::all_factions();let faction_pos = ALL_FACTIONS.iter().position(|f| *f == faction).unwrap();let opposing_index = (faction_pos + 3) % ALL_FACTIONS.len();ALL_FACTIONS[opposing_index]}}}/// Get the colors adjacent to this one.pub fn adjacent_to(self) -> [Self; 2] {match self {Self::Mundane => [Self::Mundane, Self::Mundane],faction => {pub(crate) const ALL_FACTIONS: &[Color] = &Color::all_factions();let faction_pos = ALL_FACTIONS.iter().position(|f| *f == faction).unwrap();let left_adjacent = (faction_pos + ALL_FACTIONS.len() - 1) % ALL_FACTIONS.len();let right_adjacent = (faction_pos + 1) % ALL_FACTIONS.len();[ALL_FACTIONS[left_adjacent], ALL_FACTIONS[right_adjacent]]}}}pub fn color(self) -> &'static str {match self {Color::Divine => "gold",Color::Revelation => "blue",Color::Grace => "white",Color::Growth => "green",Color::Crusade => "red",Color::Suffering => "black",Color::Mundane => "grey",}}}#[derive(Debug, Clone, Copy, Default)]pub struct ColorBalance {pub mundane: usize,pub divine: usize,pub revelation: usize,pub grace: usize,pub growth: usize,pub crusade: usize,pub suffering: usize,}impl ColorBalance {pub fn get(&self, color: Color) -> usize {match color {Color::Divine => self.divine,Color::Revelation => self.revelation,Color::Grace => self.grace,Color::Growth => self.growth,Color::Crusade => self.crusade,Color::Suffering => self.suffering,Color::Mundane => self.mundane,}}pub fn gen_mana<'a, R: Rng + ?Sized>(&self,rng: &'a mut R,) -> impl Iterator<Item = Color> + 'a {let colors = Color::all();let weights = [self.mundane.saturating_add(1),self.divine.saturating_add(1),self.revelation.saturating_add(1),self.grace.saturating_add(1),self.growth.saturating_add(1),self.crusade.saturating_add(1),self.suffering.saturating_add(1),];let dist = WeightedIndex::new(weights).unwrap();rng.sample_iter(dist).map(move |idx| colors[idx])}}#[cfg(test)]mod tests {use super::Color;#[test]fn test_color_oppose_and_adjacent() {let color = Color::Divine;assert_eq!(Color::Growth, color.opposes());assert_eq!([Color::Suffering, Color::Revelation], color.adjacent_to());}}
pub mod subtype;pub use self::subtype::Subtype;#[derive(Debug, Clone)]pub enum Card {Magister {},Fragment {},}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum FragmentProperties {/// Is not sent to the graveyard on resolutionLingering,/// Must be Set before it can be activatedPrepared,/// Can be activated during any time GES is half-closed/// (as compared to the default of when GES is open during *your* Main Phase(s))Impulse,/// Can be activated the turn it is Set.Rush,}#[derive(Debug, Clone, Copy)]pub struct CardInstance {}
//! Card subtypes//!//! Impl note: Make sure both the canonical representation [`Display`](Subtype#impl-Display-for-Subtype) and//! the exact representation [`Debug`](Subtype#impl-Debug-for-Subtype) are accessible, as membership relations//! (both quasi and not)//! use the exact to determine the relation, but canonical gets the general point across.//!//! # Examples//!//! ```//! # use magister::card::Subtype;//! let subtype = Subtype::new("Mad:Dot");//! assert!(subtype.is_quasimember("mad"));//! assert!(!subtype.is_quasimember("mad-dot"));//! assert!(subtype.is_quasimember("mad:dot"));//! assert_eq!(&subtype.to_string(), "Mad-Dot");//! assert_eq!(&format!("{subtype:?}"), "mad:dot");//! ```use heck::ToTrainCase;use regex::Regex;use serde::de::Visitor;use std::{collections::{HashMap, HashSet},fmt,sync::{Arc, Mutex, OnceLock},};#[derive(Clone)]pub struct Subtype(HashSet<Arc<str>>, Arc<str>);impl PartialEq for Subtype {fn eq(&self, other: &Self) -> bool {self.0 == other.0}}impl Eq for Subtype {}impl serde::Serialize for Subtype {fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>whereS: serde::Serializer,{serializer.serialize_str(&format!("{self:?}"))}}pub(crate) struct SubtypeVisitor;impl<'de> Visitor<'de> for SubtypeVisitor {type Value = Box<str>;fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {formatter.write_str("a string of space separated subtypes")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{Ok(Box::from(v))}}impl<'de> serde::Deserialize<'de> for Subtype {fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>whereD: serde::Deserializer<'de>,{deserializer.deserialize_str(SubtypeVisitor).map(Subtype::new)}}impl fmt::Display for Subtype {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {write!(f,"{}",self.1.split_whitespace().map(|s| s.to_train_case()).collect::<Vec<_>>().join(" "))}}impl fmt::Debug for Subtype {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {if f.alternate() {f.debug_tuple("Subtype").field(&self.0).field(&self.1).finish()} else {write!(f, "{}", self.1)}}}impl Subtype {pub fn new<S: AsRef<str>>(subtype: S) -> Self {static REMOVE_SEPARATORS: OnceLock<Regex> = OnceLock::new();let remove_seps =REMOVE_SEPARATORS.get_or_init(|| Regex::new(r"(\A|\s)[-+/\\:](\z|\s)").unwrap());let subtype = subtype.as_ref().trim().to_lowercase();let subtype = remove_seps.replace_all(&subtype, " ");Self(subtype.split_whitespace().filter(|s| !["-", "+", "/", r"\", ":"].contains(s)).map(Arc::from).collect(),Arc::from(subtype),)}/// Provide an [`ExactSizeIterator`] of all subtypespub fn subtypes(&self) -> impl ExactSizeIterator<Item = &str> {self.0.iter().map(|s| s.as_ref())}/// Check if this subtype is a member of a given subtype grouppub fn is_member<S: AsRef<str>>(&self, member: S) -> bool {let member = member.as_ref().to_lowercase();self.0.iter().any(|sm| sm.as_ref() == member)}/// Check if this subtype is a quasimember of a given subtype group////// Quasimembership is defined for strings separated by `[-+/\:]`pub fn is_quasimember<S: AsRef<str>>(&self, quasimember: S) -> bool {pub(crate) static SUBTYPE_REGEXES: OnceLock<Mutex<HashMap<Box<str>, Regex>>> =OnceLock::new();let mut subtype_regex_map = SUBTYPE_REGEXES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap();let subtype_regex = subtype_regex_map.entry(Box::from(quasimember.as_ref().to_lowercase())).or_insert_with(|| {Regex::new(&format!(r"(\A|[-+/\\:]){}(\z|[-+/\\:])",regex::escape(&quasimember.as_ref().to_lowercase()))).unwrap()});self.0.iter().any(|sm| subtype_regex.is_match(sm))}}#[cfg(test)]mod tests {use super::Subtype;#[test]fn membership_will_only_match_whole_subtypes() {let subtype = Subtype::new("Mad Relic");let subtype2 = Subtype::new("Mad-Devouring Dragon");assert!(subtype.is_member("Mad"));assert!(!subtype2.is_member("Mad"));}#[test]fn quasimembership_matches_words_in_subtypes() {let subtype = Subtype::new("Mad Relic");let subtype2 = Subtype::new("Mad-Devouring Dragon");assert!(subtype.is_quasimember("Mad"));assert!(subtype2.is_quasimember("Mad"));assert!(!subtype2.is_quasimember("evo"));}#[test]fn membership_and_quasimembership_are_caseinsensitive() {let subtype = Subtype::new("Magic-Spellcaster Ruler");assert!(subtype.is_member("MAGIC-SPELLCASTER"));assert!(subtype.is_quasimember("SPELLcAsTeR"));let subtype2 = Subtype::new("MAGIC-SPELLcaster RULer");assert_eq!(subtype, subtype2);}#[test]fn whitespace_doesnt_affect_subtype() {let subtype = Subtype::new("Magic Ruler");let subtype2 = Subtype::new("Magic \n\t\t Ruler");assert_eq!(subtype, subtype2);}#[test]fn valid_unicode_subtypes() {let subtype = Subtype::new("Hailstone (-_-/ π»-Ruler");assert!(subtype.is_member("(-_-/"));assert!(subtype.is_member("π»-ruler"));assert!(subtype.is_quasimember("(-_-/"));assert!(subtype.is_quasimember("("));assert!(subtype.is_quasimember("_"));assert!(subtype.is_quasimember("/"));assert!(subtype.is_quasimember("π»"));assert!(subtype.is_quasimember("Ruler"));}#[test]fn test_serde() {let subtype_a = Subtype::new("Mad:Dot / Dasher");let subtype_b = Subtype::new("Mad-dot - Dasher");let subtype_c = Subtype::new("Mad:Dot - Dasher");assert_eq!(subtype_a, subtype_c);assert_ne!(subtype_a, subtype_b);assert_ne!(subtype_c, subtype_b);insta::assert_ron_snapshot!([subtype_a, subtype_b, subtype_c]);}}
---source: src/card/subtype.rsexpression: "[subtype_a, subtype_b, subtype_c]"---("mad:dot dasher", "mad-dot dasher", "mad:dot dasher")
//! Handling of abilities and effectsuse std::collections::VecDeque;/// Contains stack items#[derive(Debug, Default)]pub struct Stack {/// Are we in the middle of resolving an effect?is_resolving: bool,stack: VecDeque<StackItem>,}#[derive(Debug, Clone)]pub enum StackItem {}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum GameEffectState {Closed,HalfOpen,Open,}impl Stack {// The only way to override this is to look are the Phase::is_closed result// If that is true, GES is closed no matter whatpub fn game_effect_state(&self) -> GameEffectState {if self.stack.is_empty() {GameEffectState::Open} else if self.is_resolving {GameEffectState::Closed} else {// The stack is *not* empty, but we are *not* in the middle of effect resolutionGameEffectState::HalfOpen}}}
[package]name = "magister"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]derive_more = "0.99.17"heck = "0.4.1"hecs = "0.10.4"rand = "0.8.5"regex = "1.10.3"serde = { version = "1.0.197", features = ["derive"] }thiserror = "1.0.57"ulid = { version = "1.1.2", features = ["serde"] }[dev-dependencies]insta = { version = "1.35.1", features = ["ron", "redactions"] }
//! Manages turns and phases#[derive(Debug, Default)]pub struct TurnState {current_phase: Phase,/// The game starts when this is 1.current_turn: usize,}impl TurnState {pub fn phase(&self) -> Phase {self.current_phase}}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]pub enum Phase {#[default]Ready,Standby,Draw,PrecombatMain,Battle(BattleStep),PostcombatMain,End,}impl Phase {/// Does this phase default to a closed GES?pub fn is_closed(self) -> bool {matches!(self,Phase::Ready | Phase::Battle(BattleStep::ResolveDamage))}}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum BattleStep {BeginBattle,DeclareAttackers,MakeAttacks,ResolveDamage,EndBattle,}
pub mod ability;pub mod card;pub mod color;pub mod display;pub mod turn;use std::collections::{HashMap, VecDeque};use std::fmt;use heck::ToTitleCase;use leptos::*;use leptos_router::Router;use phosphor_leptos::Horse;use ulid::Ulid;use self::{ability::Stack,card::CardInstance,color::{Color, ColorBalance},display::Player as PlayerDisplay,turn::TurnState,};#[component]fn SubtypePlayer() -> impl IntoView {use self::card::Subtype;let (subtype_str, set_subtype_str) = create_signal("".to_string());let subtype = move || Subtype::new(subtype_str.get());let (member, set_member) = create_signal(String::new());let is_member = {move || {let member = member.get();let subtype = subtype();!member.is_empty()&& member.split_whitespace().all(|mem_sub| subtype.is_member(mem_sub))}};let is_quasimember = {move || {let member = member.get();let subtype = subtype();!member.is_empty()&& member.split_whitespace().all(|mem_sub| subtype.is_quasimember(mem_sub))}};view! {<div class="w-1/2"><label class="input input-bordered flex items-center gap-2"><Horse size="24px"/><inputtype="text"class="grow"placeholder="Subtype"prop:value=move || subtype_str.get()on:input=move |ev| set_subtype_str.set(event_target_value(&ev))/></label><ul class="list-disc list-inside">{move || {subtype().subtypes().map(|st| view! { <li>{st.to_title_case()}</li> }).collect_view()}}</ul><div class="join w-full flex"><label class="input input-bordered flex items-center gap-2 join-item grow"><inputtype="text"class="grow"placeholder="Member?"prop:value=move || member.get()on:input=move |ev| set_member.set(event_target_value(&ev))/></label><button class="btn join-item" on:click=move |_| set_member.update(|qm| qm.clear())>"Clear"</button></div><ul class="list-inside list-disc"><li>"Is Member? (βSearch for a X card.β) "<spanclass=("text-error", move || !member.get().is_empty() && !is_member())class=("text-primary", is_member)>{is_member}</span></li><li>"Is Quasimember? (βSearch for a X or similar card.β) "<spanclass=("text-error", move || !member.get().is_empty() && !is_quasimember())class=("text-primary", is_quasimember)>{is_quasimember}</span></li><li>"Canonical Subtype: " {move || format!("`{}`", subtype())}</li><li>"Exact Subtype: " {move || format!("`{:?}`", subtype())}</li></ul></div>}}#[tracing::instrument]#[component]pub fn App() -> impl IntoView {let player_1_balance = ColorBalance::default();let player_1_deck = vec![];let player_1_guardians = vec![];let player_2_balance = ColorBalance {suffering: 3,divine: 3,..Default::default()};let player_2_deck = vec![];let player_2_guardians = vec![];let player_decks: Vec<PlayerInitDescriptor> = vec![(player_1_balance, player_1_deck, player_1_guardians).into(),(player_2_balance, player_2_deck, player_2_guardians).into(),];let player_decks_len = player_decks.len();view! {<GameProvider player_decks=player_decks><Router><h1 class="font-bold text-4xl">Hello</h1>{(0..player_decks_len).map(|idx| view! { <PlayerDisplay index=idx/> }).collect_view()}<SubtypePlayer/></Router></GameProvider>}}/// Represents the data of a player#[derive(Default, Debug, Clone)]pub struct Player {id: Ulid,// TODO instead of a simple usize, allow for earmarking for certain purposespub mana_pool: HashMap<Color, usize>,pub balance: ColorBalance,pub field: Field,}/// Wrapper that definitely refers to a hecs entity that *is* a card instance#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct CardInstanced(hecs::Entity);impl CardInstanced {}/// Represents all the zones and places belonging to a player#[derive(Debug, Default, Clone)]pub struct Field {magister_place: Option<CardInstanced>,aide_places: [Option<CardInstanced>; 2],fragment_places: [Option<CardInstanced>; 6],// zones have no suffixdeck: VecDeque<CardInstanced>,guardian_deck: VecDeque<CardInstanced>,hand: VecDeque<CardInstanced>,graveyard: VecDeque<CardInstanced>,exile: VecDeque<CardInstanced>,}#[derive(Default)]pub struct Game {world: hecs::World,players: Vec<Player>,stack: Stack,turn: TurnState,}#[derive(thiserror::Error, Debug)]pub enum GameCreationError {}impl Game {fn new<I>(players: I) -> Result<Self, GameCreationError>whereI: IntoIterator<Item = PlayerInitDescriptor>,{let mut game = Self::default();for (idx, player_desc) in players.into_iter().enumerate() {_ = idx;_ = player_desc;// Validate that player_dec is validgame.players.push(Player {id: Ulid::new(),mana_pool: HashMap::new(),balance: player_desc.balance,field: Field {deck: vec![].into(),guardian_deck: vec![].into(),..Default::default()},})}Ok(game)}}#[derive(Debug, Clone)]struct PlayerInitDescriptor {balance: ColorBalance,deck: Vec<CardInstance>,guardians: Vec<CardInstance>,}impl From<(ColorBalance, Vec<CardInstance>, Vec<CardInstance>)> for PlayerInitDescriptor {fn from((balance, deck, guardians): (ColorBalance, Vec<CardInstance>, Vec<CardInstance>),) -> Self {Self {deck,guardians,balance,}}}impl fmt::Debug for Game {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {f.debug_struct("Game").field("players", &self.players).field("stack", &self.stack).field("turn", &self.turn).finish_non_exhaustive()}}#[derive(Debug, Clone, Copy)]pub struct GameReader(ReadSignal<Result<Game, GameCreationError>>);#[derive(Debug, Clone, Copy)]pub struct GameWriter(WriteSignal<Result<Game, GameCreationError>>);#[tracing::instrument(skip(children, player_decks))]#[component]fn GameProvider(#[prop(into)] player_decks: MaybeSignal<Vec<PlayerInitDescriptor>>,children: Children,) -> impl IntoView {let (game, set_game) = create_signal(Game::new(player_decks.get()));provide_context(GameReader(game));provide_context(GameWriter(set_game));tracing::info!("logging something for game use...");children()}<div class="flex w-1/2 items-center"><Show when={move || !is_dem_res_error() && !dem_res.loading().get()}fallback={move || {view! {<Show when={move || dem_res_error().is_some()}><div class="text-error grow">{move || format!("ERROR: {}", dem_res_error().unwrap())}</div></Show>}}}><div class="grow">{move || dem_res_data().unwrap().join(", ")}</div></Show><button class="btn join-item" on:click=move |_| dem_res.refetch()>"Reload"</button></div>let dem_res = create_local_resource(|| (),|_| async {let response = gloo_net::http::Request::get("/api/v0/clicked").send().await.map_err(Rc::new)?;let json = response.json::<Vec<Box<str>>>().await.map_err(Rc::new)?;Ok::<_, Rc<gloo_net::Error>>(json.into_iter().map(|s| Rc::from(s) as Rc<str>).collect::<Vec<_>>(),)},);let is_dem_res_error = move || dem_res.with(|res| res.as_ref().is_some_and(|res| res.is_err()));let dem_res_data = move || {if dem_res.loading().get() {None} else {dem_res.with(|res| res.clone().and_then(|d| d.ok()))}};let dem_res_error = move || {if dem_res.loading().get() {None} else {dem_res.with(|res| res.clone().and_then(|d| d.err()))}};use std::rc::Rc;
use rand::{distributions::WeightedIndex, Rng};/// Represents the colors of the game#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]pub enum Color {/// Gold (D)Divine,/// Blue (R)Revelation,/// White (A)Grace,/// Green (G)Growth,/// Red (U)Crusade,/// Black (S)Suffering,/// Gray/Colorless (M)Mundane,}impl Color {/// Array of all colors in color wheel orderpub const fn all() -> [Color; 7] {use Color::*;[Mundane, Divine, Revelation, Grace, Growth, Crusade, Suffering,]}/// Array of all factions in color wheel orderpub const fn all_factions() -> [Color; 6] {use Color::*;[Divine, Revelation, Grace, Growth, Crusade, Suffering]}/// Get the color that opposes this one.pub fn opposes(self) -> Self {match self {Self::Mundane => Self::Mundane,faction => {pub(crate) const ALL_FACTIONS: &[Color] = &Color::all_factions();let faction_pos = ALL_FACTIONS.iter().position(|f| *f == faction).unwrap();let opposing_index = (faction_pos + 3) % ALL_FACTIONS.len();ALL_FACTIONS[opposing_index]}}}/// Get the colors adjacent to this one.pub fn adjacent_to(self) -> [Self; 2] {match self {Self::Mundane => [Self::Mundane, Self::Mundane],faction => {pub(crate) const ALL_FACTIONS: &[Color] = &Color::all_factions();let faction_pos = ALL_FACTIONS.iter().position(|f| *f == faction).unwrap();let left_adjacent = (faction_pos + ALL_FACTIONS.len() - 1) % ALL_FACTIONS.len();let right_adjacent = (faction_pos + 1) % ALL_FACTIONS.len();[ALL_FACTIONS[left_adjacent], ALL_FACTIONS[right_adjacent]]}}}pub fn color(self) -> &'static str {match self {Color::Divine => "gold",Color::Revelation => "blue",Color::Grace => "white",Color::Growth => "green",Color::Crusade => "red",Color::Suffering => "black",Color::Mundane => "grey",}}}#[derive(Debug, Clone, Copy, Default)]pub struct ColorBalance {pub(crate) mundane: usize,pub(crate) divine: usize,pub(crate) revelation: usize,pub(crate) grace: usize,pub(crate) growth: usize,pub(crate) crusade: usize,pub(crate) suffering: usize,}impl ColorBalance {pub fn get(&self, color: Color) -> usize {match color {Color::Divine => self.divine,Color::Revelation => self.revelation,Color::Grace => self.grace,Color::Growth => self.growth,Color::Crusade => self.crusade,Color::Suffering => self.suffering,Color::Mundane => self.mundane,}}pub fn gen_mana<'a, R: Rng + ?Sized>(&self,rng: &'a mut R,) -> impl Iterator<Item = Color> + 'a {let colors = Color::all();let weights = [self.mundane.saturating_add(1),self.divine.saturating_add(1),self.revelation.saturating_add(1),self.grace.saturating_add(1),self.growth.saturating_add(1),self.crusade.saturating_add(1),self.suffering.saturating_add(1),];let dist = WeightedIndex::new(weights).unwrap();rng.sample_iter(dist).map(move |idx| colors[idx])}}#[cfg(test)]mod tests {use super::Color;#[test]fn test_color_oppose_and_adjacent() {let color = Color::Divine;assert_eq!(Color::Growth, color.opposes());assert_eq!([Color::Suffering, Color::Revelation], color.adjacent_to());}}
pub mod subtype;pub use self::subtype::Subtype;#[derive(Debug, Clone)]pub enum Card {Magister {},Fragment {},}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum FragmentProperties {/// Is not sent to the graveyard on resolutionLingering,/// Must be Set before it can be activatedPrepared,/// Can be activated during any time GES is half-closed/// (as compared to the default of when GES is open during *your* Main Phase(s))Impulse,/// Can be activated the turn it is Set.Rush,}#[derive(Debug, Clone, Copy)]pub struct CardInstance {}
//! Card subtypes//!//! Impl note: Make sure both the canonical representation [`Display`](Subtype#impl-Display-for-Subtype) and//! the exact representation [`Debug`](Subtype#impl-Debug-for-Subtype) are accessible, as membership relations//! (both quasi and not)//! use the exact to determine the relation, but canonical gets the general point across.//!//! # Examples//!//! ```//! # use magister::card::Subtype;//! let subtype = Subtype::new("Mad:Dot");//! assert!(subtype.is_quasimember("mad"));//! assert!(!subtype.is_quasimember("mad-dot"));//! assert!(subtype.is_quasimember("mad:dot"));//! assert_eq!(&subtype.to_string(), "Mad-Dot");//! assert_eq!(&format!("{subtype:?}"), "mad:dot");//! ```use heck::ToTrainCase;use regex::Regex;use serde::de::Visitor;use std::{collections::{HashMap, HashSet},fmt,sync::{Arc, Mutex, OnceLock},};#[derive(Clone)]pub struct Subtype(HashSet<Arc<str>>, Arc<str>);impl PartialEq for Subtype {fn eq(&self, other: &Self) -> bool {self.0 == other.0}}impl Eq for Subtype {}impl serde::Serialize for Subtype {fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>whereS: serde::Serializer,{serializer.serialize_str(&format!("{self:?}"))}}pub(crate) struct SubtypeVisitor;impl<'de> Visitor<'de> for SubtypeVisitor {type Value = Box<str>;fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {formatter.write_str("a string of space separated subtypes")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{Ok(Box::from(v))}}impl<'de> serde::Deserialize<'de> for Subtype {fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>whereD: serde::Deserializer<'de>,{deserializer.deserialize_str(SubtypeVisitor).map(Subtype::new)}}impl fmt::Display for Subtype {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {write!(f,"{}",self.1.split_whitespace().map(|s| s.to_train_case()).collect::<Vec<_>>().join(" "))}}impl fmt::Debug for Subtype {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {if f.alternate() {f.debug_tuple("Subtype").field(&self.0).field(&self.1).finish()} else {write!(f, "{}", self.1)}}}impl Subtype {pub fn new<S: AsRef<str>>(subtype: S) -> Self {Self(subtype.split_whitespace().filter(|s| !["-", "+", "/", r"\", ":"].contains(s)).map(Arc::from).collect(),Arc::from(subtype),)}/// Provide an [`ExactSizeIterator`] of all subtypespub fn subtypes(&self) -> impl ExactSizeIterator<Item = &str> {self.0.iter().map(|s| s.as_ref())}/// Check if this subtype is a member of a given subtype grouppub fn is_member<S: AsRef<str>>(&self, member: S) -> bool {let member = member.as_ref().to_lowercase();self.0.iter().any(|sm| sm.as_ref() == member)}/// Check if this subtype is a quasimember of a given subtype group////// Quasimembership is defined for strings separated by `[-+/\:]`pub fn is_quasimember<S: AsRef<str>>(&self, quasimember: S) -> bool {pub(crate) static SUBTYPE_REGEXES: OnceLock<Mutex<HashMap<Box<str>, Regex>>> =OnceLock::new();let mut subtype_regex_map = SUBTYPE_REGEXES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap();let subtype_regex = subtype_regex_map.entry(Box::from(quasimember.as_ref().to_lowercase())).or_insert_with(|| {Regex::new(&format!(r"(\A|[-+/\\:]){}(\z|[-+/\\:])",regex::escape(&quasimember.as_ref().to_lowercase()))).unwrap()});self.0.iter().any(|sm| subtype_regex.is_match(sm))}}#[cfg(test)]mod tests {use super::Subtype;#[test]fn membership_will_only_match_whole_subtypes() {let subtype = Subtype::new("Mad Relic");let subtype2 = Subtype::new("Mad-Devouring Dragon");assert!(subtype.is_member("Mad"));assert!(!subtype2.is_member("Mad"));}#[test]fn quasimembership_matches_words_in_subtypes() {let subtype = Subtype::new("Mad Relic");let subtype2 = Subtype::new("Mad-Devouring Dragon");assert!(subtype.is_quasimember("Mad"));assert!(subtype2.is_quasimember("Mad"));assert!(!subtype2.is_quasimember("evo"));}#[test]fn membership_and_quasimembership_are_caseinsensitive() {let subtype = Subtype::new("Magic-Spellcaster Ruler");assert!(subtype.is_member("MAGIC-SPELLCASTER"));assert!(subtype.is_quasimember("SPELLcAsTeR"));let subtype2 = Subtype::new("MAGIC-SPELLcaster RULer");assert_eq!(subtype, subtype2);}#[test]fn whitespace_doesnt_affect_subtype() {let subtype = Subtype::new("Magic Ruler");let subtype2 = Subtype::new("Magic \n\t\t Ruler");assert_eq!(subtype, subtype2);}#[test]fn valid_unicode_subtypes() {let subtype = Subtype::new("Hailstone (-_-/ π»-Ruler");assert!(subtype.is_member("(-_-/"));assert!(subtype.is_member("π»-ruler"));assert!(subtype.is_quasimember("(-_-/"));assert!(subtype.is_quasimember("("));assert!(subtype.is_quasimember("_"));assert!(subtype.is_quasimember("/"));assert!(subtype.is_quasimember("π»"));assert!(subtype.is_quasimember("Ruler"));}}#[test]fn test_serde() {let subtype_a = Subtype::new("Mad:Dot / Dasher");let subtype_b = Subtype::new("Mad-dot - Dasher");let subtype_c = Subtype::new("Mad:Dot - Dasher");insta::assert_ron_snapshot!([subtype_a, subtype_b, subtype_c]);}assert_eq!(subtype_a, subtype_c);assert_ne!(subtype_a, subtype_b);assert_ne!(subtype_c, subtype_b);static REMOVE_SEPARATORS: OnceLock<Regex> = OnceLock::new();let remove_seps =REMOVE_SEPARATORS.get_or_init(|| Regex::new(r"(\A|\s)[-+/\\:](\z|\s)").unwrap());let subtype = subtype.as_ref().trim().to_lowercase();let subtype = remove_seps.replace_all(&subtype, " ");
//! Handling of abilities and effectsuse std::collections::VecDeque;/// Contains stack items#[derive(Debug, Default)]pub struct Stack {/// Are we in the middle of resolving an effect?is_resolving: bool,stack: VecDeque<StackItem>,}#[derive(Debug, Clone)]pub enum StackItem {}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum GameEffectState {Closed,HalfOpen,Open,}impl Stack {// The only way to override this is to look are the Phase::is_closed result// If that is true, GES is closed no matter whatpub fn game_effect_state(&self) -> GameEffectState {if self.stack.is_empty() {GameEffectState::Open} else if self.is_resolving {GameEffectState::Closed} else {// The stack is *not* empty, but we are *not* in the middle of effect resolutionGameEffectState::HalfOpen}}}
use std::rc::Rc;use heck::ToTitleCase;use leptos::*;use leptos_router::Router;use magister::{ColorBalance, PlayerInitDescriptor, Game, GameCreationError};use phosphor_leptos::Horse;use crate::display::Player as PlayerDisplay;#[component]fn SubtypePlayer() -> impl IntoView {use magister::card::Subtype;let (subtype_str, set_subtype_str) = create_signal("".to_string());let subtype = move || Subtype::new(subtype_str.get());let (member, set_member) = create_signal(String::new());let is_member = {move || {let member = member.get();let subtype = subtype();!member.is_empty()&& member.split_whitespace().all(|mem_sub| subtype.is_member(mem_sub))}};let is_quasimember = {move || {let member = member.get();let subtype = subtype();!member.is_empty()&& member.split_whitespace().all(|mem_sub| subtype.is_quasimember(mem_sub))}};view! {<div class="w-1/2"><label class="input input-bordered flex items-center gap-2"><Horse size="24px"/><inputtype="text"class="grow"placeholder="Subtype"prop:value=move || subtype_str.get()on:input=move |ev| set_subtype_str.set(event_target_value(&ev))/></label><ul class="list-disc list-inside">{move || {subtype().subtypes().map(|st| view! { <li>{st.to_title_case()}</li> }).collect_view()}}</ul><div class="join w-full flex"><label class="input input-bordered flex items-center gap-2 join-item grow"><inputtype="text"class="grow"placeholder="Member?"prop:value=move || member.get()on:input=move |ev| set_member.set(event_target_value(&ev))/></label><button class="btn join-item" on:click=move |_| set_member.update(|qm| qm.clear())>"Clear"</button></div><ul class="list-inside list-disc"><li>"Is Member? (βSearch for a X card.β) "<spanclass=("text-error", move || !member.get().is_empty() && !is_member())class=("text-primary", is_member)>{is_member}</span></li><li>"Is Quasimember? (βSearch for a X or similar card.β) "<spanclass=("text-error", move || !member.get().is_empty() && !is_quasimember())class=("text-primary", is_quasimember)>{is_quasimember}</span></li><li>"Canonical Subtype: " {move || format!("`{}`", subtype())}</li><li>"Exact Subtype: " {move || format!("`{:?}`", subtype())}</li></ul></div>}}#[tracing::instrument]#[component]pub fn App() -> impl IntoView {let player_1_balance = ColorBalance::default();let player_1_deck = vec![];let player_1_guardians = vec![];let player_2_balance = ColorBalance {suffering: 3,divine: 3,..Default::default()};let player_2_deck = vec![];let player_2_guardians = vec![];let player_decks: Vec<PlayerInitDescriptor> = vec![(player_1_balance, player_1_deck, player_1_guardians).into(),(player_2_balance, player_2_deck, player_2_guardians).into(),];let player_decks_len = player_decks.len();let dem_res = create_local_resource(|| (),|_| async {let response = gloo_net::http::Request::get("/api/v0/clicked").send().await.map_err(Rc::new)?;let json = response.json::<Vec<Box<str>>>().await.map_err(Rc::new)?;Ok::<_, Rc<gloo_net::Error>>(json.into_iter().map(|s| Rc::from(s) as Rc<str>).collect::<Vec<_>>(),)},);let is_dem_res_error = move || dem_res.with(|res| res.as_ref().is_some_and(|res| res.is_err()));let dem_res_data = move || {if dem_res.loading().get() {None} else {dem_res.with(|res| res.clone().and_then(|d| d.ok()))}};let dem_res_error = move || {if dem_res.loading().get() {None} else {dem_res.with(|res| res.clone().and_then(|d| d.err()))}};view! {<GameProvider player_decks=player_decks><Router><h1 class="font-bold text-4xl">Hello</h1>{(0..player_decks_len).map(|idx| view! { <PlayerDisplay index=idx/> }).collect_view()}<SubtypePlayer/><div class="flex w-1/2 items-center"><Showwhen=move || !is_dem_res_error() && !dem_res.loading().get()fallback=move || {view! {<Show when=move || dem_res_error().is_some()><div class="text-error grow">{move || format!("ERROR: {}", dem_res_error().unwrap())}</div></Show>}}><div class="grow">{move || dem_res_data().unwrap().join(", ")}</div></Show><button class="btn join-item" on:click=move |_| dem_res.refetch()>"Reload"</button></div></Router></GameProvider>}}#[derive(Debug, Clone, Copy)]pub struct GameReader(pub(crate) ReadSignal<Result<Game, GameCreationError>>);#[derive(Debug, Clone, Copy)]pub struct GameWriter(pub(crate) WriteSignal<Result<Game, GameCreationError>>);#[tracing::instrument(skip(children, player_decks))]#[component]fn GameProvider(#[prop(into)] player_decks: MaybeSignal<Vec<PlayerInitDescriptor>>,children: Children,) -> impl IntoView {let (game, set_game) = create_signal(Game::new(player_decks.get()));provide_context(GameReader(game));provide_context(GameWriter(set_game));tracing::info!("logging something for game use...");children()}