Hands-on tutorial on DAPP development for Random Encounters (IV) First self-generated DAPP app down
1. Debugging contract code
In the previous tutorial (DAPP Development Tutorial (3) First Self-Created DAPP Application [Previous]), we wrote a simple but interesting "Blackjack" poker game in Solidity language and tested its feasibility using JS test scripts. The most important feature is that it combines the "decentralized" advantages of blockchain with the reliable and secure distributed data storage features. Instead of maintaining a dedicated central server to hold each user's chips (aka the aforementioned BJT tokens), the game's developer (aka the author himself) can simply publish the contract to a public chain (e.g. Ethereum) and then provide a visual front-end to the game. And that's exactly what this tutorial attempts to accomplish.
The work itself is not that complicated, in fact, we have already scripted part of the core contract interface calls using JS in the previous tutorial, the test/blackjack.js implementation part. By reading some of the earlier tutorials as well as code (such as the WebPack project), we have also learned that these scripts are quite reusable in real front-end development. Still, before we start working on coding, it is important to successively understand how to debug smart contracts and how to package the final program, which is equally important for the actual DAPP development.
As of the time of this writing, debugging Solidity code is still far different from the debugging process in other programming languages. It doesn't have an intuitive IDE (unified development environment) interface, nor does it have particularly handy printing and Log logging tools. Although the Solidity language does not have complex garbage collection, pointer access, and other operations that can easily cause system crashes; the same problems exist during development with array out-of-bounds, type conversion overflows, or incorrect execution results for the entire function due to incorrect parameter passing - the key problem is that if you only find these errors after publishing the contract, then repeatedly modifying and re-publishing the contract means paying more real money for it, or even upsetting and annoying the DAPP users.
As such, debugging contract code is a topic that should never be ignored, and Truffle's current debug mechanism is really obscure (and we'll touch on it briefly in a later post). What's the best way to do this?
We've mentioned using Event in contract code in previous tutorials, but didn't demonstrate how it is listened to and called by front-end scripts. For a system that is difficult to debug intuitively and does not have a good logging mechanism, the smoothest debugging method is the traditional "flagging" method, which is to insert an event Event at an appropriate place in the code and then get feedback from the JS side and assist in the determination.
We added the following two event definitions to the previous BlackJackGame.sol file.
contract BlackJackGame is StandardToken {
……
event NewGameCreated(uint gameId, address playerA, address playerB);
event NewHit(uint gameId, address player, uint card);
……
}
Their trigger timing corresponds to when a new game is created and when a new playing card is issued, respectively, so in the createNewGame() function, we add.
function createNewGame(address _playerA, address _playerB, uint _initialBet) external {
……
for (uint i = 0; i
if (gameDataPool[i].finished) {
NewGameCreated(i, _playerA, _playerB); // Trigger events
……
}
}
……
NewGameCreated(gameDataPool.length - 1, _playerA, _playerB); // Trigger events
}
And at the end of the hitNewCard() function, we have.
function hitNewCard(uint _gameId, address _player, uint _bet) external {
……
NewHit(_gameId, _player, card);
}
where card is a local variable that we define in the function code, and it should always take a value between 0 and 51.
Here is the section of the test/blackjack.js file where the JS code should instantly fetch this event and its specific parameter values when a new event is triggered and feed into the successor code.
let blackjack = await BlackJackGame.deployed();
blackjack.NewGameCreated().watch(function(error, result) {
if (error) { console.log(error); return; }
blackjack.NewGameCreated().stopWatching();
});
blackjack.NewHit().watch(function(error, result) {
if (error) { console.log(error); return; }
var type = parseInt(cardValue / 13 + 1), card = cardValue % 13 + 1;
console.log("New card " + card + " (Type = " + type
});
The code is not too long and is easy to understand. Truffle compiles the resulting build file with all the interface function and event names already included, so we can call them directly from the front-end code. After getting the BlackJackGame object for the first time, we can use watch to listen for all events. The callback function of the listener has two fixed parameters error and result, the former as the name suggests is used to record some possible error information and the latter is a JSON string containing the parameter value of the current event, the contract address, and transaction records and block information etc.
The following statement can be called in an event to print the full contents of the result directly.
console.log(result);
The result of its implementation may look as follows.
{
logIndex: 0,
transactionIndex: 0,
transactionHash:……,
blockHash:……, blockNumber: 36,
address:……,type: 'mined',
event: 'NewGameCreated', //Name of the event
args: { //arguments to the event
gameId: BigNumber { s: 1, e: 0, c: [Array] },
playerA: '0xf17f52151ebef6c7334fad080c5704d77216b732',
playerB: '0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef'
}
}
Now we can also occasionally display information about the process and events in the test match via truffle.cmd test test/blackjack.js. Note that the triggering of callback events may be delayed compared to the execution flow of other JS scripts and cannot be considered as sequential execution.
In addition to inserting flags, we can also use assertions in Solidity code, and there are two functions corresponding to them: require() and assert(), which are used in a similar way. For example, we can use an assertion in the previous createNewGame() function to determine whether the user still has enough tokens in hand to pay for the chips in the initial bet: the
function createNewGame(address _playerA, address _playerB, uint _initialBet) external {
require(_initialBet
require(_initialBet
……
}
Or use the assertion at the end of hitNewCard() to determine if the generated card value is legal (between 0 and 52).
function hitNewCard(uint _gameId, address _player, uint _bet) external {
……
assert(card
}
The code at both ends above also demonstrates the difference between the functions require() and assert() in practice: require() is usually used at the beginning of a function to check that the input parameters are correct and available; if an error occurs, it does not continue to consume the user's property (Gas) to execute the code that follows.
In contrast, assert() is usually used at the end of a function, and it checks that some of the result data from the function's runtime is correct. If an error occurs (and such errors will usually be serious enough to have likely caused the entire transaction to be erroneous), then assert() will attempt to recover the previously altered data, ensuring that the entire system is not rendered unusable as a result. However, precisely because assert() has this property, it will consume all the Gas corresponding to the function execution anyway.
Let's try to trigger the assertion in createNewGame() a bit in our test code, and we modify the slightly earlier part of the JS code so that the user is only issued 1 token for user 1:.
it("Send tokens to first 2 users", async () => {
let blackjack = await BlackJackGame.deployed();
await blackjack.transfer(accounts[1], 1, );
await blackjack.transfer(accounts[2], 100, );
……
});
Recompiling and releasing the contract code and then executing the test script, we can see an error similar to the following.
It's clearly a mess, and while it's indeed a great shame to have no money to bet, it's not a crime to make the whole system look as if it's broken. For this reason, it is necessary to include try in the JS code of the test... catch syntax to catch error messages.
it("Start game and bet 10BJT each", async () => {
……
try {
await blackjack.approve(accounts[0], 100, );
await blackjack.approve(accounts[0], 100, );
await blackjack.createNewGame(accounts[1], accounts[2], 10);
gameCreated = true;
} catch (error) {
console.log("Error! Current game must stop");
gameCreated = false; return;
}
……
}
Here gameCreated is a globally defined variable that can be used later in the test process to determine if the current match was successfully established (otherwise there would be no need to continue with the later tests). Executing the test code, this time our JS program finds user 1 who has no money to enter the contest and decisively issues a warning that
Of course, in addition to the Event "flagging" debugging method described in this section, and the debugging method using assertions. Truffle also offers a currently less than intuitive solution for debugging contract code, which for the sake of space will not be covered in depth here, but the reader can get an idea of its power (and simplicity) at the following link.
http://truffleframework.com/tutorials/debugging-a-smart-contract
2, prepare the npm package configuration file
Next we will open the package.json file in the root of the BlackJack project. The npm configuration tool will download the corresponding NodeJS dependencies, execute specific scripts, or generate publishable front-end code based on the specifics of this file.
The process of configuring a NodeJS project using package.json would be enough to write a tutorial series in itself. Of course we won't spend too much space on it in this article; interested readers can refer to the following URL.
https://docs.npmjs.com/files/package.json
We insert the following directly into the beginning of the document (second line) using any text editing tool.
{
"name": "blackjack-game", // Name of the package
"version": "0.0.1", // Version of the package
"description": "An example DAPP: BlackJack", // Introduction of the package
"author": "Wang Rui", // Author Name
"license": "MIT", // Protocols followed
"scripts": { // You can usenpm run Scripts executed
"build": "webpack", // Calling a pre-defined packing script
"dev": "webpack-dev-server" // Calling a pre-defined test server script
},
"devDependencies": { // Dependency libraries required for development
"babel-cli": "^6.22.2", // Support for the newJS Compilers for syntaxBabel
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.1.8",
"babel-register": "^6.22.0",
"copy-webpack-plugin": "^4.0.1", // webpack The resource copy plugin of
"css-loader": "^0.26.1", // webpack ofCSS Loading plug-ins
"html-webpack-plugin": "^2.28.0", // webpack ofHTML Package Plugin
"json-loader": "^0.5.4", // webpack ofJSON Loading plug-ins
"style-loader": "^0.13.1", // webpack The style loading plugin of
"truffle-contract": "^1.1.11", // Truffle Contractual interaction abstraction layer provided
"web3": "^0.20.0", // Ethereum Contract interface interaction library provided
"webpack": "^2.2.1", // Front-end packaging toolswebpack
"webpack-dev-server": "^2.3.0" // webpack of the test server
},
......// The following precedes the file
A special mention here goes to a well-known front-end JS code and module packaging tool, webpack, whose official website is
In the previous tutorial, we used this tool to complete the testing and packaging output of Truffle's WebPack sample project. The final output can be just an .html file and a .js file, the latter integrating all the necessary dependency library code, as well as the user-written JS scripting part; and before that, we can also use npm run dev to start a server-side on local port 8080 to test if the front-end program is running correctly, and to modify and improve the game logic part.
Now we can go to the project root and use the following console command to load all the necessary dependency libraries, which do not exist in the local environment at the moment.
# npm install
After some waiting, we can now see enough rich content in the node_modules directory. The next step in the process is the configuration of the webpack library, for which we can find enough explanatory documentation and examples at the previously given URL; however, in this tutorial on DAPP configuration and publishing, we chose to create a new webpack.config.js file directly in the project root directory and enter the following.
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './app/javascripts/app.js', // To be packedJS documents
output: {
path: path.resolve(__dirname, 'build'),
filename: 'app.js' // Output path and file name
},
plugins: [ // HTML files are copied directly to the output path
new CopyWebpackPlugin([])
],
module: { //define rules and tools for converting files of different formats to JS
rules: [],
loaders: [
{ test: /.json$/, use: 'json-loader' },
{
test: /.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
query:
}
]
}
}
Ready to go, now it's time to write the code for the front end.
3. User interface and front-end code
In the last part of this tutorial, we start writing the user interface and front-end code for a simple DAPP game called "Blackjack". If this were a complete online board game, there would probably be "user management", "game lobby", "create game room", "chat room" and other complicated modules to start with. Unfortunately, we don't have enough space to handle that much, so here we chose to keep the front-end functionality to a minimum, i.e., assume that the game always takes place between two fixed players (e.g., User 1 and User 2 in the previous test case) and have them start the game, place their bets, deal their cards, and adjudicate the results directly under the same page.
The HTML interface for such a "Hot Seating" type of game is shown below, which is still very rudimentary, but covers the basic functionality required for a "blackjack" poker game.
In order to display the 52 playing card styles correctly, we also need to prepare a set of picture material in advance, a total of four suits (1-4 denote spades/ clubs/ diamonds/ hearts respectively), 13 cards of each suit (1-13), so the picture of the Jack of Hearts corresponds to 4-11.png, as shown in the figure.
Because of space constraints, the full code of the index.html and app.js files is not given here, but only for the key functions and their calls. Readers are welcome to visit the GitHub address for this tutorial series to access the latest source code and to suggest changes to.
https://github.com/xarray/ethereum_dapp_demos
We follow the example of the WebPack project from the previous tutorial, writing the app/app.js file and defining an App object in it, which contains all the necessary interfaces for contract calls, including.
start(): gets the contract interface object BlackJackGame, and the current list of users.
initializeBalance(): initializes the number of tokens held by users 1 and 2. The implementation of this code is similar to the first step of the previous test code. Note that this initialization process should be executed only once for the entire contract, since our contract only allocates 200 tokens and distributes them to users 1 and 2 at once; if the initialization function is executed again (e.g., refreshing the page) an error is generated because the system has no more tokens to allocate. Readers can consider for themselves how to make the distribution mechanism of BJT tokens more rigorous and reliable.
refreshBalance(): refreshes the display of the number of tokens held by users 1 and 2 on the HTML interface; this function is executed once at initialization and at the end of each game.
createNewGame(): creates a new game, which should clear all the information of the previous game on the HTML page and execute the corresponding contract function to start the new game. We might as well perform the event listening processing here as well. The two events mentioned in the first section of this tutorial both feed some useful information (game field ID, and deal information), and they need to be readily updated to the HTML interface.
hitNewCard(): calls the contract function and the specified player is dealt a card.
holdForResult(): the specified player stops, and if both players stop, the match result is automatically started.
checkWinner(): call the contract function to determine the result of the match and feed it back to the HTML interface.
In app/index.html, we need to start a new game by clicking on "Start new game".
BlackJack
Game not started
Start new game
……
Then there are the addresses of the two participants, the number of tokens held, the number of bets before each Hit (default is 2), and the "Hit" (deal) and "Hold" (stop) buttons.
Account 1 has 0 BJT
Bet:
Hit
Hold
We can observe how the HTML element is reassigned in conjunction with the contents of the app.js file. As an example, take the callback function for the NewHit event.
blackjack.NewHit().watch(function(error, result) {
if (error) { alert(error); return; }
// return parameter divided by 13 and add 1 to get the suit (1-4); take the remainder and add 1 to get the number of points
var type = parseInt(cardValue / 13 + 1), card = cardValue % 13 + 1;
// determine the player's corresponding HTML element
var elementName = "";
// Add objects directly to show cards of corresponding suits and points
document.getElementById(elementName).innerHTML +=
"";
});
Once everything is ready, type npm run build to build the release file from the app directory (which is specified in the webpack.config.js file), and the JS files and dependencies are automatically packaged together and deposited in the build directory. Note that the app/images directory is copied over as well to ensure that all images are in the current directory. Double click directly on build/index.html to open the browser, at which point the contract interface should have been initialized and the two players each hold 100 BJT tokens.
Click "Start new game" to start the game. Both players can then click "Hit" to deal themselves a hand and judge the current situation based on the cards they have. Of course, two gamers sitting in front of the same computer may quickly get into a "physical fight", which is obviously inappropriate for an MMO, but it's a bit of a treat for our first DAPP experiment.
When both players click "Hold" to stop the game, the system will automatically determine the winner of the game and output the result directly.
If you're not done, click "Start new game" again to restart the game. At this point, they still have the token chips they received earlier and can fight until one side runs out of gas: the
Of course, as mentioned before, since we didn't define a reasonable token allocation method in the initializeBalance() function of the JS script, if you refresh this page or reopen it, the system will prompt an error because the user (i.e. the system user responsible for contract execution) no longer has any extra tokens in hand. For this routine, we can only republish the contract and repackage the JS file in the console, i.e.
# truffle.cmd migrate --reset
# npm run build
Then open the build/index.html file again. This is stupid, and it's by no means a complete online board game. But in terms of writing a smart contract to implement a sample game and combining it with a visual front-end, our basic work is triumphantly done. How to refine the logic of the game, or even to finish and commoditize it; or to cite a few examples and implement more DAPP games of similar genre, that's the question you, the smart reader, are facing. And the next step for our tutorial is to consider how to make the game approach and display content a little more interesting or become a little more specialized, for example for use in some scaled scientific data visualization field?
Feel free to reprint it.