Adventure Layer Slither
The Adventure Layer Slither is a simple implementation of the Adventure Layer ECS framework, showcasing the core functionalities of the AGL. It demonstrates how to use ECS to manage game entities and components, and how to implement game logic through systems.
GitHub Repository
You can find the Adventure Layer Slither at the following link:
Adventure Layer ECS - Adventure Layer Slither.
The core modules of the Adventure Layer Slither are as follows.
1. Binding Independent Private Keys to Different Wallet Accounts
The Adventure Layer Slither allows binding independent private key accounts to different wallet accounts. Here’s how it works:
-
Private Key Generation and Storage:
Inmud/util.ts
, thegeneratePrivateKey
andprivateKeyToAccount
functions are used to generate a private key and convert it into account information. The private key can be stored in local storage for future use. -
Private Key Validation:
Also inmud/util.ts
, theassertPrivateKey
function is employed to validate the private key, ensuring it is in hexadecimal format and can successfully extract the corresponding address.
Relevant Code Snippet
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { isHex, Hex } from "viem";
export const BurnerCacheKey = 'mud:burnerWallet';
function assertPrivateKey(privateKey: string, cacheKey: string): asserts privateKey is Hex {
if (!isHex(privateKey)) {
console.error("Private key found in cache is not valid hex", { privateKey, cacheKey });
throw new Error(`Private key found in cache (${cacheKey}) is not valid hex`);
}
// Ensure the address can be extracted from the private key
privateKeyToAccount(privateKey);
}
export function setBurnerPrivateKey(privateKey: string, cacheKey = "mud:burnerWallet"): Hex {
localStorage.setItem(cacheKey, privateKey);
if (privateKey === null) {
console.error("Private key found in cache is not valid hex", { privateKey, cacheKey });
throw new Error(`Private key found in cache (${cacheKey}) is not valid hex`);
}
assertPrivateKey(privateKey, cacheKey);
console.log("Old burner wallet updated:", privateKeyToAccount(privateKey));
return privateKey;
}
2. AGL Table Structure
In mud.config.ts
, multiple AGL tables are defined, including:
- Users
- GameCodeToGameState
- UserToOthersPositions
- UserToOwnPositions
- UserToSnakeDeque
- Balance
- UserAccountMapping
A total of 7 AGL tables are used to manage game data.
Code Snippet
tables: {
Users: {
schema: {
player: "address",
gameCode: "uint32",
score: "uint32",
username: "string",
},
key: ["player"],
},
GameCodeToGameState: {
schema: {
gameCode: "uint32",
players: "address[]",
},
key: ["gameCode"],
},
UserToOthersPositions: {
schema: {
gameCode: "uint32",
snakes: "bytes32[]",
},
key: ["gameCode"]
},
UserToOwnPositions: {
schema: {
player: "address",
snake: "bytes32[]",
},
key: ["player"],
},
UserToSnakeDeque: {
schema: {
player: "address",
snakeBody: "bytes32[]",
},
key: ["player"],
},
Balance: {
schema: {
player: "address",
value: "uint256",
},
key: ["player"],
},
UserAccountMapping: {
schema: {
player: "address",
account: "string",
},
key: ["player"],
},
}
3. Main Contracts
IWorld.sol
This interface integrates all systems and associated function selectors that are dynamically registered in the World during deployment.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
/* Autogenerated file. Do not edit manually. */
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { IBalanceSystem } from "./IBalanceSystem.sol";
import { IGameStateSystem } from "./IGameStateSystem.sol";
import { IUserAccountSystem } from "./IUserAccountSystem.sol";
import { IUsersSystem } from "./IUsersSystem.sol";
/**
* @title IWorld
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @notice This interface integrates all systems and associated function selectors
* that are dynamically registered in the World during deployment.
* @dev This is an autogenerated file; do not edit manually.
*/
interface IWorld is IBaseWorld, IBalanceSystem, IGameStateSystem, IUserAccountSystem, IUsersSystem {}
BalanceSystem.sol
This contract handles balance-related operations for the game, including starting a game, querying balances, setting game fees, and managing player balances.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Balance } from "../codegen/index.sol";
contract BalanceSystem is System {
address public owner;
uint256 public gameFee = 0.01 ether; // Define game fee
event GameStartedRecharge(address indexed player);
event GamePlayedDeduct(address indexed player);
constructor() {
owner = msg.sender;
}
// Player pays and starts game
function startGame() public payable {
require(msg.value >= gameFee, "Insufficient funds to start the game");
payable(owner).transfer(gameFee);
rechargeBalance(msg.sender, gameFee);
emit GameStartedRecharge(msg.sender);
}
// Query contract balance
function getContractBalance() public view returns (uint256) {
return address(this).balance;
}
// Query player contract balance
function getPlayerBalance(address player) public view returns (uint256) {
return Balance.get(player);
}
// Query current sender balance
function getCurrentBalance() public view returns (uint256) {
return Balance.get(msg.sender);
}
// Set game fee
function setGameFee(uint256 newFee) public {
require(msg.sender == owner, "Only owner can set game fee");
gameFee = newFee;
}
// Deduct balance
function deductBalance(address player, uint256 amount) public {
uint256 currentBalance = Balance.get(player);
require(currentBalance >= amount, "Insufficient balance");
Balance.set(player, currentBalance - amount);
}
// Recharge balance
function rechargeBalance(address player, uint256 amount) public {
uint256 currentBalance = Balance.get(player);
Balance.set(player, currentBalance + amount);
}
}
UsersSystem.sol
This contract manages user-related operations, such as retrieving user data, starting a game, adding users, and generating random numbers.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { Users, UsersData, GameCodeToGameState } from "../codegen/index.sol";
import { System } from "@latticexyz/world/src/System.sol";
contract UsersSystem is System {
function getData() public view returns (UsersData memory) {
return Users.get(msg.sender);
}
function startGame(string memory name) public {
uint32 gameCode = uint32(generateRandom() % 10000);
Users.setGameCode(msg.sender, gameCode);
Users.setUsername(msg.sender, name);
addUser(gameCode);
}
function addUser(uint32 gameCode) public {
if (gameStateExistsAndRemove(gameCode, false)) {
GameCodeToGameState.pushPlayers(gameCode, msg.sender);
}
}
function gameStateExistsAndRemove(uint32 gameCode, bool remove) public returns (bool) {
address[] memory players = GameCodeToGameState.getPlayers(gameCode);
for (uint256 i = 0; i < players.length; i++) {
if (players[i] == msg.sender) {
if (remove) {
GameCodeToGameState.updatePlayers(gameCode, uint256(i), GameCodeToGameState.getItemPlayers(gameCode, 0));
GameCodeToGameState.popPlayers(gameCode);
}
return false;
}
}
return true;
}
function generateRandom() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)));
}
}
These contracts work together to manage the game’s balance, state, and user data, providing a comprehensive system for game operations.
Here’s the translated document in English:
4.Heartbeat Mechanism Implementation
-
Contract Task Management:
- The
ManageContractTask
function is used to start or stop the polling tasks for the contract, managing these tasks throughcontractMap
.
- The
-
Heartbeat Mechanism:
- The
startPolling
function uses the ABI of theadventureHeatbeat
function to package data and periodically send transactions via a timer. - The transaction sending is implemented by constructing, signing, and submitting the transaction.
func (s *EthereumAPI) startPolling(ctx context.Context, task ContractTask) { key, err := crypto.HexToECDSA(task.PrivateKey) if err != nil { log.Error("Failed to parse private key: %v", err) return } fromAddr := crypto.PubkeyToAddress(key.PublicKey) nonce, err := s.b.GetPoolNonce(ctx, fromAddr) if err != nil { log.Error("Failed to get nonce:", err) return } contractABI, err := abi.JSON(strings.NewReader(`[{"inputs":[],"name":"adventureHeatbeat","outputs":[],"stateMutability":"nonpayable","type":"function"}]`)) if err != nil { log.Error("Failed to parse contract ABI:", err) return } data, err := contractABI.Pack("adventureHeatbeat") if err != nil { log.Error("Failed to pack contract ABI:", err) return } for { select { case <-ctx.Done(): log.Info("Polling stopped for contract:", task.Address.Hex()) return default: gasLimit, err := s.estimateGas(ctx, &fromAddr, &task.Address, data) if err != nil { log.Error("Failed to estimate gas:", err) return } gasPrice, err := s.GasPrice(ctx) if err != nil { log.Error("Failed to get gas price:", err) return } task.sendTxMutex.Lock() tx := types.NewTx(&types.LegacyTx{ Nonce: nonce, To: &task.Address, Value: big.NewInt(0), Gas: gasLimit, GasPrice: big.NewInt(gasPrice.ToInt().Int64() * 2), Data: data, }) signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, key) if err != nil { log.Error("Failed to sign transaction:", err) task.sendTxMutex.Unlock() return } transaction, err := SubmitTransaction(ctx, s.b, signedTx) task.sendTxMutex.Unlock() if err != nil { log.Error("Failed to send transaction:", err) return } log.Info("Transaction sent:", transaction.Hex()) nonce++ time.Sleep(task.Interval) } } }
- The
-
Concurrency Control:
- The use of
sync.Mutex
ensures that the transaction sending is thread-safe.
- The use of
-
Periodic Invocation:
- The periodic invocation is implemented through
time.Sleep(task.Interval)
, whereInterval
is the time interval in milliseconds.
- The periodic invocation is implemented through
This mechanism ensures that the adventureHeatbeat
function can be regularly called at specified time intervals, thus achieving the heartbeat function. Each call may trigger random refreshes of food in the game, among other operations.
Was this page helpful?