An Ethereum Game Part 3

Building An Ethereum Simulation Game – Part 3 – The Front End

Building An Ethereum Simulation Game - Part 3 - The Front End

Recap

In part one I outlined the idea for a Tennis Manager simulation game on the Ethereum Blockchain. I created the ERC721 Token and the TrainableTennisPlayer contract which enables owners to increase their player stats by training or resting.

Part two walked through the introduction of the CompetingTennisPlayercontract, responsible for calculating the winners of matches between players.

Since then, I’ve written unit tests for those contracts. They are by no means complete, but I wanted to try building out a usable group of contracts as soon as possible to test out a basic front. If you’d like to help out with testing and building the application, check out the Github repo.

I’ve also altered the TennisPlayerBase contract to extend ERC721Enumerable, instead of the standard ERC721 token provided by Openzeppelin. This enables our game to retrieve all tokens owned by an address using _tokensOfOwner(address). Implementing this means not requiring the user to remember their token ID every time they log in.

As I mentioned in the previous article, I’m looking forward to getting a rudimentary interface up and running to start interacting with the game.

Pulling It Together

Figure 1 shows a diagram of the contracts after the previous installment.

Figure 1: Contract structure

I want the game Dapp accessible through as few Smart Contracts as possible, so the front-end code doesn’t get too complicated. Hence the reason for SomethingHere

I’ve created a contract in place of SomethingHere is called TennisPlayer. This is shown in the following code.

// Author: Alex Roan
pragma solidity ^0.5.5;
import "./CompetingTennisPlayer.sol";
import "./TrainableTennisPlayer.sol";
contract TennisPlayer is CompetingTennisPlayer, TrainableTennisPlayer {
function myPlayers() public view returns (uint[] memory) {
return _tokensOfOwner(msg.sender);
}
}
view raw TennisPlayer.sol hosted with ❤ by GitHub

Notice how myPlayers() on line 9 uses the _tokensOfOwner() function inherited from ERC721Enumerable. As mentioned earlier, this allows the front-end to load all players owned by the user.

Creating New Players

In part one, I created the newPlayer() function in TennisPlayerBase with the custom access modifier onlyOwner (inherited from Openzeppelin’s Ownable contract). This means that only the owner of the token can create new players, and no one else. It has to be this way if the game is to be in control of setting the starting attributes of new players. Otherwise, a user can create a new player and max out the stats on immediately.

For this to work, we need an overarching Game smart contract which itself creates the TennisPlayer ERC721 token contract when it is deployed, hence becoming the owner.

The following smart contract which does this, Game.sol.

// Author: Alex Roan
pragma solidity ^0.5.5;
import "./TennisPlayer.sol";
import "@openzeppelin/contracts/ownership/Ownable.sol";
contract Game is Ownable {
address public playerTokenAddress;
uint private xp = 100;
uint8 private condition = 255;
uint8 private agility = 1;
uint8 private power = 1;
uint8 private stamina = 2;
uint8 private technique = 1;
constructor() public {
playerTokenAddress = address(new TennisPlayer());
}
function newPlayer(string memory _name, uint8 _age, uint8 _height) public returns (uint){
return TennisPlayer(playerTokenAddress).newPlayer(
xp, _name, _age, _height, condition, agility, power, stamina, technique, msg.sender
);
}
}
view raw Game.sol hosted with ❤ by GitHub

Notice the default attribute values from lines 11 to 16. Every new player starts with these. In the future, an element of randomness could be introduced so each new player has slightly different strengths and weaknesses. (although randomness is a sticking point)

The smart contract also stores an address called publicTokenAddress representing the location of our ERC721 token on line 9. This is retrieved by the constructor when the token is created on line 19.

The newPlayer() function is exposed publically on line 22, taking only the name, age, and height of the new player from the user. This is the entry point into the game Dapp.

At this stage, users can create new players through the Game contract, and interact with functions in TennisPlayer to train rest and compete against other players. This seems like a great place to start building the front end. 


