Right now, the repo is private, but I do hope to make it public sometime in the future. Due to game state being completely (de)serializable (using serde), it should be condusive to allowing players to create their own cards and play with them. I'd want to talk a bit about the architecure bits I think are cool or I'm having trouble with here.
#Deity - A Card Game (Maybe Possibly At Least a Really Cool Card Game Engine or its Pieces)
26 messages · Page 1 of 1 (latest)
First things first: I would literally die if not for https://github.com/dtolnay/typetag. Made it really easy to not have gigantic enum that confuses me for Absolutely No Reason.
The "fun" part of the architecture are behavior trees! Immutable behavior trees! God they were so difficult to do...
Basically Actions (Behavior Tree) look like this: ```rust
pub trait Action: fmt::Debug + Send + Sync {
fn children(&self) -> Option<Vec<ActionArc>> {
None
}
/// ## Stack
/// - stack has a boolean on it for every running command returned.
fn tick(
&self,
state: &GameState,
stack: &[bool],
children: Option<&[RunningAction]>,
blackboard: &Blackboard,
rng: &mut RandomGenerator,
) -> ActionResult;
}
Blackboard just lets actions store strings, numbers, and game types between themselves in the same tree using numbered slots.
ActionResult can be running and give a Command to run, a Success, a Failure, or a request to recreate the children of the node.
Part of the magic is RunningAction, which looks like ```rust
pub struct RunningAction {
action: ActionArc,
stack: Vec<bool>,
children: Option<Vec<RunningAction>>,
}
Which contains stuff specific to the action.
The other part is the way to handle the fact that while computers get "Make player 1 have 5 less life.", players in card games don't think like that. It's generally all players, myself, or my opponent(s) that cards operate in.
We Do The Java Thing™️ and we have ActionFactory which looks like this: ```rust
pub trait ActionFactory: fmt::Debug + Send + Sync {
fn create(
&self,
applicant: command::Player,
state: &GameState,
blackboard: &Blackboard,
) -> Option<ActionArc>;
}
As an implementation thing, all Arc'd Actions are implement ActionFactory! (We could also implement ActionFactory for Actions that implement Clone, but most Actions don't implement Clone b/c they are immutable and thus Arc'd)
The blackboard here allows us to share information between different actions, the same as tick.
The important thing, though, is applicant, which tells us who we are [the player activating the factory] and thus we can go our merry way creating actions for fixed players while letting card writers think in a more card-gamey way.
This was so much trial and error though. I wish someone had an article on "How to Implement BeTrees With No Internal Mutability (for Dummies)" cuz wooooooow I had so many false starts. But I'm p happy with where I'm at now.
My main problems are how to apply effects, and how to do testing in a realistic way.
Mostly b/c I use UUIDs everywhere (well, for CardInstances, Effects, and StackItems, basically everything I foresee being negatable/counterable/targetable)
What game engine you used for this?
Nothing yet. It's just a Rust library for progressing the game state using a set of commands at the moment. I plan to use a server-client architecture once the core is in a usable state. I plan to use quinn for the server, and probably bevy for the client.
I do have a test runner that takes two states and some level of commands to try to get from one state to the other.
Ok, Imma come back to this game idea, but with a Datalog engine this time, because behavior trees were both too powerful in a different direction than I wanted and not powerful enough to express certain things.
Ok, so I wrote a Datalog engine: arde. It's just plain Datalog + holes poked in it for Rust-based queries (intrinsics).
I'm almost gonna follow how MtG Arena uses CLIPS, and have a similar structure: there's a GRE that knows the game rules, but not card details, and a scripting system that knows card details. However, I have to do it slightly differently due to the difference between Datalog and CLIPS. So Datalog is a "backwards chaining" (with "forward chaining" as an optimization) logic language, while CLIPS is a "forward chaining" logic language.
Basically, CLIPS has actions that it does when a rule matches, while Datalog simply asserts a fact is true. CLIPS actions can assert, retract, and modify facts. CLIPS rules also have a salience that determines when it runs, and also feature stuff beyond simple fact changing machinery.
So my hypothesis is that I can get a similar amount of expressiveness with Datalog + a Turing-complete scripting language (which I will use Rune for now). A "rule" is basically a salience, some condition Datalog (an arde Goal), and some action Rune script.
I've gotten conceding to work (for a gamestate with no cards), so now for everything else...
ok, so nicer display (and doesn't include irrelevant details like the random seed of the snapshot)
Once player's can do more than just pass priority and end the current phase, I might also allow skips to autoconfirm the end phase action.
But what I find really cool is that the actual game part (the GRE that runs everything and actually changes the state) knows nothing about the frontend skips and autoconfirming actions. It can do one of 2 things: tell you what actions you can take, and progress the state based off an action.
turn taking is set up!
So I spent a day switching from ggez to bevy... Why? idk sometimes I would want the card action framework to fall into my head from Rust Up Above, but that ain't happening so I gotta do the do.