Это статья — вторая (и заключительная) часть из серии о создании своих криптокотиков. В первой части мы узнали, что из себя представляет каждый Криптокотик, кто контролирует ход игры и как сделать котика в виде токена. Но для по-настоящему прорывного приложения нам необходимо определелить для них механизм размножения, а главное — рыночной торговли, чтобы участники могли выкупать друг у друга самых породистых котят.
4. KittyBreeding: котики отрываются по полной
В этом контракте содержатся методы, необходимые для скрещивания котиков, в том числе отслеживание предложений на «вязку», которые зависят от внешнего контракта генетической комбинации.
«Внешний контракт генетической комбинации» (geneScience
) хранится в отдельном контракте, код которого не является открытым.
Контракт KittyBreeding
содержит метод, с помощью которого CEO может указать адрес этого внешнего контракта:
/// @dev Update the address of the genetic contract, can only be called by the CEO.
/// @param _address An address of a GeneScience contract instance to be used from this point forward.
function setGeneScienceAddress(address _address) external onlyCEO {
GeneScienceInterface candidateContract = GeneScienceInterface(_address);
// NOTE: verify that a contract is what we expect - https://github.com/Lunyr/crowdsale-contracts/blob/cfadd15986c30521d8ba7d5b6f57b4fefcc7ac38/contracts/LunyrToken.sol#L117
require(candidateContract.isGeneScience());
// Set the new contract address
geneScience = candidateContract;
}
Разработчики решились на этот ход, чтобы игра не была слишком уж простой — если бы вы могли просто прочитать, чем определяется ДНК котика, было бы гораздо легче узнать, каких котиков скрещивать, чтобы получить породистого котика «fancy».
Этот внешний контракт geneScience
впоследствии будет использован в функции giveBirth()
(скоро мы с ней познакомимся) для определения ДНК нового котика.
Теперь посмотрим, что происходит, когда мы скрещиваем двух котиков:
/// @dev Internal utility function to initiate breeding, assumes that all breeding
/// requirements have been checked.
function _breedWith(uint256 _matronId, uint256 _sireId) internal {
// Grab a reference to the Kitties from storage.
Kitty storage sire = kitties[_sireId];
Kitty storage matron = kitties[_matronId];
// Mark the matron as pregnant, keeping track of who the sire is.
matron.siringWithId = uint32(_sireId);
// Trigger the cooldown for both parents.
_triggerCooldown(sire);
_triggerCooldown(matron);
// Clear siring permission for both parents. This may not be strictly necessary
// but it's likely to avoid confusion!
delete sireAllowedToAddress[_matronId];
delete sireAllowedToAddress[_sireId];
// Every time a kitty gets pregnant, counter is incremented.
pregnantKitties++;
// Emit the pregnancy event.
Pregnant(kittyIndexToOwner[_matronId], _matronId, _sireId, matron.cooldownEndBlock);
}
Эта функция берет учетные записи матери и отца, ищет их в основном кошачьем массиве и устанавливает в учетной записи отца показатель siringWithId
с отсылкой на мать. (Если показатель siringWithId
не равняется нулю, значит, мать беременна).
Этот контракт также применяет к обоим родителям функцию triggerCooldown
, которая делает их неспособными снова скрещиваться в течение установленного отрезка времени.
Далее у нас идет открытая функция giveBirth()
, которая создает нового котика:
/// @notice Have a pregnant Kitty give birth!
/// @param _matronId A Kitty ready to give birth.
/// @return The Kitty ID of the new kitten.
/// @dev Looks at a given Kitty and, if pregnant and if the gestation period has passed,
/// combines the genes of the two parents to create a new kitten. The new Kitty is assigned
/// to the current owner of the matron. Upon successful completion, both the matron and the
/// new kitten will be ready to breed again. Note that anyone can call this function (if they
/// are willing to pay the gas!), but the new kitten always goes to the mother's owner.
function giveBirth(uint256 _matronId)
external
whenNotPaused
returns(uint256)
{
// Grab a reference to the matron in storage.
Kitty storage matron = kitties[_matronId];
// Check that the matron is a valid cat.
require(matron.birthTime != 0);
// Check that the matron is pregnant, and that its time has come!
require(_isReadyToGiveBirth(matron));
// Grab a reference to the sire in storage.
uint256 sireId = matron.siringWithId;
Kitty storage sire = kitties[sireId];
// Determine the higher generation number of the two parents
uint16 parentGen = matron.generation;
if (sire.generation > matron.generation) {
parentGen = sire.generation;
}
// Call the sooper-sekret gene mixing operation.
uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1);
// Make the new kitten!
address owner = kittyIndexToOwner[_matronId];
uint256 kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner);
// Clear the reference to sire from the matron (REQUIRED! Having siringWithId
// set is what marks a matron as being pregnant.)
delete matron.siringWithId;
// Every time a kitty gives birth counter is decremented.
pregnantKitties--;
// Send the balance fee to the person who made birth happen.
msg.sender.send(autoBirthFee);
// return the new kitten's ID
return kittenId;
}
Комментарии по ходу разворачивания кода вполне понятны сами по себе. Так, сначала код проверяет, готова ли мать родить котика. Затем он определяет гены котенка с помощью функции geneScience.mixGenes()
, делает владельца матери также владельцем нового котика и запрашивает функцию _createKitty()
, которую мы уже видели в контракте KittyBase
.
Обратите внимание, что функция geneScience.mixGenes()
— это «кот в мешке», потому что код закрыт. Так что мы не знаем наверняка, как определяются гены ребенка, но мы знаем, что на них влияют функции генов матери, генов отца и временная метка отдыха матери cooldownEndBlock
.
5. KittyAuctions: покупаем, продаем и выставляем котиков на торги
Здесь у нас находятся открытые методы выставления котиков на аукционы по продаже и вязке котиков. Сам функционал аукциона осуществляется в двух отдельных контрактах (один для продаж, второй для вязки), а создание аукционов и торги происходит через этот раздел основного контракта.
Разработчики разделили функционал аукционов на независимые контракты, потому что, по их словам: «логика аукционов довольно сложна и всегда существует риск появления мелких багов. Если они будут храниться в своих собственных контрактах, мы сможем их обновлять, не мешая работе основного контракта, который отслеживает права собственности на котиков».
Так что в контракте KittyAuctions
содержатся функции setSaleAuctionAddress()
и setSiringAuctionAddress()
, которые, как и setGeneScienceAddress()
, могут быть вызваны только пользователем CEO. С их помощью можно указать адреса внешних контрактов, которые будут выполнять эти функции.
Примечание: «Вязка» означает выставление услуг вашего котика на аукцион. Вы выступаете своего рода сутенером, ведь другие пользователи будут платить вам эфир за возможность скрестить своего котика с вашим.
Это значит, что даже если сам по себе контракт CryptoKitties является неизменным, у CEO есть возможность позже изменить адрес этого контракта аукциона, и это автоматически изменит правила. Опять же, это не всегда плохо, потому что разработчикам периодически нужно исправлять ошибки. Просто примите этот факт во внимание.
Не будем углубляться в рассуждения о логике аукционов и торгов, иначе эта статья рискует стать слишком длинной (а она и так уже очень длинная!), вы можете сами посмотреть код на сайте EthFiddle (по ключевому слову KittyAuctions
).
6. KittyMinting: фабрика по производству котиков поколения 0
Последняя часть контракта содержит функционал, который мы используем для создания котиков поколения 0. Мы можем сделать до 5000 «промо-котиков», которых можно отдать (это особенно важно для нового сообщества), а других придется создавать и сразу же выставлять на аукцион, а стартовая цена будет определяться особым алгоритмом. Вне зависимости от способа создания, существует строгое ограничение в 50 тысяч котиков поколения 0. А после уж придется размножаться, размножаться и еще раз размножаться!
В этом контракте строго прописано количество промо-котиков и котиков поколения 0, которое вы можете создать:
uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;
А вот код, в котором пользователь “COO” может создавать промо-котиков и котиков поколения 0:
/// @dev we can create promo kittens, up to a limit. Only callable by COO
/// @param _genes the encoded genes of the kitten to be created, any value is accepted
/// @param _owner the future owner of the created kittens. Default to contract COO
function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
address kittyOwner = _owner;
if (kittyOwner == address(0)) {
kittyOwner = cooAddress;
}
require(promoCreatedCount < PROMO_CREATION_LIMIT);
promoCreatedCount++;
_createKitty(0, 0, 0, _genes, kittyOwner);
}
/// @dev Creates a new gen0 kitty with the given genes and
/// creates an auction for it.
function createGen0Auction(uint256 _genes) external onlyCOO {
require(gen0CreatedCount < GEN0_CREATION_LIMIT);
uint256 kittyId = _createKitty(0, 0, 0, _genes, address(this));
_approve(kittyId, saleAuction);
saleAuction.createAuction(
kittyId,
_computeNextGen0Price(),
0,
GEN0_AUCTION_DURATION,
address(this)
);
gen0CreatedCount++;
}
Из функции createPromoKitty()
ясно, что только COO может создавать новых котиков с любыми понравившимися ему генами, а также передавать котика кому угодно (всего он может создать 5000 таких котиков). Предполагаю, что такая функция используется для первых тестировщиков, друзей, родственников. Также бесплатные котики раздаются с целью продвижения игры.
Но это также означает, что ваш котик может и не быть таким уникальным, как вам кажется, ведь COO может наштамповать целых 5000 его клонов!
В функции createGen0Auction()
пользователь COO также указывает генетический код нового котенка. Но вместо отсылки на адрес конкретного пользователя он создает аукцион, в котором пользователи будут делать ставки в эфириумах, чтобы купить котенка.
7. KittyCore: Главный контракт
Это основной контракт игры CryptoKitties, составленный и запущенный в блокчейне Ethereum. Именно этот контракт собирает все остальные воедино.
Так как структура контрактов последовательна, последний собирает данные всех контрактов, с которыми мы познакомились ранее, и добавляет к ним несколько финальных методов, например, эту функцию для получения всех данных котика с помощью его учетной записи:
/// @notice Returns all the relevant information about a specific kitty.
/// @param _id The ID of the kitty of interest.
function getKitty(uint256 _id)
external
view
returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
) {
Kitty storage kit = kitties[_id];
// if this variable is 0 then it's not gestating
isGestating = (kit.siringWithId != 0);
isReady = (kit.cooldownEndBlock <= block.number);
cooldownIndex = uint256(kit.cooldownIndex);
nextActionAt = uint256(kit.cooldownEndBlock);
siringWithId = uint256(kit.siringWithId);
birthTime = uint256(kit.birthTime);
matronId = uint256(kit.matronId);
sireId = uint256(kit.sireId);
generation = uint256(kit.generation);
genes = kit.genes;
}
Это публичный метод, который покажет все данные конкретного котика из блокчейна. Я так думаю, что для отображения котиков на сайте сервер игры запрашивает именно эту команду.
Подождите… Я не вижу данных образа. Что же определяет внешний вид котика?
Как мы можем видеть из кода выше, котик сводится к 256-битному неподписанному целому числу, которое представляет собой генетический код.
В контрактном коде Solidity нет данных о внешнем виде котика, нет его описания или данных, которые определяют значение 256-битного целого числа. Интерпретация генетического кода котика осуществляется на веб-сервере CryptoKitty.
И хотя это довольно грамотная демонстрация игры в блокчейне, она не на 100% размещена в блокчейне. Если сайт игры сломается и никто не сохранит резервную копию всех образов, у нас останутся лишь бессмысленные 256-битные целые числа.
В контрактном коде я нашел контракт под названием ERC721Metadata
, но он нигде не используется. Так что я предполагаю, что изначально разработчики планировали хранить все в блокчейне, но позднее передумали (возможно, хранить такое количество данных на платформе Ethereum слишком дорого?), так что было решено хранить образы на веб-сервере.
Подведем итог
Что мы узнали:
- Как котики представляют собой структуру данных
- Как все существующие котики хранятся в одном смарт-контракте и как этот контракт следит за тем, какой пользователь какими котиками владеет
- Как создаются котики поколения 0
- Как котики скрещиваются для получения новых котят
За помощь в переводе большое спасибо Саше Ивановой!
Если вы хотите получить более подробное руководство по созданию своей собственной игры,
то рекомендую посетить ресурс CryptoZombies
Автор: Сергей