eth实战项目游戏开发 TICTACTOE 一

文章目录

  • 1、游戏概述
    • 1 基础知识准备
    • 2 项目提高
    • 3 功能预览
  • 2 合约编写
    • 2.1 新建 truffle-react项目
    • 2.2 创建棋盘和玩家
    • 2.3 玩家2加入游戏
    • 2.4 设置棋子
    • 2.5 游戏赢家判断
    • 2.6 设置赢家,并赢得赌金
    • 2.7 transfer&send
    • 2.8 checks-effects-iteractions模式
    • 2.8 改写setWinner
    • 2.9 增加提现功能
    • 2.11 添加超时限制条件
    • 2.12 合约事件
    • 2.13 非正常规则处理
    • 2.14 合约代码ALL

eth实战项目游戏开发 TICTACTOE 二 https://blog.csdn.net/bondsui/article/details/85755404
github代码在文末

1、游戏概述

1 基础知识准备

  1. html、css、js
  2. nodejs、react
  3. metamask 插件使用
  4. solidity合约基本语法
  5. web3.js框架
  6. truffle框架
  7. ganache-cli、geth(可选)

2 项目提高

  1. truffle+react
  2. 合约事件监听
  3. 合约安全 checks-effects-iteractions
  4. transfer&send
  5. 如何处理转账失败
  6. 不会前端(前端技术差)如何写页面?

3 功能预览

猜棋牌类游戏,将棋盘设置为3*3,率先连城直线的玩家获胜,每局1eth赌金,赢家赢得所有赌金,平局退回赌金

游戏创建成功,对方输入地址加入游戏

游戏开始,提示下一个玩家下棋

错误处理:等待对方下子、已经有棋子位置不可以下子

率先完成直连,游戏结束

2 合约编写

游戏demo仅供参考学习,该类游戏并不能进行商用,游戏涉及到的未实现部分及其他功能自行扩展

2.1 新建 truffle-react项目

truffle unbox react

在contracts目录下新建 TicTacToe.sol

2.2 创建棋盘和玩家

创建棋盘,使用二维数组【3】【3】表示,值为玩家1、2的地址

添加获取棋盘状态函数

pragma solidity ^0.4.24;

contract TicTacToe {
    // 每局1eth赌金
    uint constant GAME_COST = 1 ether;
    uint8 constant GAME_BOARD_SIZE = 3;
    address[GAME_BOARD_SIZE][GAME_BOARD_SIZE] public board; //游戏面板

    address public player1;
    address public player2;

    constructor() public payable{
        require(msg.value == GAME_COST);
        player1 = msg.sender; 
    }
    
    //获取游戏面板
    function getBoard() public view returns (address[GAME_BOARD_SIZE][GAME_BOARD_SIZE]) {
        return board;
    }
}

2.3 玩家2加入游戏

加入游戏函数,添加关键字payable接受转账

限制条件:金额,玩家2!=0的时候

游戏开始,添加变量gameActive=true

设置谁先开始游戏,随机函数

address public activePlayer;  //当前玩家

	// 加入游戏
    function joinGame() public payable {
        require(msg.value == GAME_COST);
        require(player1 != msg.sender);
        require(player2 == address(0));
        player2 = msg.sender;
        gameActive = true;

        //设置随机玩家
        if(block.number % 2 == 0) {
            activePlayer = player2;
        } else {
            activePlayer = player1;
        }
    }

2.4 设置棋子

以坐标设置棋子,将棋盘xy位置设置为 msg.sender

条件判断:

  • 【x】【y】必须为空;
  • 坐标阈值判断
  • 游戏状态判断
function setPosition(uint8 x, uint8 y) public {
    	// 空位置
        require(board[x][y] == address(0));
         //当前玩家
        require(msg.sender == activePlayer);
       // 正确的位置
        assert(x < GAME_BOARD_SIZE && y < GAME_BOARD_SIZE);
        // 游戏开始中
        assert(gameActive);

        board[x][y] = msg.sender; 
}

2.5 游戏赢家判断

每次下棋子,都应判断是否获胜、或者平局

获胜判断:率先直连,分步骤判断。行,列,对角线,反对角线

平局判断规则:总步数等于棋盘长度

function setPosition(uint8 x, uint8 y) public {
        require(board[x][y] == address(0));
      
        assert(x < GAME_BOARD_SIZE && y < GAME_BOARD_SIZE);
        // 正确的位置
    
        assert(gameActive);

        board[x][y] = msg.sender;
        steps++;
   		
    // 规则判断
        // 行 00,01,02
        for (uint8 i = 0; i < GAME_BOARD_SIZE; i++) {
            // 如果不是activePlayer,说明未获胜
            if (board[x][i] != activePlayer) {
                break;
            }
			// 如果本行都是当前玩家 00 01 02值都为player
            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }
		
    	// 列规则和行一样
        // 列  01,11,21
        for (i = 0; i < GAME_BOARD_SIZE; i++) {
            if (board[i][y] != activePlayer) {
                break;
            }

            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }


        // 对角线 00,11,22
        if (x == y) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][i] != activePlayer) {
                    break;
                }
                // win 如果22 = player,获胜
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 反对角线 02,11,20
        if (x + y == GAME_BOARD_SIZE - 1) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][GAME_BOARD_SIZE - i - 1] != activePlayer) {
                    break;
                }
                // win  如果20等于player获胜
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 平局
        if (steps == GAME_BOARD_SIZE * GAME_BOARD_SIZE) {
            // 提现
            setDraw();
            return ;
        }
		// 设置下一个玩家
        if (msg.sender == player1) {
            activePlayer = player2;
        } else {
            activePlayer = player1;
        }
    }

2.6 设置赢家,并赢得赌金

function setWinner(address player) private {
        gameActive = false;
        uint payBalance = address(this).balance;
    	player.transfer(payBalance);
    }

2.7 transfer&send

英文翻译解释

  • someAddress.send()and someAddress.transfer() are considered safe against reentrancy. While these methods still trigger code execution, the called contract is only given a stipend of 2,300 gas which is currently only enough to log an event.
  • x.transfer(y) is equivalent to require(x.send(y)), it will automatically revert if the send fails.
  • someAddress.call.value(y)() will send the provided ether and trigger code execution. The executed code is given all available gas for execution making this type of value transfer unsafe against reentrancy.

2.8 checks-effects-iteractions模式

  • checks:执行前,先进行权限及安全性检查
  • effects:检查通过后,对合约状态变量进行更改
  • Interactions:合约状态变量更改后,在进行交互

用户已经赢得胜利,如果因网络等原因失败,用户本应该赢得100eth赌金,因transfer交易失败导致回滚,使用send,并处理失败后的处理。

2.8 改写setWinner

  • send不会自动回滚,必须处理转账失败的情况,用户赢后,如转账失败可使用提现功能
  • 使用send改写函数,setWinner和setDraw函数
function setWinner(address player) private {
    gameActive = false;
    // 转账 如果发送失败,允许用户提现
    if (!player.send(this.balance)) {
        if (player == player1) {
            withDrawBalance1 = this.balance;
        } else {
            withDrawBalance2 = this.balance;
        }
    }
}

// 设置平局
function setDraw() private {
    gameActive = false;
    if (!player1.send(GAME_COST)) {
        withDrawBalance1 = GAME_COST;
    }
    if (!player2.send(GAME_COST)) {
        withDrawBalance2 = GAME_COST;
    }
}

2.9 增加提现功能

条件:balance>0,这里可以使用transfer。思考为什么?

// 允许用户提现
    function withdraw() public {
        if (msg.sender == player1) {
            // 先修改状态,在transfer
            require(withDrawBalance1 > 0);
            withDrawBalance1 = 0;
            player1.transfer(withDrawBalance1);

        } else if (msg.sender == player2) {
            require(withDrawBalance2 > 0);
            withDrawBalance2 = 0;
            player2.transfer(withDrawBalance2);
        }
    }

2.11 添加超时限制条件

如果玩家2即将要输,玩家2估计不执行下子操作或者玩家2离开游戏,怎么办?

考虑到网络的延迟 ,可自定义延迟时长