Front End

Truffle suite provides a bunch of Truffle boxes that come packaged with a framework of your choice. I’m using the React box, with Redux (which I added in myself).

To enable our DApp to interact with the game, the Redux store needs to hold information about the Blockchain it’s connecting too, the Smart Contracts, and the account being used by the user.

I use this pattern to ensure all of this gets loaded correctly. Initially, it can look a bit overkill, but if something goes wrong in the loading phase, it helps to visualize it.

Figure 4: Loading the Blockchain
Figure 5: Loading the Blockchain

Displaying Players

To display all the players owned by the user, the DApp needs to call the myPlayers() function in the TennisPlayer contract, and store the results in the Redux store. From there, the component can select the data, and display it on the page.

I’ve written a component responsible for this called PlayerList, shown in the following code.

import React, {Component} from 'react';
import {connect} from 'react-redux';
import { tennisPlayerSelector, accountLoadedSelector, gameLoadedSelector, tennisPlayerLoadedSelector, accountSelector, ownedPlayersSelector, newPlayerNameSelector, newPlayerAgeSelector, newPlayerHeightSelector, gameSelector } from './redux/selectors';
import { newPlayerNameChange, newPlayerAgeChange, newPlayerHeightChange} from "./redux/actions";
import { createNewPlayer, loadSelectedPlayer } from './redux/interactions';
const playerSelected = (props, id) => {
console.log(id);
const {dispatch, tennisPlayer} = props;
loadSelectedPlayer(dispatch, tennisPlayer, id);
}
const getOwnedPlayers = (props) => {
const {ownedPlayers} = props;
return ownedPlayers.map((id) =>
<a href="#" onClick={playerSelected(props, id)} key={id} className="list-group-item">{id}</a>
);
}
class PlayerList extends Component {
render() {
const {dispatch, game, account, newPlayerName, newPlayerAge, newPlayerHeight} = this.props;
const newPlayer = async (e) => {
e.preventDefault();
console.log(newPlayerName, newPlayerAge, newPlayerHeight);
await createNewPlayer(dispatch, game, account, newPlayerName, newPlayerAge, newPlayerHeight);
}
const nameChange = (e) => dispatch(newPlayerNameChange(e.target.value));
const ageChange = (e) => dispatch(newPlayerAgeChange(e.target.value));
const heightChange = (e) => dispatch(newPlayerHeightChange(e.target.value));
return (
<div className="col-4">
<div className="card">
<div className="card-header">
Player List
</div>
<div className="list-group list-group-flush">
{this.props.showOwnedPlayers ? getOwnedPlayers(this.props) : <a className="list-group-item">No Players</a>}
</div>
<div className="card-body">
Create New Player
<form onSubmit={newPlayer}>
<div className="input-group input-group-sm mb-3">
<div className="input-group-prepend">
<span className="input-group-text" id="inputGroup-sizing-sm">Name</span>
</div>
<input onChange={nameChange} required name="name" type="text" className="form-control" aria-label="Name" aria-describedby="inputGroup-sizing-sm"></input>
</div>
<div className="input-group input-group-sm mb-3">
<div className="input-group-prepend">
<span className="input-group-text" id="inputGroup-sizing-sm">Age</span>
</div>
<input onChange={ageChange} required name="age" type="number" min="15" max="40" className="form-control" aria-label="Age" aria-describedby="inputGroup-sizing-sm"></input>
</div>
<div className="input-group input-group-sm mb-3">
<div className="input-group-prepend">
<span className="input-group-text" id="inputGroup-sizing-sm">Height (cm)</span>
</div>
<input onChange={heightChange} required name="height" type="number" min="100" max="255" className="form-control" aria-label="Height" aria-describedby="inputGroup-sizing-sm"></input>
</div>
<div className="input-group input-group-sm mb-3">
<input type="submit" className="form-control btn btn-primary btn-sm" aria-label="Submit" aria-describedby="inputGroup-sizing-sm"></input>
</div>
</form>
</div>
</div>
</div>
)
}
}
function mapStateToProps(state){
const accountLoaded = accountLoadedSelector(state);
const gameLoaded = gameLoadedSelector(state);
const tennisPlayerLoaded = tennisPlayerLoadedSelector(state);
return {
showOwnedPlayers: accountLoaded && gameLoaded && tennisPlayerLoaded,
game: gameSelector(state),
tennisPlayer: tennisPlayerSelector(state),
account: accountSelector(state),
ownedPlayers: ownedPlayersSelector(state),
newPlayerName: newPlayerNameSelector(state),
newPlayerAge: newPlayerAgeSelector(state),
newPlayerHeight: newPlayerHeightSelector(state)
}
}
export default connect(mapStateToProps)(PlayerList);
view raw PlayerList.js hosted with ❤ by GitHub

