-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGameEngine.sol
More file actions
279 lines (218 loc) · 8.68 KB
/
GameEngine.sol
File metadata and controls
279 lines (218 loc) · 8.68 KB
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.3/contracts/token/ERC20/IERC20.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
interface IMysteryBox {
function rewardBox(address to, uint boxType) external;
}
interface IGameToken is IERC20 {
function mint(address to, uint256 amount) external;
}
contract GameEngine is ConfirmedOwner, FunctionsClient, VRFConsumerBaseV2Plus {
using FunctionsRequest for FunctionsRequest.Request;
event MatchProcessed(address indexed player, uint256 matchID, bool won, uint256 boxIndex);
event MatchRequestSent(address indexed player, uint256 matchID, bytes32 requestId);
event MatchRequestFailed(address indexed player, uint256 matchID, string error);
event XPGained(address indexed player, uint256 totalXP);
IGameToken public gameToken;
IMysteryBox public mysteryBox;
mapping(uint => bool) public processedMatches;
// Chainlink Functions
uint64 public s_subscriptionId;
bytes32 public donID;
string private apiUrl; // game server API URL
uint32 public callbackGasLimit = 300000;
// Chainlink VRF
bytes32 public keyHash;
uint256 public s_vrfSubscriptionId;
uint32 public vrfCallbackGasLimit = 200000;
uint16 public requestConfirmations = 3;
uint32 public numWords = 1;
// Request tracking
mapping(bytes32 => address) public requestToPlayer;
mapping(bytes32 => uint256) public requestToMatchID;
mapping(uint256 => bytes32) public vrfRequestToFunctionsRequest;
struct MatchResult {
address player;
uint256 matchID;
bool won;
uint256 gainXP;
uint256 gainCoins;
}
mapping(bytes32 => MatchResult) public pendingMatches;
mapping(address => uint) public playerXP;
constructor(
address _token,
address _box,
// Chainlink API
address _functionsRouter,
bytes32 _donID,
uint64 _subscriptionId,
// Chainlink VRF
address _vrfCoordinator,
uint256 _vrfSubscriptionId,
bytes32 _keyHash,
// server api url
string memory _apiUrl
) FunctionsClient(_functionsRouter) VRFConsumerBaseV2Plus(_vrfCoordinator){
gameToken = IGameToken(_token);
mysteryBox = IMysteryBox(_box);
s_subscriptionId = _subscriptionId;
s_vrfSubscriptionId = _vrfSubscriptionId;
donID = _donID;
keyHash = _keyHash;
apiUrl = _apiUrl;
}
function reportMatch(address player, uint256 matchID) external {
require(player != address(0), "Invalid player address");
require(!processedMatches[matchID], "Match already processed");
processedMatches[matchID] = true;
string memory JS_SOURCE =
"const matchID = args[0]; "
"const apiResponse = await Functions.makeHttpRequest({ "
"url: args[1] + matchID, "
"method: 'GET' "
"}); "
"if (apiResponse.error) throw Error('Request failed'); "
"return Functions.encodeUint256(apiResponse.data.won ? 1 : 0);";
string[] memory args = new string[](2);
args[0] = uint2str(matchID);
args[1] = apiUrl;
FunctionsRequest.Request memory req;
req.initializeRequestForInlineJavaScript(JS_SOURCE);
req.setArgs(args);
bytes32 requestId = _sendRequest(
req.encodeCBOR(),
s_subscriptionId,
callbackGasLimit,
donID
);
requestToPlayer[requestId] = player;
requestToMatchID[requestId] = matchID;
emit MatchRequestSent(player, matchID, requestId);
}
// API response
function fulfillRequest(
bytes32 requestId,
bytes memory response,
bytes memory err
) internal override {
if (err.length > 0) {
emit MatchRequestFailed(requestToPlayer[requestId], requestToMatchID[requestId], string(err));
delete requestToPlayer[requestId];
delete requestToMatchID[requestId];
processedMatches[requestToMatchID[requestId]] = false;
return;
}
address player = requestToPlayer[requestId];
uint256 matchID = requestToMatchID[requestId];
uint256 wonValue = abi.decode(response, (uint256));
bool won = (wonValue == 1);
uint256 gainXP = won ? 100 : 30; // ToDo change
uint256 gainCoins = won ? 20 * 1e18 : 5 * 1e18; // ToDo change
pendingMatches[requestId] = MatchResult({
player: player,
matchID: matchID,
won: won,
gainXP: gainXP,
gainCoins: gainCoins
});
uint oldLevel = getLevelFromXP(playerXP[player]);
playerXP[player] += gainXP;
uint curLevel = getLevelFromXP(playerXP[player]);
gameToken.mint(player, gainCoins);
emit XPGained(player, playerXP[player]);
// Mystery Box reward
if (curLevel > oldLevel) {
uint256 vrfRequestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: s_vrfSubscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: vrfCallbackGasLimit,
numWords: numWords,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({
nativePayment: false
})
)
})
);
vrfRequestToFunctionsRequest[vrfRequestId] = requestId;
}
delete requestToPlayer[requestId];
delete requestToMatchID[requestId];
}
// VRF response
function fulfillRandomWords(uint256 vrfRequestId, uint256[] calldata randomWords) internal override {
bytes32 functionsRequestId = vrfRequestToFunctionsRequest[vrfRequestId];
MatchResult memory matchData = pendingMatches[functionsRequestId];
if (matchData.player != address(0)) {
uint256 boxIndex = randomWords[0] % 3; // 0, 1, 2
mysteryBox.rewardBox(matchData.player, boxIndex);
emit MatchProcessed(matchData.player, matchData.matchID, matchData.won, boxIndex);
}
delete vrfRequestToFunctionsRequest[vrfRequestId];
delete pendingMatches[functionsRequestId];
}
// +==================+
// | getter functions |
// +==================+
function getPlayerXP(address _player) public view returns (uint) {
return playerXP[_player];
}
function getLevelFromXP(uint256 totalXP) public pure returns (uint256) {
if (totalXP < 100) return 0;
if (totalXP == 100) return 1;
uint256 left = 1;
uint256 right = 255;
while (left < right) {
uint256 mid = (left + right + 1) >> 1;
uint256 requiredXP = getXPForLevel(mid);
if (totalXP >= requiredXP) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
function getXPForLevel(uint256 level) public pure returns (uint256) {
if (level < 1) return 0;
return 100 * level * (100 + level) / 100;
}
// +==================+
// | Helper functions |
// +==================+
function uint2str(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
// +==================+
// | set functions |
// +==================+
function setApiUrl(string memory _newUrl) external onlyOwner {
apiUrl = _newUrl;
}
}