Как мы игру «Камень – ножницы – бумага» на блокчейне Ethereum делали. Ч.2 Техническая

    Учтя комментарии к моей предыдущей статье я решил написать вторую часть, где будут более подробна рассмотрена техническая составляющая игры.

    И так, начнём. Клиентскую часть мы сделали на javascript c использованием nodejs фреймворка Meteor. Единственное серверное решение в игре это чат на mongoDB. Посмотрите, какой алгоритм игры мы задумали перед началом работы:



    Описание смарт контракта. Не существует каких-либо заготовок или шаблонных вариантов для создания игры «Камень, ножницы, бумага» на блокчейне. Для этого мы провели собственные исследования и разработки. Наш смарт контракт позволяет создавать и закрывать игровые столы. Информация о всех столах содержится в памяти смарт контракта.

    struct room
    {
    
        address player1;
        address player2;
    
        bytes32 bit1;
        bytes32 bit2;
    
        uint8 res1;
        uint8 res2;
    
        uint256 bet;
        uint8 counter;
        uint8 c1;
        uint8 c2;
    
        uint roomTime;
        uint startTime;
    
        bool open;
        bool close;
    }
    
    mapping (uint => room) rooms;

    Как видно, мы создаем mapping (ассоциативный массив) rooms, ключами которого являются номера столов, а значениями — объект стола. В объекте стола мы декларируем пространство под адреса игроков и их ходы. Так же в столе хранится общий счёт матча, количество побед с каждой стороны, и временные метки создания стола и начала игры. Номер стола генерируется клиентской частью случайным образом. Это число от 0 до 99999999. Одно и тоже число может быть использовано множество раз, при условии штатного закрытия созданного стола. Накладки с номерами столов могут происходить крайне редко, но даже в таком случае, это никак не нарушит работу смарт контракта, транзакция по созданию дубликата просто не сможет состояться. В случае если стол не будет закрыт штатно, использованный номер на некоторое время выпадет из возможных вариантов для создания очередного стола. По логике смарт контракта, открытый стол имеет срок актуальности одни стуки. После этого, любое действие на столе приводит к его закрытию, в том числе попытка подключиться к нему любого желающего. Закрытие стола происходит за счет функции revertRoom и затем closeRoom, которая удаляет информацию о столе из ассоциативного массива rooms.

    function revertRoom(uint id) internal {
        if( rooms[id].bet > 0 ) {
            rooms[id].player1.transfer(rooms[id].bet);
            if( rooms[id].player2 != 0x0 ) rooms[id].player2.transfer(rooms[id].bet);
        }
        RevertRoom(id);
        closeRoom(id);
    }
    
    function closeRoom(uint id) internal {
        rooms[id].close = true;
        RoomClosed( id );
        delete rooms[id];
    }

    Функции смарт контракта, которые начинаются с большой буквы, являются событиями. Игровой процесс в клиентской части во многом опирается на прослушивании тех или иных событий, имеющих отношение к текущему столу. Мы создали 14 событий:

    event RoomOpened( uint indexed room, address indexed player1, uint256 bet, uint8 counter, uint openedTime, bool indexed privat );
    event RoomClosed( uint indexed room );
    event JoinPlayer1( uint indexed room, address indexed player1 );
    event JoinPlayer2( uint indexed room, address indexed player2, uint countdownTime );
    event BetsFinished(uint indexed room );
    event BetsAdd(address indexed from, uint indexed room );
    event OneMoreGame(uint indexed room );
    event SeedOpened( uint indexed room );
    event RoundFinished( uint indexed room, uint8 res1, uint8 res2 );
    event Revard(address win, uint256 amount, uint indexed room );
    event Winner(address win, uint indexed room );
    event Result(address indexed player, uint8 r,  uint indexed room );
    event RevertRoom(uint indexed room);
    event ScoreChanged(uint indexed room, uint8 score1, uint8 score2);

    Пометка indexed после декларации типа переменной позволяет осуществлять фильтрацию событий по указанному полю. Прослушивание событий на блокчейне из клиентской части осуществляется так (Мы используем coffee script, его можно конвертировать в javascript сервисе https://js2.coffee):

    #Слушаем победителя
    this.autorun =>
    filter5 = contractInstance.Winner {room: Number(FlowRouter.getParam('id')),  }, {fromBlock:0, toBlock: 'latest', address: contrAdress}
    filter5.watch (error, result) ->
    console.log result
    if result
        instance.winner.set result.args.win
    console.log result.args.win
    UIkit.modal("#modal-winner").show()

    Как видно, мы обращаемся к событию на блокчейне под названием Winner, предварительно подготовив образ смарт контракта constactInstance. Настройка web3 и подготовка образа осуществляется следующим скриптом:

    Длиннокод
    if (typeof web3 !== 'undefined') {
        web3 = new Web3(web3.currentProvider);
    
        var contrAdress = '0x80dd7334a28579a9e96601573555db15b7fe523a';
    
        var contrInterface = [
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    },
                    {
                        "indexed": false,
                        "name": "score1",
                        "type": "uint8"
                    },
                    {
                        "indexed": false,
                        "name": "score2",
                        "type": "uint8"
                    }
                ],
                "name": "ScoreChanged",
                "type": "event"
            },
            {
                "constant": false,
                "inputs": [],
                "name": "deleteContract",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "exitRoom",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "fixResults",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "fixTimerResults",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "joinRoom",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint256"
                    }
                ],
                "payable": true,
                "stateMutability": "payable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    },
                    {
                        "name": "count",
                        "type": "uint8"
                    },
                    {
                        "name": "privat",
                        "type": "bool"
                    }
                ],
                "name": "newRoom",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint256"
                    }
                ],
                "payable": true,
                "stateMutability": "payable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    },
                    {
                        "name": "bet",
                        "type": "bytes32"
                    }
                ],
                "name": "setBet",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "OneMoreGame",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "player",
                        "type": "address"
                    },
                    {
                        "indexed": false,
                        "name": "r",
                        "type": "uint8"
                    },
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "Result",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "SeedOpened",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": false,
                        "name": "win",
                        "type": "address"
                    },
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "Winner",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    },
                    {
                        "indexed": false,
                        "name": "res1",
                        "type": "uint8"
                    },
                    {
                        "indexed": false,
                        "name": "res2",
                        "type": "uint8"
                    }
                ],
                "name": "RoundFinished",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": false,
                        "name": "win",
                        "type": "address"
                    },
                    {
                        "indexed": false,
                        "name": "amount",
                        "type": "uint256"
                    },
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "Revard",
                "type": "event"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "mreic",
                        "type": "uint256"
                    }
                ],
                "name": "setMaxReic",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "BetsFinished",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    },
                    {
                        "indexed": true,
                        "name": "player2",
                        "type": "address"
                    },
                    {
                        "indexed": false,
                        "name": "countdownTime",
                        "type": "uint256"
                    }
                ],
                "name": "JoinPlayer2",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "RevertRoom",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    },
                    {
                        "indexed": true,
                        "name": "player1",
                        "type": "address"
                    }
                ],
                "name": "JoinPlayer1",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "RoomClosed",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    },
                    {
                        "indexed": true,
                        "name": "player1",
                        "type": "address"
                    },
                    {
                        "indexed": false,
                        "name": "bet",
                        "type": "uint256"
                    },
                    {
                        "indexed": false,
                        "name": "counter",
                        "type": "uint8"
                    },
                    {
                        "indexed": false,
                        "name": "openedTime",
                        "type": "uint256"
                    },
                    {
                        "indexed": true,
                        "name": "privat",
                        "type": "bool"
                    }
                ],
                "name": "RoomOpened",
                "type": "event"
            },
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "from",
                        "type": "address"
                    },
                    {
                        "indexed": true,
                        "name": "room",
                        "type": "uint256"
                    }
                ],
                "name": "BetsAdd",
                "type": "event"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    },
                    {
                        "name": "seed",
                        "type": "uint256"
                    }
                ],
                "name": "setSeed",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [],
                "name": "transferOutAll",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "constant": false,
                "inputs": [
                    {
                        "name": "newOwner",
                        "type": "address"
                    }
                ],
                "name": "transferOwnership",
                "outputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "inputs": [],
                "payable": false,
                "stateMutability": "nonpayable",
                "type": "constructor"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomBet",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint256"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomBet1",
                "outputs": [
                    {
                        "name": "",
                        "type": "bytes32"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomBet2",
                "outputs": [
                    {
                        "name": "",
                        "type": "bytes32"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomCounter",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint8"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    },
                    {
                        "name": "player",
                        "type": "address"
                    }
                ],
                "name": "checkRoomIsBet",
                "outputs": [
                    {
                        "name": "",
                        "type": "bytes32"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomNotClosed",
                "outputs": [
                    {
                        "name": "",
                        "type": "bool"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomOpened",
                "outputs": [
                    {
                        "name": "",
                        "type": "bool"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomPlayer1",
                "outputs": [
                    {
                        "name": "",
                        "type": "address"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomPlayer2",
                "outputs": [
                    {
                        "name": "",
                        "type": "address"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomRes1",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint8"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomRes2",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint8"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomScore1",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint8"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomScore2",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint8"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkRoomStartTime",
                "outputs": [
                    {
                        "name": "",
                        "type": "uint256"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [
                    {
                        "name": "id",
                        "type": "uint256"
                    }
                ],
                "name": "checkSenderBet",
                "outputs": [
                    {
                        "name": "",
                        "type": "bytes32"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            },
            {
                "constant": true,
                "inputs": [],
                "name": "owner",
                "outputs": [
                    {
                        "name": "",
                        "type": "address"
                    }
                ],
                "payable": false,
                "stateMutability": "view",
                "type": "function"
            }
        ];
    
        var contr =  web3.eth.contract(contrInterface);
    
        var contractInstance = contr.at(contrAdress);
    
        var address = web3.eth.defaultAccount;
    
        var block = 0;
    
        web3.eth.getBlockNumber( function(er, res){ if(res) block = res });
    }


    ContractInterface это abi смарт контракта – совокупность названий функций, переменных и аргументов, для обращения и взаимодействия со смарт контрактом. Если вы используете remix – ide для работы со смарт контрактами ethereum в браузере, то его легко найти во вкладке Compite -> Details -> Abi.

    Большой проблемой было, и до сих пор частично остается, определение игрового состояния в отдельно взятый момент времени, при перезагрузках страницы или при переключении между столами. Не забывайте, что мы работаем не с сервером, на котором, такая проблема решается элементарно, а с блокчейном, получать информацию откуда приходится довольно экзотично. Мы постоянно слушаем все эти 14 игровых событий: присоединение соперника, начало ходов, завершение ходов и прочие. Мы так же постоянно отправляем запросы на получение некоторой игровой информации, например, текущий счёт, время начала раунда и других. Причём большинство игровых состояний определяются не одним событием, и не одной переменной, а наложением сразу нескольких событий с некоторыми переменными – результатами прямого получения данных из блокчейна. Например, ситуация, когда игра идёт до двух и более побед, и когда на третьем раунде пришло время отправки приватных ключей. Приходится отслеживать, что игра началась, подключился соперник, прошло три раунда, и прошла отправка зашифрованных ходов с обоих сторон. Попробуйте перезагрузить страницу в этот момент и приходится восстанавливать заново все взаимосвязи с блокчейном. Каждый запрос или получение информации происходит асинхронно. Задержки накладываются одна на другую и в результате приложение работает ощутимо медленнее серверного варианта.

    Но вернёмся к смарт контракту нашей игры. Мы уже поговорили о том, как мы храним игровые данные, какие используем события для взаимодействия с клиентской стороной, и как создаём столы. Теперь я хотел бы описать шифрование ходов. Мы имеем всего три возможные варианта хода, 1 – камень, 2 – ножницы, 3 – бумага. Всё начинается на клиенте, при выборе игроком своего хода. Скрипт, которые отрабатывает выбор карты, генерирует случайное число – мы называем его seed (далее — Сид). Сид сохраняется в куки в привязке к номеру стола, и хранится до последующего использования в них и в игровой сессии. В первый раз сид используется при определении игроком своего хода: выбрать камень, ножницы или бумагу. К нему прибавляется число 1, 2 или 3, — что выбрал игрок. Результат хешируется sha3 методом. Важный момент: web3.sha3() метод работает только со строками. На стороне клиента, мы легко конвертируем результирующее число в строку и затем хешируем ее для отправки на смарт контракт в переменную bit1 или bit2.

    function setBet(uint id, bytes32 bet) public {
    	    require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 );
    	    //таймер не вышел
    	    if(rooms[id].startTime + 5 minutes > now) {
    
    	        if(msg.sender == rooms[id].player1) { rooms[id].res2 = 5; rooms[id].bit1 = bet; }
    	        else if(msg.sender == rooms[id].player2) { rooms[id].res1 = 5; rooms[id].bit2 = bet; }
    
    	        if(rooms[id].bit1 != 0x0 && rooms[id].bit2 != 0x0) {
    	            SeedOpened(id);
    	            BetsFinished(id);
    	        }
    	        BetsAdd(msg.sender , id);
    	    } else {
    	        Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id);
    	    }
    	}

    Функция setBet обеспечивает главным образом гарантию принятия ходов с обеих сторон. До тех пор, пока оба игрока не сделают свои ходы, и они не будут записаны, игровой процесс не может продолжаться. При этом она же следит за соблюдением таймеров и осуществляет необходимые первоначальные проверки самой возможности принять ход. Когда ходы сделаны, функция посылает сигналы на клиент: SeedOpened и BetsFinisfed. Получив эти сигналы, клиент предлагает игрокам, как мы говорим «раскрыть карты», то есть отправить на смарт контакт ту самую первую часть числа до суммирования, которое он сгенерировал случайно, перед хешированием хода. Подтвердив своё согласие на «раскрытие карт», игрок отправляет это число на другую функцию смарт контракта – setSeed

    function setSeed(uint256 id, uint256 seed) public {
    
        require( rooms[id].bit2 != 0x0 && rooms[id].bit1 != 0x0  );
        require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 );
        //таймер не вышел
        if(rooms[id].startTime + 5 minutes > now) {
    
            if(msg.sender == rooms[id].player1) decodeHash1(id, seed);
            else if(msg.sender == rooms[id].player2) decodeHash2(id, seed);
    
        } else {
            Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id);
        }
    }

    Которая в свою очередь, передаёт полученный seed на внутреннюю функции смарт контракта decodeHash1 и decodeHash2

    function decodeHash1(uint id, uint seed) internal {
        uint e1 = seed + 1;
        bytes32 bitHash1a = keccak256(uintToString(e1));
    
        uint e2 = seed + 2;
        bytes32 bitHash1b = keccak256(uintToString(e2));
    
        uint e3 = seed + 3;
        bytes32 bitHash1c = keccak256(uintToString(e3));
    
    
        if(rooms[id].bit1 == bitHash1a) rooms[id].res1 = 1;
        if(rooms[id].bit1 == bitHash1b) rooms[id].res1 = 2;
        if(rooms[id].bit1 == bitHash1c) rooms[id].res1 = 3;
        Result(rooms[id].player1, rooms[id].res1, id);
        // return res1;
    }

    Тут завершается цикл шифрования. Функция воспроизводит уже описанную для клиента процедуру хеширования только внутри ethereum причём делает это по три раза для каждого игрока – по трём возможным ходам. Затем происходит обычное сравнение результата хеширования в ethereum с имеющимся результатом хеширования с клиента. Соответствие даёт понять нам, какой ход сделал игрок. Мы не декларируем и не храним ключи, которые приходят от игроков во время раскрытия, вместо них мы сразу записываем результат декодирования в переменных res для каждого игрока в виде числа от 0 до 4.

    На этом этапе есть ещё один интересный нюанс. Стандартная функция solidity keccak256, которая соответствует sha3 методу в web3 js библиотеке, как оказалось, дает адекватный результат только при подаче на вход именно строки, а не числа. Keccak256 позволяет работать и с числами, но поскольку на клиенте web3.sha3() принимает только строки, keccak256 должен так же получать строку на входе. А конвертация чисел в строки на solidity реализуется не так просто, как на javascript. Для этого нужно написать дополнительную внутреннюю функцию: uintToString(). Пометка pure означает, что эта функция не имеет права как либо влиять на состояние памяти смарт контракта: читать и писать. Вот такой нюанс.

    function uintToString(uint i) internal pure returns (string){
        // bytes memory bstr = new bytes;
        if (i == 0) return "0";
        uint j = i;
        uint length;
        while (j != 0){
            length++;
            j /= 10;
        }
        bytes memory bstr = new bytes(length);
        uint k = length - 1;
        while (i != 0){
            bstr[k--] = byte(48 + i % 10);
            i /= 10;
        }
        return string(bstr);
    }

    И наконец, игровой цикл завершает внутренняя функция определения победителя winRes(). В ней заложены все возможные исходы игры, а именно – при ничьей начинается переигровка текущего раунда. Определение победителя раунда или матча. Соответственно появляется понятие окончательная и промежуточная победа. Функция понимает ситуации, когда один из игроков не успел сделать действие и засчитывает ему поражение в текущем раунде. В ситуации, когда оба игрока бездействовали дольше положенного, стол закрывается не зависимо от текущего счета. В таком случае игра прекращается и всё возвращается в исходное состояние.

    Итоговый код
    function winRes(uint id) internal {
        require(rooms[id].res1 > 0 || rooms[id].res2 > 0);
    
        address win = 0x0;
    
        if(rooms[id].res1 == 1 && rooms[id].res2 == 2) win = rooms[id].player1;
        if(rooms[id].res1 == 1 && rooms[id].res2 == 3) win = rooms[id].player2;
    
        if(rooms[id].res1 == 2 && rooms[id].res2 == 1) win = rooms[id].player2;
        if(rooms[id].res1 == 2 && rooms[id].res2 == 3) win = rooms[id].player1;
    
        if(rooms[id].res1 == 3 && rooms[id].res2 == 1) win = rooms[id].player1;
        if(rooms[id].res1 == 3 && rooms[id].res2 == 2) win = rooms[id].player2;
    
        if(rooms[id].res1 == 4 && rooms[id].res2 != 4 ) win = rooms[id].player2;
        if(rooms[id].res2 == 4 && rooms[id].res1 != 4 ) win = rooms[id].player1;
    
        if(rooms[id].res1 == 5 && rooms[id].res2 != 5 ) win = rooms[id].player2;
        if(rooms[id].res2 == 5 && rooms[id].res1 != 5 ) win = rooms[id].player1;
    
        if((rooms[id].res2 == 4 && rooms[id].res1 == 4 ) || (rooms[id].res2 == 5 && rooms[id].res1 == 5 )) revertRoom(id);
        else
        {
            //Ничья - начинаем игру заново
            if(win == 0x0) {
                replay(id);
                OneMoreGame(id);
    
            } else {
                //Кто то победил
                if( win == rooms[id].player1 ) rooms[id].c1 += 1;
                if( win == rooms[id].player2 ) rooms[id].c2 += 1;
    
                //Если игра длится до n-побед и счетчик не достиг лимита
                if( rooms[id].counter > 1 && rooms[id].c1 < rooms[id].counter && rooms[id].c2 < rooms[id].counter ) {
                    ScoreChanged(id, rooms[id].c1, rooms[id].c2);
                    replay(id);
                    OneMoreGame(id);
                } else {
                    //Тут мы точно знаем что наш победитель одержал окончательную победу
                    ScoreChanged(id, rooms[id].c1, rooms[id].c2);
    
                    if( rooms[id].bet > 0 ) {
                        rewardWin(win, id);
                    }
                    Winner(win, id);
                    closeRoom(id);
                }
            }
        }
    }


    Алгоритм игры создавался с нуля, поэтому данный вариант, является успешным результатом серии экспериментов и корректировок. Насколько он эффективен и оптимален сказать сложно, поскольку сравнивать мы можем его только с самим собой. Относительно предыдущих 4х версий нашего алгоритма, этот в несколько раз эффективнее. Вероятно, есть ещё много возможных решений по оптимизации, но это уже вопрос будущего.

    Похожие публикации

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 813 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 4

      0

      Прочитал обе части статьи и так и не понял, какой стимул в нее играть? Что получит победитель в итоге? И кто платит за газ? Вы написали в первой части, что комиссия может меняться и от нее зависит длительность хода (чем выше, тем быстрее), но кто и как ее в итоге устанавливает? По мне так как раз такие моменты прежде всего и надо было осветить, а не заполнять статью интерфейсами контракта, которые ни о чем сами по себе не говорят, и инфой о том, как же плохо, что нельзя использовать числа вместо строк при хэшировании.

        0
        Тут не стимул, а смысл. Он состоит в том, чтобы узнать и посмотреть возможности и принципы блокчейна для обычных пользователей. Мы сделали игру на тестовом эфире и соответственно комиссия платиться тоже в нём. Количество комиссии может задаваться игроком в метамаске.
        А если так подумать, гипотетически, можно было бы реализовать её в main network, что повысило бы интерес у игроков.
          0

          Интерес игроков в чем тут? Заплатить комиссию, чтобы потом сказать себе, мол, да, эту игру можно сделать с помощью эфириума?

            –1
            Повторюсь, комиссия так же платиться в тестовом эфире. Но в теории это всё можно перевести на «боевой» эфир, где и ставки будут соответствующие.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое