-
Notifications
You must be signed in to change notification settings - Fork 3
/
BreedingGame.sol
249 lines (212 loc) · 10.6 KB
/
BreedingGame.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Converter} from "../libraries/Converter.sol";
import {CollectionMinter} from "../CollectionMinter.sol";
import {TokenMinter, Attribute, CrossAddress} from "../TokenMinter.sol";
import {TokenManager} from "../TokenManager.sol";
import {AddressValidator} from "../AddressValidator.sol";
/// @notice TokenStats represents an NFT's attributes and lifecycle.
struct TokenStats {
uint32 breed; // The id of an NFT's type. Every breed has a different image.
uint32 generation; // Every NFT starts with generation 0 and can evolve with experience growth.
uint64 victories; // Number of victories.
uint64 defeats; // Number of defeats.
uint64 experience; // Experience earned.
}
/// @title Breeding Simulator
/// @dev A contract that simulates breeding, evolving, and battling of NFTs.
/// This contract aims to demonstrate how to mutate tokens' images and traits.
/// Users can `breed` new monsters and evolve them with experience growth.
/// You can see the example usage:
/// - For Ethereum accounts: use your Ethereum address in the CrossAddress structure when calling `breed`.
/// - For Substrate accounts: use your Substrate address in the CrossAddress structure when calling `breed`.
contract BreedingGame is CollectionMinter, TokenMinter, TokenManager, AddressValidator {
/// @dev This library provides data type conversion utilities.
using Converter for *;
/// @dev Total number of breeds available. For simplicity, only 2 types of tokens (different images) are available.
uint32 constant BREEDS = 2;
/// @dev Experience required to evolve to the next generation and change NFT's image.
uint256 constant EVOLUTION_EXPERIENCE = 150;
/// @dev Address of the NFT collection. Created at the deploy time.
address private immutable COLLECTION_ADDRESS;
/// @dev Mapping from generation to IPFS base URL for images.
mapping(uint256 generation => string ipfs) private s_generationIpfs;
/// @dev Mapping from token ID to its stats.
mapping(uint256 tokenId => TokenStats) private s_tokenStats;
/// @dev Token ID awaiting for an opponent.
uint256 private s_gladiator;
/// @dev This contract mints a fighting collection in the constructor.
/// CollectionMinter(true, true, false) means token attributes will be:
/// mutable (true) by the collection admin (true), but not by the token owner (false).
constructor() payable CollectionMinter(true, true, false) {
// Monsters can be of generation 0 or 1. Each generation has its own IPFS base URL.
s_generationIpfs[
0
] = "https://orange-impressed-bonobo-853.mypinata.cloud/ipfs/QmedQFp656axCAvKjo1iXqozH4Ew7AvDx8SFM4sH3hYHj6/";
s_generationIpfs[
1
] = "https://orange-impressed-bonobo-853.mypinata.cloud/ipfs/QmPqsyQRozG1vs2ZpgbPWQDbySqibaG6Q3sV7PGmSCxrBH/";
// The contract mints a collection and becomes the collection owner,
// so it has permissions to mutate its tokens' attributes.
COLLECTION_ADDRESS = _mintCollection(
"Evolved",
"Breeding simulator",
"EVLD",
"https://orange-impressed-bonobo-853.mypinata.cloud/ipfs/QmQgGuP4LFST3tMF46vQKow1Ki6oe47GKan1GDjD7z2JPD"
);
}
receive() external payable {}
/**
* @notice Breeds a new token for the given owner.
* @param _owner CrossAddress representing the owner of the new token.
*/
function breed(CrossAddress memory _owner) external {
// For simplicity, we have only 2 predefined images, type 1 or type 2.
// Each player receives a pseudo-random token breed.
uint32 randomTokenBreed = _getPseudoRandom(BREEDS, 1);
// Construct token image URL.
string memory randomImage = string.concat(
s_generationIpfs[0],
"monster-",
Converter.uint2str(randomTokenBreed),
".png"
);
Attribute[] memory attributes = new Attribute[](4);
// Each NFT has 3 traits. These traits are mutated when the `_fight` method is invoked.
attributes[0] = Attribute({trait_type: "Experience", value: "0"});
attributes[1] = Attribute({trait_type: "Victories", value: "0"});
attributes[2] = Attribute({trait_type: "Defeats", value: "0"});
attributes[3] = Attribute({trait_type: "Generation", value: "0"});
uint256 tokenId = _createToken(COLLECTION_ADDRESS, randomImage, attributes, _owner);
s_tokenStats[tokenId] = TokenStats({
breed: randomTokenBreed,
generation: 0,
victories: 0,
defeats: 0,
experience: 0
});
}
/**
* @notice Evolves the token to the next generation if it has enough experience.
* The token's image changes upon evolution.
* @param _tokenId The ID of the token to evolve.
*/
function evolve(uint256 _tokenId) external onlyTokenOwner(_tokenId, COLLECTION_ADDRESS) {
TokenStats memory tokenStats = s_tokenStats[_tokenId];
require(tokenStats.experience >= EVOLUTION_EXPERIENCE, "Experience not enough");
require(tokenStats.generation == 0, "Already evolved");
s_tokenStats[_tokenId].generation = 1;
_setTrait(COLLECTION_ADDRESS, _tokenId, "Generation", "1");
_setImage(_tokenId, false);
}
/**
* @notice Enters the token into the arena for battle.
* As a result of a battle, the token's traits and image will change.
* @param _tokenId The ID of the token to enter the arena.
*/
function enterArena(uint256 _tokenId) external onlyTokenOwner(_tokenId, COLLECTION_ADDRESS) {
if (s_gladiator != 0 && s_gladiator != _tokenId) _fight(s_gladiator, _tokenId);
else s_gladiator = _tokenId;
}
/**
* @notice Recovers the token after being exhausted from battle.
* Changes the token's image to the normal state.
* Note: Currently, there is no cooldown period, but this can be extended to include one.
* @param _tokenId The ID of the token to recover.
*/
function recover(uint256 _tokenId) external onlyTokenOwner(_tokenId, COLLECTION_ADDRESS) {
_setImage(_tokenId, false);
}
function getGladiator() external view returns (uint256) {
return s_gladiator;
}
function canEvolve(uint256 _tokenId) external view returns (bool) {
TokenStats memory tokenStats = s_tokenStats[_tokenId];
return tokenStats.experience >= EVOLUTION_EXPERIENCE && tokenStats.generation == 0;
}
/**
* @dev Function to mint a new collection.
* @param _name Name of the collection.
* @param _description Description of the collection.
* @param _symbol Symbol prefix for the tokens in the collection.
* @param _collectionCover URL of the cover image for the collection.
* @return Address of the created collection.
*/
function _mintCollection(
string memory _name,
string memory _description,
string memory _symbol,
string memory _collectionCover
) private returns (address) {
address collectionAddress = _createCollection(_name, _description, _symbol, _collectionCover);
// You may also set sponsorship for the collection to create a fee-less experience:
// import {UniqueNFT} from "@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol";
// UniqueNFT collection = UniqueNFT(collectionAddress);
// collection.setCollectionSponsorCross(CrossAddress({eth: address(this), sub: 0}));
// collection.confirmCollectionSponsorship();
return collectionAddress;
}
/**
* @dev Internal function to conduct a fight between two tokens.
* @param _tokenId1 ID of the first token.
* @param _tokenId2 ID of the second token.
*/
function _fight(uint256 _tokenId1, uint256 _tokenId2) private {
// Randomly decide the winner and loser.
(uint256 winner, uint256 loser) = _getPseudoRandom(2, 0) == 0 ? (_tokenId1, _tokenId2) : (_tokenId2, _tokenId1);
// Update winner's stats.
TokenStats memory winnerStats = s_tokenStats[winner];
winnerStats.victories += 1;
winnerStats.experience += 50;
s_tokenStats[winner] = winnerStats;
// Update loser's stats.
TokenStats memory loserStats = s_tokenStats[loser];
loserStats.defeats += 1;
loserStats.experience += 10;
s_tokenStats[loser] = loserStats;
// Update winner's token attributes.
_setTrait(COLLECTION_ADDRESS, winner, "Experience", Converter.uint2bytes(winnerStats.experience));
_setTrait(COLLECTION_ADDRESS, winner, "Victories", Converter.uint2bytes(winnerStats.victories));
// Update loser's token attributes.
_setTrait(COLLECTION_ADDRESS, loser, "Experience", Converter.uint2bytes(loserStats.experience));
_setTrait(COLLECTION_ADDRESS, loser, "Defeats", Converter.uint2bytes(loserStats.defeats));
// Make the loser exhausted (change NFT's image).
_makeExhausted(loser);
delete s_gladiator;
}
/**
* @dev Changes an NFT's image to exhausted version.
* Note: Currently, there is no cooldown period before recovery.
* @param _tokenId ID of the token to mark as exhausted.
*/
function _makeExhausted(uint256 _tokenId) private {
_setImage(_tokenId, true);
// TODO: we can set a cooldown period to recover the token
}
/**
* @dev Updates the image of a token to exhausted or to normal state.
* @param _tokenId ID of the token.
* @param _exhausted Boolean indicating whether the token is exhausted.
*/
function _setImage(uint256 _tokenId, bool _exhausted) private {
TokenStats memory tokenStats = s_tokenStats[_tokenId];
string memory extension = _exhausted ? "b.png" : ".png";
string memory imageUrl = string.concat(
s_generationIpfs[tokenStats.generation],
"monster-",
Converter.uint2str(tokenStats.breed),
extension
);
_setImage(COLLECTION_ADDRESS, _tokenId, bytes(imageUrl));
}
/**
* @dev Generates a pseudo-random number.
* @param _modulo The modulo to apply to the random number.
* @param startFrom The starting number to add to the random result.
* @return A pseudo-random uint32 number.
*/
function _getPseudoRandom(uint256 _modulo, uint256 startFrom) private view returns (uint32) {
uint256 randomHash = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)));
return uint32((randomHash % _modulo) + startFrom);
}
}