PixeLAW


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.

website

Choose An Application and Play

After click the play button, you can see such a screen. Screenshot

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.

  • Dojo - install here
  • Scarb - install here
  • Docker - install here
  • Docker compose plugin - install here

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: PixelCore

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.

  • Dojo - install here
  • Scarb - install here
  • Docker - install here
  • Docker compose plugin - install here

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: PixelCore

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

  1. The APP_KEY is the unique username of your app, and has to be the same across the entire platform.

  2. The APP_ICON will define the icon shown on the front-end to select your app.

  3. 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 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

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: PixelCore

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 deployment
    • src/ 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

  1. The APP_ICON will define the icon shown on the front-end to select your app.

  2. The NAMESPACE is the namespace you set in mud.config.ts, which is the namespace after the current app contract is deployed.

  3. The SYSTEM_NAME is the system name set for the current contract in systems of mud.config.ts

  4. The APP_NAME is the unique username of your app, and has to be the same across the entire platform.

  5. 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 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. overview

Core Layer

To understand core layer, we delve into components and systems for that.

Core Components

Here are the components.

For System Properties

  • x: u64
  • y: u64
  • created_at: u64
  • updated_at: u64

For User-changeable Properties

  • app: ContractAddress
  • color: u32
  • owner: ContractAddress
  • text: felt252
  • timestamp: u64
  • action: felt252

Components

Core Systems

These systems interact with core components.

  • init: Initialize the PixeLAW action model
  • update_permissions: Grant permissions to a system by the caller
  • update_app: Updates the name of an app in the registry
  • has_write_access: Check the access for writing
  • shedule_queue: Shedule the process for queue
  • process_queue: Execute the process in queue
  • update_pixel: Update pixel information
  • get_player_address: Get the address
  • get_system_address: Get the address
  • new_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. Paint

Sanke Game

For snake game, the queue system is important. Snake

Queue System

We use queue system to execute stacked processes. Queue

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