Skip to content

Overview

This tutorial will teach you how to build a game that runs on Oasis using the Gaming SDK. If you're unfamiliar with the basics of the Gaming SDK, first take a look at our overview documentation.

It's best to start simple, so we'll walk you through the steps for creating the simplest turn-based game there is: Tic-Tac-Toe. At the end of this tutorial you will have a Tic-Tac-Toe implementation with all game logic running directly on the blockchain!

Note

This tutorial does not use confidentiality since Tic-Tac-Toe has no secret state (i.e. the board is visible to all player). To see examples of games that uses confidentiality, check out our Battleship and Poker demos.

Defining Game Logic

To build a new game you minimally must implement 3 things which fully define your game's behavior: State, Moves, and Flow.

State

State is a structure defining the state that is stored for the game. For Tic-Tac-Toe the state is the content of each cell. For a card game, the state might be the cards in each player's hands as well as the contents of the deck.

The state is defined as a structure with arbitrary fields. It's up to the developer to decide how to encode the game state. For grid games such as Tic-Tac-Toe it's often convenient to store the grid as an array.

Moves

Moves is a set of functions that define the valid moves according to the game rules. Each function accepts the current game state and returns a new state reflecting the result of executing that move on the state. For example, in Tic-Tac-Toe a valid move is clicking on an unoccupied cell. A game can define any number of moves, and each move is implemented as a separate function.

Note

A move method must be a pure function, that is it must not modify any external state or produce other side-effects.

Flow

Flow contains definitions of the game flow: when a turn ends, when a player has won, etc. The SDK defines a fixed set of flow-related functions that can be implemented to customize the flow of the game. For this tutorial it suffices to implement only a few of these methods, but the SDK supports quite a bit more customization. See engine/src/flow.rs for the full list of flow methods.

Once these definitions are implemented the game SDK will take care of the rest—you'll have a fully functional game that can deployed as a smart contract!

Web UI

To allow players to interact with your game you need to develop a UI. Developing a UI is a separate process from developing the smart contracts. Since Oasis is compatible with web3, you can use existing Ethereum tools and web frameworks to create your game's front-end. We've also designed the SDK to be API-compatible with boardgame.io, so you're able to reuse their entire library of front-end components.

In this tutorial we'll use React to display our game in the browser.

We have created a boilerplate project initialized with an example, Tic-Tac-Toe, and a handful of supporting scripts to ease development. You can download this boilerplate as a Truffle box by running the following command withing a Contract Kit container:

$ truffle unbox oasislabs/game-box

At this point, you can follow the instructions outlined in the box's README to complete your installation, build your contracts, and launch your Web UI. The rest of the tutorial will walk you through the details of what's happening inside the SDK, and how you can implement your own game logic. Feel free to skip the Migration steps for now -- we'll get to those later.

Once you've completed everything up to the Playing section, minus the migrations, launch the web server with npm start, then navigate to http://localhost:8080/singleplayer in your browser:

Right now your web UI should show the following:

Setup

The panel on the right is a debugging tool provided by the game SDK (this is visible only in development mode). It allows you to inspect the current state of the game as well as trigger certain game actions.

The two important fields are G which shows the current game state, and ctx which shows parameters of the game such as the current player. Take a look at the gameover field in ctx. When the game finishes this field will indicate which player won or whether the game ended in a draw.

You'll notice many of the fields are empty because we haven't implemented anything else.

Tic-Tac-Toe

Let's define the game logic for Tic-Tac-Toe. The game is implemented in Rust and compiled to WASM bytecode, which can be deployed to Oasis as a smart contract.

Note

In the first part of this tutorial we'll use development mode, in which our smart contracts run entirely in the browser. This mode helpful for development as it allows us to implement and test the functionality of our game as if it were running on the blockchain without worrying about wallets, accounts, gas, etc.

In Step 3 we'll deploy the smart contracts to Oasis and update the client to use the deployed smart contracts.

The core game logic is implemented in file core/src/lib.rs.

Step 1: Defining the Game Logic

Defining State

Let's start by defining the game state object. Tic-Tac-Toe is played on a 3x3 grid, which we can represent as follows:

/// Define the state shape.
/// State type.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct State {
    pub cells: [i32; 9];
}
impl Default for State {
    fn default() -> Self {
        State {
            cells: [-1; 9]
        }
    }
}

This defines a single field names cells, which is an array of type i32 and length 9. We will use value 1 to represent cells occupied by player 1, value 2 for cells by player 2, and value -1 to represent empty cells. We take advantage of the Default trait to make it simpler to initialize empty states automatically inside the SDK.

Now that we've defined the type of the state, we need to define the initial state for new games. This is accomplished by implementing the initial_state method in Flow:

/// Define the game flow.
#[flow]
trait Flow {
    fn initial_state(&self) -> State {
        Default::default()
    }
}

This code defines our intial state as an empty grid (value -1 for each cell). More complicated games (such as our Battleship demo) might perform additional steps inside initial_state, such as placing pieces or randomly drawing cards.