PlayerList renders a Bootstrap card with a title, a list of players, and a form to create a new player. Without any players, it looks like Figure 7, and once a player is created by the user, the list displays the ID, shown in figure 8.

Figure 7: PlayerList with no players
Figure 8: PlayerList with player 0 in list

This list is selectable. By clicking an item in the list the DApp retrieves the details of the player and stores them in the Redux store. 

Displaying Player Details

Another component called PlayerDetails is responsible for selecting player details from the Redux store and displaying them on the page.

The following shows the code for the PlayerDetails component.

import React, {Component} from 'react';
import {connect} from 'react-redux';
import { selectedPlayerDetailsSelector } from './redux/selectors';
const getPlayerDetails = (props) => {
const {playerDetails} = props;
return (
<ul className="list-group list-group-flush">
<li key="xp" className="list-group-item">{playerDetails.xp}</li>
<li key="name" className="list-group-item">{playerDetails.name}</li>
<li key="age" className="list-group-item">{playerDetails.age}</li>
<li key="height" className="list-group-item">{playerDetails.height}</li>
<li key="condition" className="list-group-item">{playerDetails.condition}</li>
<li key="agility" className="list-group-item">{playerDetails.agility}</li>
<li key="power" className="list-group-item">{playerDetails.power}</li>
<li key="stamina" className="list-group-item">{playerDetails.stamina}</li>
<li key="technique" className="list-group-item">{playerDetails.technique}</li>
</ul>
);
}
class PlayerDetails extends Component {
render() {
return (
<div className="col-4">
<div className="card">
<div className="card-header">
Player Details
</div>
{this.props.playerDetails ? getPlayerDetails(this.props) : <p>No Details</p>}
</div>
</div>
);
}
}
function mapStateToProps(state){
return {
playerDetails: selectedPlayerDetailsSelector(state)
}
}
export default connect(mapStateToProps)(PlayerDetails);
view raw PlayerDetails.js hosted with ❤ by GitHub

This component renders another Bootstrap card, and when active looks like Figure 10.

Figure 10: Player Details card

Pulled all together, Figure 11 shows the front end in its current state. Very basic, very clunky, and nowhere near finished. It doesn’t yet implement all of the features available in the Smart Contracts, but it’s a good start.

Figure 11: Full front-end

Next Steps

This project is in active development. At the time of writing, the current state of the code is as described in this article.

I want to be able to train, rest and compete with players against each other through the interface as soon as possible. Training and resting are going to be the next features I work on since they only require one player, where competing requires at least two so is slightly more compilated.

Once all available Smart Contract features are supported, I’ll look to expand the match logic, or jazz up the front-end to make it look more enticing and game-like.

There’s still a long way to go, but getting the full stack working together is a big step!

The code repo is on Github so it’s public and open to pull requests. If you’re interested in helping out please to contribute! Here is the repo.


Further Reading

If you want to learn more about the Crypto ecosystem, sign up for the weekly newsletter.

Default image
Alex Roan
Blockchain Developer, writer. https://alexroan.co.uk

Wanna learn about Crypto?

Subscribe to our weekly newsletter

Leave a Reply