An-Ethereum-Simulation-Game-Part-2

Building An Ethereum Simulation Game – Part 2 – Playing Tennis Matches

Building An Ethereum Simulation Game - Part 2 - Playing Tennis Matches

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.

http://13.232.74.58/an-ethereum-simulation-game-part-1/

Since that article, I’ve written some unit tests and made a few small changes to those contracts. Events have been added to TrainableTennisPlayer to emit when players are trained or rested. Some utility functions that were initially written in TrainableTennisPlayer have been moved to the TennisPlayerBase.

At this stage, our Ethereum Tennis game can create new players, and the owner of the player can level up attributes and conditions by training and resting.

The following code shows the TennisPlayerBase smart contract

// Author: Alex Roan
pragma solidity ^0.5.5;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/ownership/Ownable.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/SafeCast.sol";
// TennisPlayer ERC721 Token
contract TennisPlayerBase is ERC721, Ownable {
using SafeMath for uint;
using SafeCast for uint;
// Player information
struct Player {
// game details
bool isBot;
uint xp;
// personal details
string name;
uint8 age;
uint8 height;
uint8 condition;
// attributes
uint8 agility;
uint8 power;
uint8 stamina;
uint8 technique;
}
// List of all players
Player[] public players;
// Create new player on behalf of manager
function newPlayer(
bool _isBot,
uint _xp,
string memory _name,
uint8 _age,
uint8 _height,
uint8 _condition,
uint8 _agility,
uint8 _power,
uint8 _stamina,
uint8 _technique,
address _to
) public onlyOwner returns (uint)
{
uint id = players.length;
players.push(
Player( _isBot, _xp, _name, _age, _height, _condition,
_agility, _power, _stamina, _technique)
);
_safeMint(_to, id);
return id;
}
// Cast and add a uint8 to a uint8
function castAdd8(uint8 _a, uint8 _b) internal pure returns (uint8) {
return uint(_a).add(uint(_b)).toUint8();
}
// Cast and subtract a uint8 from uint8
function castSubtract8(uint8 _a, uint8 _b) internal pure returns (uint8) {
return uint(_a).sub(uint(_b)).toUint8();
}
// Cast and subtract a uint8 from a uint
function castSubtract256(uint _a, uint8 _b) internal pure returns (uint) {
return _a.sub(uint(_b));
}
}
view raw TennisPlayerBase.sol hosted with ❤ by GitHub

The following code shows the TrainableTennisPlayer smart contract.

// Author: Alex Roan
pragma solidity ^0.5.5;
import "./TennisPlayerBase.sol";
contract TrainableTennisPlayer is TennisPlayerBase {
// TODO – xp costs to change depending on current attribute level?
// Training costs
uint8 public conditionCostToTrain = 5;
uint8 public xpCostToTrain = 8;
uint8 public attributeGainOnTrain = 1;
// Rest costs and gains
uint8 public xpCostToRest = 6;
uint8 public conditionGainOnRest = 15;
enum Attribute { agility, power, stamina, technique }
event Train(uint indexed playerId, Attribute attribute);
event Rest(uint indexed playerId);
// Train a player increasing an attribute
function train(uint _id, Attribute _attr) public {
// Only the owner of the player can train
require(ownerOf(_id) == msg.sender, "Must be owner of player to train");
// The player must be fit enough to train
players[_id].condition = castSubtract8(players[_id].condition, conditionCostToTrain);
// Must have enough XP
players[_id].xp = castSubtract256(players[_id].xp, xpCostToTrain);
// Increase the chosen attribute
if (_attr == Attribute.agility) {
players[_id].agility = castAdd8(players[_id].agility, attributeGainOnTrain);
}
else if (_attr == Attribute.power) {
players[_id].power = castAdd8(players[_id].power, attributeGainOnTrain);
}
else if (_attr == Attribute.stamina) {
players[_id].stamina = castAdd8(players[_id].stamina, attributeGainOnTrain);
}
else if (_attr == Attribute.technique) {
players[_id].technique = castAdd8(players[_id].technique, attributeGainOnTrain);
}
emit Train(_id, _attr);
}
// Rest player, increasing condition
function rest(uint _id) public {
// Only the owner of the player can rest
require(ownerOf(_id) == msg.sender, "Must be owner of player to rest");
// Must have enough XP
players[_id].xp = castSubtract256(players[_id].xp, xpCostToRest);
players[_id].condition = castAdd8(players[_id].condition, conditionGainOnRest);
emit Rest(_id);
}
}


Playing Matches

I mentioned sundayleague.com as an inspiration for this game in the previous article. The way it works is by playing matches every day at a set time, I believe it’s 3 am GMT, so every player needs to have their tactics set for the upcoming game before that time. 

If mimicked on the Blockchain, the system itself would need to pay gas costs for simulating the matches. The more players competing, the more gas needed. 

(alternatively, each game could have a stake by each player, some of it contributing to gas and the rest going to the winner?)

We don’t want that. Instead, we want the players to be able to compete against others when they have enough resources (XP and condition). That way matches can be played as soon as the attacker submits the transaction (forwarding gas cost for the match of course). The winner is then calculated as soon as the block is confirmed.

Because matches cost player condition (they get worn out), players who aren’t in match condition can’t play. They need rest, hence the TrainableTennisPlayer contract.

If the game allows any player to play a match against anyone else, higher-level players would obliterate lower level players over and over rinsing the lower level player’s condition. 