You'll see that we've used the #[flow] macro here. This eliminates a bit of Rust boilerplate so you can focus on the important bits of your game logic. Rust is intimidating enough as is! We do the same trick with the #[moves] macro, described below.

Returning to the debug panel in the browser, we can see our web app already knows about our game state:

Setup

Notice we didn't need to write any additional code—the SDK reads our game state from the smart contract, deserializes it, and exposes it as JSON to our web app. Cool!

Defining Moves

The game is pretty boring right now because players can't do anything. Let's define the moves for our game.

In Tic-Tac-Toe, a player has only 1 type of move: capturing an unoccupied cell. The effect of this move is that the captured cell belongs to that player. We can express this logic as follows:

#[moves]
trait Moves {
    pub fn click_cell(state: &UserState<State>, player_id: u16, args: &Option<Value>) -> Result<UserState<State>, Box<Error>> {
        let id = Moves::load_cell_id(args).ok_or(Errors::InvalidCell)?;
        if state.g.cells[id] != -1 {
            // Cell is already occupied
            Err(Box::new(Errors::InvalidCell))
        } else {
            let mut new_state = state.clone();
            new_state.g.cells[id] = state.ctx.current_player as i32;
            Ok(new_state)
        }
    }
}

This defines a new move named click_cell. This method validates the parameters (making sure the cell is on our grid and is currently unoccupied) and updates the clicked cell to the current player. The method returns the updated state for valid moves and Err if the move is invalid.

Warning

You must validate the move before updating the state. In particular, you should assume the args parameter is untrusted even if your web app performs validation. A player can generate a move with arbitrary parameter values (by circumventing your web app) so it's up to your smart contract—not the web app—to reject invalid moves! You can use the can_make_move Flow method, which is executed before every move, to simplify this.

Each move method must have the same function signature as our click_cell method above, which accepts two parameters: the current game state and a parameter type Option<Value> which contains parameters of the move (in our case, the cell being clicked).

The method returns a new State resulting from executing the move on the input state. If the move is invalid the method returns Err, which instructs the SDK to reject the move and wait for the player to make a different move.

Returning to our web app we can now see click_cell listed under the Moves section. Although we don't yet have a UI to interact with, the debugging panel allows us to execute moves manually. Click on the move name (or press a) and enter a cell index (e.g 0), then press return.

Setup

You can see the game state is updated, with the specified cell becoming occupied by Player 1. Do this again to execute a move for Player 2.

Note

The SDK automatically manages game flow by switching the active player when a turn ends. You can see this in the Players section of the debugging panel, where the current player changes each time a move is made.

The definition of a turn is defined by the flow methods and can be customized by the developer. Since we didn't override any of these methods,our game is using the default implementations in which a player's turn ends after a single move and the game alternates turns between the two players.

By overriding the appropriate flow methods you can define more complex game flows, for example allowing multiple moves per turn or different turn orders.

Next, try to generate invalid moves such as a cell greater than 8 or a cell that is already occupied. You'll notice the game state is not updated and the game flow does not proceed. This is exactly what we implemented in our click_cell method above.

Congratulations! You've implemented the core game mechanics for Tic-Tac-Toe.

Defining Victory Condition

If you generate enough moves in the debugging panel you may notice that the game never ends (ctx.gameover remains null) even after the grid is full or a player successfully captures 3 cells in a row. This is because we haven't told the SDK how to determine the winner.

The flow method end_game_if serves this purpose. This method is invoked after each valid move. It inputs the current game state and returns a status indicating whether the game is over and if so, which player (if any) won the game.

An implementation of this method for Tic-Tac-Toe is shown below (you can add it immediately below the initial_state method). For simplicity we have separated the victory checking code into a separate method is_victory which you can add anywhere in the file.

impl UserFlow<State> for Flow {
    ...

    fn end_game_if(&self, state: &UserState<State>) -> ??? {
        // If the most recent move results in 3 in a row, the current player is the winner.
        if is_victory(state.g.cells) {
            return Winner(state.ctx.current_player);
        }

        // If the grid is full (without a winner) the game ends in a draw.
        if state.g.cells.into_iter().all(|c| *c != -1) {
            return Draw;
        }

        // Otherwise the game is not finished.
        None
    }
}

fn is_victory (cells: Cells) -> bool {
    let positions: [[usize; 3]; 8] = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    for (_, pos) in positions.iter().enumerate() {
        let symbol = cells[pos[0]];
        if symbol == -1 {
            continue;
        }
        let mut won = true;
        for (_, i) in pos.iter().enumerate() {
            if cells[*i] != symbol {
                won = false;
                break;
            }
        }
        if won {
            return true;
        }
    }
    false
}

This code simply tests every horizontal and diagonal sequence on the grid and determines whether any player occupies three cells in a row. If the board is full and no player has won, the method returns Draw. Otherwise, the method returns None indicating that game play should continue.

