The GameModule trait
A game on the side-chain is one Rust type that implements GameModule (dg_core::module):
pub trait GameModule: Send + Sync {
fn id(&self) -> &'static str;
fn apply(&self, ctx: &mut dyn Ctx, sender: &str, op: &serde_json::Value) -> Result<(), ActionError>;
fn view(&self, state: &dyn StateRead, query: &Query) -> Result<serde_json::Value, ActionError>;
fn end_block(&self, _ctx: &mut dyn Ctx) -> Result<(), ActionError> { Ok(()) } // optional
}
idis the stable name used inaction.gameand in state keys, for example"tictactoe".applyruns one op, the JSON a player sent.senderis the address that signed the transaction, already verified. ReturnErrand every write the transaction made is thrown away.viewanswers read-only HTTP queries:GET /v1/games/<id>?…calls it with the query string.end_blockruns once per block after every transaction, for things like expiring stale games. Most games do not need it.
How a move reaches your code
A player signs a transaction whose action is:
{ "type": "game", "game": "tictactoe", "op": { "kind": "move", "id": "00000000000000a1", "cell": 4 } }
The executor looks up the module whose id() is tictactoe and calls apply(ctx, sender, op). The reserved
ids market, htlc, backend, certificate, bridge and system belong to system modules and cannot be
used by a game.
Registering a game
A new game is one file in crates/dg-games/src/ plus one line in dg_games::all():
pub fn all() -> Vec<Box<dyn dg_core::GameModule>> {
vec![Box::new(TicTacToe), Box::new(Blackjack), Box::new(TowerDefense)]
}
The one rule
apply must be deterministic. Every node replays the chain and must reach the same state. No clock, no
random numbers, no floating point, no HashMap iteration order. Use what Ctx gives you instead
(ctx.height(), ctx.block_time_ms(), ctx.public_random(...)).