To combat this, I’m going to use a registration system, where players enlisted to compete can be challenged and can challenge others. They can be delisted by their managers at any time. They’ll also be auto-delisted if they complete a match and are subsequently below the minimum condition required.

Code Structure

Figure 3 shows a rudimentary diagram of how I’m envisaging the structure of the first few contracts.

Figure 3: Initial Contract Structure

TennisPlayerBase and TrainableTennisPlayer were coded in the previous article. CompetingTennisPlayer is where our match logic exists.

It enables players to enlist and compete against other enlisted players. I’m not worrying too much about the match mechanics just yet, other than a simple comparison between stats. I’ll work on that later.

Smart Contract

Figure 4 shows the CompetingTennisPlayer Smart Contract.

// Author: Alex Roan
pragma solidity ^0.5.5;
import "./TennisPlayerBase.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/SafeCast.sol";
contract CompetingTennisPlayer is TennisPlayerBase {
using SafeMath for uint;
using SafeCast for uint;
// conditionCostToPlay
uint8 public conditionCostToPlay = 20;
// xpGainWin
uint8 public xpGainWin = 10;
// xpGainLose
uint8 public xpGainLose = 5;
// enlistedPlayers
mapping(uint => bool) enlistedPlayers;
// enlistEvent
event Enlist(uint indexed playerId);
// delistEvent
event Delist(uint indexed playerId);
// matchPlayed
event MatchPlayed(uint indexed playerId, uint indexed opponentId, uint indexed winner);
// Enlist player to compete with others
function enlist(uint _id) public {
// Only the owner of the player can train
require(ownerOf(_id) == msg.sender, "Must be owner of player to enlist");
// Must not be enlisted already
require(enlistedPlayers[_id] == false, "Must not already be enlisted");
// Match fit check
require(players[_id].condition >= conditionCostToPlay, "Must be match fit to enlist");
// Enlist
enlistedPlayers[_id] = true;
// Emit event
emit Enlist(_id);
}
// Delist player from competing with others
function delist(uint _id) public {
// Only the owner of the player can train
require(ownerOf(_id) == msg.sender, "Must be owner of player to enlist");
// Must be enlisted
require(enlistedPlayers[_id] == true, "Must be enlisted");
// Run delist
_delist(_id);
}
// Play match against opponent
function playMatch(uint _id, uint _opponentId) public {
// Only the owner of the player can train
require(ownerOf(_id) == msg.sender, "Must be owner of player to enlist");
// Must be enlisted
require(enlistedPlayers[_id] == true, "Must be enlisted");
// Opponent must be enlisted
require(enlistedPlayers[_opponentId] == true, "Opponent must be enlisted");
// Ensure player's condition is high enough
_requireMatchCondition(_id, "Player not match condition");
// Ensure opponents's condition is high enough
_requireMatchCondition(_opponentId, "Opponent not match condition");
// TODO move this to train?
// TODO Better match mechanics
uint playerScore = uint(players[_id].agility)
.add(uint(players[_id].power))
.add(uint(players[_id].stamina))
.add(uint(players[_id].technique));
uint opponentScore = uint(players[_opponentId].agility)
.add(uint(players[_opponentId].power))
.add(uint(players[_opponentId].stamina))
.add(uint(players[_opponentId].technique));
// condition changes
players[_id].condition = uint(players[_id].condition).sub(uint(conditionCostToPlay)).toUint8();
players[_opponentId].condition = uint(players[_opponentId].condition).sub(uint(conditionCostToPlay)).toUint8();
// determine winner
(uint winner, uint loser) = (playerScore >= opponentScore) ? (_id, _opponentId) : (_opponentId, _id);
// xp changes
players[winner].xp = uint(players[winner].xp).add(uint(xpGainWin));
players[loser].xp = uint(players[loser].xp).add(uint(xpGainLose));
// emit matchplayed
emit MatchPlayed(_id, _opponentId, winner);
// check condition again
_isMatchCondition(_id);
_isMatchCondition(_opponentId);
}
// Perform the delisting
function _delist(uint _id) private {
// Delist
enlistedPlayers[_id] = false;
// Emit event
emit Delist(_id);
}
// Revert if not match condition
function _requireMatchCondition(uint _id, string memory _message) private {
if (!_isMatchCondition(_id)) {
revert(_message);
}
}
// Check player is in match condition, if not, delist
function _isMatchCondition(uint _id) private returns (bool) {
// Ensure player's condition is high enough
// Delist if not
if (players[_id].condition <= conditionCostToPlay) {
_delist(_id);
return false;
}
return true;
}
}

There are three public functions:

  • enlist(): Adds a player to the enlisted players so long as the caller of the function is the owner of the player, the player is in good enough condition and is not already enlisted.
  • delist(): Removes a player so long as the caller is the owner and the player is currently listed.
  • playMatch(): Plays a match between two players providing they are both enlisted, the caller is the owner of the attacker, and both are in match condition. It then delists if either of the players is no longer match condition after the match.

I haven’t yet fully tested this contract, so that’s the next piece of work. I’ll likely need to change some of the code, but in terms of premise, I think most bases are covered for now.

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 need to make sure what’s there is working as expected, which means lots of unit tests before moving on to the next piece of the puzzle.

After that, I’ll look to add the SomethingHere contract in Figure 4, yet to be named. Once that’s in place it should act as the main entry point for the game, and a front end can be built up around it. I’ll use React and Redux for that.

I’m already looking forward to being able to interact with our Ethereum tennis game despite its basic state right now. It’ll be great to see it working in the browser no matter how clunky for the time being.

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

Also, Read

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