Building an event sourced game with Zeitgeist.
Building with the Zeitgeist SDK.
As a demo for the polkadot global hackathon we built a small tick tack toe game using the zeitgeist SDK and polkadot.js. Storing the game events on chain using remarks and integrating prediction markets as a simple betting mechanism. The architecture of the application has two main components and some shared packages that is used in both the client and the server/referee component of the application.
Full code @github
Shared Game Logic[goto]
The game logic is a simple typescript library that defines the state of a game and how to fold over game events to produce a new state. Think event sourcing, map-reduce or redux and you get the picture.
The ui is a react application that uses the Zeitgeist SDK and polkadot.js to render the gamestate visually and create new transactions for creating a game, making a move within a game, buying the assets for a participant in the game(betting) and possibly disputing an outcome.
New game -> sdk.models.createCpmmMarketAndDeployAssets()[goto]
This creates a new market on chain and deploys a liquidity pool for the market with equal amount of assets(50/50) for the two players involved. In this demo the address for the oracle(the account that can resolve the market and determine the winner) is hardcoded to an address of our choice and that our referee oracle has access to.
Make move ->sdk.api.tx.system.remarkWithEvent()[goto]
When a player makes a move we create a valid json string representing the move and store them on the chain as a remark. We call remarkWithEvent so its easier for our little referee indexer to pick it up.
Place bet -> sdk.api.tx.swaps.swapExactAmountIn()[goto]
When we bet on a player we swap some of our ZTG for the players asset. For a fresh game 1 ZTG will give us 0.5 of the asset and shifting the price in favour of who we betted on.
Dispute outcome -> (market: SDK.Market).dispute()[goto]
When the player thinks the referee has made a wrong call and the client code detects some discrepancies in local state vs referee state(see section “Disputing game market-outcome.”, the user can dispute the outcome. The disputed outcome will automatically be inferred to be the reported looser since we only have to participants in the game.
The referee is a node.js(ts) process that can basically run anywhere you want to. It listens and tails on chain events as mentioned above, notably the create market and remark events and aggregates game state based on that. It has access to the key-pair for the account designated to be our game oracle.
The referee code listens to the following on chain events:
MarketCreated -> Creates a fresh game state and stores it in the referee mongodb .
Remarked -> Will apply game even JSON in extrinsic to the game and store the new game state in the db.
MarketClosed -> Will call (market::SDK.Market).reportOutcome() on the closed market with the state of the winner as the outcome. NB: unfinished work as the close event was not implemented on chain when coding up the demo.
The referee also exposes a simple api to fetch all the aggregated games that are stored in the db. Our own little indexer.
Disputing game market-outcome.
Because we are sharing the game logic as a pure function over event and state, we can also fetch all the game events mentioned above directly from the chain in the client and compute the state of the game locally. We then compare the state that the referee is reporting and our locally computed state. If the states doesn’t match our client enables the user to dispute the outcome of the game using the on chain dispute mechanism.
Thats a wrap! Haux!