WBBGVTIE2LOWQOAMAHVKGA3BPHCFPUU6DLJ5OJRKIVA3KJEIKGPAC WU27XQNRC2MSQS3XGYNM5DMR5ZBHHI4OTYXSDKTL3TKKTKWDZQWQC VA2AH6FLKMMTKJTJKVBMTZYPM7QIEJ4RUDWJYG5BLPRVBOKRP3QQC ALKPKOQUNWOGPIY4T6X4EPSBHWD6LVZOFR6IUIK77XC2FXBMVMTAC Z5HB3ZOD4DSZCXDRGKDDMBW7FQW2PWWXVD2RTIL5MFH56BKFQVLQC WJQ3536KELGGHQ53X7CL4TQYYGYWTPIQBHOMHAUBZSSJ5CBTW3YQC 62BXKX6RMC7TXVHKGAYUCKNS7FK2TXGBJEAFQPCQZPIA4Y46IKKAC 3ZXXZNMR6EA4HB4AFWBMTESO6QOYFOL2MBEJ534O22OYA22HSKGAC GYVESXMMN2WA6ZE4NHRFIBVD5E62CWC4BGQHP24GYJQ6WJB2G2YAC HXVQ65W4R5U2H2TMOJAPOSD53HKRJFWNZUQHOHPHYQVUQUFZEK2AC X7KNY5KWCOLA2V345PUT4EVXVNHL6RGSJK3NNR6DHPOUC33HHCCAC GVQM77M3PEI6CNAZ5D3ITAUYNRXN5WZCYDA5XKNWODJDCQZDT4ZAC CADBHJ3WAUC7AYLSECEZPFBPUBPYHP3GJPWOIAMIL2OEIOX6MKDAC ILH7JI6H226Y2MQCLT5IQNXBI3ZEOGSP4W7AJDZNH3HDS57I6D7AC PYEWGJYLRURPMRVREJRWJMSS7DITHAOVIIDYDJADDZWN4JH7TNTQC J7BRIGVBKBZRXMEATT4EJ53YXMXQZLWQ7XRKV7DSJD5UBSJM2S5QC OBV5YHA4JNKEAZB4KWFR5OSOUBR47N5P6F6RL2CX4OVIQ4TF35NQC EXTCVWVXRQ5XII6BJMYJPAU4JUMXHEZ52OFCUVKRMYI6IU5ALADAC //! 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}}}
#[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 {}mod subtype;pub use self::subtype::Subtype;
//! module for displaying current game stateuse std::collections::HashMap;use heck::ToTitleCase;use leptos::{html::Canvas, *};use plotters::{chart::{ChartBuilder, LabelAreaPosition},coord::ranged1d::{IntoSegmentedCoord, SegmentValue},drawing::IntoDrawingArea,series::Histogram,style::{Color as _, RED, WHITE},};use plotters_canvas::CanvasBackend;use web_sys::HtmlCanvasElement;use crate::app::{color::Color, GameReader};#[component]pub fn Player(#[prop(into)] index: MaybeSignal<usize>) -> impl IntoView {// Display the manapool and balanceconst COLORS: &[Color] = &Color::all();let game = expect_context::<GameReader>();let player_data = move || {game.0.with(|gs| {let gs = gs.as_ref().ok()?;Some(gs.players[index.get()].clone())})};view! {{move || {player_data().map(|pd| {let id = pd.id;let balance = pd.balance;let pool = pd.mana_pool;let (gen_count, set_gen_count) = create_signal(1usize);let (has_error, set_has_error) = create_signal(false);let (gen_mana, set_gen_mana) = create_signal(None);let (all_gen_mana, set_all_gen_mana) = create_signal(HashMap::new());let plot_ref = create_node_ref::<Canvas>();let plotted = move |plot_ref: &HtmlElement<Canvas>, data: &[(Color, usize)]| {let backend = CanvasBackend::with_canvas_object(Clone::clone(HtmlCanvasElement::as_ref(plot_ref)),).expect("plotters canvas failed to initialize").into_drawing_area();backend.fill(&WHITE.mix(1.0)).expect("failed to clear canvas");let mut chart = ChartBuilder::on(&backend).set_label_area_size(LabelAreaPosition::Left, 40).set_label_area_size(LabelAreaPosition::Bottom, 40).build_cartesian_2d((0usize..6).into_segmented(),0..all_gen_mana.get().values().copied().max().map(|x| x + 3).unwrap_or(3),).expect("Failed to create chart");const COLORS: &[Color] = &Color::all();chart.configure_mesh().disable_x_mesh().x_desc("Color").y_desc("Count").y_labels(5).x_labels(7).x_label_formatter(&|idx| {match idx {SegmentValue::Exact(idx) => format!("{}?", COLORS[*idx]),SegmentValue::CenterOf(idx) => {COLORS.get(*idx).map(ToString::to_string).unwrap_or(String::new())}SegmentValue::Last => String::new(),}},).draw().expect("Failed to draw axises");chart.draw_series(Histogram::vertical(&chart).style(RED.mix(0.5).filled()).data(data.into_iter().map(|(c, i)| (COLORS.iter().position(|oc| oc == c).unwrap(),*i,)),),).expect("Failed to draw data");backend.present().expect("failed to present chart");};create_effect(move |_| {let data = all_gen_mana.get();if let Some(plot_ref) = plot_ref.get().as_ref() {plotted(plot_ref, &data.into_iter().collect::<Vec<_>>())}});view! {<div><h2 class="italic text-xl">"Player " {move || index.get() + 1} " : "<span class="font-mono not-italic bg-base-200 rounded p-1">{move || format!("{id}")}</span></h2><ul>{COLORS.iter().map(|c| {view! {<li>{c.color().to_title_case()} " Mana: "{pool.get(c).copied().unwrap_or(0)} " / (" {balance.get(*c)}")"</li>}}).collect_view()}</ul><div class="join"><inputclass="input input-bordered join-item"type="number"prop:value=move || gen_count.get()class:has-error=has_erroron:input=move |ev| {set_gen_count.set(match event_target_value(&ev).parse() {Ok(val) => {set_has_error.set(false);val}Err(_e) => {set_has_error.set(true);return;}},);}/><buttonclass="btn join-item"on:click=move |_| {let generated_mana: Vec<_> = balance.gen_mana(&mut rand::thread_rng()).take(gen_count.get()).collect();let counts = Color::all().into_iter().map(|c| (c,generated_mana.iter().filter(|col| col == &&c).count(),)).collect::<HashMap<_, _>>();set_all_gen_mana.update(|all_gen_mana| {for color in Color::all() {*all_gen_mana.entry(color).or_insert(0)+= counts.get(&color).copied().unwrap_or(0);}});set_gen_mana.set(Some(generated_mana));}>"Generate Mana"</button></div><div class="font-mono">{move || {if let Some(gen_mana) = gen_mana.get() {let mapped = Color::all().into_iter().map(|c| (c,gen_mana.iter().filter(|col| col == &&c).count(),)).collect::<HashMap<_, _>>();format!("{mapped:?}")} else {"".to_string()}}}</div><Show when=move || {all_gen_mana.with(|agm| agm.values().copied().sum::<usize>() > 0)}><canvas width="600" height="200" _ref=plot_ref></canvas></Show></div>}})}}}}<div>"Total Mana: "{move || all_gen_mana.with(|agm| agm.values().sum::<usize>())}</div>
//! 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,}
/// Represents the colors of the gamepub 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 {pub const fn all() -> [Color; 7] {use Color::*;[Mundane, Divine, Revelation, Grace, Growth, Crusade, Suffering,]}pub const fn all_factions() -> [Color; 6] {use Color::*;[Divine, Revelation, Grace, Growth, Crusade, Suffering]}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]}}}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 = [];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());}}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),/// Get the colors adjacent to this one./// Get the color that opposes this one./// Array of all factions in color wheel order/// Array of all colors in color wheel order#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]use rand::{distributions::WeightedIndex, Rng};
use heck::ToTrainCase;use regex::Regex;use serde::de::Visitor;use std::{collections::{HashMap, HashSet},fmt,sync::{Arc, Mutex, OnceLock},};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 Subtype {pub fn new<S: AsRef<str>>(subtype: S) -> Self {Self(subtype.as_ref().trim().to_lowercase().split_whitespace().filter(|s| !["-", "+", "/", r"\", ":"].contains(s)).map(|s| Arc::from(s)).collect(),Arc::from(subtype.as_ref().trim().to_lowercase()),)}/// 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(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"));}}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)}}}#[derive(Clone)]
use leptos::*;#[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! {<Router><h1 class="font-bold text-4xl">Hello</h1>{(0..player_decks_len).into_iter().map(|idx| view! { <PlayerDisplay index=idx/> }).collect_view()}<SubtypePlayer/></Router></GameProvider>}}/// Represents the data of a playerpub struct Player {id: Ulid,// TODO instead of a simple usize, allow for earmarking for certain purposes}#[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,}}}#[derive(Debug, Clone, Copy)]#[derive(Debug, Clone, Copy)]#[tracing::instrument(skip(children, player_decks))]#[component]provide_context(GameReader(game));provide_context(GameWriter(set_game));tracing::info!("logging something for game use...");children()}fn GameProvider(#[prop(into)] player_decks: MaybeSignal<Vec<PlayerInitDescriptor>>,children: Children,) -> impl IntoView {let (game, set_game) = create_signal(Game::new(player_decks.get()));pub struct GameWriter(WriteSignal<Result<Game, GameCreationError>>);pub struct GameReader(ReadSignal<Result<Game, GameCreationError>>);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()pub 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>; 5],// zones have no suffixdeck: VecDeque<CardInstanced>,guardian_deck: VecDeque<CardInstanced>,hand: VecDeque<CardInstanced>,graveyard: VecDeque<CardInstanced>,exile: VecDeque<CardInstanced>,#[derive(Default, Debug, Clone)]<GameProvider player_decks=player_decks>use self::{ability::Stack,card::CardInstance,color::{Color, ColorBalance},display::Player as PlayerDisplay,turn::TurnState,};#[tracing::instrument]#[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());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"><inputtype="text"class="grow"/></label>"Clear"</button></div></div>}}<ul class="list-inside list-disc"><li></li><li></li></ul><li>"Canonical Subtype: " {move || format!("`{}`", subtype())}</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>"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><button class="btn join-item" on:click=move |_| set_member.update(|qm| qm.clear())>placeholder="Member?"prop:value=move || member.get()on:input=move |ev| set_member.set(event_target_value(&ev))<label class="input input-bordered flex items-center gap-2 join-item grow">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))}};use leptos_router::Router;use ulid::Ulid;use phosphor_leptos::Horse;mod ability;mod card;mod color;mod display;mod turn;use heck::ToTitleCase;use std::collections::{HashMap, VecDeque};use std::fmt;
//! 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).into_iter().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>; 5],// 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()}
//! module for displaying current game stateuse std::collections::HashMap;use heck::ToTitleCase;use leptos::{html::Canvas, *};use plotters::{chart::{ChartBuilder, LabelAreaPosition},coord::ranged1d::{IntoSegmentedCoord, SegmentValue},drawing::IntoDrawingArea,series::Histogram,style::{Color as _, RED, WHITE},};use plotters_canvas::CanvasBackend;use web_sys::HtmlCanvasElement;use crate::{color::Color, GameReader};#[component]pub fn Player(#[prop(into)] index: MaybeSignal<usize>) -> impl IntoView {// Display the manapool and balanceconst COLORS: &[Color] = &Color::all();let game = expect_context::<GameReader>();let player_data = move || {game.0.with(|gs| {let gs = gs.as_ref().ok()?;Some(gs.players[index.get()].clone())})};view! {{move || {player_data().map(|pd| {let id = pd.id;let balance = pd.balance;let pool = pd.mana_pool;let (gen_count, set_gen_count) = create_signal(1usize);let (has_error, set_has_error) = create_signal(false);let (gen_mana, set_gen_mana) = create_signal(None);let (all_gen_mana, set_all_gen_mana) = create_signal(HashMap::new());let plot_ref = create_node_ref::<Canvas>();let plotted = move |plot_ref: &HtmlElement<Canvas>, data: &[(Color, usize)]| {let backend = CanvasBackend::with_canvas_object(Clone::clone(HtmlCanvasElement::as_ref(plot_ref)),).expect("plotters canvas failed to initialize").into_drawing_area();backend.fill(&WHITE.mix(1.0)).expect("failed to clear canvas");let mut chart = ChartBuilder::on(&backend).set_label_area_size(LabelAreaPosition::Left, 40).set_label_area_size(LabelAreaPosition::Bottom, 40).build_cartesian_2d((0usize..6).into_segmented(),0..all_gen_mana.get().values().copied().max().map(|x| x + 3).unwrap_or(3),).expect("Failed to create chart");const COLORS: &[Color] = &Color::all();chart.configure_mesh().disable_x_mesh().x_desc("Color").y_desc("Count").y_labels(5).x_labels(7).x_label_formatter(&|idx| {match idx {SegmentValue::Exact(idx) => format!("{}?", COLORS[*idx]),SegmentValue::CenterOf(idx) => {COLORS.get(*idx).map(ToString::to_string).unwrap_or(String::new())}SegmentValue::Last => String::new(),}},).draw().expect("Failed to draw axises");chart.draw_series(Histogram::vertical(&chart).style(RED.mix(0.5).filled()).data(data.into_iter().map(|(c, i)| (COLORS.iter().position(|oc| oc == c).unwrap(),*i,)),),).expect("Failed to draw data");backend.present().expect("failed to present chart");};create_effect(move |_| {let data = all_gen_mana.get();if let Some(plot_ref) = plot_ref.get().as_ref() {plotted(plot_ref, &data.into_iter().collect::<Vec<_>>())}});view! {<div><h2 class="italic text-xl">"Player " {move || index.get() + 1} " : "<span class="font-mono not-italic bg-base-200 rounded p-1">{move || format!("{id}")}</span></h2><ul>{COLORS.iter().map(|c| {view! {<li>{c.color().to_title_case()} " Mana: "{pool.get(c).copied().unwrap_or(0)} " / (" {balance.get(*c)}")"</li>}}).collect_view()}</ul><div class="join"><inputclass="input input-bordered join-item"type="number"prop:value=move || gen_count.get()class:has-error=has_erroron:input=move |ev| {set_gen_count.set(match event_target_value(&ev).parse() {Ok(val) => {set_has_error.set(false);val}Err(_e) => {set_has_error.set(true);return;}},);}/><buttonclass="btn join-item"on:click=move |_| {let generated_mana: Vec<_> = balance.gen_mana(&mut rand::thread_rng()).take(gen_count.get()).collect();let counts = Color::all().into_iter().map(|c| (c,generated_mana.iter().filter(|col| col == &&c).count(),)).collect::<HashMap<_, _>>();set_all_gen_mana.update(|all_gen_mana| {for color in Color::all() {*all_gen_mana.entry(color).or_insert(0)+= counts.get(&color).copied().unwrap_or(0);}});set_gen_mana.set(Some(generated_mana));}>"Generate Mana"</button></div><div class="font-mono">{move || {if let Some(gen_mana) = gen_mana.get() {let mapped = Color::all().into_iter().map(|c| (c,gen_mana.iter().filter(|col| col == &&c).count(),)).collect::<HashMap<_, _>>();format!("{mapped:?}")} else {"".to_string()}}}</div><Show when=move || {all_gen_mana.with(|agm| agm.values().copied().sum::<usize>() > 0)}><div>"Total Mana: "{move || all_gen_mana.with(|agm| agm.values().sum::<usize>())}</div><canvas width="600" height="200" _ref=plot_ref></canvas></Show></div>}})}}}}
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.as_ref().trim().to_lowercase().split_whitespace().filter(|s| !["-", "+", "/", r"\", ":"].contains(s)).map(|s| Arc::from(s)).collect(),Arc::from(subtype.as_ref().trim().to_lowercase()),)}/// 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(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"));}}
//! 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}}}