// 超时时间,3分钟不处理,判断该玩家失败,每次设置棋子后,都应该更新该时间

  uint constant TIME_INTERVAL = 3 minutes;
  uint timeValid;
  constructor() public payable{
        require(msg.value == GAME_COST);
        player1 = msg.sender;
        // 超时时间判断
        timeValid = now + TIME_INTERVAL;
    }
    // 加入游戏
    function joinGame() public payable {
        require(msg.value == GAME_COST);
        require(player1 != msg.sender);
        require(player2 == address(0));
        player2 = msg.sender;
        gameActive = true;
	}
        
     function setPosition(uint8 x, uint8 y) public {
          //时间判断
        require(timeValid > now);
        timeValid = now + TIME_INTERVAL;
         ...
      }

2.12 合约事件

合约和前端页面进行交互,游戏开始结束通知,下一个玩家通知等。

通过合约发送事件,在前端监听(watch),处理不同的事件进行页面交互

定义事件

    event GameStart(address player1,address player2); // 玩家加入事件
    event NextPlayer(address nextPlayer);   // 下一个玩家
    event GameOver(address winner);    // 游戏结束事件
    event Withdraw(address to, uint balance); // 支付成功

在游戏玩家2加入时,发送游戏开始事件和下一个玩家事件

// 加入游戏
function joinGame() public payable {
    require(msg.value == GAME_COST);
    require(player1 != msg.sender);
    require(player2 == address(0));
    player2 = msg.sender;
    gameActive = true;

    // 发送消息
    emit GameStart(player1,msg.sender);

    //时间判断
    timeValid = now + TIME_INTERVAL;

    //设置随机玩家
    if(block.number % 2 == 0) {
        activePlayer = player2;
    } else {
        activePlayer = player1;
    }
    // 发送下个玩家事件
    emit NextPlayer(activePlayer);
}

设置棋子最后发送下一个玩家事件

function setPosition(uint8 x, uint8 y) public {
    ...
     emit NextPlayer(activePlayer);
}

设置winner时,发送游戏结束事件

function setWinner(address player) private {
        gameActive = false;
        // 发送消息
        emit GameOver(player);
    ...
}
     // 设置平局
    function setDraw() private {
        gameActive = false;
        emit GameOver(0);
        ...
    }

2.13 非正常规则处理

如果player1 即将要输,player1停止玩游戏。player2 无法赢得赌金。

  function drawback() public {
        require(timeValid < now); // 超时之后可以提现
        if (!gameActive) {
            // 如果游戏没开始,退款给创建者
            setWinner(player1);
        }else{
            //如果已经开始, 平局退款流程
            setDraw();
            // TODO 恶意退出游戏,应该退全部赌给该赢的玩家或者自定义
        }
    }

2.14 合约代码ALL

pragma solidity ^0.4.24;

