-Scaffold-OP is a fork of Scaffold-ETH2 with minimal differences, providing additional dApp examples, native support for Superchain testnets, and more low-level instructions. We highly recommend the Scaffold-ETH2 docs as the primary guideline.
+Ceptor-Scaffold-OP is a fork of Scaffold-ETH2 with fantastic differences, providing additional dApp examples, native support for Superchain testnets, and more low-level instructions. We highly recommend the Scaffold-ETH2 docs as the primary guideline.
+
+# Games Contracts
+
+### [Game World Generator](BuyMeACeptor.sol)
+Step into the realms of creativity with our Game World Generator. This contract spawns a unique game world, sculpted by your chosen vibe and number of players. Picture a planet alive with scenarios, locations, descriptions, maps, denizens, secrets, goals, and players. Each world, distinct and thriving on its own blockchain, is birthed for 10 gameTokens. Craft your world, or join the adventure: 5 gT as a GM, 2 gT as a player.
+
+**Inside worlds:**
+- Games: Rich with adventure.
+- Schedules: Timed meticulously.
+- Sessions: Verified attendance.
+
+### [Character Generator](packages/hardhat/contracts/CeptorCharacterGenerator.sol)
+Unleash your avatar in the game world. This contract creates characters with abilities, class, name, alignment, and background. Each character boasts unique attributes. Note: Currently restricted to contract owner, slated for VRF2.5 upgrade.
+
+### [World and Game Management](packages/hardhat/contracts/WorldFactory.sol)
+The World Generator deploys a World contract. Each World tracks its games, ensuring verifiable truth within sessions.
+
+### [NPC Generator]
+Forge the worldβs inhabitants. The NPC Generator creates non-player characters, each with unique abilities, class, name, alignment, hometown, and background. This, too, is destined for the VRF2.5 upgrade.
+
+---
+
+Crafted with the prowess of a Level 5 Barbarian, the ingenuity of an Artificer 2, and the cosmic insight of a Druid of the Stars 2, by Tippi Fifestarr.
π§ͺ An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts.
diff --git a/packages/hardhat/contracts/BuyMeACeptor.sol b/packages/hardhat/contracts/BuyMeACeptor.sol
new file mode 100644
index 0000000..fcbb20f
--- /dev/null
+++ b/packages/hardhat/contracts/BuyMeACeptor.sol
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0 <0.9.0;
+
+/**
+ * @title World
+ * @dev World struct
+ */
+struct World {
+ string vibe;
+ string gameMasterName;
+ string gameMasterTwitterHandle;
+ string description;
+ uint256 time;
+ address gameMasterAddress;
+}
+
+/**
+ * @title BuyMeACeptorWorld
+ * @dev BuyMeACeptorWorld contract to accept donations and for our users to create a world for us
+ */
+contract BuyMeACeptor{
+ address payable public owner;
+ uint256 public price;
+ World[] public worlds;
+
+ error InsufficientFunds();
+ error InvalidArguments(string message);
+ error OnlyOwner();
+
+ event BuyMeACeptorWorldEvent(address indexed buyer, uint256 price);
+ event NewWorld(address indexed gameMasterAddress, uint256 time, string vibe, string gameMasterName, string gameMasterTwitterHandle, string description);
+
+ constructor() {
+ owner = payable(msg.sender);
+ price = 0.0001 ether;
+ }
+
+ /**
+ * WRITE FUNCTIONS *************
+ */
+
+ /**
+ * @dev Function to buy a world
+ * @param gameMasterName The name of the game master
+ * @param gameMasterTwitterHandle The Twitter handle of the game master
+ * @param description The description of the world
+ * (Note: Using calldata for gas efficiency)
+ */
+ function buyWorld(string calldata vibe, string calldata gameMasterName, string calldata gameMasterTwitterHandle, string calldata description) public payable {
+ if (msg.value < price) {
+ revert InsufficientFunds();
+ }
+
+ emit BuyMeACeptorWorldEvent(msg.sender, msg.value);
+
+ if (bytes(gameMasterName).length == 0 && bytes(description).length == 0) {
+ revert InvalidArguments("Invalid gameMasterName or description");
+ }
+
+ worlds.push(World(vibe, gameMasterName, gameMasterTwitterHandle, description, block.timestamp, msg.sender));
+
+ emit NewWorld(msg.sender, block.timestamp, vibe, gameMasterName, gameMasterTwitterHandle, description);
+ }
+
+ /**
+ * @dev Function to remove a world
+ * @param index The index of the world
+ */
+ function removeWorld(uint256 index) public {
+ if (index >= worlds.length) {
+ revert InvalidArguments("Invalid index");
+ }
+
+ World memory world = worlds[index];
+
+ // if operation isnt sent from the same game master or the owner, then not allowed
+ if (world.gameMasterAddress != msg.sender && msg.sender != owner) {
+ revert InvalidArguments("Operation not allowed");
+ }
+
+ World memory indexWorld = worlds[index];
+ worlds[index] = worlds[worlds.length - 1];
+ worlds[worlds.length - 1] = indexWorld;
+ worlds.pop();
+ }
+
+ /**
+ * @dev Function to modify a world description
+ * @param index The index of the world
+ * @param description The description of the world
+ */
+ function modifyWorldDescription(uint256 index, string memory description) public {
+ if (index >= worlds.length) {
+ revert InvalidArguments("Invalid index");
+ }
+
+ World memory world = worlds[index];
+
+ if (world.gameMasterAddress != msg.sender || msg.sender != owner) {
+ revert InvalidArguments("Operation not allowed");
+ }
+
+ worlds[index].description = description;
+ }
+
+ /**
+ * @dev Function to withdraw the balance
+ */
+ function withdrawTips() public {
+ if (msg.sender != owner) {
+ revert OnlyOwner();
+ }
+
+ if (address(this).balance == 0) {
+ revert InsufficientFunds();
+ }
+
+ (bool sent,) = owner.call{value: address(this).balance}("");
+ require(sent, "Failed to send Ether");
+ }
+
+ /**
+ * READ FUNCTIONS *************
+ */
+
+ /**
+ * @dev Function to get the worlds
+ */
+ function getWorlds() public view returns (World[] memory) {
+ return worlds;
+ }
+
+ /**
+ * @dev Recieve function to accept ether
+ */
+ receive() external payable {}
+}
diff --git a/packages/hardhat/contracts/BuyMeACoffee.sol b/packages/hardhat/contracts/BuyMeACoffee.sol
deleted file mode 100644
index 2e09998..0000000
--- a/packages/hardhat/contracts/BuyMeACoffee.sol
+++ /dev/null
@@ -1,156 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.0 <0.9.0;
-/**
- * ----------------------------------------------------------------------------------------------------------------
- * ---------βββββββ βββ βββββββββ βββββββ βββββββ ββββ βββ ββββββββββ βββ ββββββ βββββββ βββ-----
- * ---------βββββββββββ βββββββββ ββββββββ ββββββββββββββ ββββββββββββββ βββββββββββββββββββ βββ-----
- * ---------βββββββββββ βββββββββ βββ ββββββββββββ βββββββββ ββββββ βββββββββββββββββββββββββ βββ-----
- * ---------βββββββββββ βββββββββ βββ ββββββββββββ ββββββββββββββββ βββββββββββββββββββββββββββββ-----
- * ---------ββββββββββββββββββββββββββββββββββββ ββββββββββββ βββββββββββββββββ ββββββ βββββββββ ββββββ-----
- * ---------βββββββ βββββββ ββββββββββββββββββ βββββββ βββ βββββ ββββββββββ ββββββ βββββββββ βββββ-----
- * ----------------------------------------------------------------------------------------------------------------
- * https://github.com/coinbase/build-onchain-apps
- *
- * Disclaimer: The provided Solidity contracts are intended solely for educational purposes and are
- * not warranted for any specific use. They have not been audited and may contain vulnerabilities, hence should
- * not be deployed in production environments. Users are advised to seek professional review and conduct a
- * comprehensive security audit before any real-world application to mitigate risks of financial loss or other
- * consequences. The author(s) disclaim all liability for any damages arising from the use of these contracts.
- * Use at your own risk, acknowledging the inherent risks of smart contract technology on the blockchain.
- *
- */
-
-/**
- * @title Memos
- * @dev Memo struct
- */
-struct Memo {
- uint numCoffees;
- string userName;
- string twitterHandle;
- string message;
- uint256 time;
- address userAddress;
-}
-
-/**
- * @title BuyMeACoffee
- * @dev BuyMeACoffee contract to accept donations and for our users to leave a memo for us
- */
-contract BuyMeACoffee {
- address payable public owner;
- uint256 public price;
- Memo[] public memos;
-
- error InsufficientFunds();
- error InvalidArguments(string message);
- error OnlyOwner();
-
- event BuyMeACoffeeEvent(address indexed buyer, uint256 price);
- event NewMemo(address indexed userAddress, uint256 time, uint numCoffees, string userName, string twitterHandle, string message);
-
- constructor() {
- owner = payable(msg.sender);
- price = 0.0001 ether;
- }
-
- /**
- * WRITE FUNCTIONS *************
- */
-
- /**
- * @dev Function to buy a coffee
- * @param userName The name of the user
- * @param twitterHandle The Twitter handle of the user
- * @param message The message of the user
- * (Note: Using calldata for gas efficiency)
- */
- function buyCoffee(uint numCoffees, string calldata userName, string calldata twitterHandle, string calldata message) public payable {
- if (msg.value < price*numCoffees) {
- revert InsufficientFunds();
- }
-
- emit BuyMeACoffeeEvent(msg.sender, msg.value);
-
- if (bytes(userName).length == 0 && bytes(message).length == 0) {
- revert InvalidArguments("Invalid userName or message");
- }
-
- memos.push(Memo(numCoffees, userName, twitterHandle, message, block.timestamp, msg.sender));
-
- emit NewMemo(msg.sender, block.timestamp, numCoffees, userName, twitterHandle, message);
- }
-
- /**
- * @dev Function to remove a memo
- * @param index The index of the memo
- */
- function removeMemo(uint256 index) public {
- if (index >= memos.length) {
- revert InvalidArguments("Invalid index");
- }
-
- Memo memory memo = memos[index];
-
- // if operation isnt sent from the same user or the owner, then not allowed
- if (memo.userAddress != msg.sender && msg.sender != owner) {
- revert InvalidArguments("Operation not allowed");
- }
-
- Memo memory indexMemo = memos[index];
- memos[index] = memos[memos.length - 1];
- memos[memos.length - 1] = indexMemo;
- memos.pop();
- }
-
- /**
- * @dev Function to modify a memo
- * @param index The index of the memo
- * @param message The message of the memo
- */
- function modifyMemoMessage(uint256 index, string memory message) public {
- if (index >= memos.length) {
- revert InvalidArguments("Invalid index");
- }
-
- Memo memory memo = memos[index];
-
- if (memo.userAddress != msg.sender || msg.sender != owner) {
- revert InvalidArguments("Operation not allowed");
- }
-
- memos[index].message = message;
- }
-
- /**
- * @dev Function to withdraw the balance
- */
- function withdrawTips() public {
- if (msg.sender != owner) {
- revert OnlyOwner();
- }
-
- if (address(this).balance == 0) {
- revert InsufficientFunds();
- }
-
- (bool sent,) = owner.call{value: address(this).balance}("");
- require(sent, "Failed to send Ether");
- }
-
- /**
- * READ FUNCTIONS *************
- */
-
- /**
- * @dev Function to get the memos
- */
- function getMemos() public view returns (Memo[] memory) {
- return memos;
- }
-
- /**
- * @dev Recieve function to accept ether
- */
- receive() external payable {}
-}
diff --git a/packages/hardhat/contracts/CeptorCS.txt b/packages/hardhat/contracts/CeptorCS.txt
new file mode 100644
index 0000000..01ea2ed
--- /dev/null
+++ b/packages/hardhat/contracts/CeptorCS.txt
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/access/Ownable.sol";
+import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
+import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
+
+contract CeptorCharacterSheets is ERC721URIStorage, VRFConsumerBaseV2, Ownable {
+ struct Stats {
+ uint8 strength;
+ uint8 dexterity;
+ uint8 constitution;
+ uint8 intelligence;
+ uint8 wisdom;
+ uint8 charisma;
+ uint8 luck;
+ }
+
+ struct Character {
+ Stats stats;
+ string name;
+ uint swapsLeft;
+ }
+
+ mapping(address => uint256) public ownerToTokenId;
+ mapping(uint256 => Character) public tokenIdToCharacter;
+
+ uint256 public tokenIdCounter;
+ bytes32 public keyHash;
+ uint256 public fee;
+ uint64 public subscriptionId;
+
+ event CharacterCreated(uint256 indexed tokenId, address owner);
+ event StatsSwapped(uint256 indexed tokenId, address owner);
+
+ constructor(
+ address vrfCoordinator,
+ address linkToken,
+ bytes32 _keyHash,
+ uint64 _subscriptionId
+ )
+ VRFConsumerBaseV2(vrfCoordinator)
+ ERC721("CeptorCharacterSheets", "CCS")
+ {
+ keyHash = _keyHash;
+ fee = 0.1 * 10**18; // Chainlink VRF fee
+ subscriptionId = _subscriptionId;
+ tokenIdCounter = 1;
+ }
+
+ function createCharacter(string memory name) external {
+ require(ownerToTokenId[msg.sender] == 0, "Owner already has a character");
+ uint256 tokenId = tokenIdCounter++;
+ ownerToTokenId[msg.sender] = tokenId;
+ tokenIdToCharacter[tokenId] = Character({
+ name: name,
+ stats: Stats(0, 0, 0, 0, 0, 0, 0),
+ swapsLeft: 3
+ });
+ _safeMint(msg.sender, tokenId);
+ emit CharacterCreated(tokenId, msg.sender);
+ requestStats(tokenId);
+ }
+
+ function requestStats(uint256 tokenId) internal {
+ require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
+ requestRandomWords(keyHash, subscriptionId, 3, fee, 1);
+ }
+
+ function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
+ uint256 tokenId = ownerToTokenId[msg.sender];
+ Character storage character = tokenIdToCharacter[tokenId];
+ character.stats.strength = uint8(randomWords[0] % 16 + 3);
+ character.stats.dexterity = uint8(randomWords[1] % 16 + 3);
+ character.stats.constitution = uint8(randomWords[2] % 16 + 3);
+ character.stats.intelligence = uint8(randomWords[3] % 16 + 3);
+ character.stats.wisdom = uint8(randomWords[4] % 16 + 3);
+ character.stats.charisma = uint8(randomWords[5] % 16 + 3);
+ character.stats.luck = uint8(randomWords[6] % 16 + 3);
+ }
+
+ function swapStats(uint256 tokenId) external {
+ require(ownerOf(tokenId) == msg.sender, "Not the owner");
+ Character storage character = tokenIdToCharacter[tokenId];
+ require(character.swapsLeft > 0, "No swaps left");
+ character.swapsLeft--;
+ requestStats(tokenId);
+ emit StatsSwapped(tokenId, msg.sender);
+ }
+}
diff --git a/packages/hardhat/contracts/CeptorCharacterGenerator.sol b/packages/hardhat/contracts/CeptorCharacterGenerator.sol
new file mode 100644
index 0000000..95fadec
--- /dev/null
+++ b/packages/hardhat/contracts/CeptorCharacterGenerator.sol
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.7;
+
+import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
+import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
+import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
+
+contract DnDCharacterGenerator is VRFConsumerBaseV2, ConfirmedOwner {
+ VRFCoordinatorV2Interface COORDINATOR;
+ uint64 s_subscriptionId;
+ bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
+ // forge-test gas report & gas limit plugin on hardhat
+ uint32 callbackGasLimit = 100000;
+ uint16 requestConfirmations = 3;
+ uint32 numWords = 7; // 6 ability scores + 1 for class
+
+ struct Character {
+ uint256[6] abilities;
+ string class;
+ string name;
+ string alignment;
+ string background;
+ uint8 swaps;
+ }
+
+ mapping(uint256 => address) requestToSender;
+ mapping(address => Character) public characters;
+
+ event CharacterCreated(address owner, uint256 requestId);
+ event CharacterUpdated(address owner, string name, string alignment, string background);
+ event ScoresSwapped(address owner);
+ event RequestFulfilled(uint256 requestId, uint256[] randomWords);
+
+ constructor(uint64 subscriptionId) VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625)
+ ConfirmedOwner(msg.sender) {
+ COORDINATOR = VRFCoordinatorV2Interface(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625);
+ s_subscriptionId = subscriptionId;
+ }
+
+ function createCharacter() external onlyOwner {
+ require(characters[msg.sender].abilities[0] == 0, "Character already created");
+ uint256 requestId = COORDINATOR.requestRandomWords(
+ keyHash,
+ s_subscriptionId,
+ requestConfirmations,
+ callbackGasLimit,
+ numWords
+ );
+ requestToSender[requestId] = msg.sender;
+ emit CharacterCreated(msg.sender, requestId);
+ }
+
+ function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
+ address owner = requestToSender[requestId];
+ uint256[6] memory abilities;
+ for (uint i = 0; i < 6; ++i) { // ++i saves 2 gas
+ abilities[i] = (randomWords[i] % 16) + 3; // Score range: 3-18
+ }
+ characters[owner] = Character({
+ abilities: abilities,
+ class: getClass(randomWords[6]),
+ name: "",
+ alignment: "",
+ background: "",
+ swaps: 0
+ });
+ emit RequestFulfilled(requestId, randomWords);
+ }
+
+ function updateCharacterDetails(string calldata name, string calldata alignment, string calldata background) external {
+ require(characters[msg.sender].abilities[0] != 0, "Character not created");
+ characters[msg.sender].name = name;
+ characters[msg.sender].alignment = alignment;
+ characters[msg.sender].background = background;
+ emit CharacterUpdated(msg.sender, name, alignment, background);
+ }
+
+ function swapScores(uint8 index1, uint8 index2) external {
+ require(characters[msg.sender].swaps < 3, "Max swaps reached");
+ require(index1 < 6 && index2 < 6, "Invalid index");
+
+ (characters[msg.sender].abilities[index1], characters[msg.sender].abilities[index2]) =
+ (characters[msg.sender].abilities[index2], characters[msg.sender].abilities[index1]);
+ characters[msg.sender].swaps++;
+ emit ScoresSwapped(msg.sender);
+ }
+
+ function getClass(uint256 randomNumber) private pure returns (string memory) {
+ string[12] memory classes = ["Barbarian", "Bard", "Cleric", "Druid", "Fighter", "Monk", "Paladin", "Ranger", "Rogue", "Sorcerer", "Warlock", "Wizard"];
+ return classes[randomNumber % classes.length];
+ }
+}
diff --git a/packages/hardhat/contracts/CharacterSheets.txt b/packages/hardhat/contracts/CharacterSheets.txt
new file mode 100644
index 0000000..0eb0de6
--- /dev/null
+++ b/packages/hardhat/contracts/CharacterSheets.txt
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: MIT
+// An example of a consumer contract that relies on a subscription for funding.
+pragma solidity ^0.8.7;
+
+// Useful for debugging. Remove when deploying to a live network.
+import "hardhat/console.sol";
+
+// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
+import "@openzeppelin/contracts/access/Ownable.sol";
+import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
+import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
+import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
+
+/**
+ * Request testnet LINK and ETH here: https://faucets.chain.link/
+ * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
+ */
+
+/**
+ * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
+ * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
+ * DO NOT USE THIS CODE IN PRODUCTION.
+ */
+
+contract VRFv2Consumer is VRFConsumerBaseV2, ConfirmedOwner {
+ event RequestSent(uint256 requestId, uint32 numWords);
+ event RequestFulfilled(uint256 requestId, uint256[] randomWords);
+
+ struct RequestStatus {
+ bool fulfilled; // whether the request has been successfully fulfilled
+ bool exists; // whether a requestId exists
+ uint256[] randomWords;
+ }
+ mapping(uint256 => RequestStatus)
+ public s_requests; /* requestId --> requestStatus */
+ VRFCoordinatorV2Interface COORDINATOR;
+
+ // Your subscription ID.
+ uint64 s_subscriptionId;
+
+ // past requests Id.
+ uint256[] public requestIds;
+ uint256 public lastRequestId;
+
+ // The gas lane to use, which specifies the maximum gas price to bump to.
+ // For a list of available gas lanes on each network,
+ // see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations
+ bytes32 keyHash =
+ 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
+
+ // Depends on the number of requested values that you want sent to the
+ // fulfillRandomWords() function. Storing each word costs about 20,000 gas,
+ // so 100,000 is a safe default for this example contract. Test and adjust
+ // this limit based on the network that you select, the size of the request,
+ // and the processing of the callback request in the fulfillRandomWords()
+ // function.
+ uint32 callbackGasLimit = 100000;
+
+ // The default is 3, but you can set this higher.
+ uint16 requestConfirmations = 3;
+
+ // For this example, retrieve 2 random values in one request.
+ // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
+ uint32 numWords = 2;
+
+ /**
+ * HARDCODED FOR SEPOLIA
+ * COORDINATOR: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
+ */
+ constructor(
+ uint64 subscriptionId
+ )
+ VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625)
+ ConfirmedOwner(msg.sender)
+ {
+ COORDINATOR = VRFCoordinatorV2Interface(
+ 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
+ );
+ s_subscriptionId = subscriptionId;
+ }
+
+ // Assumes the subscription is funded sufficiently.
+ function requestRandomWords()
+ external
+ onlyOwner
+ returns (uint256 requestId)
+ {
+ // Will revert if subscription is not set and funded.
+ requestId = COORDINATOR.requestRandomWords(
+ keyHash,
+ s_subscriptionId,
+ requestConfirmations,
+ callbackGasLimit,
+ numWords
+ );
+ s_requests[requestId] = RequestStatus({
+ randomWords: new uint256[](0),
+ exists: true,
+ fulfilled: false
+ });
+ requestIds.push(requestId);
+ lastRequestId = requestId;
+ emit RequestSent(requestId, numWords);
+ return requestId;
+ }
+
+ function fulfillRandomWords(
+ uint256 _requestId,
+ uint256[] memory _randomWords
+ ) internal override {
+ require(s_requests[_requestId].exists, "request not found");
+ s_requests[_requestId].fulfilled = true;
+ s_requests[_requestId].randomWords = _randomWords;
+ emit RequestFulfilled(_requestId, _randomWords);
+ }
+
+ function getRequestStatus(
+ uint256 _requestId
+ ) external view returns (bool fulfilled, uint256[] memory randomWords) {
+ require(s_requests[_requestId].exists, "request not found");
+ RequestStatus memory request = s_requests[_requestId];
+ return (request.fulfilled, request.randomWords);
+ }
+}
diff --git a/packages/hardhat/contracts/DynamicHooty.sol b/packages/hardhat/contracts/DynamicHooty.sol
new file mode 100644
index 0000000..39bbe4c
--- /dev/null
+++ b/packages/hardhat/contracts/DynamicHooty.sol
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: MIT
+// An example of a consumer contract that relies on a subscription for funding.
+pragma solidity ^0.8.7;
+
+import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
+import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
+import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
+
+/**
+ * Request testnet LINK and ETH here: https://faucets.chain.link/
+ * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
+ */
+
+/**
+ * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
+ * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
+ * DO NOT USE THIS CODE IN PRODUCTION.
+ */
+
+contract VRFv2Consumer is VRFConsumerBaseV2, ConfirmedOwner {
+ event RequestSent(uint256 requestId, uint32 numWords);
+ event RequestFulfilled(uint256 requestId, uint256[] randomWords);
+
+ struct RequestStatus {
+ bool fulfilled; // whether the request has been successfully fulfilled
+ bool exists; // whether a requestId exists
+ uint256[] randomWords;
+ }
+ mapping(uint256 => RequestStatus)
+ public s_requests; /* requestId --> requestStatus */
+ VRFCoordinatorV2Interface COORDINATOR;
+
+ // Your subscription ID.
+ uint64 s_subscriptionId;
+
+ // past requests Id.
+ uint256[] public requestIds;
+ uint256 public lastRequestId;
+
+ // The gas lane to use, which specifies the maximum gas price to bump to.
+ // For a list of available gas lanes on each network,
+ // see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations
+ bytes32 keyHash =
+ 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
+
+ // Depends on the number of requested values that you want sent to the
+ // fulfillRandomWords() function. Storing each word costs about 20,000 gas,
+ // so 100,000 is a safe default for this example contract. Test and adjust
+ // this limit based on the network that you select, the size of the request,
+ // and the processing of the callback request in the fulfillRandomWords()
+ // function.
+ uint32 callbackGasLimit = 100000;
+
+ // The default is 3, but you can set this higher.
+ uint16 requestConfirmations = 3;
+
+ // For this example, retrieve 2 random values in one request.
+ // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
+ uint32 numWords = 2;
+
+ /**
+ * HARDCODED FOR SEPOLIA
+ * COORDINATOR: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
+ */
+ constructor(
+ uint64 subscriptionId
+ )
+ VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625)
+ ConfirmedOwner(msg.sender)
+ {
+ COORDINATOR = VRFCoordinatorV2Interface(
+ 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
+ );
+ s_subscriptionId = subscriptionId;
+ }
+
+ // Assumes the subscription is funded sufficiently.
+ function requestRandomWords()
+ external
+ onlyOwner
+ returns (uint256 requestId)
+ {
+ // Will revert if subscription is not set and funded.
+ requestId = COORDINATOR.requestRandomWords(
+ keyHash,
+ s_subscriptionId,
+ requestConfirmations,
+ callbackGasLimit,
+ numWords
+ );
+ s_requests[requestId] = RequestStatus({
+ randomWords: new uint256[](0),
+ exists: true,
+ fulfilled: false
+ });
+ requestIds.push(requestId);
+ lastRequestId = requestId;
+ emit RequestSent(requestId, numWords);
+ return requestId;
+ }
+
+ function fulfillRandomWords(
+ uint256 _requestId,
+ uint256[] memory _randomWords
+ ) internal override {
+ require(s_requests[_requestId].exists, "request not found");
+ s_requests[_requestId].fulfilled = true;
+ s_requests[_requestId].randomWords = _randomWords;
+ emit RequestFulfilled(_requestId, _randomWords);
+ }
+
+ function getRequestStatus(
+ uint256 _requestId
+ ) external view returns (bool fulfilled, uint256[] memory randomWords) {
+ require(s_requests[_requestId].exists, "request not found");
+ RequestStatus memory request = s_requests[_requestId];
+ return (request.fulfilled, request.randomWords);
+ }
+}
diff --git a/packages/hardhat/contracts/World.sol b/packages/hardhat/contracts/World.sol
new file mode 100644
index 0000000..07d9d5d
--- /dev/null
+++ b/packages/hardhat/contracts/World.sol
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0 <0.9.0;
+
+contract World {
+ struct Game {
+ address gameMaster;
+ uint256 startTime;
+ uint256 endTime;
+ address[] players;
+ mapping(address => bool) isPlayer;
+ }
+
+ string public vibe;
+ string public gameMasterName;
+ string public gameMasterTwitterHandle;
+ string public description;
+ uint256 public creationTime;
+ address public worldCreator;
+ uint256 public treasuryBalance;
+
+ uint256 public gameCount;
+ mapping(uint256 => Game) public games;
+
+ event WorldUpdated(string vibe, string gameMasterName, string gameMasterTwitterHandle, string description);
+ event GameCreated(uint256 gameId, address gameMaster, uint256 startTime, uint256 endTime);
+ event PlayerJoined(uint256 gameId, address player);
+ event PlayerApproved(uint256 gameId, address player);
+
+ constructor(
+ string memory _vibe,
+ string memory _gameMasterName,
+ string memory _gameMasterTwitterHandle,
+ string memory _description,
+ address _worldCreator
+ ) {
+ vibe = _vibe;
+ gameMasterName = _gameMasterName;
+ gameMasterTwitterHandle = _gameMasterTwitterHandle;
+ description = _description;
+ creationTime = block.timestamp;
+ worldCreator = _worldCreator;
+ }
+
+ modifier onlyGameMaster(uint256 gameId) {
+ require(msg.sender == games[gameId].gameMaster, "Not the game master");
+ _;
+ }
+
+ function createGame(uint256 startTime, uint256 endTime) public {
+ games[gameCount].gameMaster = msg.sender;
+ games[gameCount].startTime = startTime;
+ games[gameCount].endTime = endTime;
+ gameCount++;
+
+ emit GameCreated(gameCount - 1, msg.sender, startTime, endTime);
+ }
+
+ function joinGame(uint256 gameId) public payable {
+ require(gameId < gameCount, "Game does not exist");
+ require(msg.value >= 1 ether, "Insufficient stake");
+
+ games[gameId].players.push(msg.sender);
+ games[gameId].isPlayer[msg.sender] = true;
+ treasuryBalance += msg.value;
+
+ emit PlayerJoined(gameId, msg.sender);
+ }
+
+ function approvePlayer(uint256 gameId, address player) public onlyGameMaster(gameId) {
+ require(games[gameId].isPlayer[player], "Player not in the game");
+
+ // Additional logic to approve player if needed
+ emit PlayerApproved(gameId, player);
+ }
+
+ function updateDescription(string memory _description) public {
+ require(msg.sender == worldCreator, "Operation not allowed");
+ description = _description;
+ emit WorldUpdated(vibe, gameMasterName, gameMasterTwitterHandle, description);
+ }
+}
diff --git a/packages/hardhat/contracts/WorldFactory.sol b/packages/hardhat/contracts/WorldFactory.sol
new file mode 100644
index 0000000..69f6373
--- /dev/null
+++ b/packages/hardhat/contracts/WorldFactory.sol
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0 <0.9.0;
+import "./World.sol";
+
+contract WorldFactory {
+ address public owner;
+ uint256 public priceToCreate;
+ uint256 public priceToJoinGM;
+ uint256 public priceToJoinPlayer;
+ World[] public worlds;
+
+ error InsufficientFunds();
+ error InvalidArguments(string message);
+ error OnlyOwner();
+
+ event WorldCreated(address indexed worldCreator, address worldContract, uint256 time, string vibe, string gameMasterName, string gameMasterTwitterHandle, string description);
+ event PlayerJoinedWorld(address indexed worldContract, address player);
+ event GameScheduled(address indexed worldContract, uint256 gameId, address gameMaster, uint256 startTime, uint256 endTime);
+ event GameStarted(address indexed worldContract, uint256 gameId);
+ event GameJoined(address indexed worldContract, uint256 gameId, address player);
+ event GameLeft(address indexed worldContract, uint256 gameId, address player);
+ event GameReviewed(address indexed worldContract, uint256 gameId, address reviewer);
+
+ constructor() {
+ owner = msg.sender;
+ priceToCreate = .1 ether; // Example price to create a world
+ priceToJoinGM = .05 ether; // Example price for GM to join a world
+ priceToJoinPlayer = .02 ether; // Example price for player to join a world
+ }
+
+ /**
+ * @dev Function to create a world
+ * @param vibe The vibe of the world
+ * @param gameMasterName The name of the game master
+ * @param gameMasterTwitterHandle The Twitter handle of the game master
+ * @param description The description of the world
+ */
+ function createWorld(
+ string calldata vibe,
+ string calldata gameMasterName,
+ string calldata gameMasterTwitterHandle,
+ string calldata description
+ ) public payable {
+ if (msg.value < priceToCreate) {
+ revert InsufficientFunds();
+ }
+
+ World world = new World(vibe, gameMasterName, gameMasterTwitterHandle, description, msg.sender);
+ worlds.push(world);
+
+ emit WorldCreated(msg.sender, address(world), block.timestamp, vibe, gameMasterName, gameMasterTwitterHandle, description);
+ }
+
+ /**
+ * @dev Function for a GM to join a world
+ * @param worldIndex The index of the world to join
+ */
+ function joinWorldAsGM(uint256 worldIndex) public payable {
+ if (worldIndex >= worlds.length) {
+ revert InvalidArguments("World does not exist");
+ }
+ if (msg.value < priceToJoinGM) {
+ revert InsufficientFunds();
+ }
+
+ World world = worlds[worldIndex];
+ payable(world.worldCreator()).transfer(priceToJoinGM / 2);
+ payable(address(world)).transfer(priceToJoinGM / 2);
+
+ emit PlayerJoinedWorld(address(world), msg.sender);
+ }
+
+ /**
+ * @dev Function for a player to join a world
+ * @param worldIndex The index of the world to join
+ */
+ function joinWorldAsPlayer(uint256 worldIndex) public payable {
+ if (worldIndex >= worlds.length) {
+ revert InvalidArguments("World does not exist");
+ }
+ if (msg.value < priceToJoinPlayer) {
+ revert InsufficientFunds();
+ }
+
+ World world = worlds[worldIndex];
+ payable(world.worldCreator()).transfer(priceToJoinPlayer / 2);
+ payable(address(world)).transfer(priceToJoinPlayer / 2);
+
+ emit PlayerJoinedWorld(address(world), msg.sender);
+ }
+
+ /**
+ * @dev Function to withdraw the balance
+ */
+ function withdrawFunds() public {
+ if (msg.sender != owner) {
+ revert OnlyOwner();
+ }
+
+ if (address(this).balance == 0) {
+ revert InsufficientFunds();
+ }
+
+ (bool sent,) = owner.call{value: address(this).balance}("");
+ require(sent, "Failed to send Ether");
+ }
+
+ /**
+ * @dev Function to get the worlds
+ */
+ function getWorlds() public view returns (World[] memory) {
+ return worlds;
+ }
+
+ /**
+ * @dev Receive function to accept ether
+ */
+ receive() external payable {}
+}
diff --git a/packages/hardhat/contracts/WorldFactory2.sol b/packages/hardhat/contracts/WorldFactory2.sol
new file mode 100644
index 0000000..b5fee9a
--- /dev/null
+++ b/packages/hardhat/contracts/WorldFactory2.sol
@@ -0,0 +1,69 @@
+// inspired by Josh "W" Comeau's open house lessson on useState and react dom magic
+// https://courses.joshwcomeau.com/joy-of-react/open-house/01-why-the-dance
+
+// SPDX-License-Identifier: MIT
+// Why do we always use this one? - Tippi
+// what does a world look like, that's up to it's Creator.
+// what does a world factory look like? it all starts at the gift shop.
+pragma solidity ^0.8.0;
+// "public variables automatically generate getter functions" - Andrej
+// what does the world factory need to make public?
+// number of worlds
+// number of gifts remaining in the gift shop
+// number of games
+// uri of each worlds' Creator (owner provided unique 8-bit wizard character) .gif file
+// each worlds "vibe", "scenarios", "locations", "descriptions", "maps", "denizens", "secrets", "goals", "ruleset" and "players"
+// if a game is currently happening and what world it's in
+// what else?
+/* A contract that generates a game world based on a user's vibe and number of players. The world is generated with a visual of the planet, scenarios, locations, descriptions, maps, denizens, secrets, goals, and players. Each world has its own blockchain. Creating a World costs 10 gameTokens. when creating a game, i want to have my own world or play with others. each world should be locked to a blockchain. 10 gT to make a world. 5 gT to join one as a GM, 2 gT to join as player.
+*/
+// to gain access to world factory, you must buy a gift from the gift shop.
+// for 10 dollars of eth, you get 1 gameToken.
+// guess how much a gift costs? half gameToken.
+// 10 gameTokens = Create 1 world
+// what are the functions that the world factory needs?
+// createWorld
+// joinWorld
+// leaveWorld
+// getWorld
+// moneyIn
+// do we want to allow each game owner (Creator) to have a vote on how money is spent?
+// i vote yes - tippi. add your vote - name above if you agree.
+// what else?
+// what are the functions that a world needs?
+// setSchedule
+// scheduleGame
+// startGame
+// joinGame
+// leaveGame
+// reviewGame
+// what else?
+// what are the events that the world factory needs?
+// worldCreated
+// worldJoined
+// worldLeft
+// gameScheduled
+// gameStarted
+// gameJoined
+// gameLeft
+// gameReviewed
+// what else?
+// what are the modifiers that the world factory needs?
+// onlyWorldOwner
+// onlyWorldGameMaster
+// onlyWorldPlayer
+// what else?
+// what are the structs that the world factory needs?
+// World
+// Game
+// Player
+// what else?
+
+contract WorldFactory {
+ address public owner;
+
+ constructor() {
+ owner = msg.sender;
+ }
+
+}
\ No newline at end of file
diff --git a/packages/hardhat/contracts/YourContract.sol b/packages/hardhat/contracts/YourContract.sol
index 3d364a0..ccd9a12 100644
--- a/packages/hardhat/contracts/YourContract.sol
+++ b/packages/hardhat/contracts/YourContract.sol
@@ -70,7 +70,15 @@ contract YourContract {
// emit: keyword used to trigger an event
emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value);
}
+// Mapping from address to number
+mapping(address => uint) public userNumbers;
+event NumberUpdated(address indexed user, uint number);
+// Function to store a number
+function storeNumber(uint _number) public {
+ userNumbers[msg.sender] = _number;
+ emit NumberUpdated(msg.sender, _number);
+ }
/**
* Function that allows the owner to withdraw all the Ether in the contract
* The function can only be called by the owner of the contract as defined by the isOwner modifier
diff --git a/packages/hardhat/contracts/readme.md b/packages/hardhat/contracts/readme.md
new file mode 100644
index 0000000..93eb659
--- /dev/null
+++ b/packages/hardhat/contracts/readme.md
@@ -0,0 +1,14 @@
+# Games Contracts
+
+1. [Game World Generator](BuyMeACeptor.sol) - A contract that generates a game world based on a user's vibe and number of players. The world is generated with a visual of the planet, scenarios, locations, descriptions, maps, denizens, secrets, goals, and players. Each world has its own blockchain. Creating a World costs 10 gameTokens. when creating a game, i want to have my own world or play with others. each world should be locked to a blockchain. 10 gT to make a world. 5 gT to join one as a GM, 2 gT to join as player.
+
+Inside worlds, there are games
+inside games there are schedules
+inside schedules there are sessions
+(and we verify who shows up)
+
+1. [Character Generator](CeptorCharacterGenerator.sol) - A contract that generates a character for a user in the game world. The character is generated with abilities, class, name, alignment, and background. Each character has its own unique attributes. Creating a Character is only allowed by the owner of the contract. This is a mistake, and will be replaced in upgrade to VRF2.5
+
+2. Is the World Generator deploying a World contract? Yes. Is the World contract tracking all its games, or deploying each game as its own contract which tracks the sessions. Verifiable Truth.
+
+3. NPC Generator - Unlike the PCG which is usable by any Verified Credential having hooty in their hey hey. The NPCG is a contract that generates a non-player character for a user in the game world. The character is generated with abilities, class, name, alignment, hometown, and background. Each character has its own unique attributes. Creating a Character is only allowed by the owner of the contract. VRF2.5 because reusable code choices.
\ No newline at end of file
diff --git a/packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts b/packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts
similarity index 75%
rename from packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts
rename to packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts
index 474d826..fd61c54 100644
--- a/packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts
+++ b/packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts
@@ -3,12 +3,12 @@ import { DeployFunction } from "hardhat-deploy/types";
import { Contract } from "ethers";
/**
- * Deploys a contract named "BuyMeACoffee" using the deployer account and
+ * Deploys a contract named "BuyMeACeptor" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
-const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
+const deployBuyMeACeptor: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
@@ -22,7 +22,7 @@ const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEn
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
- await deploy("BuyMeACoffee", {
+ await deploy("BuyMeACeptor", {
from: deployer,
// Contract constructor arguments
log: true,
@@ -32,12 +32,12 @@ const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEn
});
// Get the deployed contract to interact with it after deploying.
- const buyMeACoffeeContract = await hre.ethers.getContract("BuyMeACoffee", deployer);
- console.log("π Buy this person a coffee!", await buyMeACoffeeContract.owner());
+ const buyMeACeptorContract = await hre.ethers.getContract("BuyMeACeptor", deployer);
+ console.log("π Buy this person a Ceptor!", await buyMeACeptorContract.owner());
};
-export default deployBuyMeACoffee;
+export default deployBuyMeACeptor;
// Tags are useful if you have multiple deploy files and only want to run one of them.
-// e.g. yarn deploy --tags BuyMeACoffee
-deployBuyMeACoffee.tags = ["BuyMeACoffee"];
+// e.g. yarn deploy --tags BuyMeACeptor
+deployBuyMeACeptor.tags = ["BuyMeACeptor"];
diff --git a/packages/hardhat/deploy/03_deploy_world_factory.ts b/packages/hardhat/deploy/03_deploy_world_factory.ts
new file mode 100644
index 0000000..8f4650f
--- /dev/null
+++ b/packages/hardhat/deploy/03_deploy_world_factory.ts
@@ -0,0 +1,43 @@
+import { HardhatRuntimeEnvironment } from "hardhat/types";
+import { DeployFunction } from "hardhat-deploy/types";
+import { Contract } from "ethers";
+
+/**
+ * Deploys a contract named "BuyMeACeptor" using the deployer account and
+ * constructor arguments set to the deployer address
+ *
+ * @param hre HardhatRuntimeEnvironment object.
+ */
+const deployWorldFactory: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
+ /*
+ On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
+
+ When deploying to live networks (e.g `yarn deploy --network goerli`), the deployer account
+ should have sufficient balance to pay for the gas fees for contract creation.
+
+ You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
+ with a random private key in the .env file (then used on hardhat.config.ts)
+ You can run the `yarn account` command to check your balance in every network.
+ */
+ const { deployer } = await hre.getNamedAccounts();
+ const { deploy } = hre.deployments;
+
+ await deploy("WorldFactory", {
+ from: deployer,
+ // Contract constructor arguments
+ log: true,
+ // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
+ // automatically mining the contract deployment transaction. There is no effect on live networks.
+ autoMine: true,
+ });
+
+ // Get the deployed contract to interact with it after deploying.
+ const worldFactoryContract = await hre.ethers.getContract("WorldFactory", deployer);
+ console.log("WorldFactory deployed by:", await worldFactoryContract.owner());
+};
+
+export default deployWorldFactory;
+
+// Tags are useful if you have multiple deploy files and only want to run one of them.
+// e.g. yarn deploy --tags BuyMeACeptor
+deployWorldFactory.tags = ["WorldFactory"];
diff --git a/packages/hardhat/package.json b/packages/hardhat/package.json
index 918b5d0..c5a9a2a 100644
--- a/packages/hardhat/package.json
+++ b/packages/hardhat/package.json
@@ -50,6 +50,7 @@
"typescript": "^5.1.6"
},
"dependencies": {
+ "@chainlink/contracts": "^1.1.0",
"@openzeppelin/contracts": "^4.8.1",
"@typechain/ethers-v6": "^0.5.1",
"dotenv": "^16.0.3",
diff --git a/packages/nextjs/app/games/page.tsx b/packages/nextjs/app/games/page.tsx
new file mode 100644
index 0000000..42ab391
--- /dev/null
+++ b/packages/nextjs/app/games/page.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import type { NextPage } from "next";
+import { useAccount } from "wagmi";
+import CharacterCard from "~~/components/ceptor/CharacterCard";
+import characters from "~~/components/ceptor/CharacterData";
+import { Address } from "~~/components/scaffold-eth";
+
+const Games: NextPage = () => {
+ const { address: connectedAddress } = useAccount();
+
+ return (
+ <>
+