The Decentraland SDK includes all sorts of hidden gems. One of these is the ability to port existing games and content from other platforms into Decentraland!

My name is Juan Cazala – I’m part of the dApps crew at Decentraland. We’re the team behind the Decentraland Marketplace and the Agora voting platform. If you ever have any feedback for our dApps, you can always reach out to me on Discord!

Today, I’d like to teach you how to port a simple chess game from Redux into our SDK so that you can play a live, 3D version of chess right in the metaverse.

You can clone the repo that I created for this tutorial by following the installation guide, or you can follow along with each of the steps to recreate the game yourself.

Let’s get started!

Porting your Redux game into Decentraland #

My goal with this tutorial is to show you how you can take an existing Redux game and port it to Decentraland using our SDK. The game I chose to use is @zackpudil’s redux-chess-app, which looks something like this:

A quick note on prerequisites #

This tutorial does assume you are using a unix machine (or at least using a unix-like terminal), with git, npm and Decentraland’s SDK installed. If you haven’t installed them yet, you can by following these steps:

  • Install git.
  • Install nvm and then run nvm use stable to install npm.
  • Once you have npm installed, run npm install -g decentraland to install Decentraland’s SDK.

Want to fast-forward through some of my steps? #

Each step in this tutorial is represented by its own release in the tutorial’s repo, so you can fast-foward to any step using git clone, like this:

git clone --branch step-0 https://github.com/cazala/decentraland-redux-chess-app.git --depth=1

Just replace step-0 with the step number you want to jump to.

Step 0: Initialize your scene #

The first step is to clone the repo from the original Redux game that we want to work with:

git clone https://github.com/zackpudil/redux-chess-app
cd redux-chess-app

Then, install its dependencies by running the following command from the repo’s folder:

npm install

Now we can initialize our Decentraland scene in a folder inside this same repo. We’ll create a scene directory and initialize the SDK in it:

mkdir scene
cd scene
dcl init

The SDK will prompt you with a few questions in order to initialize the scene. It will ask you which type of scene is this going to be. Make sure you select “Remote” (the third option).

It will also ask you which parcel(s) comprise this scene. If you don’t have any parcels that’s okay, you can enter any coordinates like 0,0 since we are running this scene locally and you don’t need to own any LAND to build and preview scenes using the SDK.

Now that we have initialized our scene, we are (almost) ready to start porting the game.

First, we need to replace the aliased imports with relative ones, because the former are not compatible with Decentraland’s SDK, like this:

If you don’t want to deal with replacing all of those aliases, and just want to jump into the fun stuff, you can skip this step by running the fast-forward command from below:

Diff: To see the full diff of changes for this step check this commit.

Fast-Forward: to jump to the end of this step, run:

git clone --branch step-0 https://github.com/cazala/decentraland-redux-chess-app.git --depth=1

Step 1: Port your game to the Decentraland scene #

Now we can start porting the chess game to our scene. Inside the scene directory that we created during the previous step, you’ll find a server folder. Let’s navigate into that folder:

cd server

We can start by deleting the State.ts file, since the game state will now reside in the Redux store.

rm State.ts

Now let’s add a Store.ts file where we will import the redux store, initialize it, and then export it with all of the actions that we will use in our new Decentraland scene:

We need to modify ConectedClient.ts so that all of the clients get updated when there’s a change in the Redux store:

We can now create an assets folder inside our scene directory to place the models for all the pieces:

cd ..
mkdir assets

(If you want, you can find all of these assets here.)

Once all the models have been placed in the assets folder we can back up to the server’s folder:

cd ..
cd server

Finally, we need to modify RemoteScene.tsx so that it imports the Redux store, gets its state, and uses that state to render the board and all of the pieces in the scene.

Let’s start by removing the import of State.ts and replace it with Store.ts

Now let’s replace the render function with the following:

async render() {
  return <scene>{this.renderBoard()}</scene>
}

This will render the output of the renderBoard method. Let’s define this method:

renderBoard() {
  return (
    <entity
      position={{
        x: 1.5,
        y: 0,
        z: 1.5
      }}
    >
      {store.getState().squares.map(this.renderSquare)}
    </entity>
  )
}

That will render a base entity and position it on the scene, then it will get the squares array from the Redux store’s state and run a map operation over it, running the renderSquare method for each square.

This method will receive each square and its index (which goes from 0 to 63). Each square contains the state for a particular tile on the board. The state indicates:

  • If there’s a chess piece on the tile, and which piece that is
  • If the tile is selected
  • If the tile is highlighted
  • If the tile is in check

We can then use this information to render the board on the scene.

The first thing we need to do is convert the 1-dimensional index (0 to 63) into 3D coords (x, y, z).