contract TicTacToe {
    uint constant GAME_COST = 1 ether;
    uint8 constant GAME_BOARD_SIZE = 3;
    uint constant TIME_INTERVAL = 1 minutes;

    address[GAME_BOARD_SIZE][GAME_BOARD_SIZE] public board; //游戏面板

    address public   player1;
    address public player2;
    address public activePlayer;  //当前玩家
    bool gameActive = false; // 游戏是否开始
    uint steps = 0;    //游戏步数
    uint withDrawBalance1;  //用户1余额
    uint withDrawBalance2;  //用户2余额
    uint timeValid;

    event GameStart(address player1,address player2); // 玩家加入事件
    event NextPlayer(address nextPlayer);   // 下一个玩家
    event GameOver(address winner);    // 游戏结束事件
    event Withdraw(address to, uint balance); // 支付成功

    constructor() public payable{
        require(msg.value == GAME_COST);
        player1 = msg.sender;

        // 超时时间判断
        timeValid = now + TIME_INTERVAL;
    }

    // 加入游戏
    function joinGame() public payable {
        require(msg.value == GAME_COST);
        require(player1 != msg.sender);
        require(player2 == address(0));
        player2 = msg.sender;
        gameActive = true;

        // 发送消息
        emit GameStart(player1,msg.sender);

        //时间判断
        timeValid = now + TIME_INTERVAL;

        //设置随机玩家
        if(block.number % 2 == 0) {
            activePlayer = player2;
        } else {
            activePlayer = player1;
        }
        emit NextPlayer(activePlayer);
    }

    //获取游戏面板
    function getBoard() public view returns (address[GAME_BOARD_SIZE][GAME_BOARD_SIZE]) {
        return board;
    }

    function setPosition(uint8 x, uint8 y) public {
        // 该位置未设置过
        require(board[x][y] == address(0));

        // 时间判断
        require(timeValid > now);

        //当前玩家
        require(msg.sender == activePlayer);

        // 正确的位置
        assert(x < GAME_BOARD_SIZE && y < GAME_BOARD_SIZE);
        // 游戏开始中
        assert(gameActive);

        board[x][y] = msg.sender;
        steps++;
        //时间判断
        timeValid = now + TIME_INTERVAL;


        // 行 00,01,02
        for (uint8 i = 0; i < GAME_BOARD_SIZE; i++) {
            if (board[x][i] != activePlayer) {
                break;
            }

            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }

        // 列  01,11,21
        for (i = 0; i < GAME_BOARD_SIZE; i++) {
            if (board[i][y] != activePlayer) {
                break;
            }

            if (i == GAME_BOARD_SIZE - 1) {
                setWinner(activePlayer);
                return;
            }
        }


        // 对角线 00,11,22
        if (x == y) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][i] != activePlayer) {
                    break;
                }
                // win
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 反对角线 02,11,20
        if (x + y == GAME_BOARD_SIZE - 1) {
            for (i = 0; i < GAME_BOARD_SIZE; i++) {
                if (board[i][GAME_BOARD_SIZE - i - 1] != activePlayer) {
                    break;
                }
                // win
                if (i == GAME_BOARD_SIZE - 1) {
                    setWinner(activePlayer);
                    return;
                }
            }
        }

        // 平局
        if (steps == GAME_BOARD_SIZE * GAME_BOARD_SIZE) {
            // 提现
            setDraw();
            return ;
        }

        if (msg.sender == player1) {
            activePlayer = player2;
        } else {
            activePlayer = player1;
        }
        emit NextPlayer(activePlayer);
    }

    function setWinner(address player) private {
        gameActive = false;
        // 发送消息
        emit GameOver(player);
        // 转账 如果发送失败,允许用户提现
        uint payBalance = address(this).balance;
        if (!player.send(payBalance)) {
            if (player == player1) {
                withDrawBalance1 = payBalance;
            } else {
                withDrawBalance2 = payBalance;
            }
        } else {
           emit Withdraw(player, payBalance);
        }
    }



    // 设置平局
    function setDraw() private {
        gameActive = false;
        emit GameOver(0);
        uint payBalance = address(this).balance / 2;

        // 用户1提现
        if (!player1.send(GAME_COST)) {
            withDrawBalance1 = payBalance;
        } else {
            emit Withdraw(player1, payBalance);
        }

        // 用户2 提现
        if (!player2.send(GAME_COST)) {
            withDrawBalance2 = payBalance;
        } else {
           emit Withdraw(player2, payBalance);
        }
    }

    // 允许用户提现
    function withdraw() public {
        if (msg.sender == player1) {
            // 先修改状态,在transfer
            require(withDrawBalance1 > 0);
            withDrawBalance1 = 0;
            player1.transfer(withDrawBalance1);

            // 提现消息
            emit  Withdraw(player1, withDrawBalance1);
        } else if (msg.sender == player2) {
            require(withDrawBalance2 > 0);
            withDrawBalance2 = 0;
            player2.transfer(withDrawBalance2);
            emit Withdraw(player2, withDrawBalance2);
        }
    }

    function drawback() public {
        require(timeValid < now); // 超时之后可以提现
        if (!gameActive) {
            // 如果游戏没开始,退款给创建者
            setWinner(player1);
        }else{
            //如果已经开始, 平局退款流程
            setDraw();
            // TODO 恶意退出游戏,应该退全部赌给该赢的玩家
        }
    }
}

转载请说明出处
代码地址:https://github.com/bigsui/eth-game-tictactoe
联系邮箱:[email protected]

你可能感兴趣的:(区块链)