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 purposes
pub 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 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 {
pub 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(),
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 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 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 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 {
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 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_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.rs
expression: "[subtype_a, subtype_b, subtype_c]"
---
("mad:dot dasher", "mad-dot dasher", "mad:dot dasher")
//! 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
}
}
}
[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"/>
<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)
.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>; 6],
// 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()
}
<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 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
.split_whitespace()
.filter(|s| !["-", "+", "/", r"\", ":"].contains(s))
.map(Arc::from)
.collect(),
Arc::from(subtype),
)
}
/// 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_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 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
}
}
}
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"/>
<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();
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">
<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>
</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()
}