renderSquare(square: any, index: number) {
  const x = 7 - (index % 8)
  const y = 0
  const z = Math.floor(index / 8)
  const position = { x, y, z }

The second thing we have to do is figure out the color of the tile, which will alternate between black and white, unless the tile is in a special state (highlighted, selected, or in check):

let color = (x + z) % 2 === 0 ? '#FFFFFF' : '#000000'
if (square.selected) {
  color = '#7FFF00'
} else if (square.highlighted) {
  color = square.pieceId !== '_' ? '#FF0000' : '#FFFF00'
} else if (square.check) {
  color = '#FFA500'
}
  • If the square is selected, we make it green.
  • If the square is highlighted, we compare the pieceId with the string ‘_’ (which means there’s no chess piece on that square).
  • If there are no pieces on the square, the player can move to it, so we make it yellow.
  • If there’s a piece on the square, the player can eat it, so we make it red.
  • If the square is in check we will make it orange.

Before rendering the actual tile, let’s add a modelsById map at the top of our file that maps each pieceId to the corresponding model, like this:

const modelsById: { [key: string]: string } = {
  B: 'assets/LP Bishop_White.gltf',
  b: 'assets/LP Bishop_Black.gltf',
  K: 'assets/LP King_White.gltf',
  k: 'assets/LP King_Black.gltf',
  N: 'assets/LP Knight_White.gltf',
  n: 'assets/LP Knight_Black.gltf',
  P: 'assets/LP Pawn_White.gltf',
  p: 'assets/LP Pawn_Black.gltf',
  Q: 'assets/LP Queen_White.gltf',
  q: 'assets/LP Queen_Black.gltf',
  R: 'assets/LP Rook_White.gltf',
  r: 'assets/LP Rook_Black.gltf'
}

Now we can write the last part of the renderSquare method:

const tileSize = { x: 1, y: 0.1, z: 1 }
return (
  <entity position={position}>
    <box color={color} scale={tileSize} id={`${square.id}-tile`} />
    {square.pieceId in modelsById ? (
      <gltf-model src={modelsById[square.pieceId]} id={`${square.id}-piece`} />
    ) : null}
  </entity>
)

For each square, we are rendering an <entity> as a wrapper in its corresponding position, inside this entity we render a <box> that represents the tile. If the square.pieceId is inside our modelsById map, we also render a <glft-model> that represents the piece.

We need to add ids to both the <box> and the <gltf-model>.

In order to listen for a click event on an entity, the entity must have its own id.

To listen for events we need to modify the eventSubscriber that was created as part of the default scene, let’s start by removing the call to setState:

We can get the elementId from the event object that receives the listener:

const { elementId } = event.data
if (elementId != null) {
  // dispatch redux action
}

That elementId can have either a -tile suffix or a -piece suffix. We need to get the squareId from that elementIdby removing the suffix. Let’s add this helper function at the top of the file:

const getSquareId = (elementId: string) => elementId.split('-')[0]

This expression takes the string from elementId, splits it in two at the - character, and returns the first part. Let’s use it to get the squareId and dispatch a squareClick action:

sceneDidMount() {
  this.eventSubscriber.on('click', event => {
    const { elementId } = event.data
    if (elementId != null) {
      const squareId = getSquareId(elementId)
      const square = store
        .getState()
        .squares.find((square: any) => square.id === squareId)
      store.dispatch(squareClick(square.id, square.pieceId, square.color))
    }
  })
}

Now we are all set!

Let’s run a build for the server and test out what we have so far:

npm run build
npm start

That will start our server, which we must keep running. On a second terminal window, go to the scene directory and start the SDK preview:

cd ..
dcl preview

From your web browser, open http://localhost:8000 and you should see the board rendered on the screen, you should be able to click on the piece to move them, in the same way as in the original game!

Diff: to see the full diff of changes for this step check this commit.

Fast-Forward: to jump to the end of this step run:

git clone --branch step-1 https://github.com/cazala/decentraland-redux-chess-app.git --depth=1

Step 2: Supporting multiple players! #

It’s no fun to play chess by yourself, so in this last step we will modify the server so that it:

  • Keeps track of which clients are playing
  • Keeps track of the status of the match
  • Only lets the clients who are currently playing move the pieces
  • Only let clients move their own pieces

So the first thing we will do is add a match module to the redux game, under src/modules/match.

In that new module, let’s add three actions: registerPlayer, unregisterPlayer and checkmate:

// src/modules/match/actions.js
export const REGISTER_PLAYER = 'chess/match/register_player'
export const UNREGISTER_PLAYER = 'chess/match/unregister_player'
export const CHECKMATE = 'chess/match/checkmate'

export const registerPlayer = (playerId, isWhite) => ({
  type: REGISTER_PLAYER,
  playerId,
  isWhite
})

export const unregisterPlayer = playerId => ({
  type: UNREGISTER_PLAYER,
  playerId
})

export const checkmate = () => ({
  type: CHECKMATE
})

Let’s add a reducer to handle those actions:

// src/modules/match/reducer.js
import { INIT_SQUARES } from '../squares/actions'
import { REGISTER_PLAYER, UNREGISTER_PLAYER, CHECKMATE } from './actions'

const STATUS = {
  idle: 'idle',
  started: 'started',
  checkmate: 'checkmate'
}

const initialState = {
  playerWhite: null,
  playerBlack: null,
  status: STATUS.idle
}

export default (state = initialState, action) => {
  switch (action.type) {
   case INIT_SQUARES:
      return initialState

    case REGISTER_PLAYER: {
      // ...
    }

    case UNREGISTER_PLAYER: {
      // ...
    }

    case CHECKMATE: {
      // ...
    }

    default:
      return state
  }
}

In this section of the state, we will keep track of the game status. The game status can be either idle, started or checkmate. We will also store the id of the clients that are using either the black pieces or the white ones. Below we will see how to handle each of the possible actions. We’ll start by the action of resetting to the initial state, which is already defined:

case INIT_SQUARES:
      return initialState

The INIT_SQUARES action is quite simple, it just resets the state to its default values.

Let’s look at the action required to add a new player into the game:

case REGISTER_PLAYER: {
  let { playerWhite, playerBlack, status } = state

  if (playerWhite && playerBlack) {
    return state // both players already registered
  }

  if (playerWhite === action.playerId || playerBlack === action.playerId) {
    return state // this player is already registered
  }

  // register the player
  if (action.isWhite) {
    playerWhite = action.playerId
  } else {
    playerBlack = action.playerId
  }

  if (playerWhite && playerBlack) {
    status = STATUS.started // if both players are registered the game can start
  }

  return {
    playerWhite: playerWhite,
    playerBlack: playerBlack,
    status: status
  }
}
  • If both players are already registered, we will ignore this action.
  • If this player id is already registered as the white or the black player, we will also ignore this action.

We then check the value of action.isWhite to find out which color to register the new client with. Once there are two clients registered, with each one assigned to a color, we change the status to started.

That’s all we have to do to register the players for the game. Now, let’s see how to unregister them:

case UNREGISTER_PLAYER: {
  const { playerWhite, playerBlack } = state
  if (playerWhite === action.playerId || playerBlack === action.playerId) {
    return initialState // one of the players playing left, so we reset the game
  }
}

We simply check if the client that wants to unregister is one of the two active players, and if so, we reset the entire game to the idle state.

Finally, we handle the “checkmate” action:

case CHECKMATE: {
  if (state.status === STATUS.started) {
    return Object.assign({}, state, {
      status: STATUS.checkmate
    })
  }
  return state
}

So, now our server is capable of registering which players are currently playing. We can plug this new reducer into our store (this is src/store):

The last thing we need to do on the Redux side is to modify the Analysis Middleware to handle the “checkmate” action:

Basically, this code checks to see if the king is in check. If it is, we filter all squares with pieces from the player who is in check:

const squares = store.getState().squares
const squaresWithPiecesFromPlayerInCheck = squares.filter(
  square =>
    square.pieceId !== '_' &&
    (isWhite ? square.color === 'piece_black' : 'piece_white')
)

Then we run our game engine on each of those squares, and compute the amount of valid moves that the player has left:

const amountOfValidMoves = squaresWithPiecesFromPlayerInCheck.reduce(
  (moves, square) => moves + engine(squares)(square.pieceId)(square.id).length,
  0
)

If the amount of valid moves is 0, then we know the player is in checkmate, so we can dispatch a checkmate() action, followed by a game reset 10 seconds later:

if (amountOfValidMoves === 0) {
  if (store.getState().match.status === 'started') {
    next(checkmate())
    setTimeout(() => next(initSquares()), 10000)
  }
}

That wraps up everything we had to do on the Redux side! Let’s go back to the server in (scene/server) and modify the Server.ts file so that it dispatches an unregisterPlayer() action when a client disconnects:

Finally, we need to modify RemoteScene.tsx to handle both game states (game started and idle), allow user registration, and prevent users from playing when it’s not their turn.

The first thing we need to do is to add an id to each client (this will be used as the player id). Let’s generate this id randomly:

Now let’s replace the render method with the following:

async render() {
  const status = store.getState().match.status
  return (
    <scene>
      {status === 'idle' ? this.renderIdle() : this.renderBoard()}
    </scene>
  )
}

That will read the game status from the Redux store and render either the idle state or the board.

We can now define what we render when the scene is idle in renderIdle(). We will render a white queen and a black queen on the scene, and some text that reads “Choose your color”.

We will assign the ids register-white and register-black to the queens so we can listen to click events on them. When any of the players is registered, we will render that queen in the position y: 1 (elevated up in the air) to indicate that it has already been selected by another player:

renderIdle() {
  const { playerWhite, playerBlack } = store.getState().match
  return (
    <entity>
      <gltf-model
        src={modelsById['Q']}
        id="register-white"
        position={{ x: 3.5, y: playerWhite ? 1 : 0, z: 5 }}
      />
      <gltf-model
        src={modelsById['q']}
        id="register-black"
        position={{ x: 6.5, y: playerBlack ? 1 : 0, z: 5 }}
      />
      <text
        value="Choose your color"
        color="#000000"
        position={{ x: 5, y: 2, z: 5 }}
        width={3}
        billboard={7}
      />
    </entity>
  )
}

Now we only need to modify the eventSubscriber to dispatch the register actions, and to prevent dispatching click actions when it’s not the player’s turn.

First, we will read whose turn it is (black or white) and both player ids from the state:

this.eventSubscriber.on('click', event => {
  const { elementId } = event.data
  const state = store.getState()
  const {
    game: { whiteTurn },
    match: { playerWhite, playerBlack }
  } = state

Now we can check to see either of the two “register” queens have been clicked on, and if so, dispatch a registerPlayeraction. The first player who registers will also dispatch an initSquares to reset the board from any previous games:

if (elementId === 'register-white') {
  if (!playerBlack) {
    store.dispatch(initSquares()) // let the first player who registers init the board
  }
  store.dispatch(registerPlayer(this.id, true))
} else if (elementId === 'register-black') {
  if (!playerWhite) {
    store.dispatch(initSquares()) // let the first player who registers init the board
  }
  store.dispatch(registerPlayer(this.id, false))
}

Finally, if the elementId doesn’t match any of the “register” queens, we can dispatch a squareClick event, but only after checking that it is this client’s turn:

} else if (elementId != null) {
   // players can click squares only on their turn
  if (whiteTurn && this.id !== playerWhite) return
  if (!whiteTurn && this.id !== playerBlack) return

  // click on square
  const squareId = getSquareId(elementId)
  const square = state.squares.find(
    (square: any) => square.id === squareId
  )
  store.dispatch(squareClick(square.id, square.pieceId, square.color))

Finally let’s add a <text> that lets the players know whose turn it is, and announces when there’s a “checkmate”.

For this, we will add a renderMessage method and call it from renderBoard:

Let’s define renderMessage using the following:

renderMessage() {
  const state = store.getState()
  const { whiteTurn } = state.game
  const { playerWhite, playerBlack, status } = state.match
  const yourTurn =
    (whiteTurn && playerWhite === this.id) ||
    (!whiteTurn && playerBlack === this.id)
  const theirTurn =
    (whiteTurn && playerBlack === this.id) ||
    (!whiteTurn && playerWhite === this.id)

  return status === 'checkmate' ? (
    <text
      value="Checkmate!"
      color="#FF0000"
      position={{ x: 3.5, y: 2, z: 3.5 }}
      width={3}
      billboard={7}
    />
  ) : yourTurn ? (
    <text
      value="It's your turn!"
      color="#000000"
      position={{ x: 3.5, y: 2, z: 3.5 }}
      width={2}
      billboard={7}
    />
  ) : theirTurn ? (
    <text
      value="It's your opponent's turn"
      color="#AAAAAA"
      position={{ x: 3.5, y: 2, z: 3.5 }}
      width={2}
      billboard={7}
    />
  ) : null
}

That’s all there is to it!

Now, two players can start a match and play against each other. Any other connected client will be able to see the game, but they won’t be able to move any of the pieces:

Remember that after making any changes to the code you will need to re-build your server and restart it:

npm run build
npm start

Diff: to see the full diff of changes for this step check this commit.

Fast-Forward: to jump to the end of this step run:

git clone --branch step-2 https://github.com/cazala/decentraland-redux-chess-app.git --depth=1

Thank you for following along with this tutorial! I hope that it’s helped to show you how easy it is to port simple Redux games into the Decentraland SDK, opening up even more options that you can use your LAND for.

Don’t forget to check out some of our other developer tutorials:*

  • Build Your First Interactive Scene Using the SDK
  • Building a Memory Game Using Decentraland’s SDK
Our SDK provides everything you need to start developing games and applications.
Get Started