PixeLAW: Autonomous Pixel Playground
PixeLAW is an anutonomous pixel playground, there are two implementations, one is built using Cairo and Dojo on top of Starknet, another is built using Solidity and MUD on top of Redstone. PixeLAW has a core layer so that builders can create their own applications top of it. These apps share states and interact each other.
This book is dedicated to familiarizing you with PixeLAW and its vision to create an autonomous world.
When reading this book, you are on your way to become a Pixel World Builder.
The main contents are as follows.
How do I get involved?
Check out our Github, Twitter, Discord and contribution guide.
Tutorials
Go to Website
Firstly, please access to PixeLAW website. Then, click one of play buttons.
Choose An Application and Play
After click the play button, you can see such a screen.
The icons on the right represent game applications. From top to bottom:
- 2048: Click buttons and slide tiles to create bigger numbers.
- Tic Tac Toe against an AI Agent
- Snake: Specify the direction to move.
- Minesweeper: Specify the grid size and number of mines to play Minesweeper.
- Paint: Paint a pixel.
- Hunter: Basically paint a pixel. But you might see a star if you are luckey.
- Rock-Paper-Scissors: Play RPS game against other users.
Quick Start(For Dojo)
Deploy the Pixel Core locally
Everything in PixeLAW starts with the core pixel layer into which you will deploy your own app.
Let's get started by deploying the core pixel layer, which includes both the contracts and the PixeLAW front-end.
Clone app_template
Please go to app_template. And clone it.
git clone https://github.com/pixelaw/app_template.git app_template
Prerequisites
Download these libraries.
Run your own tests
To make sure everything is working correctly, run the following command to run tests:
sozo test
Deploy the Pixel Core
We are using devcontainer environments. Please open this code with vscode and launch the container with devcontainer.
Wait for the Core to be deployed (Optional)
For convenience, you can run the following script that will print out "Ready for app deployment", once contracts fully initialised:
scarb run ready_for_deployment
After some time (around 1 minute) you should be able to see PixeLAW running on http://localhost:3000. There is a docker-compose file in this repository specifically for running a local image of PixeLAW core. Wait until http://localhost:3000/manifests/core stops returning NOT FOUND.
You should be able to see PixeLAW in its true glory:
If you run into any issues you can check out the github repo, and check out alternatives to deploy the core.
Next Step
Awesome, you just successfully deployed the Pixel Core.
The next step should be for you to build your own PixeLAW App. We will remain in the app_template
repo.
Go and be a Pixel Builder and deploy your own App to the core!
Quick Start(For Dojo)
Deploy the Pixel Core locally
Everything in PixeLAW starts with the core pixel layer into which you will deploy your own app.
Let's get started by deploying the core pixel layer, which includes both the contracts and the PixeLAW front-end.
Clone app_template
Please go to app_template. And clone it.
git clone https://github.com/pixelaw/app_template.git app_template
Prerequisites
Download these libraries.
Run your own tests
To make sure everything is working correctly, run the following command to run tests:
sozo test
Deploy the Pixel Core
We are using devcontainer environments. Please open this code with vscode and launch the container with devcontainer.
Wait for the Core to be deployed (Optional)
For convenience, you can run the following script that will print out "Ready for app deployment", once contracts fully initialised:
scarb run ready_for_deployment
After some time (around 1 minute) you should be able to see PixeLAW running on http://localhost:3000. There is a docker-compose file in this repository specifically for running a local image of PixeLAW core. Wait until http://localhost:3000/manifests/core stops returning NOT FOUND.
You should be able to see PixeLAW in its true glory:
If you run into any issues you can check out the github repo, and check out alternatives to deploy the core.
Next Step
Awesome, you just successfully deployed the Pixel Core.
The next step should be for you to build your own PixeLAW App. We will remain in the app_template
repo.
Go and be a Pixel Builder and deploy your own App to the core!
Build your first Pixel App
REQUIRED: To create your own application in PixeLAW, you must have set up the PixeLAW Core environment locally.
PixeLAW in 15 minutes
Lets check out the folder structure first of the App Template:
app_template/src
contains all your cairo contracts.src/app.cairo
contains your app's core logic.src/lib.cairo
contains a module declaration.src/tests.cairo
contains your tests.
app_template/scripts
contains scripts for deployment.scripts/default_auth.sh
provides necessary authorization and writer permission to your app.scripts/upload_manifest.sh
uploads your manifest.json required by the front-end.scripts/ready_for_deployment.sh
is a helper script used to notify final docker deployment.
app_template/scarb.toml
specifies all dependencies.app_template/docker-compose.yml
required for docker compose
The default App Template includes the contract code of the Paint App which allows users to paint any pixel with any color.
You can either directly deploy it to the Core or adjust the code as you see fit.
Let's dive into the code of the App Template.
Imports
At first we require certain imports from the core to enable our paint app. Depending on your use case you might require different imports. Refer to the Pixel Core, other App examples or talk to us on Discord.
use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait};
use pixelaw::core::models::pixel::{Pixel, PixelUpdate};
use pixelaw::core::utils::{get_core_actions, Direction, Position, DefaultParameters};
use starknet::{get_caller_address, get_contract_address, get_execution_info, ContractAddress};
Interface
In the interface we prototype our functions and expose them publicly. Every PixeLAW App requires the init and interact function. The init functions adds your App to the world registry, whereas the interact function defines the default functionality of your app. The core will by default call the interact function of your app.
The DefaultParameters parameter is a convention we use to input all pixel-related information to the function.
#[starknet::interface]
trait IMyAppActions<TContractState> {
fn init(self: @TContractState);
fn interact(self: @TContractState, default_params: DefaultParameters);
}
Declaring your App
-
The APP_KEY is the unique username of your app, and has to be the same across the entire platform.
-
The APP_ICON will define the icon shown on the front-end to select your app.
-
The APP_MANIFEST simply has to be adjusted according to your APP_KEY.
/// APP_KEY must be unique across the entire platform
const APP_KEY: felt252 = 'myapp';
/// Core only supports unicode icons for now
const APP_ICON: felt252 = 'U+263A';
/// prefixing with BASE means using the server's default manifest.json handler
const APP_MANIFEST: felt252 = 'BASE/manifests/myapp';
App Contract
#[dojo::contract]
///PixeLAW Naming Convention: contracts must be named as such (APP_KEY + underscore + "actions")
mod myapp_actions {
use starknet::{
get_tx_info, get_caller_address, get_contract_address, get_execution_info, ContractAddress
};
use super::IMyAppActions;
use pixelaw::core::models::pixel::{Pixel, PixelUpdate};
use pixelaw::core::models::permissions::{Permission};
use pixelaw::core::actions::{
IActionsDispatcher as ICoreActionsDispatcher,
IActionsDispatcherTrait as ICoreActionsDispatcherTrait
};
use super::{APP_KEY, APP_ICON, APP_MANIFEST};
use pixelaw::core::utils::{get_core_actions, Direction, Position, DefaultParameters};
use debug::PrintTrait;
Below we continue with function implementations.
As mentioned above, the init
and interact
function are required for any PixeLAW App.
Additionally, we provide the permission to another app called Snake to interact with any Pixel occupied by our app.
// impl: implement functions specified in trait
#[external(v0)]
impl ActionsImpl of IMyAppActions<ContractState> {
/// Initialize the MyApp App
fn init(self: @ContractState) {
let world = self.world_dispatcher.read();
let core_actions = pixelaw::core::utils::get_core_actions(world);
//add app to the world registry
core_actions.update_app(APP_KEY, APP_ICON, APP_MANIFEST);
//Grant permission to the snake App
core_actions
.update_permission(
'snake',
Permission {
app: false,
color: true,
owner: false,
text: true,
timestamp: false,
action: false
}
);
}
Now that we get to the interact function, which is called by default by the front end unless otherwise specified.
Most importantly it calls the core_actions.update_pixel
to change the color of a pixel that has been clicked.
/// Put color on a certain position
///
/// # Arguments
///
/// * `position` - Position of the pixel.
/// * `new_color` - Color to set the pixel to.
fn interact(self: @ContractState, default_params: DefaultParameters) {
'put_color'.print();
// Load important variables
let world = self.world_dispatcher.read();
let core_actions = get_core_actions(world);
let position = default_params.position;
let player = core_actions.get_player_address(default_params.for_player);
let system = core_actions.get_system_address(default_params.for_system);
// Load the Pixel
let mut pixel = get!(world, (position.x, position.y), (Pixel));
let COOLDOWN_SECS = 5;
// Check if 5 seconds have passed or if the sender is the owner
assert(
pixel.owner.is_zero() || (pixel.owner) == player || starknet::get_block_timestamp()
- pixel.timestamp < COOLDOWN_SECS,
'Cooldown not over'
);
// We can now update color of the pixel
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x,
y: position.y,
color: Option::Some(default_params.color),
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None // Not using this feature for myapp
}
);
'put_color DONE'.print();
}
}
}
The above specifies the same functionality like our Paint App. Feel free to deploy it locally, make changes and try it out.
Next Steps
The guide above should get you familiar with how the Paint App is structured. The next step would be to:
- Deploy your app to the front-end.
- Check out other tutorials of other PixeLAW Apps.
- Check out the PixeLAW Core.
Deploy your app locally
Test your contract
Before building your smart contracts, you should test them.
sozo test
There might be the following problem due to an issue with Dojo. Just ignore it.
test myapp::tests::tests::test_myapp_actions ... fail (gas usage est.: 1044740) failures: myapp::tests::tests::test_myapp_actions - panicked with [6445855526543742234424738320591137923774065490617582916 ('CLASS_HASH_NOT_DECLARED'), 23583600924385842957889778338389964899652 ('ENTRYPOINT_FAILED'), 23583600924385842957889778338389964899652 ('ENTRYPOINT_FAILED'), ]
Building your contracts
Run the following to compile the cairo contracts, generating the necessary artifacts for deployment.
sozo build
Migrate/Deploy your App
This will deploy your contracts to the local PixeLAW world.
sozo migrate apply --name pixelaw
Initialise your App
This will run scripts/default_auth.sh
and provide necessary authorization and writer permission to your app.
scarb run initialize
Uploading your manifest
This will run scripts/upload_manifest.sh
and upload your manifest.json required by the front-end.
scarb run upload_manifest
For zsh users,
scarb run initialize_zsh
Awesome, you just successfully deployed your own PixeLAW App! If you fail, please read app_template README to see another way to deploy.
Deploying your app to PixeLAW
Deploying to https://demo.pixelaw.xyz/ is almost the same process as developing in your local development. The only difference is that you will require the RPC_URL of the Demo environment. For that, please reach out through Discord.
Build your contracts
sozo build
Deploy your contracts
This will deploy your app to the local PixeLAW using sozo migrate.
sozo migrate apply --name pixelaw --rpc-url <replace-this-with-provided-rpc-url>
Initializing your contracts
This will grant writer permission to your app for any custom models made.
scarb run initialize <replace-this-with-provided-rpc-url>
Uploading your manifest
scarb run upload_manifest http://demo.pixelaw.xyz/manifests/
Quick Start(For MUD)
Deploy the Pixel Core locally
Everything in PixeLAW starts with the core pixel layer into which you will deploy your own app.
Let's get started by deploying the core pixel layer, which includes both the contracts and the PixeLAW front-end.
Prerequisites
- Node.js v18
- git
- Foundry
- pnpm
sudo npm install -g pnpm
Clone repo
Please go to pixelaw_core_mud. And clone it.
git clone https://github.com/themetacat/pixelaw_core pixelaw_core
Build and deploy locally
To make sure everything is working correctly, run the following command to do all things:
cd pixelaw_core && pnpm install
pnpm run start
After some time (around 10 minute) you should be able to see PixeLAW running on http://localhost:3000.
You should be able to see PixeLAW in its true glory:
If you run into any issues you can check out the github repo, and read start.sh to see the build and deploy details.
Next Step
Awesome, you just successfully deployed the Pixel Core.
The next step should be for you to build your own PixeLAW App. We will remain in the app_template_mud
repo.
Go and be a Pixel Builder and deploy your own App to the core!
Build your first Pixel App
REQUIRED: To create your own application in PixeLAW, you must have set up the PixeLAW Core environment locally.
PixeLAW in 15 minutes
Lets check out the folder structure first of the App Template:
contracts/
contains all your solidity contracts and scripts for deploymentsrc/
contains your app's codegen and logic.codegen/
contains your app's store codegen and contracts interface.core_codegen/
contains PixeLAW Core's store codegen and contracts interface.systems/
contains your app's logic contract.
test/
contains your app's test contract.script/
contains your app's extension contract.scripts/
contains scripts for deployment.deploy.sh
deploy your app contract and init your app.upload_json.sh
upload your abi to github.
mud.config.ts
your app's table and namespace config..env
contains the values required to deploy the MUD and PixeLAW core
The default App Template includes the contract code of Paint App which allows users to paint any pixel with any color.
You can either directly deploy it to the Core or adjust the code as you see fit.
Let's dive into the code of the App Template.
Imports
At first we require certain imports from the core to enable our paint app. Depending on your use case you might require different imports. Refer to the PixeLAW Core,
import { System } from "@latticexyz/world/src/System.sol";
import { ICoreSystem } from "../core_codegen/world/ICoreSystem.sol";
import { PermissionsData, DefaultParameters, Position, PixelUpdateData, Pixel, PixelData, TestParameters } from "../core_codegen/index.sol";
Declaring your App
-
The APP_ICON will define the icon shown on the front-end to select your app.
-
The NAMESPACE is the namespace you set in mud.config.ts, which is the namespace after the current app contract is deployed.
-
The SYSTEM_NAME is the system name set for the current contract in systems of mud.config.ts
-
The APP_NAME is the unique username of your app, and has to be the same across the entire platform.
-
The APP_MANIFEST simply has to be adjusted according to your APP_NAME.
// Core only supports unicode icons for now
string constant APP_ICON = 'U+1F58B';
// The NAMESPACE and SYSTEM_NAME of the current contract in mudConfig
string constant NAMESPACE = 'myapp';
string constant SYSTEM_NAME = 'MyAppSystem';
// APP_NAME must be unique across the entire platform
string constant APP_NAME = 'myapp';
// prefixing with BASE means using the server's default abi.json handler, the following is consistent with the current contract name.
string constant APP_MANIFEST = 'BASE/MyAppSystem';
App Contract
The init
and interact
function are required for any PixeLAW App.
Additionally, we provide the permission to another app called Snake to interact with any Pixel occupied by our app.
function init() public {
// init my app
ICoreSystem(_world()).update_app(APP_NAME, APP_ICON, APP_MANIFEST, NAMESPACE, SYSTEM_NAME);
// Grant permission to the snake App
ICoreSystem(_world()).update_permission("snake",
PermissionsData({
app: true, color: true, owner: true, text: true, timestamp: false, action: false
}));
}
Now that we get to the interact function, which is called by default by the front end unless otherwise specified.
Most importantly it calls the ICoreSystem(_world()).update_pixel
to change the color of a pixel that has been clicked.
When calling update_pixel, if you do not want to set a value for one of the parameters or change the original value of the pixel, please do this:
If the parameter type is address, please pass in address(1), If the parameter type is string, please pass in "_Null" This will automatically skip the permission check and assignment of the parameter.
//Put color on a certain position
// Arguments
//`position` - Position of the pixel.
//`new_color` - Color to set the pixel to.
function interact(DefaultParameters memory default_parameters) public {
// Load important variables
Position memory position = default_parameters.position;
address player = default_parameters.for_player;
string memory app = default_parameters.for_app;
// Load the Pixel
PixelData memory pixel = Pixel.get(position.x, position.y);
// TODO: Load MyApp App Settings like the fade steptime
// For example for the Cooldown feature
uint256 COOLDOWN_SECS = 5;
// Check if 5 seconds have passed or if the sender is the owner
require(pixel.owner == address(0) || pixel.owner == player || block.timestamp - pixel.timestamp < COOLDOWN_SECS, 'Cooldown not over');
// We can now update color of the pixel
// If you don't want to assign a value of type address(like owner), you should pass in address(1)
// If you don't want to assign a value of type string(like app、color、text...), you should pass in "_Null"
ICoreSystem(_world()).update_pixel(
PixelUpdateData({
x: position.x,
y: position.y,
color: default_parameters.color,
timestamp: 0,
text: "_Null",
app: app,
owner: player,
action: "_Null"
}));
}
The above specifies the same functionality like our Paint App. Feel free to deploy it locally, make changes and try it out.
Next Steps
The guide above should get you familiar with how the Paint App is structured. The next step would be to:
- Deploy your app to the front-end.
- Check out other tutorials of other PixeLAW Apps.
- Check out the PixeLAW Core.
Deploy your app locally
Building your contracts
Before deploy your smart contracts, you should run the following to compile the solidity contracts.
Make sure you are currently in the pixelaw_app_template_mud/contracts/
pnpm mud build
Deploy/Update your App:
This will deploy your contracts to the local PixeLAW world and init your app.
If the app contract is deployed for the first time:
pnpm run deploy
If you want to update a deployed app:
First comment out the registerNamespace and registerFunctionSelector parts in ./script/MyAppExtension.s.sol:
// world.registerNamespace(namespaceResource);
// world.registerFunctionSelector(systemResource, "init()");
// world.registerFunctionSelector(systemResource, "interact((address,string,(uint32,uint32),string))");
then:
pnpm run deploy INIT=false
Upload your ABI JSON:
pnpm run upload
Awesome, you just successfully deployed your own PixeLAW App! If you fail, please read app_template README to see another way to deploy.
Deploying to Demo
Building your contracts:
pnpm mud build
Deploy/Update your App:
If the app contract is deployed for the first time:
pnpm run deploy RPC_URL=<replace-this-with-provided-rpc-url> CHAIN_ID=<replace-this-with-chain-id>
If you want to update a deployed app:
First comment out the registerNamespace and registerFunctionSelector parts in ./script/MyAppExtension.s.sol:
// world.registerNamespace(namespaceResource);
// world.registerFunctionSelector(systemResource, "init()");
// world.registerFunctionSelector(systemResource, "interact((address,string,(uint32,uint32),string))");
then:
pnpm run deploy INIT=false RPC_URL=<replace-this-with-provided-rpc-url> CHAIN_ID=<replace-this-with-chain-id>
Upload your ABI JSON:
pnpm run upload
Command
pnpm run deploy
if you set RPC_URL, you should set CHAIN_ID
pnpm run deploy
INIT if INIT=false,update the system, default true
RPC_URL, default http://127.0.0.1:8545
CHAIN_ID, default 31337
Get Started
Check out our Github, Twitter, Discord and contribution guide(coming soon).
Please contact us on Discord freely.
Architecture
Here, we describe PixeLAW's architecture.
Overview
We built core layer. Builders can create their own application by interacting with core layer.
Core Layer
To understand core layer, we delve into components and systems for that.
Core Components
Here are the components.
For System Properties
x
: u64y
: u64created_at
: u64updated_at
: u64
For User-changeable Properties
app
: ContractAddresscolor
: u32owner
: ContractAddresstext
: felt252timestamp
: u64action
: felt252
Core Systems
These systems interact with core components.
init
: Initialize the PixeLAW action modelupdate_permissions
: Grant permissions to a system by the callerupdate_app
: Updates the name of an app in the registryhas_write_access
: Check the access for writingshedule_queue
: Shedule the process for queueprocess_queue
: Execute the process in queueupdate_pixel
: Update pixel informationget_player_address
: Get the addressget_system_address
: Get the addressnew_app
: Register an app
Applications on PixeLAW
There are already some applications top of core layer for example.
Paint pixel
By utilizing core systems, we can create our own pixel art game.
Sanke Game
For snake game, the queue system is important.
Queue System
We use queue system to execute stacked processes.
Interoperability
The Core contract facilitates app-to-app communication via the Interoperability Trait:
use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait};
use pixelaw::core::models::pixel::PixelUpdate;
use pixelaw::core::models::registry::App;
use starknet::ContractAddress;
#[starknet::interface]
trait IInteroperability<TContractState> {
fn on_pre_update(
self: @TContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
);
fn on_post_update(
self: @TContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
);
}
These functions are then called during update by the Core contract like so:
fn update_pixel(
self: @ContractState,
for_player: ContractAddress,
for_system: ContractAddress,
pixel_update: PixelUpdate
) {
'update_pixel'.print();
let world = self.world_dispatcher.read();
let mut pixel = get!(world, (pixel_update.x, pixel_update.y), (Pixel));
assert(
self.has_write_access(for_player, for_system, pixel, pixel_update), 'No access!'
);
let old_pixel_app = pixel.app;
old_pixel_app.print();
// pre update is done after checking if an update can be done
if !old_pixel_app.is_zero() {
let interoperable_app = IInteroperabilityDispatcher { contract_address: old_pixel_app };
let app_caller = get!(world, for_system, (App));
interoperable_app.on_pre_update(pixel_update, app_caller, for_player)
}
/// pixel updates are done here
// post updates are called after the updates are done
if !old_pixel_app.is_zero() {
let interoperable_app = IInteroperabilityDispatcher { contract_address: old_pixel_app };
let app_caller = get!(world, for_system, (App));
interoperable_app.on_post_update(pixel_update, app_caller, for_player)
}
'update_pixel DONE'.print();
}
Implementing Interoperability
To use these functions an app just has to follow these steps:
Step 1: Import the trait
Inside the dojo contract, import the interoperability trait
#[dojo::contract]
mod paint_actions {
// put import here
use pixelaw::core::traits::IInteroperability;
Step 2: Implement the trait
Like any trait, just implement it like so:
#[dojo::contract]
mod paint_actions {
// put import here
use pixelaw::core::traits::IInteroperability;
#[external(v0)] // makes sure that this can be called by the Core contract
impl ActionsInteroperability of IInteroperability<ContractState> {
fn on_pre_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
) {
// put pre_update_code here
}
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
// put post_update_code here
}
}
Examples
Snake passing through a Paint Pixel
When Snake is done passing through a pixel, it reverts the pixel back to its original pixel state. To demonstrate interoperability, when a Snake reverts back a Paint Pixel, it gets the Paint pixel to use fade on it.
Snake first imports the trait
#[dojo::contract]
mod snake_actions {
use pixelaw::core::traits::IInteroperability;
Snake implements the trait
#[external(v0)]
impl ActionsInteroperability of IInteroperability<ContractState> {
fn on_pre_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
) {
// do nothing
}
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
// put in code here
}
}
Snake first determines that the on_post_update is being called by the core contract:
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
let core_actions = get_core_actions(self.world_dispatcher.read());
let core_actions_address = get_core_actions_address(self.world_dispatcher.read());
assert(core_actions_address == get_caller_address(), 'caller is not core_actions');
}
Next it makes sure that this is indeed a reversal of pixel state, and it's been called by the Snake Contract:
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
let core_actions = get_core_actions(self.world_dispatcher.read());
let core_actions_address = get_core_actions_address(self.world_dispatcher.read());
assert(core_actions_address == get_caller_address(), 'caller is not core_actions');
// checks if this is a pixel state reversal called by the Snake Contract
if pixel_update.app.is_some() && app_caller.system == get_contract_address() {
}
}
Then, Snake needs to check if it's reverting back to a Paint pixel
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
let core_actions = get_core_actions(self.world_dispatcher.read());
let core_actions_address = get_core_actions_address(self.world_dispatcher.read());
assert(core_actions_address == get_caller_address(), 'caller is not core_actions');
// checks if this is a pixel state reversal called by the Snake Contract
if pixel_update.app.is_some() && app_caller.system == get_contract_address() {
let old_app = pixel_update.app.unwrap();
let world = self.world_dispatcher.read();
let old_app = get!(world, old_app, (App));
// is this reverting back to paint
if old_app.name == 'paint' {
}
}
}
Lastly, it calls the paint contract to let it fade
fn on_post_update(
self: @ContractState,
pixel_update: PixelUpdate,
app_caller: App,
player_caller: ContractAddress
){
let core_actions = get_core_actions(self.world_dispatcher.read());
let core_actions_address = get_core_actions_address(self.world_dispatcher.read());
assert(core_actions_address == get_caller_address(), 'caller is not core_actions');
// checks if this is a pixel state reversal called by the Snake Contract
if pixel_update.app.is_some() && app_caller.system == get_contract_address() {
let old_app = pixel_update.app.unwrap();
let world = self.world_dispatcher.read();
let old_app = get!(world, old_app, (App));
// is this reverting back to paint
if old_app.name == 'paint' {
// creating fade params
let mut calldata: Array<felt252> = ArrayTrait::new();
let pixel = get!(world, (pixel_update.x, pixel_update.y), (Pixel));
calldata.append(pixel.owner.into());
calldata.append(old_app.system.into());
calldata.append(pixel_update.x.into());
calldata.append(pixel_update.y.into());
calldata.append(pixel_update.color.unwrap().into());
// 0x89ce6748d77414b79f2312bb20f6e67d3aa4a9430933a0f461fedc92983084 is fade as a selector
starknet::call_contract_syscall(old_app.system, 0x89ce6748d77414b79f2312bb20f6e67d3aa4a9430933a0f461fedc92983084, calldata.span());
}
}
}
Tutorial
Here we will have a growing collection of PixeLAW Apps that you can check out as use as use cases for your own.
Remember to firstly go through the Quickstart to deploy the core and get familiar with how to build your own PixeLAW App.
Tutorial:
- Minesweeper: You can learn how to build your game by building minesweeper.
- Check out other App Examples in our repo.
Functions
We need following functions:
- init: initialize application settings.
- interact: interact with PixeLAW field. When we start game, we call this function to set up field.
- reveal: reveal the pixel if bomb or not.
- explode: if a player reveals a bomb, this function is called.
- ownerless_space:
We can write interface like this:
#[starknet::interface]
trait IMinesweeperActions<TContractState> {
fn init(self: @TContractState);
fn interact(self: @TContractState, default_params: DefaultParameters, size: u64, mines_amount: u64);
fn reveal(self: @TContractState, default_params: DefaultParameters);
fn explode(self: @TContractState, default_params: DefaultParameters);
fn ownerless_space(self: @TContractState, default_params: DefaultParameters, size: u64) -> bool;
}
Set constants
We set some constants. We added GAME_MAX_DURATION
to avoid permanent sessions.
/// APP_KEY must be unique across the entire platform
const APP_KEY: felt252 = 'minesweeper';
/// Core only supports unicode icons for now
const APP_ICON: felt252 = 'U+1F4A3'; // bomb
/// prefixing with BASE means using the server's default manifest.json handler
const APP_MANIFEST: felt252 = 'BASE/manifests/minesweeper';
/// The maximum duration of a game in milliseconds
const GAME_MAX_DURATION: u64 = 20000;
We used this to search the unicode icon.
Create enum and struct
We use State and Game. State represents statement of pixel in minesweeper, and Game struct handle the components for this game.
#[derive(Serde, Copy, Drop, PartialEq, Introspect)]
enum State {
None: (),
Open: (),
Finished: ()
}
#[derive(Model, Copy, Drop, Serde, SerdeLen)]
struct Game {
#[key]
x: u64,
#[key]
y: u64,
id: u32,
creator: ContractAddress,
state: State,
size: u64,
mines_amount: u64,
started_timestamp: u64
}
Import them
Don't forget to import them.
use super::{Game, State};
use super::{APP_KEY, APP_ICON, APP_MANIFEST, GAME_MAX_DURATION};
Set up functions
In ActionsImpl
, please declare functions.
#[dojo::contract]
mod minesweeper_actions {
/// ...
use poseidon::poseidon_hash_span;
#[derive(Drop, starknet::Event)]
struct GameOpened {
game_id: u32,
creator: ContractAddress
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
GameOpened: GameOpened
}
/// ...
#[external(v0)]
impl ActionsImpl of IMinesweeperActions<ContractState> {
fn init(self: @TContractState) {}
fn interact(self: @TContractState, default_params: DefaultParameters, size: u64, mines_amount: u64) {}
fn reveal(self: @TContractState, default_params: DefaultParameters) {}
fn explode(self: @TContractState, default_params: DefaultParameters) {}
fn ownerless_space(self: @TContractState, default_params: DefaultParameters, size: u64) -> bool {}
}
}
Write fn init()
Same as default.
fn init(self: @ContractState) {
let world = self.world_dispatcher.read();
let core_actions = pixelaw::core::utils::get_core_actions(world);
core_actions.update_app(APP_KEY, APP_ICON, APP_MANIFEST);
// TODO: replace this with proper granting of permission
core_actions.update_permission('snake',
Permission {
alert: false,
app: false,
color: true,
owner: false,
text: true,
timestamp: false,
action: false
});
core_actions.update_permission('paint',
Permission {
alert: false,
app: false,
color: true,
owner: false,
text: true,
timestamp: false,
action: false
});
}
Write a draft of fn interact()
Initially, load some information
let caller_address = get_caller_address();
let mut game = get!(world, (position.x, position.y), (Game));
let timestamp = starknet::get_block_timestamp();
We call this interact()
function when we click pixels. So, we use pixel.alert
to call functions.
if pixel.alert = 'reveal' {
// call reveal function
self.reveal(default_params);
} else if pixel.alert = 'explode' {
// call explode function
self.explode(default_params);
} else if self.ownerless_space(default_params, size: size) == true {
// declare a new game
} else {
// we can't do anything, so we just return
'find a free area'.print();
}
Implement function when we start game
If self.ownerless_space()
returns true, we can start new game. Let's start new game.
let mut id = world.uuid();
// set game information
game =
Game {
x: position.x,
y: position.y,
id,
creator: player,
state: State::Open,
size: size,
mines_amount: mines_amount,
started_timestamp: timestamp
};
emit!(world, GameOpened {game_id: game.id, creator: player});
set!(world, (game));
let mut i: u64 = 0;
let mut j: u64 = 0;
// update pixels to set color and alert. Then, if player click the pixel, they call reveal function.
loop {
if i >= size {
break;
}
j = 0;
loop {
if j >= size {
break;
}
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x + j,
y: position.y + i,
color: Option::Some(default_params.color), //should I pass in a color to define the minesweepers field color?
alert: Option::Some('reveal'),
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None,
}
);
j += 1;
};
i += 1;
};
Then, set mines.
let mut num_mines = 0;
loop {
if num_mines >= mines_amount {
break;
}
let timestamp_felt252 = timestamp.into();
let x_felt252 = position.x.into();
let y_felt252 = position.y.into();
let m_felt252 = num_mines.into();
//random = (timestamp + i) + position.x.into() + position.y.into();
let hash: u256 = poseidon_hash_span(array![timestamp_felt252, x_felt252, y_felt252, m_felt252].span()).into();
random_number = hash % (size * size).into();
core_actions
.update_pixel(
player,
system,
PixelUpdate {
//x: (position.x + random_x),
x: position.x + (random_number / size.into()).try_into().unwrap(),
//y: (position.y + random_y),
y: position.y + (random_number % size.into()).try_into().unwrap(),
color: Option::Some(default_params.color),
alert: Option::Some('explode'),
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None,
}
);
num_mines += 1;
};
Whole code in minesweeper_actions
is like this
#[dojo::contract]
/// contracts must be named as such (APP_KEY + underscore + "actions")
mod minesweeper_actions {
use starknet::{
get_tx_info, get_caller_address, get_contract_address, get_execution_info, ContractAddress
};
use super::IMinesweeperActions;
use pixelaw::core::models::pixel::{Pixel, PixelUpdate};
use pixelaw::core::models::permissions::{Permission};
use pixelaw::core::actions::{
IActionsDispatcher as ICoreActionsDispatcher,
IActionsDispatcherTrait as ICoreActionsDispatcherTrait
};
use super::{Game, State};
use super::{APP_KEY, APP_ICON, APP_MANIFEST, GAME_MAX_DURATION};
use pixelaw::core::utils::{get_core_actions, Direction, Position, DefaultParameters};
use debug::PrintTrait;
use poseidon::poseidon_hash_span;
#[derive(Drop, starknet::Event)]
struct GameOpened {
game_id: u32,
creator: ContractAddress
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
GameOpened: GameOpened
}
fn subu8(nr: u8, sub: u8) -> u8 {
if nr >= sub {
return nr - sub;
} else {
return 0;
}
}
// ARGB
// 0xFF FF FF FF
// empty: 0x 00 00 00 00
// normal color: 0x FF FF FF FF
fn encode_color(r: u8, g: u8, b: u8) -> u32 {
(r.into() * 0x10000) + (g.into() * 0x100) + b.into()
}
fn decode_color(color: u32) -> (u8, u8, u8) {
let r = (color / 0x10000);
let g = (color / 0x100) & 0xff;
let b = color & 0xff;
(r.try_into().unwrap(), g.try_into().unwrap(), b.try_into().unwrap())
}
// impl: implement functions specified in trait
#[external(v0)]
impl ActionsImpl of IMinesweeperActions<ContractState> {
/// Initialize the MyApp App (TODO I think, do we need this??)
fn init(self: @ContractState) {
let world = self.world_dispatcher.read();
let core_actions = pixelaw::core::utils::get_core_actions(world);
core_actions.update_app(APP_KEY, APP_ICON, APP_MANIFEST);
// TODO: replace this with proper granting of permission
core_actions.update_permission('snake',
Permission {
alert: false,
app: false,
color: true,
owner: false,
text: true,
timestamp: false,
action: false
});
core_actions.update_permission('paint',
Permission {
alert: false,
app: false,
color: true,
owner: false,
text: true,
timestamp: false,
action: false
});
}
/// Put color on a certain position
///
/// # Arguments
///
/// default_params: Default parameters for the action
/// size: Size of the board
/// mines_amount: Amount of mines to place
fn interact(self: @ContractState, default_params: DefaultParameters, size: u64, mines_amount: u64) {
'put_color'.print();
// Load important variables
let world = self.world_dispatcher.read();
let core_actions = get_core_actions(world);
let position = default_params.position;
let player = core_actions.get_player_address(default_params.for_player);
let system = core_actions.get_system_address(default_params.for_system);
// Load the Pixel
let mut pixel = get!(world, (position.x, position.y), (Pixel));
let caller_address = get_caller_address();
let mut game = get!(world, (position.x, position.y), (Game));
let timestamp = starknet::get_block_timestamp();
if (pixel.alert == 'reveal') {
// call reveal function
self.reveal(default_params);
} else if (pixel.alert == 'explode') {
// call explode function
self.explode(default_params);
} else if (self.ownerless_space(default_params, size: size) == true ){
// start a new game
let mut id = world.uuid();
game =
Game {
x: position.x,
y: position.y,
id,
creator: player,
state: State::Open,
size: size,
mines_amount: mines_amount,
started_timestamp: timestamp
};
emit!(world, GameOpened {game_id: game.id, creator: player});
set!(world, (game));
let mut i: u64 = 0;
let mut j: u64 = 0;
loop {
if i >= size {
break;
}
j = 0;
loop {
if j >= size {
break;
}
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x + j,
y: position.y + i,
color: Option::Some(default_params.color), //should I pass in a color to define the minesweepers field color?
alert: Option::Some('reveal'),
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None,
}
);
j += 1;
};
i += 1;
};
let mut random_number: u256 = 0;
let mut num_mines = 0;
loop {
if num_mines >= mines_amount {
break;
}
let timestamp_felt252 = timestamp.into();
let x_felt252 = position.x.into();
let y_felt252 = position.y.into();
let m_felt252 = num_mines.into();
let hash: u256 = poseidon_hash_span(array![timestamp_felt252, x_felt252, y_felt252, m_felt252].span()).into();
random_number = hash % (size * size).into();
core_actions
.update_pixel(
player,
system,
PixelUpdate {
//x: (position.x + random_x),
x: position.x + (random_number / size.into()).try_into().unwrap(),
//y: (position.y + random_y),
y: position.y + (random_number % size.into()).try_into().unwrap(),
color: Option::Some(default_params.color),
alert: Option::Some('explode'),
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None,
}
);
num_mines += 1;
};
} else {
// we can't do anything, so we just return
'find a free area'.print();
}
assert(
pixel.owner.is_zero() || (pixel.owner) == player || starknet::get_block_timestamp()
- pixel.timestamp < COOLDOWN_SECS,
'Cooldown not over'
);
// We can now update color of the pixel
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x,
y: position.y,
color: Option::Some(default_params.color),
alert: Option::None,
timestamp: Option::None,
text: Option::None,
app: Option::Some(system),
owner: Option::Some(player),
action: Option::None // Not using this feature for myapp
}
);
'put_color DONE'.print();
}
/// Reveal a pixel on a certain position
///
/// # Arguments
/// default_params: Default parameters for the action
fn reveal(self: @ContractState, default_params: DefaultParameters) {
}
/// Explode a pixel on a certain position
///
/// # Arguments
/// default_params: Default parameters for the action
fn explode(self: @ContractState, default_params: DefaultParameters) {
}
/// Check if a certain position is ownerless
///
/// # Arguments
/// default_params: Default parameters for the action
/// size: Size of the board
fn ownerless_space(self: @ContractState, default_params: DefaultParameters, size: u64) -> bool {
return true; // for debug
}
}
}
Test so far
Please do not foget adding grant_writer:
world.grant_writer('Game',minesweeper_actions_address);
Check your codes by this command:
sozo test
Implement reveal()
/// Reveal a pixel on a certain position
///
/// # Arguments
/// default_params: Default parameters for the action
fn reveal(self: @ContractState, default_params: DefaultParameters) {
let world = self.world_dispatcher.read();
let core_actions = get_core_actions(world);
let position = default_params.position;
let player = core_actions.get_player_address(default_params.for_player);
let system = core_actions.get_system_address(default_params.for_system);
let mut pixel = get!(world, (position.x, position.y), (Pixel));
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x,
y: position.y,
color: Option::Some(default_params.color),
alert: Option::None,
timestamp: Option::None,
text: Option::Some('U+1F30E'),
app: Option::None,
owner: Option::None,
action: Option::None,
}
);
}
Implement explode()
/// Explode a pixel on a certain position
///
/// # Arguments
/// default_params: Default parameters for the action
fn explode(self: @ContractState, default_params: DefaultParameters) {
let world = self.world_dispatcher.read();
let core_actions = get_core_actions(world);
let position = default_params.position;
let player = core_actions.get_player_address(default_params.for_player);
let system = core_actions.get_system_address(default_params.for_system);
let mut pixel = get!(world, (position.x, position.y), (Pixel));
core_actions
.update_pixel(
player,
system,
PixelUpdate {
x: position.x,
y: position.y,
color: Option::Some(default_params.color),
alert: Option::None,
timestamp: Option::None,
text: Option::Some('U+1F4A3'),
app: Option::None,
owner: Option::None,
action: Option::None,
}
);
}
Implement owner_less()
fn ownerless_space(self: @ContractState, default_params: DefaultParameters, size: u64) -> bool {
let world = self.world_dispatcher.read();
let core_actions = get_core_actions(world);
let position = default_params.position;
let mut pixel = get!(world, (position.x, position.y), (Pixel));
let mut i: u64 = 0;
let mut j: u64 = 0;
let mut check_test: bool = true;
let check = loop {
if !(pixel.owner.is_zero() && i <= size)
{
break false;
}
pixel = get!(world, (position.x, (position.y + i)), (Pixel));
j = 0;
loop {
if !(pixel.owner.is_zero() && j <= size)
{
break false;
}
pixel = get!(world, ((position.x + j), position.y), (Pixel));
j += 1;
};
i += 1;
break true;
};
check
}
Test
Test code is gonna be like this:
fn test_create_minefield() {
// Deploy everything
let (world, core_actions, minesweeper_actions) = deploy_world();
core_actions.init();
minesweeper_actions.init();
// Impersonate player
let player = starknet::contract_address_const::<0x1337>();
starknet::testing::set_account_contract_address(player);
//computer variables
let size: u64 = 5;
let mines_amount: u64 = 10;
// Player creates minefield
minesweeper_actions
.interact(
DefaultParameters {
for_player: Zeroable::zero(),
for_system: Zeroable::zero(),
position: Position { x: 1, y: 1 },
color: 0
},
size,
mines_amount
);
}
Please check your implementation so far
sozo test
Deploy
After this, please deploy by following this