Let's return to the debugging panel. Try to a generate a set of moves which causes one player to win (for example, have Player 1 select the first 3 cells in the array, corresponding to the top row of the grid).

When this happens, you'll see the ctx.gameover value change to indicate that the player has won:

Setup

Restart the game and experiment with different moves (for example, see if you can force a draw) and verify this is reflected in ctx.gameover

Hooray! We now have fully working game logic for Tic-Tac-Toe.

Step 2: Defining a Web UI

Our game works but it's not very fun to play Tic-Tic-Toe from the debugging panel. The final step for building a fully playable game is rendering the game in the browser.

Note that by design the web UI is "dumb" and contains none of the core game logic. Instead, the web UI simply receives game state updates from the SDK and renders the game to reflect that state. The web UI also capable of generating moves (just like we did from the debugging panel) in response to player interactions such as clicking a cell.

The UI can be implemented with any web framework and is mostly beyond the scope of this tutorial.

Our example use React to render our grid. Here is code for translating the game state into a grid show allowing players to click on a cell in order to make their move:

  render() {
    let tbody = [];
    for (let i = 0; i < 3; i++) {
      let cells = [];

      for (let j = 0; j < 3; j++) {
        const id = 3 * i + j;

        let cellValue = '';
        switch (this.props.G.cells[id]) {
            case 1:
                cellValue = 'X';
                break;
            case 2:
                cellValue = "O";
                break;
        }

        cells.push(
          <td
            key={id}
            className={this.isActive(id) ? 'active' : ''}
            onClick={() => this.onClick(id)}
          >
            {cellValue}
          </td>
        );
      }
      tbody.push(<tr key={i}>{cells}</tr>);
    }

    let winner = null;
    if (this.props.ctx.gameover) {
      winner =
        this.props.ctx.gameover.winner !== undefined ? (
          <div id="winner">Winner: {this.props.ctx.gameover.winner}</div>
        ) : (
          <div id="winner">Draw!</div>
        );
    }

    let player = null;
    if (this.props.playerID) {
      player = <div id="player">Player: {this.props.playerID}</div>;
    }

    let rendered = (
      <div>
        <table id="board">
          <tbody>{tbody}</tbody>
        </table>
        {player}
        {winner}
      </div>
    );

    return rendered;
  }

Fortunately, the SDK has automatically generated methods for each of the moves we defined in our Moves trait, in this case click_cell, and these methods are passed directly into our Board component as props! All we need to do is add a click handler that responds to a cell click and calls the move method with that cell's ID:

onClick = id => {
  if (this.isActive(id)) {
    this.props.moves.click_cell(id)
  }
}

The arguments to your move function can be any number of JSON-serializable objects -- they will ultimately be converted into a JSON list, which becomes our Value in the Rust move method.

Adding this Javascript method was the final step in completing the cycle between the Javascript and Rust code. We can now play our game entirely from the web UI:

Setup

Step 3: Deploying the Smart Contracts

Time to deploy our smart contracts to the blockchain! Fortunately, our Truffle box makes this as easy as possible. It already contains boilerplate code for a game server contract, and all you need to do is migrate it to the Oasis testnet. With your game logic completed, run the following command from your project root:

$ truffle migrate --network oasis

Your game server is now live and ready-to-go. The included contract manages multiple game instances, and you register each player's Oasis address with a game instance when you create the game. Creating a game is simple: under-the-hood, it's a single web3 send. We've wrapped this in a helper script that makes it even easier to create player-vs-player games:

$ truffle exec scripts/create.js --network oasis
> ✔ Created a new game with ID: 1
>    Share this link with Player 1: http://localhost:8080/multiplayer?gameId=1&token=eyJwcm...
>    Share this link with Player 2: http://localhost:8080/multiplayer?gameId=1&token=eyJwcm...

This script will generate a list of "magic links," one for each player, that encodes a game-specific token and a soft wallet private key. This means that you can test your game on-chain locally without even needing to install Metamask!

Now you can share that link with someone else on your local network to play your newly-tested game with friends, or you can publish your static site (in the dist/ directory) on a public server to play with a larger audience. Good luck!

Next steps

This is just the beginning! There are many exciting possibilities to explore as you develop your own game.

Here are a few challenge exercises for this tutorial, ranging from easiest to hardest:

Challenge 1: Extend the board

3x3 Tic-Tac-Toe is too simple to be much fun. Modify the code to support a 4x4 grid of Tic-Tac-Toe. Once you've finished this, try to figure out how to support 3 players in the game.

Challenge 2: Add wagering

Create an ERC20 token contract and integrate it into the game. Players should be able to transfer tokens into a "pot" before the game begins, and the game automatically pays out the pot to the winner (or return funds in case of a tie.)

Acknowledgements

Thanks to Andrew Osheroff for designing and developing the game SDK and examples.

The game SDK is based on the excellent boardgame.io project. We have reused many of their APIs and abstractions as well as Javascript libraries for the web UI. We re-implemented the core engine and backend code to replace the server-client model with smart contracts and web3 calls.