WBBGVTIE2LOWQOAMAHVKGA3BPHCFPUU6DLJ5OJRKIVA3KJEIKGPAC
WU27XQNRC2MSQS3XGYNM5DMR5ZBHHI4OTYXSDKTL3TKKTKWDZQWQC
VA2AH6FLKMMTKJTJKVBMTZYPM7QIEJ4RUDWJYG5BLPRVBOKRP3QQC
ALKPKOQUNWOGPIY4T6X4EPSBHWD6LVZOFR6IUIK77XC2FXBMVMTAC
Z5HB3ZOD4DSZCXDRGKDDMBW7FQW2PWWXVD2RTIL5MFH56BKFQVLQC
WJQ3536KELGGHQ53X7CL4TQYYGYWTPIQBHOMHAUBZSSJ5CBTW3YQC
62BXKX6RMC7TXVHKGAYUCKNS7FK2TXGBJEAFQPCQZPIA4Y46IKKAC
3ZXXZNMR6EA4HB4AFWBMTESO6QOYFOL2MBEJ534O22OYA22HSKGAC
GYVESXMMN2WA6ZE4NHRFIBVD5E62CWC4BGQHP24GYJQ6WJB2G2YAC
HXVQ65W4R5U2H2TMOJAPOSD53HKRJFWNZUQHOHPHYQVUQUFZEK2AC
X7KNY5KWCOLA2V345PUT4EVXVNHL6RGSJK3NNR6DHPOUC33HHCCAC
GVQM77M3PEI6CNAZ5D3ITAUYNRXN5WZCYDA5XKNWODJDCQZDT4ZAC
CADBHJ3WAUC7AYLSECEZPFBPUBPYHP3GJPWOIAMIL2OEIOX6MKDAC
ILH7JI6H226Y2MQCLT5IQNXBI3ZEOGSP4W7AJDZNH3HDS57I6D7AC
PYEWGJYLRURPMRVREJRWJMSS7DITHAOVIIDYDJADDZWN4JH7TNTQC
J7BRIGVBKBZRXMEATT4EJ53YXMXQZLWQ7XRKV7DSJD5UBSJM2S5QC
OBV5YHA4JNKEAZB4KWFR5OSOUBR47N5P6F6RL2CX4OVIQ4TF35NQC
EXTCVWVXRQ5XII6BJMYJPAU4JUMXHEZ52OFCUVKRMYI6IU5ALADAC
//! Handling of abilities and effects
use 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 what
pub 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 resolution
GameEffectState::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 resolution
Lingering,
/// Must be Set before it can be activated
Prepared,
/// 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 state
use 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 balance
const 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">
<input
class="input input-bordered join-item"
type="number"
prop:value=move || gen_count.get()
class:has-error=has_error
on: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;
}
},
);
}
/>
<button
class="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 game
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 {
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>
where
S: 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>
where
E: serde::de::Error,
{
Ok(Box::from(v))
}
}
impl<'de> serde::Deserialize<'de> for Subtype {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: 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 subtypes
pub 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 group
pub 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 player
pub 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>
where
I: 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 valid
game.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 suffix
deck: 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"/>
<input
type="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">
<input
type="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.”) "
<span
class=("text-error", move || !member.get().is_empty() && !is_quasimember())
class=("text-primary", is_quasimember)
>
{is_quasimember}
</span>
"Is Member? (“Search for a X card.”) "
<span
class=("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"/>
<input
type="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">
<input
type="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.”) "
<span
class=("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.”) "
<span
class=("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 purposes
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 suffix
deck: 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>
where
I: 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 valid
game.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 state
use 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 balance
const 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">
<input
class="input input-bordered join-item"
type="number"
prop:value=move || gen_count.get()
class:has-error=has_error
on: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;
}
},
);
}
/>
<button
class="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 order
pub const fn all() -> [Color; 7] {
use Color::*;
[
Mundane, Divine, Revelation, Grace, Growth, Crusade, Suffering,
]
}
/// Array of all factions in color wheel order
pub 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 resolution
Lingering,
/// Must be Set before it can be activated
Prepared,
/// 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>
where
S: 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>
where
E: serde::de::Error,
{
Ok(Box::from(v))
}
}
impl<'de> serde::Deserialize<'de> for Subtype {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: 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 subtypes
pub 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 group
pub 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 effects
use 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 what
pub 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 resolution
GameEffectState::HalfOpen
}
}
}