Dojo

Dojo 是一个开源项目,目前处于早期开发阶段,并热忱的欢迎贡献者们。更多相关资源,请在 Discord上加入我们的社区,并查看我们的Github


Dojo:可证明游戏引擎

Dojo 采用 Cairo语言,为设计自主世界和链上游戏提供了强大的架构和工具集。它拥有一个集成的实体组件系统(ECS),包括一个本地索引器、RPC 测试网和一个全面的 CLI 管理工具包。

本指南旨在让您熟悉 Dojo 引擎和 可证明(Provable) 游戏的变革潜力。 理论专节阐明了自主世界和 可证明游戏这一新兴概念。

说明者

以下是CartridgeTarrence 在 2023 年Autonomous Anonymous Summit 上说明 Dojo 如何工作的视频:

组织结构

Dojo 是一项开源计划,采用 MIT 许可,致力于推广和推进自主世界 (AW) 概念。它由CartridgeRealms & BibliothecaDAObriq和更多贡献者们牵头。

中文版由Starknet AstroAW Research共同翻译。

我该如何参与?

查看我们的 GithubTwitterDiscord贡献指南

Dojo 是什么?

Dojo 是在游戏行业的新兴领域--链上游戏 的尝试中吸取的经验教训的结晶。任何尝试过开发链上游戏的开发者都会明白其中固有的工程障碍--这种认识促使我们创建了 Dojo。正如您不会在每次开发新游戏时都重新创建 Unity 一样,同样的原则也适用于此。Dojo 旨在处理复杂的基础架构,让开发人员能够专注于其游戏的独特之处。

Dojo 立志成为构建可证明游戏的首选工具。它完全开源,欢迎所有形式的贡献。


停止做基础设施,开始做游戏

Dojo 的工具套件消除了构建链上游戏的基础设施复杂性。它包括:

实体组件系统(ECS)

Dojo 提供了一种在智能合约上构建游戏的标准化方法。Dojo 认识到游戏设计的复杂性,简化了开发流程,使创作者能够专注于游戏逻辑。这种标准化为世界互联网络铺平了道路,让开发人员的专业知识能够有效的发挥,促进了游戏整合。

Dojo 采用 ECS(实体组件系统)作为架构模式,以有效管理和组织自主世界(AW)的状态和行为。在这种模式中,计算被定义为在一组实体上运行的系统列表,每个实体都由一组动态的纯数据组件组成。各系统通过对实体组件进行持久、高效的查询,选择要处理的实体。

阅读有关 Dojo ECS 的详细信息。

Torii - Starknet索引器

构建链上游戏通常需要解决链上状态索引的难题。不过,Dojo 将合约状态标准化,以反映传统的关系数据库。通过这种设置,Torii 索引器可以自动索引所有合约状态,确保高效、精简的查询。然后,Torii 通过 GraphQL API 或 gRPC(即将推出)公开这些状态,允许开发人员轻松查询和检索数据。

使用 Torii 大大减少了构建链上游戏所需的时间和精力。它还消除了手动创建索引器的需要,而手动创建索引器可能是一个乏味且容易出错的过程。

Katana - 如闪电般快速的开发网

Katana 是一个可定制的 StarkNet 开发网。它速度极快,可让您迅速迭代游戏逻辑。

Sozo CLI - CLI 管理工具

一些Dojo 世界有望成为链上最为庞大的那些合约。Sozo 是一款 CLI 工具,可帮助您管理您的世界。通过它,您可以创建、构建、测试和部署您的世界。此外,您还可以制作新的组件和系统,并将它们注册到您的世界中。

Dojo 不提供的东西

  1. 可视化图形 - Dojo 提供网络访问和合约,但不提供图形引擎。您可以自行选择图形引擎!将您的 Dojo 世界与虚幻、Godot 或 Unity 整合。

理解 Dojo的工作流程:一份可视化指南

为了帮助您了解 Sozo 的工作原理,我们制作了一份可视化指南,概述了使用功能强大的 Sozo 工具和 Katana 开发网的执行流程。

这种可视化表示法将帮助您掌握使用 Dojo 的基本步骤,指导您完成创建和管理链上游戏的过程。

Dojo Sozo 工作流程

自主世界

"自主世界代表着持久、无许可和分散的开放环境,用户可以自由地与之互动并做出贡献"。

自主世界(AW)的确切定义仍然有些难以捉摸,因为它更像是一个抽象概念,尚未完全具体化。Lattice 在 2022 年首次介绍 了这一术语,但在区块链上运行的开放世界这一概念已经存在了一段时间。MUD 引入的抽象概念是让市场认识到这些世界潜力的催化剂。

自主世界在基本性质上与区块链有显著的相似之处。一旦建立,它们就会持续存在,在链的整个生命周期内保持状态。玩家可以加入或退出,开发者可以部署功能来扩展这些世界--这一切都无需权限,就像将合约添加到链上一样。虽然自主世界并没有公认的定义,但我们认为游戏必须至少具备以下两个基本特征才能被视为自主世界:

  1. 分散数据可用层:虽然状态执行可能位于中心化层,但如果执行层不复存在,状态能否重建至关重要。Rollup提供了一种解决方案,在确保数据永久沉淀在以太坊上的同时,提供了容量更大的执行层。这保证了世界的永久持久性。

  2. 用于扩展世界的无需权限入口:世界合约必须能够接受新的系统和组件,而无需获得许可。虽然这并不意味着每个组件和系统都会被利用,但它们必须遵守这一模式,确保开放和不受限制地访问潜在的增强功能。

我们坚信,自主世界有潜力在 zk 证明和区块链技术提供的媒介中催化对新形式的探索。这不仅关系到游戏,还关系到新形式的艺术作品、协调、乐趣,它们都来自修补和激进创新,最终在这个勇敢的去中心化和无信任的新世界中对 "游玩" 的概念提出质疑。

阅读作业

可证明的游戏

可证明博弈需要零知识特性,以便高效地扩展和验证计算。Cairo通过提供一种通用语言来满足这一需求,消除了需要创建电路以纳入SNARKs的复杂性。

您只需用Cairo语言编程,您的应用程序就自动成为可证明的

此外,您还可以在Cairo虚拟机 (CVM)上部署您的程序,该虚拟机与Starknet Layer2、Starknet应用链兼容,甚至可以通过 WebAssembly (WASM) 在浏览器中部署!Dojo 的目标是为您的游戏开发提供直接的 ZK 基元。

有关Starknet、Cairo及其技术栈的更多信息,请查阅 Starknet & Cairo book

Cairo

Cairo 是由 Starkware 开发的一种开源、图灵完备的智能合约语言,旨在为有效性卷积 Starknet提供支持。该语言可实现高表达性和可验证的计算,非常适合构建可扩展的安全应用,包括去中心化金融(DeFi)项目。

Dojo 以 Cairo 为基础,为开发自主世界(AW)创建了一个强大的框架。通过利用 Cairo 的功能,Dojo 旨在简化开发流程,提高可维护性,并增强自主世界的性能。

Dojo 框架的一个主要特点是使用 命令。命令是一种设计模式,有助于减少模板代码,使应用程序更简洁、更易维护。它们通过将特定的操作或运行封装在自足的、可重用的单元中来实现这一目标。

开发人员可以在系统中自由编写命令,而 Cairo 编译器则负责内嵌相应的函数。

必读

作为 L2的Starknet

Starknet 是一个有效性卷积第二层(L2)解决方案,旨在扩展以太坊。它的运行方式是提供高交易吞吐量和低气体成本,同时保持与以太坊第一层(L1)相同的安全级别。它采用的策略类似于解决数独难题:验证一个解决方案比从头开始寻找解决方案更容易。同样,Starknet 通过使用链外计算的 STARK 证明,以更便宜的 L1 验证取代了繁重而昂贵的 L1 计算。

用更专业的术语来说,Starknet 是一种无权限的 Validity-Rollup(也称为 "ZK-Rollup"),支持一般计算,目前作为以太坊上的二级网络运行。该网络的一级安全性由其使用的 STARK 加密证明系统提供保障,该系统被认为是最安全、最可扩展的系统之一。

作为应用链的Starknet

Cairo语言是一种同构的通用语言,针对零知识(ZK)证明进行了优化。它是 Starknet、Starkex 和 应用链 背后的驱动力。值得注意的是,你还可以在 WebAssembly (WASM) 中运行它,在客户端生成证明!Dojo 团队正在与 Madara 团队紧密合作,使 Starknet 应用链能够无缝运行 Dojo 世界。

常见问题

谁拥有Dojo?

Dojo 严格遵循 Apache 2.0 许可协议,是开源软件。任何人都可以免费使用 Dojo,任何人都可以为项目做出贡献。

为何选择Dojo?

Dojo 的创建旨在解决创始人在构建链上游戏时遇到的问题。它规范了构建此类游戏的流程,并提供了一整套工具,使其变得更加容易。

Dojo 路线图是怎样的?

Dojo 正在快速发展。您可以在 Dojo Github 上找到open状态的issue,也可以加入 Discord 参与其中。如果您对项目有想法,请打开一个issue。

什么是链上游戏?

链上游戏是完全存在于公共区块链网络上的游戏;所有状态和逻辑都在链上。客户端(如网络浏览器)并不存在于链上,而是纯粹为了与链上状态进行交互和解释而存在。

什么是自主世界?

自主世界是一个完全在链上存在的世界。它不受任何单一实体的控制,而是由该世界中设定的规则所支配。这篇文章深入探讨了这一主题:Autonomous Worlds

Cairo是什么?

Cairo 是由 Starkware 发明的一种开源编程语言。它是一种图灵完备的语言,用于通用计算。它是一种低级语言,旨在编译到Cairo虚拟机。点击此处了解更多信息:Cairo

什么是可证明的游戏?

由于零知识证明的神奇作用,我们可以通过验证链外创建的 zk 证明来确保游戏的公平性。但这意味着什么呢?请看一盘国际象棋。我们的目标是让棋手们彼此信任对方的棋步。如果是在区块链环境中,考虑到国际象棋的简单规则,用一种直接的方法,每一步棋都是在区块链上进行的交易。这样做成本很高。我们只想知道胜负,而不是每一步棋。

通过 zk 证明和客户端通信,玩家可以建立状态通道,在链外共享移动并确保其有效性。最后,可以向区块链提交 zk 证明,以确认游戏的公平性。这就构成了一个可证明的游戏。

快速启动

在深入研究代码之前,这里有值得你阅读一下的 理论 用以来熟悉自主世界(AW)的概念,以及Cairo生态系统

安装 Dojoup

Dojo 由 Katana、Torii 和 Sozo 三种开发工具组成。使用 Dojoup 可以轻松安装它们。有关 Dojoup 的详细信息,请点击 此处

curl -L https://install.dojoengine.org | bash

这将安装 Dojoup,然后只需按照屏幕上的说明操作、 即可在 CLI 中使用 dojoup 命令。

dojoup

有关 dojoup 的完整参考和调试,请参阅 Dojoup

下一步

前往 Hello Dojo ,创建你的第一个 Dojo 世界。

从源码构建

如果您只是想试试工具链,我们强烈建议您参考 快速入门 指南。

先决条件

您需要 Rust 编译器和 Rust 软件包管理器 Cargo。 安装这两种工具的最简单方法是使用 rustup.rs

在 Windows 系统中,您还需要已经安装了 "使用 C++ 进行桌面开发 "工作负载选项的、 最新版本的 Visual Studio

构建

您可以使用不同的 [Dojoup](#using-dojoup)标记:

dojoup --branch master
dojoup --path path/to/dojo

或者,使用一条Cargo命令:

cargo install --git https://github.com/dojoengine/dojo --force sozo katana torii

或者从本地的 Dojo repository 拷贝中手动构建:

# clone the repository
git clone https://github.com/dojoengine/dojo.git
cd dojo
# install Sozo
cargo install --path ./crates/sozo --force
# install Katana
cargo install --path ./crates/katana --force
# install Torii
cargo install --path ./crates/torii --force

开发设置

本文是为 Dojo 设置开发环境的指南。如果您只是想试试工具链,则不建议遵循本指南。我们强烈建议您阅读 快速入门指南。

先决条件

  • Rust
  • Cairo

指引

Clone

git clone https://github.com/dojoengine/dojo.git

Linux & Mac

1. 安装 Rust 和依赖

首先安装 Rust 并运行测试套件来确认你的安装是否成功:

rustup override set stable && rustup update && cargo test

注意:根据您的 Linux 发行版,您可能需要安装其他依赖项。请确保你安装了在设置过程中出现的任何建议的或缺失的依赖项。

2. 安装 Scarb 软件包管理器

接下来,通过运行如下命令来进行 Scarb 软件包管理器的安装:

curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh

3. 添加 Cairo 1.0 VSCode 扩展

安装 Visual Studio Code 的 Cairo 1.0 扩展。

Windows

即将推出

容器

即将推出

为核心做出贡献

Dojo 是一个开源项目,目前处于早期开发阶段并热忱的欢迎贡献者们。

如何贡献

请前往 Github 查看open状态的的issue,如果你看到一个未分配的issue,请在评论中申请将它分配给你。如果你有关于新功能的想法,请创建一个带有 enhancement 标签的issue。

开始加入社区

Hello Dojo

本节假定您已安装 Dojo 工具链并熟悉 Cairo。如果没有,请参阅 快速入门部分。

十五分钟进道场(dojo)

你可以将 Dojo 视为 Cairo 的抽象,类似于 React 对 JavaScript 的抽象。它能让您编写速记命令,并在编译时扩展为复杂的查询。Dojo 基于著名的实体组件系统(Entity Component System,ECS)架构。

在 Dojo 中,您可以使用系统和组件来设计您的世界。系统概括了世界的逻辑,而组件则表示状态。这种强大的模式允许你以高度模块化的方式构建逻辑。如果你还不明白,不要着急,我们将在下文中详细介绍。

首先,让我们建立一个在本地运行的项目。在一个空目录下执行:

sozo init

恭喜您您现在有了一个本地 Dojo 项目。该命令会在您的当前目录下创建一个 dojo-starter 项目。它是新项目的理想起点,为您提供了开始项目所需的一切。

Dojo项目剖析

检查 dojo-starter 项目的内容,你会发现其结构如下(不包括非Cairo代码文件):

src
  - components.cairo
  - systems.cairo
  - lib.cairo
Scarb.toml

Dojo 项目在很大程度上类似于标准的 Cairo 项目,区别在于创建 组件系统 时使用的一些特殊属性标记。让我们接下来探讨一下。

打开src/components.cairo文件以继续。

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Moves {
    #[key]
    player: ContractAddress,
    remaining: u8,
}

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Position {
    #[key]
    player: ContractAddress,
    x: u32,
    y: u32
}

...rest of code

请注意 #[derive(Component, Copy, Drop, Serde, SerdeLen)]属性。要识别组件,我们必须包含 Component。这将向 Dojo 编译器发出信号,表明此结构体应被视为组件。

我们的 Moves 组件在其状态中包含一个 remaining 值。#[key] 属性告知 Dojo,该组件是由 player字段索引的。如果你对这一点不熟悉,我们将在本章后面的内容中说明它的重要性。从本质上讲,它意味着您可以使用 player 字段来查询此组件。

同样,我们还拥有一个保存 xy 值的 Position 组件。同样,该组件由 player 字段索引。

现在,让我们检查一下src/systems.cairo文件:

#[system]
mod spawn {
    use array::ArrayTrait;
    use box::BoxTrait;
    use traits::Into;
    use dojo::world::Context;

    use dojo_examples::components::Position;
    use dojo_examples::components::Moves;

    fn execute(ctx: Context) {
        let position = get!(ctx.world, ctx.origin, (Position));
        set!(
            ctx.world,
            (
                Moves {
                    player: ctx.origin, remaining: 10
                },
                Position {
                    player: ctx.origin, x: position.x + 10, y: position.y + 10
                },
            )
        );
        return ();
    }
}

让我们来分析一下:

#[system]

正如我们使用的 #[derive(Component)] 属性一样,#[system] 属性通知 Dojo 编译器此结构是一个系统,并指示它进行相应的编译。

fn execute(ctx: Context)

您会发现该系统有一个 execute 函数。需要注意的是,所有 Dojo 系统都需要一个 execute 函数。该函数接受一个 Context 作为参数。Context(上下文) 是一个独特的结构,它提供了关于世界和调用者的信息。

值得一提的是,一个系统可以不仅仅包含 execute 函数。您可以根据需要自由添加多个函数。不过,execute 函数是必须的,因为系统执行时会调用它。

现在让我们看看下一行:

let position = get!(ctx.world, ctx.origin, (Position));

在这里,我们使用 get! 命令 来获取 ctx.origin 实体的 Position 组件。ctx.origin 是调用者的地址。第一次调用时,它将返回:

Position {
  player: 0x0, // zero address
  x: 0,
  y: 0
}

现在是下一行:

set!(
    ctx.world,
    (
        Moves {
            player: ctx.origin, remaining: 10
            }, Position {
            player: ctx.origin, x: position.x + 10, y: position.y + 10
        },
    )
);

这里我们使用 set! 命令ctx.origin 实体设置MovesPosition组件。

在很短的时间内,我们在这里做了很多事情。让我们来回顾一下:

  • 解释了 Dojo 项目的解剖结构
  • 解释 #[derive(Component)]#[system]属性的含义
  • 解释了 execute 函数
  • 解释了 Context 结构
  • 介绍了 get!set! 命令

在本地运行!

既然我们已经掌握了一些理论知识,那就来构建 Dojo 项目吧!

sozo build

这就把组件和系统编译成了一个可以部署的工件!就这么简单!

现在,让我们将其部署到 Katana 中!首先,我们需要让 Katana 运行:

katana --disable-fee

成功!现在,Katana应已在本地计算机上运行。现在开始部署!

sozo migrate --name test

这将把工件部署到 Katana 中。你应该会看到类似下面的终端输出:

Migration account: 0x33c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c

[1] 🌎 Building World state....
  > No remote World found
[2] 🧰 Evaluating Worlds diff....
  > Total diffs found: 7
[3] 📦 Preparing for migration....
  > Total items to be migrated (7): New 7 Update 0
  
# Executor
  > Contract address: 0x1a8cc7a653543337be184d21ceeb5cfc7e97af5ab7da5e4be77f373124d7e48
# World
  > Contract address: 0x71b95a2c000545624c51813444b57dbcdcc153dfc79b6b0e3a9a536168d1e16
# Components (2)
Moves
  > class hash: 0x3240ca67c41c5ae5557f87f44cca2b590f40407082dd390d893a514cfb2b8cd
Position
  > class hash: 0x4caa1806451739b6fb470652b8066a11f80e847d49003b43cca75a2fd7647b6
# Systems (3)
spawn
  > class hash: 0x1b949b00d5776c8ba13c2fdada38d4b196f3717c93c5c254c4909ed0eb249f7
move
  > class hash: 0x2534c514efeab524f24cd4b03add904eb540391e9966ebc96f8ce98453a4e1e
library_call
  > class hash: 0xabfd55d9bb6552aac17d78b33a6e18b06b1b95d4f684637e661dd83053fd45

🎉 Successfully migrated World on block #4 at address 0x71b95a2c000545624c51813444b57dbcdcc153dfc79b6b0e3a9a536168d1e16

Your 🌎 is now deployed at 0x71b95a2c000545624c51813444b57dbcdcc153dfc79b6b0e3a9a536168d1e16!

让我们讨论一下项目中的 Scarb.toml 文件。该文件包含环境变量,可使在项目中运行 CLI 命令变得轻而易举。(点击此处了解更多信息)。

在文件底部添加世界地址:

world_address = "0x71b95a2c000545624c51813444b57dbcdcc153dfc79b6b0e3a9a536168d1e16"

这样就为项目建立了世界地址。然后就可以运行以下命令:

sozo execute spawn

这样就激活了产卵系统。现在你有了一个可以互动的本地世界。

索引

设置好本地世界后,让我们来深入研究索引。你可以用这个简单的命令为整个世界建立索引:

torii

执行上述操作可激活本地 torii 服务器,使用 SQLite 作为其数据库,数据库地址为 http://0.0.0.0:8080。它会自动将你的世界索引到表中,允许你使用 GraphQL 查询。

我们已经讲了很多!下面我们来回顾一下:

  • 构建一个 Dojo 世界
  • 将项目部署到 Katana
  • 在本地运行生成系统
  • 用 Torii 索引世界

接下来的步骤

本概述仅仅提供了一个对 Dojo 端到端的快速了解。然而,这些世界的潜力是巨大的!Dojo 被设计为用于管理数百个系统和组件,可发挥无限的创造力。那么,您下一步将制作什么呢?

配置

Dojo 世界在其 Scarb.toml 文件中定义。这是一个 Scarb 文件,Scarb是一个出色的Cairo软件包管理器和项目管理器。

Scarb.toml 文件的完整示例:

[package]
cairo-version = "2.1.0-rc4"
name = "dojo_examples"
version = "0.1.0"

[cairo]
sierra-replace-ids = true

[dependencies]
dojo = { git = "https://github.com/dojoengine/dojo" }

[[target.dojo]]

[tool.dojo]
initializer_class_hash = "0xbeef"

[tool.dojo.env]
rpc_url = "http://localhost:5050/"

# account address of world deployer
account_address = "0x33c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c"

# private key of world deployer
private_key = "0x333803103001800039980190300d206608b0070db0012135bd1fb5f6282170b"

# world contract address
world_address = "0x789c94ef39aeebc7f8c4c4633030faefb8bee454e358ae53d06ced36136d7d6"

世界合约

世界合约是一个中央系统内核,是启动和解决所有交互的基础。在这个内核中,合约被部署、注册和执行,简化了下游系统的流程,使得客户只需与一个合约而不是潜在的数百个合约打交道。

Dojo 核心将此合约抽象化,作为开发者,您无需编写此合约,也无需在构建世界时对其进行修改。但是,了解它的工作原理以及它与系统其他部分的交互方式非常重要。

思考: 将自治世界视为驻留在另一个区块链中的主权区块链--可以说是嵌套区块链。正如你可以在以太坊上部署合约来增强其功能一样,你也可以在世界合约中引入系统来丰富其功能。与以太坊类似,任何人都可以为 "世界 "做出贡献,但与组件状态交互需要授权。有一个专门的主题会讨论授权。

Context(上下文)

你会注意到,每个 System 都接受一个 Context 结构体作为第一个参数。这是一个特殊的结构体,包含有关世界和调用者的信息。

#[derive(Copy, Drop, Serde)]
struct Context {
    world: IWorldDispatcher, // Dispatcher to the world contract
    origin: ContractAddress, // Address of the origin
    system: felt252, // Name of the calling system
    system_class_hash: ClassHash, // Class hash of the calling system
}

uuid()命令

为实体生成唯一 ID 通常很有用。uuid() 函数可用来生成唯一 ID。

像这样使用它:

let game_id = ctx.world.uuid();

完整的世界API

世界公开了一个接口,任何客户端都可以与之交互。

// World interface
#[starknet::interface]
trait IWorld<T> {
    fn component(self: @T, name: felt252) -> ClassHash;
    fn register_component(ref self: T, class_hash: ClassHash);
    fn system(self: @T, name: felt252) -> ClassHash;
    fn register_system(ref self: T, class_hash: ClassHash);
    fn uuid(ref self: T) -> usize;
    fn emit(self: @T, keys: Array<felt252>, values: Span<felt252>);
    fn execute(ref self: T, system: felt252, calldata: Array<felt252>) -> Span<felt252>;
    fn entity(
        self: @T, component: felt252, keys: Span<felt252>, offset: u8, length: usize
    ) -> Span<felt252>;
    fn set_entity(
        ref self: T, component: felt252, keys: Span<felt252>, offset: u8, value: Span<felt252>
    );
    fn entities(
        self: @T, component: felt252, index: felt252, length: usize
    ) -> (Span<felt252>, Span<Span<felt252>>);
    fn set_executor(ref self: T, contract_address: ContractAddress);
    fn executor(self: @T) -> ContractAddress;
    fn delete_entity(ref self: T, component: felt252, keys: Span<felt252>);
    fn origin(self: @T) -> ContractAddress;

    fn is_owner(self: @T, account: ContractAddress, target: felt252) -> bool;
    fn grant_owner(ref self: T, account: ContractAddress, target: felt252);
    fn revoke_owner(ref self: T, account: ContractAddress, target: felt252);

    fn is_writer(self: @T, component: felt252, system: felt252) -> bool;
    fn grant_writer(ref self: T, component: felt252, system: felt252);
    fn revoke_writer(ref self: T, component: felt252, system: felt252);
}

组件

组件 = 数据

组件是定义世界结构的基础,封装了系统变异的状态。

在设计一个世界中的组件时,必须仔细考虑所创建的抽象概念,并始终牢记可组合性。

组件是结构体

组件在 Cairo 中被定义为结构体。它们可以包含任意数量的字段,但在 ECS 中,最好的做法是使用小的孤立组件。这样可以促进模块化和可组合性,使您可以在多个实体类型中重复使用组件。

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Moves {
    #[key]
    player: ContractAddress,
    remaining: u8,
}

#[key]属性

#[key] 属性向 Dojo 表明,该组件是由 player 字段索引的。您需要为每个组件定义一个键,因为这是您查询组件的方式。不过,您可以通过将多个字段定义为键来创建复合键。

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Resource {
    #[key]
    player: ContractAddress,
    #[key]
    location: ContractAddress,
    balance: u8,
}

在这种情况下,您就可以通过player和location字段来设置组件:

set!(
    ctx.world,
    (
        Resource {
            player: ctx.origin,
            location: 12,
            balance: 10
        },
    )
);

实现Trait

组件可以实现trait。这对于定义跨组件的通用功能非常有用。例如,您可能想定义一个实现了 PositionTrait trait的 Position 组件。该trait可定义 is_zerois_equal 等函数,这些函数可在访问组件时使用。

trait PositionTrait {
    fn is_zero(self: Position) -> bool;
    fn is_equal(self: Position, b: Position) -> bool;
}

impl PositionImpl of PositionTrait {
    fn is_zero(self: Position) -> bool {
        if self.x - self.y == 0 {
            return true;
        }
        false
    }

    fn is_equal(self: Position, b: Position) -> bool {
        self.x == b.x && self.y == b.y
    }
}

自定义设置的组件

假设我们需要一个地方来保存一个全局值,并能在将来灵活地修改它。例如,一个全局的战斗冷却参数(combat_cool_down)定义了一个实体准备再次攻击所需的持续时间。为此,我们可以制作一个组件,专门用于存储这个值,同时还允许通过分散式管理模式对其进行修改。

要创建这些组件,您可以按照通常的创建方法去创建。不过,在初始化它们时,请使用一个常量标识符,如 GAME_SETTINGS_ID。

const GAME_SETTINGS_ID: u32 = 9999999999999;

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct GameSettings {
    #[key]
    game_settings_id: u32,
    combat_cool_down: u32,
}

类型

支持的组件类型:

  • u8
  • u16
  • u32
  • u64
  • u128
  • u256
  • ContractAddress

目前无法使用数组。

在实践中考虑模块性

下面是个具体的例子:人类和哥布林。虽然它们有着本质上的区别,但却有着共同的trait,比如都会拥有位置(position)和健康(health)。不过,人类拥有一个额外的组件。此外,我们还引入了 "计数器"(Counter)组件,它是用以统计人类和哥布林数量的独特机能。

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Potions {
    #[key]
    entity_id: u32,
    quantity: u8,
}

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Health {
    #[key]
    entity_id: u32,
    health: u8,
}

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Position {
    #[key]
    entity_id: u32,
    x: u32,
    y: u32
}

// Special counter component
#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Counter {
    #[key]
    counter: u32,
    goblin_count: u32,
    human_count: u32,
}

因此,"人类 "将拥有 Potions, HealthPosition 组件,而 "哥布林 "将拥有 HealthPosition 组件。这样,我们就不必为每种实体类型创建 HealthPosition组件了。

因此,一个系统会像是这样:

#[system]
mod spawnHuman {
    use array::ArrayTrait;
    use box::BoxTrait;
    use traits::Into;
    use dojo::world::Context;

    use dojo_examples::components::Position;
    use dojo_examples::components::Health;
    use dojo_examples::components::Potions;
    use dojo_examples::components::Counter;

    // we can set the counter value as a const, then query it easily! This pattern is useful for settins.
    const COUNTER_ID: u32 = 9999999999999;

    fn execute(ctx: Context, entity_id: u32) {

        let counter = get!(ctx.world, COUNTER_ID, (Counter));

        let human_count = counter.human_count + 1;
        let goblin_count = counter.goblin_count + 1;

        // spawn a human
        set!(
            ctx.world,
            (
                Health {
                    entity_id: human_count, health: 100
                    }, 
                Position {
                    entity_id: human_count, x: position.x + 10, y: position.y + 10,
                    }, 
                Potions {
                    entity_id: human_count, quantity: 10
                    
                },
            )
        );

        // spawn a goblin
        set!(
            ctx.world,
            (
                Health {
                    entity_id: goblin_count, health: 100
                    }, 
                Position {
                    entity_id: goblin_count, x: position.x + 10, y: position.y + 10,
                    },
            )
        );

        // increment the counter
        set!(
            ctx.world,
            (
                Counter {
                    counter: COUNTER_ID, human_count: human_count, goblin_count: goblin_count
                },
            )
        );
        
        return ();
    }
}

完整示例见 Dojo Starter

系统

系统 = 逻辑

系统是世界逻辑的基础。虽然系统本质上是无状态的,但其主要作用是修改组件的状态。每个系统都有一个'execute'函数,可在世界内部交互时调用。

让我们来看一个最简单的系统,它可以改变 Moves 组件的状态。

#[system]
mod Spawn {
    use array::ArrayTrait;
    use traits::Into;

    use dojo::world::Context;
    use dojo_examples::components::Position;
    use dojo_examples::components::Moves;

    fn execute(ctx: Context) {
        set !(
            ctx.world, ctx.origin, (
                Moves { player: ctx.origin, remaining: 10 }
            )
        );
        return ();
    }
}

执行函数

execute 函数在系统中是强制性的,调用时以 Context 为第一个参数。更多信息请参阅 Context

系统中的其他函数

您可以自由地在系统中添加其他函数,但这些函数不能从世界中调用。这对于将你的逻辑分割成小块非常有用。

使用视图函数

有时,我们需要动态地计算组件的值,而不是获取其静态状态。例如,在 VRGDA 中,如果要确定当前价格,仅查询组件状态是不够的。相反,您需要根据某些参数和当前状态来计算价格。

这就是视图函数发挥作用的地方。

什么是视图函数?

视图函数是从组件的现有状态推导或计算值的一种方法。它们由世界调用,并接收组件的当前状态作为参数。随后,这些函数会返回一个基于该状态的计算值。

来自 VRGDA 的示例

下面的片段摘自 这个链接 中的 VRGDA 示例,说明了如何实现视图函数:

#[system]
mod view_price {
    //... other code ...

    fn execute(ctx: Context, game_id: u64, item_id: u128, amount: u128) -> Fixed {
        let mut auction = get!(ctx.world, (game_id, item_id), Auction);

        // Convert auction to VRGDA
        let VRGDA = auction.to_LogisticVRGDA();

        // Calculate time since the auction began
        let time_since_start: u128 = get_block_timestamp().into() - auction.start_time.into();

        // Compute the current price
        VRGDA.get_vrgda_price(
            FixedTrait::new(time_since_start, false), // Time elapsed since auction start
            FixedTrait::new(auction.sold, false)      // Quantity sold
        )
    }
}

在此示例中,函数根据正在进行的拍卖状态计算并返回 VRGDA 的当前价格。

如何调用视图函数?

  • 使用 Dojo Core:如果您在 Dojo Core 中开发,请使用 call 函数。

  • 适用于 Rust 用户Starkli 库提供了在 Rust 中调用视图函数的便捷方法。

我希望修订后的版本能使您想要传达的信息更加清晰流畅!

系统验证

系统必须获得写入组件的权限。默认情况下,它们没有权限。不过,我们可以通过 sozo 赋予系统写入组件的权限。

sozo auth writer Moves Spawn 

在这里,我们授权 Spawn 系统写入Moves 组件。

阅读 sozo 文档中的更多内容。

实体

实体是世界中的主要关键值,组件可以附加到实体上。

不同的 ECS 系统处理实体的方式各不相同。在 Dojo 中,实体被视为世界中的一个主键值,可以附加组件。为了说明这一概念,请看一个简单的例子:游戏中的角色有一个 Moves 和一个 Position组件。

在定义该实体的组件时,需要注意的是,我们并不直接引用该实体。相反,我们只是提供了实体将包含的两个结构。这种方法强调了 ECS 系统的灵活性和可组合性,允许轻松创建和修改具有各种组件组合的实体。

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Moves {
    #[key]
    player: ContractAddress,
    remaining: u8,
}

#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Health {
    #[key]
    player: ContractAddress,
    x: u32,
    y: u32
}

现在,让我们为角色创建一个 Spawn。需要注意的是,我们没有在任何地方明确定义实体。相反,我们使用 ctx.origin 来引用当前实体。

在本例中,我们使用 ctx.origin 来引用当前实体。

#[system]
mod spawn {
    use array::ArrayTrait;
    use box::BoxTrait;
    use traits::Into;
    use dojo::world::Context;

    use dojo_examples::components::Position;
    use dojo_examples::components::Moves;

    fn execute(ctx: Context) {
        let position = get!(ctx.world, ctx.origin, (Position));
        set!(
            ctx.world,
            (
                Moves {
                    player: ctx.origin, remaining: 10
                    }, Position {
                    player: ctx.origin, x: position.x + 10, y: position.y + 10
                },
            )
        );
        return ();
    }
}

ECS 理论:关于 ECS 系统的文章很多,如需深入了解,请阅读 ECS-FAQ

授权

授权对一个世界至关重要,就像授权对任何智能合约都至关重要一样。

正如World一章所讨论的,自治世界(AW)的功能是嵌套在公共区块链中的主权链。这些世界也对公众开放。这种结构允许任何人通过部署组件或系统来增强世界。然而,这种开放性也带来了安全方面的考虑。与以太坊类似,在系统中与组件的状态进行交互需要获得组件所有者的适当授权。

验证架构

每次在 System中调用 set!时,世界都会检查 System是否拥有更新组件状态的授权。只有当System拥有必要的授权时,set!才会被执行。下图说明了授权架构。

授权架构

提供授权

组件的部署者是其初始所有者。组件所有者可以授予别人 ownerwriter角色。只有所有者才能授予系统 writer角色,使其可以更新组件。

sozo 提供了一个授权系统的便捷工具。

sozo auth writer Moves spawn

该命令将为 spawn 系统生成一个 writer 授权,以更新 Moves 组件。

命令

理解命令是理解 Dojo 的关键。您将在设计系统时大量使用它们。

Dojo 中的命令是在编译时扩展的通用函数,便于系统执行。它们通过抽象常见操作(如检索或更新组件以及生成唯一 ID),为系统提供了一种与世界状态交互的便捷方式。通过利用这些命令,开发人员可以简化系统实现并提高代码的可读性。

使用命令

命令用于系统内部与世界状态的交互。它们的调用语法如下:

let (position, moves) = get!(ctx.world, ctx.origin, (Position, Moves));

get! 命令

get!命令用于从世界状态中检索组件。

像这样使用它:

let (position, moves) = get!(ctx.world, ctx.origin, (Position, Moves));

在这里,我们从世界状态中获取 PositionMoves组件。我们还使用 ctx.origin 来检索当前实体的组件。

然后,您可以像使用其他 Cairo 结构体一样使用 positionmoves

set!命令

set! 命令用于更新组件状态。

像这样使用它:

set !(ctx.world, (
    Moves {
        player: ctx.origin, remaining: 10
    }, 
    Position {
        player: ctx.origin, x: position.x + 10, y: position.y + 10
    },
));

// If the structs are already defined it can also be written as:
set!(ctx.world, (moves, position));

在这里,我们使用 ctx.origin 作为实体 ID,更新世界状态中的 MovesPosition 组件。

emit!命令

emit!命令用于发射自定义事件。

像这样使用它:

emit !(ctx.world, Moved { address: ctx.origin, direction });

事件

事件在让外部了解 Dojo 世界中的动态时起着关键作用。每次Component更新时,World 合约都会发出这些事件。更令人兴奋的是,你可以根据特定需求制作自己的自定义事件!此外,由于使用了 Torii,所有这些事件都被无缝地索引起来,确保了轻松高效的查询。

组件事件

请看 Moves 组件的示例:

#[component]
struct Moves {
    #[key]
    player: Address,
    remaining: u32,
}

当该组件更新时,World 合约将发出一个结构如下的事件:

#[derive(Drop, starknet::Event)]
struct StoreSetRecord {
    table: felt252, // Moves
    keys: Span<felt252>, // [player]
    offset: u8, // 0
    value: Span<felt252>, // [remaining]
}

然后,Torii将捕获这些信息,并编制索引以供查询。这样,你就可以重建世界的状态。

同样,当一个组件被删除时,World 合约将发出一个结构如下的事件:

#[derive(Drop, starknet::Event)]
struct StoreDelRecord {
    table: felt252,
    keys: Span<felt252>,
}

世界事件

当初始化以及注册新组件和系统时,World合约也会发出事件。这些事件通过以下结构体发出:

#[derive(Drop, starknet::Event)]
struct WorldSpawned {
    address: ContractAddress,
    caller: ContractAddress
}
#[derive(Drop, starknet::Event)]
struct ComponentRegistered {
    name: felt252,
    class_hash: ClassHash
}
#[derive(Drop, starknet::Event)]
struct SystemRegistered {
    name: felt252,
    class_hash: ClassHash
}

Torii 也会捕获这些事件,并编制索引以供查询。

自定义事件

在您的系统中,发射自定义事件是非常有益的。幸运的是,有一个方便的 emit!宏可以让您直接从您的世界中发射事件。使用方法如下:

emit !(ctx.world, Moved { address: ctx.origin, direction });

在系统中加入此机能后,它将发出一个结构如下的事件:

#[derive(Drop, starknet::Event)]
struct Moved {
    address: felt252,
    direction: felt252,
}

现在是一个使用自定义事件的完整示例:

fn execute(ctx: Context, direction: Direction) {
    let (mut position, mut moves) = get !(ctx.world, ctx.origin, (Position, Moves));
    moves.remaining -= 1;

    let next = next_position(position, direction);
    
    set !(ctx.world, (moves, next));
    emit !(ctx.world, Moved { address: ctx.origin, direction });
    return ();
}

注意:请阅读 命令 中的 get!set! 宏。

测试

测试在任何软件开发过程中都是重要组成部分。Dojo 提供了一个测试框架,允许您为智能合约编写测试。由于 Dojo 使用自定义编译器,因此您需要使用 sozo 来测试您的合约。

在项目目录中,只需:

sozo test

这将搜索项目中的所有测试并运行它们。

编写单元测试

最佳做法是将单元测试包含在与正在编写的组件/系统相同的文件中。

让我们展示一个来自 dojo-starterComponent 测试示例:

components.cairo


...rest of code

#[cfg(test)]
mod tests {
    use debug::PrintTrait;
    use super::{Position, PositionTrait};

    #[test]
    #[available_gas(100000)]
    fn test_position_is_zero() {
        let player = starknet::contract_address_const::<0x0>();
        assert(PositionTrait::is_zero(Position { player, x: 0, y: 0 }), 'not zero');
    }

    #[test]
    #[available_gas(100000)]
    fn test_position_is_equal() {
        let player = starknet::contract_address_const::<0x0>();
        let position = Position { player, x: 420, y: 0 };
        position.print();
        assert(PositionTrait::is_equal(position, Position { player, x: 420, y: 0 }), 'not equal');
    }
}

在本测试中,我们将测试 Position 组件的 is_zerois_equal 函数。测试组件的所有函数是一种很好的实践。

编写集成测试

集成测试是测试整个系统的 e2e 测试。您可以在项目根目录下创建一个 tests 目录,为您的世界编写集成测试。然后为要编写的每个集成测试创建一个文件。

这是来自 dojo-starter: 的示例:

systems.cairo

#[cfg(test)]
mod tests {
    use core::traits::Into;
    use array::ArrayTrait;

    use dojo::world::IWorldDispatcherTrait;

    use dojo::test_utils::spawn_test_world;

    use dojo_examples::components::position;
    use dojo_examples::components::Position;
    use dojo_examples::components::moves;
    use dojo_examples::components::Moves;
    use dojo_examples::systems::spawn;
    use dojo_examples::systems::move;

    #[test]
    #[available_gas(30000000)]
    fn test_move() {
        let caller = starknet::contract_address_const::<0x0>();

        // components
        let mut components = array::ArrayTrait::new();
        components.append(position::TEST_CLASS_HASH);
        components.append(moves::TEST_CLASS_HASH);

        // systems
        let mut systems = array::ArrayTrait::new();
        systems.append(spawn::TEST_CLASS_HASH);
        systems.append(move::TEST_CLASS_HASH);

        // deploy executor, world and register components/systems
        let world = spawn_test_world(components, systems);

        let spawn_call_data = array::ArrayTrait::new();
        world.execute('spawn', spawn_call_data);

        let mut move_calldata = array::ArrayTrait::new();
        move_calldata.append(move::Direction::Right(()).into());
        world.execute('move', move_calldata);
        let mut keys = array::ArrayTrait::new();
        keys.append(caller.into());

        let moves = world.entity('Moves', keys.span(), 0, dojo::SerdeLen::<Moves>::len());
        assert(*moves[0] == 9, 'moves is wrong');
        let new_position = world
            .entity('Position', keys.span(), 0, dojo::SerdeLen::<Position>::len());
        assert(*new_position[0] == 11, 'position x is wrong');
        assert(*new_position[1] == 10, 'position y is wrong');
    }
}

有用的 Dojo 测试函数

spawn_test_world(components, systems) - 这个函数将用你传入的组件和系统创建一个测试世界。它还将部署该世界并注册组件和系统。

Dojo 的模块

通过系统和组件的标准化,我们可以为 Dojo 创建一个模块架构。这样,我们就可以创建可在任何 Dojo 世界中使用的可重用模块。

模块架构

我们可将模块视为 Dojo 的 ERC。它们是创建和共享功能的标准方式。模块是可导入 Dojo 世界的系统和组件的集合。Dojo 遵循 ERC 模式,并已经为 ERC20、ERC721 和 ERC1155 定义了模块。

ERC20

Dojo 的 ERC20 模块是 ERC20 token标准的标准实现,但它使用了 Dojo 系统和组件。这使我们能够利用 ERC20 标准的出色特性,并在 Dojo 环境中使用它。

集成到你的世界

要将 ERC20 模块集成到您的世界中,您必须首先部署 ERC20 Dojo 合约。然后,将系统和组件安装到您的世界中。

概览

Dojo 是 BYO 客户端,这意味着您可以使用任何客户端连接到 Dojo 网络。

参考客户端可在以下平台获取:

Javascript 库

Javascript 是入门 Dojo 的好方法。它简单易用,几分钟就能上手。

使用的例子:

@dojoengine/core

这是最底层的库,所有其他下游库都使用它。它包含 Dojo 的核心功能,并公开合约接口。如果您想在 Dojo 的基础上构建自己的库,请使用它。

文档

yarn add @dojoengine/core

@dojoengine/react

这是对 Mud React 的直接分叉

该库包含一组 React 组件,可用于使用 Dojo 构建 React 应用程序。

文档

yarn add @dojoengine/react

@dojoengine/create-burner

Create burner 是将burner钱包集成到 Dojo 应用程序中的简单方法。

Reopsitory

yarn add @dojoengine/create-burner

@dojoengine/core

该库抽象了世界界面,并提供了一组与世界交互的辅助函数。与直接与世界交互相比,我们更倾向于使用该库。

  • 世界探索者
  • 世界部署者
  • 游戏
  • 分析

开始

yarn add @dojoengine/core

使用示例

本例来自 Dojo React App

import { defineContractComponents } from "./contractComponents";
import { world } from "./world";
import { RPCProvider, Query, } from "@dojoengine/core";
import { Account, num } from "starknet";
import { GraphQLClient } from 'graphql-request';
import { getSdk } from '../generated/graphql';

export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>;

export async function setupNetwork() {

    const provider = new RPCProvider(import.meta.env.VITE_PUBLIC_WORLD_ADDRESS, import.meta.env.VITE_PUBLIC_NODE_URL);

    return {
        contractComponents: defineContractComponents(world),
        provider,
        execute: async (signer: Account, system: string, call_data: num.BigNumberish[]) => provider.execute(signer, system, call_data),
        entity: async (component: string, query: Query) => provider.entity(component, query),
        entities: async (component: string, partition: number) => provider.entities(component, partition),
        world,
        graphSdk: getSdk(new GraphQLClient(import.meta.env.VITE_PUBLIC_TORII)),
        call: async (selector: string, call_data: num.BigNumberish[]) => provider.call(selector, call_data),
    };
}

dojoup

轻松更新或还原到特定的 Dojo 分支。

安装

curl -L https://install.dojoengine.org | bash

使用方法

安装最新的稳定版本:

dojoup

注意:您可能需要安装 jq 才能使用 dojoup。您可以使用以下命令进行安装:

# Debian
sudo apt-get install jq

# Mac
brew install jq

安装特定版本(此处为 "nightly "版本):

dojoup --version nightly

安装特定的分支(在本例中为 release/0.1.0 分支的最新提交):

dojoup --branch release/0.1.0

安装分叉的主分支(本例中为 tarrencev/dojo 的主分支):

dojoup --repo tarrencev/dojo

安装分叉中的特定分支(在本例中,安装tarrencev/dojopatch-10分支的最新提交):

dojoup --repo tarrencev/dojo --branch patch-10

特定的Pull Request进行安装:

dojoup --pr 1071

特定提交进行安装:

dojoup -C 94bfdb2

安装本地目录或版本库(例如位于 ~/git/dojo 的版本库,假设您在主目录下)

注意:本地安装时,--branch、--repo 和--version 标识符将被忽略。
dojoup --path ./git/dojo

提示:所有标识符都有对应的单字符缩写!您可以使用 -v 代替 --version 等。


预编译的二进制文件

预编译的Dojo二进制文件可从GitHub上的发行页面获取。 使用Dojoup可以更好地管理这些二进制文件。

ℹ️

如果您使用的是 Windows 系统,则需要安装并使用 Git BASHWSL、 因为 Dojoup 目前不支持 Powershell 或 Cmd。

Sozo

sozo(译者注:sozo是日文“創造”的罗马字) 是一款功能强大的多合一工具,用于管理 Dojo 项目。从搭建新项目的脚手架,一直到部署和与 Dojo Worlds 交互,它都能提供帮助。它包括一个迁移规划工具,旨在简化 AW 的更新和部署。它提供了一个强大的命令行界面(CLI),可简化世界管理任务,让您专注于世界构建的创意方面。将来,它还可能包括图形用户界面。

功能

  • 二进制包 CLI:Sozo 提供直观的二进制包 CLI,确保了用户能轻松管理世界,无论是更新现有世界还是部署新世界。

安装

sozo 二进制文件可通过dojoup安装,这是我们专用的安装包管理器。

从源码安装

git clone https://github.com/dojoengine/dojo
cd dojo
cargo install --path ./crates/sozo --locked --force

这将在本地系统中安装 Sozo 和所需的依赖项。

📚 参考资料

关于所有可用子命令的完整概述,请参阅 sozo参考资料

sozo 参考资料

项目命令

世界命令

sozo init

init 用于初始化一个新项目。它将通过克隆 dojo-starter 在当前目录下初始化一个新项目。

sozo init

sozo build

build 用于编译 cairo 合约,生成部署所需的构件。

sozo build

sozo test

test用于测试项目的Cairo合约。它将运行项目中的所有测试。

sozo test

sozo migrate

migrate用于执行迁移(部署)流程,根据部署或更新世界的需要声明和部署合约。

在初始部署后对本地世界所做的更改,可以通过运行 sozo migrate --world <WORLD_ADDRESS>(其中 WORLD_ADDRESS是远程世界的地址)轻松推送到远程世界。在后台,migrate 会计算本地世界和远程世界的差异,然后开始构建迁移策略,以确定(如果有的话)本地世界的哪一部分需要推送到上游。

用例

sozo migrate [OPTIONS]

选项

常规选项

--name NAME
    世界的名称。目前,该选项的唯一用途是在部署 "世界 "合约时用作 "盐",以避免地址冲突。在执行 "世界 "的初始迁移时,该选项是必需的

世界选项

--world WORLD_ADDRESS
    世界合约的地址。
    ENV: DOJO_WORLD_ADDRESS

Starknet选项

--rpc-url URL
    Starknet RPC 端点。[默认: http://localhost:5050]
    ENV: STARKNET_RPC_URL

账户选项

--account-address ACCOUNT_ADDRESS
    Starknet账户地址。
    ENV: DOJO_ACCOUNT_ADDRESS

签名者选项 - Raw

--private-key PRIVATE_KEY
    与账户合同相关的Raw私钥。
    ENV: DOJO_PRIVATE_KEY

签名者选项 - Keystore

--keystore PATH
    使用指定文件夹或文件中的keystore。

--password PASSWORD
    keystore的密码. 需要与 --keystore 参数一起使用.
    ENV: DOJO_KEYSTORE_PASSWORD

示例

  1. 首次在本地 Katana 节点上部署你的世界
sozo migrate --name ohayo --rpc-url http://localhost:5050
  1. 更改后更新远程世界
sozo migrate --world 0x123456

sozo execute

execute 用于执行一个世界的系统。

执行系统需要发送事务,因此,execute需要一个账户地址及其私钥,以便在发送事务之前对其进行签名。

用例

sozo execute <SYSTEM> [OPTIONS]

选项

常规选项

--calldata CALLDATA
    要传给系统执行的 calldata。
    以逗号分隔的值,例如 0x12345,0x69420。

世界选项

--world WORLD_ADDRESS
    世界合约的地址。
    ENV: DOJO_WORLD_ADDRESS

Starknet选项

--rpc-url URL
    Starknet RPC 端点。[默认: http://localhost:5050]
    ENV: STARKNET_RPC_URL

账户选项

--account-address ACCOUNT_ADDRESS
    Starknet账户地址。
    ENV: DOJO_ACCOUNT_ADDRESS

签名者选项 - Raw

--private-key PRIVATE_KEY
    与账户合同相关的Raw私钥。
    ENV: DOJO_PRIVATE_KEY

签名者选项 - Keystore

--keystore PATH
    使用指定文件夹或文件中的keystore。

--password PASSWORD
    keystore的密码. 需要与 --keystore 参数一起使用.
    ENV: DOJO_KEYSTORE_PASSWORD

示例

  1. 执行 position 系统,该系统取两个值( x : 0x77 和 y : 0x44)
sozo execute position --calldata 0x77,0x44

sozo register

register 用于注册新系统和组件。

sozo register [OPTIONS] <COMMAND>
Commands:
  component  Register a component to a world.
  system     Register a system to a world.
  help       Print this message or the help of the given subcommand(s)
# example: component - register a component to a world
# this will register the Moves component to the world
sozo register component Moves

# example: system - register a system to a world
# this will register the spawn system to the world
sozo register system spawn

sozo system

system用于与世界的系统进行交互。它有助于查询系统信息。

用例

sozo system <COMMAND>

Commands:
  get         Get the class hash of a system.
  dependency  Retrieve the component dependencies of a system.

子命令

get

获取系统的Class Hash

sozo system get <NAME>
参数

NAME
    系统名称

dependency

检索系统的组件依赖关系

sozo system dependency <NAME>
参数

NAME
    系统名称

选项

世界选项

--world WORLD_ADDRESS
    世界合约的地址。
    ENV: DOJO_WORLD_ADDRESS

Starknet选项

--rpc-url URL
    Starknet RPC 端点。[默认: http://localhost:5050]
    ENV: STARKNET_RPC_URL

示例

  1. 获取 spawn 系统的Class Hash
sozo system get spawn
  1. 获取 spawn 系统的组件依赖关系
sozo system dependency spawn

sozo component

component用于与世界的组件进行交互。它可用于查询组件信息或实体的组件值。

用例

sozo component <COMMAND>

Commands:
  get     Get the class hash of a component
  schema  Retrieve the schema for a component
  entity  Get the component value for an entity

子命令

get

获取组件的Class Hash

sozo component get <NAME>
参数

NAME
    组件名称

schema

获取组件的scbema

sozo component schema <NAME>
参数

NAME
    组件名称

entity

获取实体的组件值

sozo component entity <NAME> [KEYS]...
参数

NAME
    组件名称

KEYS
    要查询的实体的键值。
    以逗号分隔的值,例如:0x12345,0x69420,...

选项

世界选项

--world WORLD_ADDRESS
    世界合约的地址。
    ENV: DOJO_WORLD_ADDRESS

Starknet选项

--rpc-url URL
    Starknet RPC 端点。[默认: http://localhost:5050]
    ENV: STARKNET_RPC_URL

sozo events

events用于查询世界事件。

sozo events

sozo auth

auth 用于管理世界授权。

sozo auth [OPTIONS] <COMMAND>
Commands:
  writer  Auth a system with the given calldata.
  help    Print this message or the help of the given subcommand(s)
# example: writer - auth a system with the given calldata
# This will auth the spawn system with the writer role for Position component
sozo auth writer Position spawn

Katana

katana (译者注:katana是日文“刀”的罗马字)是一个速度惊人的本地星网节点,旨在支持使用 Dojo 进行本地开发。

功能

安装

katana二进制文件可通过 dojoup 获取。

从源码安装

git clone https://github.com/dojoengine/dojo
cd dojo
cargo install --path ./crates/katana --locked --force

使用方法

$ katana



██╗  ██╗ █████╗ ████████╗ █████╗ ███╗   ██╗ █████╗
██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗████╗  ██║██╔══██╗
█████╔╝ ███████║   ██║   ███████║██╔██╗ ██║███████║
██╔═██╗ ██╔══██║   ██║   ██╔══██║██║╚██╗██║██╔══██║
██║  ██╗██║  ██║   ██║   ██║  ██║██║ ╚████║██║  ██║
╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝



PREFUNDED ACCOUNTS
==================

| Account address |  0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
| Private key     |  0x300001800000000300000180000000000030000000000003006001800006600
| Public key      |  0x1b7b37a580d91bc3ad4f9933ed61f3a395e0e51c9dd5553323b8ca3942bb44e

| Account address |  0x33c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c
| Private key     |  0x333803103001800039980190300d206608b0070db0012135bd1fb5f6282170b
| Public key      |  0x4486e2308ef3513531042acb8ead377b887af16bd4cdd8149812dfef1ba924d


ACCOUNTS SEED
=============
0


🚀 JSON-RPC server started: http://127.0.0.1:5050


📚 参考资料

请参阅 katana参考资料,了解有关 Katana 的深度参考资料和文档。

katana参考资料

名称

katana - 创建本地测试网节点,用于部署和测试Starknet智能合约。

用例

katana [OPTIONS]

描述

创建本地测试网节点,用于部署和测试Starknet智能合约。Katana 支持部署和执行 以及 (Cairo 0)Cairo合约。

本节包含有关挖矿模式、支持的 RPC 方法、Katana 标志符及其用法的大量信息。您可以同时运行多个标志符。

挖矿模式

在 Katana 中,挖矿模式决定了区块的挖矿频率。默认情况下,一旦有交易提交,就会自动挖出一个新区块。

您可以将默认的挖矿行为切换为间隔挖矿,即按照用户选择的固定时间间隔创建新区块。要启用这种挖矿模式,请使用--block-time <SECONDS> 标识符,如下例所示。

# Produces a new block every 10 seconds
katana --block-time 10

支持的传输层

目前只支持 HTTP 连接。服务器默认监听 5050 端口,但可以通过运行以下命令进行更改:

katana --port <PORT>

Starknet功能兼容性

支持的交易类型
TypeVersion
INVOKE1
DECLARE1, 2
DEPLOY_ACCOUNT

支持的 RPC 方法

Starknet方法

Katana 支持版本v0.3.0的星网 JSON-RPC 规范。标准方法基于 此处 的参考文献。

  • starknet_blockNumber

  • starknet_blockHashAndNumber

  • starknet_getBlockWithTxs

  • starknet_getBlockWithTxHashes

  • starknet_getBlockTransactionCount

  • starknet_getTransactionByHash

  • starknet_getTransactionByBlockIdAndIndex

  • starknet_getTransactionReceipt

  • starknet_pendingTransactions

  • starknet_getStateUpdate

  • starknet_call

  • starknet_estimateFee

  • starknet_chainId

  • starknet_getNonce

  • starknet_getEvents

  • starknet_getStorageAt

  • starknet_getClassHashAt

  • starknet_getClass

  • starknet_getClassAt

  • starknet_syncing

  • starknet_addInvokeTransaction

  • starknet_addDeclareTransaction

  • starknet_addDeployAccountTransaction

自定义方法

Katana 提供了一套方便的自定义 RPC 方法,可快速、轻松地配置节点,以适应测试环境。

katana_generateBlock
挖掘一个新区块,其中包括当前所有待处理的交易。

katana_nextBlockTimestamp
获取下一个区块的时间。

katana_increaseNextBlockTimestamp
将区块的时间增加给定的时间量,以秒为单位。

katana_setNextBlockTimestamp
katana_increaseNextBlockTimestamp 类似,但会在下一个区块中使用你想要的确切时间戳。

katana_predeployedAccounts
获取所有预部署账户的信息。

katana_setStorageAt 设置合约存储槽中的精确值。

选项

常规选项

--silent
     启动时不打印任何内容。

--no-mining
     禁用自动采矿和间隔采矿,改为按需采矿。

-b, --block-time <SECONDS>
     以秒为单位的区间挖掘时间。

--dump-state <PATH>
     将退出时链的状态转存到给定文件。
     如果值是目录,状态将被写入 <PATH>/state.bin

--load-state <PATH>
    从先前保存的状态快照初始化链。

-h, --help
     打印帮助(用"-h "查看摘要)。

-V, --version
     打印版本信息。

服务器选项

-p, --port <PORT>
     要监听的端口号[默认:5050]。

--host <HOST>
     服务器监听的 IP 地址。

Starknet选项

--seed <SEED>
     指定要预先部署的账户随机性种子。

--accounts <NUM>
     要生成的预注资账户数 [默认值:10]。

--disable-fee
    禁用交易收gas费。

环境选项

--chain-id <CHAIN_ID>
     链 ID [默认值:KATANA]。

--gas-price <GAS_PRICE>
     Gas价格。

--validate-max-steps <VALIDATE_MAX_STEPS>
     账户验证逻辑的最大步骤数。

--invoke-max-steps <INVOKE_MAX_STEPS>
     账户执行逻辑的最大步数。

Shell 补完

katana completions shell

为给定的 shell 生成 shell 补完脚本。

支持的shell有:

  • bash
  • elvish
  • fish
  • powershell
  • zsh

示例

bash 生成 shell 补完脚本,并将其添加到 .bashrc 文件中:

katana completions bash >> ~/.bashrc

示例

  1. 创建 15 个开发用账户并禁用交易费机制
katana --accounts 15 --disable-fee
  1. 将链 id 设置为 SN_GOERLI,并在 8545 端口运行服务器
katana --chain-id SN_GOERLI --port 8545
  1. 加载先前存储的状态,并在网络关闭时将此会话的状态转储到文件中
katana --load-state ./dump-state.bin --dump-state ./dump-state.bin

Torii - 网络和索引

Torii(译者注:torii是日文“鳥居”的罗马字) 是 dojo 世界的自动索引器。采用 rust 构建,速度极快,可扩展性极强。

Dojo 索引器

Torii 可为你的 dojo 世界建立索引,并提供 GraphQL API 以进行查询。只需运行:

torii

就可以在 http://localhost:8080 上运行 GraphQL API 了!

安装

你可以通过我们专用的安装包管理器 dojoup安装 torii 二进制文件。

从源码安装

如果您更喜欢从源代码中安装请:

cargo install --path ./crates/torii --profile local --force

这将在本地系统中安装 Torii 和所需的依赖项。

📚 参考资料

请参阅 torii参考 获取完整参考。

torii 参考资料

名称

torii - 世界合约的自动索引器和网络层。

用例

torii [OPTIONS]

描述

torii会启动索引器,并公开 GraphQL/gRPC API 端点。索引器会向指定的 Starknet RPC 端点查询事务块,并监听与世界契约相关的事务。这些事务包括组件/系统注册、实体状态更新、系统调用和事件。解析后的数据将存储在本地 SQLite 数据库中。

GraphQL 和 gRPC API 端点与索引器同步运行,为客户端应用程序提供特定于世界契约的自定义查询。

数据库 URL

torii 使用sqlite数据库存储索引数据。数据库既可以存储在内存中,也可以持久地存储在文件系统中。

  • 内存数据库是非持久存在的,只有索引器运行时才会存在。这是在开发/测试中启动索引器的快速而简单的选择。
  • 生产环境中应使用持久存储。它依赖本地文件系统进行存储。

注意:如果使用内存数据库,在一段时间不活动后内存将被垃圾回收,导致查询出错。解决方法是使用 --block-time 选项启动 katana 或使用持久数据库。

# Persistent database storage using file indexer.db
torii --database-url sqlite:indexer.db

选项

常规选项

-w, --world      要索引的世界合同的地址

--rpc      要使用的Starknet RPC 终端 [默认:http//localhost:5050]

-m, --manifest <MANIFEST>      指定要初始化的本地清单

-d, --database-url <DATABASE_URL>      数据库 URL(更多信息请阅读上文) [默认:sqlite::memory:]

-s, --start-block <START_BLOCK>      指定要从哪个块开始建立索引,如果存在存储头,则忽略 [默认:0]

-h, --help      打印帮助

-V, --version      打印版本信息

本地部署

Dojo 专为快速开发而设计,拥有一个名为 Katana的快如闪电的本地开发环境。Katana 可作为设备上的Starknet区块链,允许您在将智能合约迁移到远程测试网络之前对其进行严格测试。

Katana 部署

部署合约到 Katana 再简单不过了。

这里假设您已按照 快速入门 指南进行了操作,并已初始化了一个项目。

在项目目录下运行:

katana --disable-fee

这样就启动了一个本地Katana,您现在可以在上面进行部署!

部署到Katana

要将项目部署到 Katana,请运行:

sozo migrate --name test

注意 - 只有在你的合约已经编译过的情况下才能运行。如果没有,请运行:

sozo build

部署到远程网络

重要提示:Dojo 未经审计。使用风险自负。

Dojo 可让您轻松部署到远程网络,您只需拥有一个有效账户和网络端点。

Scarb.toml

[package]
name = "ohayoo"
version = "0.1.0"
cairo-version = "2.1.1"

[cairo]
sierra-replace-ids = true

[dependencies]
dojo = { git = "https://github.com/dojoengine/dojo.git" }

# # Katana
# rpc_url = "http://localhost:5050"
# account_address = "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0"
# private_key = "0x0300001800000000300000180000000000030000000000003006001800006600"

#Madara
rpc_url = "https://api.cartridge.gg/x/shinai/madara"
account_address = "0x2"
private_key = "0xc1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d"
#world_address = "0x5b328933afdbbfd44901fd69a2764a254edbb6e992ae87cf958c70493f2d201"

远程 Katana

Katanas 可以作为远程测试网托管和运行,但不建议用于生产。

todo:添加部署到远程 katana 的说明

Madara

Madara是一款高速Starknet排序器。Madara 基于强大的 Substrate 框架构建,由于采用了 Rust 🦀,因此速度极快,可提供无与伦比的性能和可扩展性,为您的基于Starknet的Validity Rollup链(译者注:如通常意义上的appchain)提供动力。

已有公共的 Madara 测试网可供部署:

Testnet RPC: https://api.cartridge.gg/x/shinai/madara

您可以使用以下账户进行部署:

# ...rest of Scarb.toml

rpc_url = "https://api.cartridge.gg/x/shinai/madara"
account_address = "0x2"
private_key = "0xc1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d"

Starknet

todo:添加部署到远程Starknet的说明

构建国际象棋游戏

“我刚刚读完Dojo Book。接下来我该做什么?”

对这个问题的回答总是 "做点什么吧!",有时还会列举一些很酷的项目。这对有些人来说是个不错的答案,但有些人可能需要更多的指导。

本指南旨在填补指导性较强的初学者教程与项目工作之间的空白。这里的主要目标是让你编写代码。次要目标是让你阅读文档。

如果您还没有阅读过Dojo Book,强烈建议您在开始此项目之前先阅读一下。

我们在创造什么?

我们正在构建一个链上国际象棋游戏合约,让您可以开始新游戏并下棋。本指南并不涵盖国际象棋游戏的所有规则。您将按照以下步骤逐步构建:

  1. 生成所有棋子的系统
  2. 让棋子移动的系统
  3. 加入检查移动合法性的函数
  4. 下棋 ♟♙ - 集成测试!

教程的完整代码基于 这个仓库

如果这看起来太难,不用担心!本指南适合初学者。如果你了解Cairo和Dojo的一些基本知识,那就没问题了。我们不会制作包含所有规则的完整游戏。我们将保持简单。

这个教程之后是什么?

我们正在制作另一份教程,以帮助设计前端。这将使我们的棋局更加完整。

完成全部五章后,我们就可以继续学习前端指南了。

0.设置

在开始之前,建议阅读 hello-dojo一章,以获得对 Dojo 游戏的基本了解。

初始化项目

创建一个新的 Dojo 项目文件夹。您可以随心所欲地为项目命名。

mkdir dojo-chess

打开项目文件夹。

cd dojo-chess

然后使用 sozo init 初始化项目。

sozo init

清理模板

该项目带有大量模板代码。请全部清除。确保 components.cairosystems.cairo 文件为空。

lib.cairo中,仅保留:

mod components;
mod systems;

用以下命令编译项目:

sozo build

基本组件

虽然有很多种方法使用 ECS 模型设计象棋游戏,但我们将采用这种方法:

棋盘上的每个方格(例如 A1)都将被视为一个实体。如果某个方格上有棋子,则该方格实体将包含该棋子。

首先,将这个基本组件添加到components.cairo文件中。如果你不熟悉 Dojo 引擎中的组件语法,请回顾这个 章节.。

#[derive(Component)]
struct Square {
    #[key]
    game_id: felt252,
    #[key]
    x: u32,
    #[key]
    y: u32,
    piece: Option<PieceType>,
}

enum PieceType {
    WhitePawn,
    WhiteKnight,
    WhiteBishop,
    WhiteRook,
    WhiteQueen,
    WhiteKing,
    BlackPawn,
    BlackKnight,
    BlackBishop,
    BlackRook,
    BlackQueen,
    BlackKing,
}

基本系统

从下一章节开始,您将在每一章节中依次实现 initiatemove 系统。为了更好地实现模块化,我们会在单独的文件中创建每个系统。

src 创建一个 systems 文件夹。在文件夹内创建 initiate.cairomove.cairo 两个文件。每个文件都应包含一个基本的系统结构。

例如,initiate.cairo 看起来像这样:

#[system]
mod initiate_system {

}

systems.cairo 中,我们将这样使用 initiate_system

mod initiate;

use initiate::initiate_system;

对其他系统执行同样操作。更新 systems.cairo 为:

mod initiate;
mod move;

use initiate::initiate_system;
use move::move_system;

编译项目

现在尝试 sozo build 进行构建。是不是遇到了错误?

error: Trait has no implementation in context:

您可能会遇到一些trait实现上的错误,您可以像这样通过派生来实现:


#[derive(Component, Drop, SerdeLen, Serde)]
struct Square {
    #[key]
    game_id: felt252,
    #[key]
    x: u32,
    #[key]
    y: u32,
    piece: Option<PieceType>,
}

#[derive(Serde, Drop, Copy, PartialEq)]
enum PieceType {
    WhitePawn,
    WhiteKnight,
    WhiteBishop,
    WhiteRook,
    WhiteQueen,
    WhiteKing,
    BlackPawn,
    BlackKnight,
    BlackBishop,
    BlackRook,
    BlackQueen,
    BlackKing,
}

很好!那就让我们来解决这个错误吧。

error: Trait has no implementation in context: dojo::serde::SerdeLen::<core::option::Option::<dojo_chess::components::PieceType>>
 --> Square:80:54
                dojo::SerdeLen::<Option<PieceType>>::len()
                                                     ^*^

必须明确的一点是,<Option<PieceType>> 是我们创建的类型。因此,该类型没有实现 SerdeLen 等基本traits。你需要自己定义实现。

impl PieceOptionSerdeLen of dojo::SerdeLen<Option<PieceType>> {
    #[inline(always)]
    fn len() -> usize {
        2
    }
}

修复上述其他问题,以便成功运行 sozo build 命令。

运行测试

在进入下一章之前,请记住sozo buildsozo test是确保代码正确的重要步骤。

运行 sozo 测试。出现错误了吗?

error: Trait has no implementation in context:
error: Variable not dropped. Trait has no implementation in context:

对于no implementation错误,请实现 PrintTrait 以成功运行 sozo test。对于not dropped错误,添加 Drop trait。其他错误可通过添加派生或实现来逐一解决。

添加更多组件

在继续之前,请添加更多组件,以便在下一章创建系统时使用。

要求

  • 具有白色和黑色值的 Color 枚举
  • Game组件:
    game_id: felt252,
    winner: Option<Color>,
    white: ContractAddress,
    black: ContractAddress
  • GameTurn组件:
    game_id: felt252,
    turn: Color
  • 稍后我们将设置由 GameGameTurn 组件组成的游戏实体。
  • 运行 sozo buildsozo test 并确保所有测试通过。

在进行下一步之前,请试着自己解题并对照下面的答案。

点击查看完整的 `components.cairo` 代码
use debug::PrintTrait;
use starknet::ContractAddress;

#[derive(Component, Drop, SerdeLen, Serde)]
struct Square {
    #[key]
    game_id: felt252,
    #[key]
    x: u32,
    #[key]
    y: u32,
    piece: Option<PieceType>,
}

#[derive(Serde, Drop, Copy, PartialEq)]
enum PieceType {
    WhitePawn,
    WhiteKnight,
    WhiteBishop,
    WhiteRook,
    WhiteQueen,
    WhiteKing,
    BlackPawn,
    BlackKnight,
    BlackBishop,
    BlackRook,
    BlackQueen,
    BlackKing,
}

#[derive(Serde, Drop, Copy, PartialEq)]
enum Color {
    White,
    Black,
}


impl PieceOptionSerdeLen of dojo::SerdeLen<Option<PieceType>> {
    #[inline(always)]
    fn len() -> usize {
        2
    }
}

impl ColorPrintTrait of PrintTrait<Color> {
    #[inline(always)]
    fn print(self: Color) {
        match self {
            Color::White(_) => {
                'White'.print();
            },
            Color::Black(_) => {
                'Black'.print();
            },
        }
    }
}

impl ColorOptionPrintTrait of PrintTrait<Option<Color>> {
    #[inline(always)]
    fn print(self: Option<Color>) {
        match self {
            Option::Some(color) => {
                color.print();
            },
            Option::None(_) => {
                'None'.print();
            }
        }
    }
}


impl BoardPrintTrait of PrintTrait<(u32, u32)> {
    #[inline(always)]
    fn print(self: (u32, u32)) {
        let (x, y): (u32, u32) = self;
        x.print();
        y.print();
    }
}


impl PieceTypeOptionPrintTrait of PrintTrait<Option<PieceType>> {
    #[inline(always)]
    fn print(self: Option<PieceType>) {
        match self {
            Option::Some(piece_type) => {
                piece_type.print();
            },
            Option::None(_) => {
                'None'.print();
            }
        }
    }
}


impl PieceTypePrintTrait of PrintTrait<PieceType> {
    #[inline(always)]
    fn print(self: PieceType) {
        match self {
            PieceType::WhitePawn(_) => {
                'WhitePawn'.print();
            },
            PieceType::WhiteKnight(_) => {
                'WhiteKnight'.print();
            },
            PieceType::WhiteBishop(_) => {
                'WhiteBishop'.print();
            },
            PieceType::WhiteRook(_) => {
                'WhiteRook'.print();
            },
            PieceType::WhiteQueen(_) => {
                'WhiteQueen'.print();
            },
            PieceType::WhiteKing(_) => {
                'WhiteKing'.print();
            },
            PieceType::BlackPawn(_) => {
                'BlackPawn'.print();
            },
            PieceType::BlackKnight(_) => {
                'BlackKnight'.print();
            },
            PieceType::BlackBishop(_) => {
                'BlackBishop'.print();
            },
            PieceType::BlackRook(_) => {
                'BlackRook'.print();
            },
            PieceType::BlackQueen(_) => {
                'BlackQueen'.print();
            },
            PieceType::BlackKing(_) => {
                'BlackKing'.print();
            },
        }
    }
}

impl ColorSerdeLen of dojo::SerdeLen<Color> {
    #[inline(always)]
    fn len() -> usize {
        1
    }
}

#[derive(Component, Drop, SerdeLen, Serde)]
struct Game {
    /// game id, computed as follows pedersen_hash(player1_address, player2_address)
    #[key]
    game_id: felt252,
    winner: Option<Color>,
    white: ContractAddress,
    black: ContractAddress
}


#[derive(Component, Drop, SerdeLen, Serde)]
struct GameTurn {
    #[key]
    game_id: felt252,
    turn: Color,
}

impl OptionPieceColorSerdeLen of dojo::SerdeLen<Option<Color>> {
    #[inline(always)]
    fn len() -> usize {
        1
    }
}

恭喜您!您已经完成了构建链上国际象棋游戏的基本设置 🎉

1. 初始化系统

本章将讨论 initiate_system 的实现,它将生成游戏和包含棋子的方格。

什么是initiate_system

要下棋,您需要开始游戏并生成棋子。initiate_system 会生成游戏实体,然后将每个棋子放置在适当的位置。确保游戏状态与正确的棋子类型相匹配,并且正确颜色的棋子位于棋盘上的指定位置。

image

需求

复制下面的单元测试,并将其粘贴到 systems/initiate.cairo文件的底部

  1. 在系统中编写一个 execute函数接受world context, white address, 以及 black address 作为输入。
  2. 实现由我们上一步骤里创建的 GameGameTurn 组件组成的游戏实体。
  3. Square组件中实现方格实体,从a1到a8并包含正确的PieceType
  4. 运行 sozo test 并通过所有测试。

测试流程

  • 生成测试世界并导入测试中使用的组件和系统。
  • 提供黑白玩家的钱包地址作为输入来执行initiate_system
  • 获取initiate_system过程中创建的游戏实体以及棋子实体。
  • 确保游戏已正确创建。
  • 确认每个Piece 都放在正确的Square上。

单元测试

#[cfg(test)]
mod tests {
    use starknet::ContractAddress;
    use dojo::test_utils::spawn_test_world;
    use dojo_chess::components::{Game, game, GameTurn, game_turn, Square, square, PieceType};

    use dojo_chess::systems::initiate_system;
    use array::ArrayTrait;
    use core::traits::Into;
    use dojo::world::IWorldDispatcherTrait;
    use core::array::SpanTrait;

    #[test]
    #[available_gas(3000000000000000)]
    fn test_initiate() {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();

        // components
        let mut components = array::ArrayTrait::new();
        components.append(game::TEST_CLASS_HASH);
        components.append(game_turn::TEST_CLASS_HASH);
        components.append(square::TEST_CLASS_HASH);

        //systems
        let mut systems = array::ArrayTrait::new();
        systems.append(initiate_system::TEST_CLASS_HASH);
        let world = spawn_test_world(components, systems);

        let mut calldata = array::ArrayTrait::<core::felt252>::new();
        calldata.append(white.into());
        calldata.append(black.into());
        world.execute('initiate_system'.into(), calldata);

        let game_id = pedersen(white.into(), black.into());

        //get game
        let game = get!(world, (game_id), (Game));
        assert(game.white == white, 'white address is incorrect');
        assert(game.black == black, 'black address is incorrect');

        //get a1 square
        let a1 = get!(world, (game_id, 0, 0), (Square));
        match a1.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhiteRook, 'should be White Rook');
            },
            Option::None(_) => assert(false, 'should have piece'),
        };
    }
}

需要帮助?

如果您遇到困难,请随时到 Dojo 社区 提问!

您可以在这里找到第 1 章的 答案

Move系统

本章将讨论move_system的实现,它用于在棋盘上重新定位棋子。

move_system 是什么?

下棋时,玩家必须移动棋盘上的棋子。由于我们用Square实体来表示棋子的位置,因此 move_system 会以 (x,y) 的形式获取当前位置。它还会以 (x,y) 的形式获取下一个位置,并将当前位置方格中的棋子视为要移动到下一个位置的目标。

需求

复制下面的单元测试,并将其粘贴到systems/move.cairo文件的底部。

  1. 在系统中编写一个execute函数,输入如下内容:
 fn execute(
        ctx: Context,
        curr_position: (u32, u32),
        next_position: (u32, u32),
        caller: ContractAddress,
        game_id: felt252
    )
  1. 更新带有 next_position 的方格,使其包含新棋子,并确保带有 curr_position 的方格不再包含棋子。

  2. 运行 sozo test 并确保所有测试通过。

测试流程

  • 跟随上一章中test_initiate的逻辑。
  • 通过move_system把White Knight从(1,0) 移动到 (2,2)
  • 获取更新之后的位置并且验证棋子成功移动到了新位置。

单元测试

#[cfg(test)]
mod tests {
    use starknet::ContractAddress;
    use dojo::test_utils::spawn_test_world;
    use dojo_chess::components::{Game, game, GameTurn, game_turn, Square, square, PieceType};

    use dojo_chess::systems::initiate_system;
    use dojo_chess::systems::move_system;
    use array::ArrayTrait;
    use core::traits::Into;
    use dojo::world::IWorldDispatcherTrait;
    use core::array::SpanTrait;

    #[test]
    #[available_gas(3000000000000000)]
    fn test_move() {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();

        // components
        let mut components = array::ArrayTrait::new();
        components.append(game::TEST_CLASS_HASH);
        components.append(game_turn::TEST_CLASS_HASH);
        components.append(square::TEST_CLASS_HASH);

        //systems
        let mut systems = array::ArrayTrait::new();
        systems.append(initiate_system::TEST_CLASS_HASH);
        systems.append(move_system::TEST_CLASS_HASH);
        let world = spawn_test_world(components, systems);

        // initiate
        let mut calldata = array::ArrayTrait::<core::felt252>::new();
        calldata.append(white.into());
        calldata.append(black.into());
        world.execute('initiate_system'.into(), calldata);

        let game_id = pedersen(white.into(), black.into());

         //White Knight is in (1,0)
        let b1 = get!(world, (game_id, 1, 0), (Square));
        match b1.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhiteKnight, 'should be White Knight in (1,0)');
            },
            Option::None(_) => assert(false, 'should have piece in (1,0)'),
        };

        // Move White Knight (1,0) -> (2,2)
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(1);
        move_calldata.append(0);
        move_calldata.append(2);
        move_calldata.append(2);
        move_calldata.append(white.into());
        move_calldata.append(game_id);
        world.execute('move_system'.into(), move_calldata);

        //White Knight is in (2,2)
        let c3 = get!(world, (game_id, 2, 2), (Square));
        match c3.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhiteKnight, 'should be White Knight in (2,2)');
            },
            Option::None(_) => assert(false, 'should have piece in (2,2)'),
        };
    }
}

需要帮助?

如果您遇到困难,请随时到 Dojo 社区 提问!

3.检查合法移动

在本章中,我们将编写用于检查的函数:

  • 如果下一步在棋盘之外
  • 是否有棋子可以被吃掉。
  • 棋子类型是否允许下一步棋。
  • 用户是否允许进行操作(基于棋子的颜色)。
  • ...您还可以添加其他自定义校验功能。

编写校验函数

我们需要在 move_system 中添加一些检查函数。这些函数将有助于确保下一步移动是被允许的。

  1. 查看下一个位置该类型的棋子是否可以移动。
  fn is_right_piece_move(
        maybe_piece: Option<PieceType>, curr_position: (u32, u32), next_position: (u32, u32)
    ) -> bool {}
  1. 看看下一个位置是否还在棋盘上。
  fn is_out_of_board(next_position: (u32, u32)) -> bool{}
  1. 看看尝试这个动作的人是否在正确的时间且以正确的阵营移动棋子。
 fn is_correct_turn(maybe_piece: PieceType, caller: ContractAddress, game_id: felt252) -> bool{}
  1. 您还可以添加其他检查功能,以额外确保移动是允许的。

编写了这些校验函数之后,就可以在主 move_system 函数中使用它们。您可以决定如何设置它们以及使用哪些。我们将举例说明:

    fn execute(
        ctx: Context,
        curr_position: (u32, u32),
        next_position: (u32, u32),
        caller: ContractAddress,
        game_id: felt252
    ) {
        //… upper code is the same
        //Check if next_position is out of board or not
        assert(is_out_of_board(next_position), ‘Should be inside board’);

        //Check if this is the right piece type move

        assert(
            is_right_piece_move(current_square.piece, curr_position, next_position),
            ‘Should be right piece move’
        );

        let target_piece = current_square.piece;

        // make current_square piece none and move piece to next_square
        current_square.piece = Option::None(());
        let mut next_square = get!(ctx.world, (game_id, next_x, next_y), (Square));

        //Check the piece already in next_suqare
        let maybe_next_square_piece = next_square.piece;
        match maybe_next_square_piece {
            Option::Some(maybe_piece) => {
                if is_piece_is_mine(maybe_piece) {
                    panic(array![‘Already same color piece exist’])
                } else {
                    //Occupy the piece
                    next_square.piece = target_piece;
                }
            },
            //if not exist, then just move the original piece
            Option::None(_) => {
                next_square.piece = target_piece;
            },
        };
        // … below code is the same

    }

测试每个函数

由于我们有不同的校验函数,因此需要对每个函数进行测试。为了方便起见,我们可以在许多测试中使用相同的部分。

首先,创建一个名为 init_world_test 的辅助函数。这会返回一个 IWorldDispatcher,我们可以在移动系统测试中多次使用它。

    #[test]
    #[available_gas(3000000000000000)]
    fn init_world_test() -> IWorldDispatcher {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();

        // components
        let mut components = array::ArrayTrait::new();
        components.append(game::TEST_CLASS_HASH);
        components.append(game_turn::TEST_CLASS_HASH);
        components.append(square::TEST_CLASS_HASH);

        //systems
        let mut systems = array::ArrayTrait::new();
        systems.append(initiate_system::TEST_CLASS_HASH);
        systems.append(move_system::TEST_CLASS_HASH);
        let world = spawn_test_world(components, systems);

        let mut calldata = array::ArrayTrait::<core::felt252>::new();
        calldata.append(white.into());
        calldata.append(black.into());
        world.execute(‘initiate_system’.into(), calldata);
        world
    }

这样,我们的主 test_move 函数就会变得更简单。

    #[test]
    #[available_gas(3000000000000000)]
    fn test_move() {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();
        let world = init_world_test();
        let game_id = pedersen(white.into(), black.into());
        // other codes are same
    }

现在我们可以进行测试,如果尝试不允许的动作,测试就会显示错误。让我们创建一个 test_piecetype_illegal 函数。这将检查你在移动系统中实现的 is_right_piece_move 函数是否正常工作。

    #[test]
    #[should_panic]
    fn test_piecetype_ilegal() {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();
        let world = init_world_test();
        let game_id = pedersen(white.into(), black.into());

        let b1 = get!(world, (game_id, 1, 0), (Square));
        match b1.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhiteKnight, ‘should be White Knight’);
            },
            Option::None(_) => assert(false, ‘should have piece’),
        };

        // Knight cannot move to that square
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(1);
        move_calldata.append(0);
        move_calldata.append(2);
        move_calldata.append(3);
        move_calldata.append(white.into());
        move_calldata.append(game_id);
        world.execute(‘move_system’.into(), move_calldata);
    }

最后进行测试。这些测试应能发现错误动作并反馈错误信息。

需要帮助?

如果您遇到困难,请随时到 Dojo 社区 提问!

您可以在这里找到第 3 章的 答案

4.测试合约

在本章中,我们将利用所学知识运行一个完整的国际象棋游戏场景。

下面是我们要做的测试:

  1. white_pawn_1 出生在 (0,1)
  2. 移动 white_pawn_1 到 (0,3)
  3. 移动 black_pawn_2 到 (1,6)
  4. 移动 white_pawn_1 到 (0,4)
  5. 移动 black_pawn_2 到 (1,5)
  6. 移动 white_pawn_1 到 (1,5)
  7. 吃掉 black_pawn_2

要放置棋子,请使用我们的initiate_system。要移动棋子,请使用move_system。使用 move_system 时,请记住检查棋子是否能被吃掉。

在我们开始编写代码之前,请这样设置集成测试:

  • 复制下面的测试并将其添加到你的 src/tests.cairo 文件中。
  • 在你的 src 中创建一个 test.cairo ,更新 lib.cairo 添加 mod tests;

完整代码

#[cfg(test)]
mod tests {
    use starknet::ContractAddress;
    use dojo::test_utils::spawn_test_world;
    use dojo_chess::components::{Game, game, GameTurn, game_turn, Square, square, PieceType};

    use dojo_chess::systems::initiate_system;
    use dojo_chess::systems::move_system;
    use array::ArrayTrait;
    use core::traits::Into;
    use dojo::world::IWorldDispatcherTrait;
    use core::array::SpanTrait;


    #[test]
    #[available_gas(3000000000000000)]
    fn integration() {
        let white = starknet::contract_address_const::<0x01>();
        let black = starknet::contract_address_const::<0x02>();

        // components
        let mut components = array::ArrayTrait::new();
        components.append(game::TEST_CLASS_HASH);
        components.append(game_turn::TEST_CLASS_HASH);
        components.append(square::TEST_CLASS_HASH);

        //systems
        let mut systems = array::ArrayTrait::new();
        systems.append(initiate_system::TEST_CLASS_HASH);
        systems.append(move_system::TEST_CLASS_HASH);
        let world = spawn_test_world(components, systems);

        // initiate
        let mut calldata = array::ArrayTrait::<core::felt252>::new();
        calldata.append(white.into());
        calldata.append(black.into());
        world.execute('initiate_system'.into(), calldata);

        let game_id = pedersen(white.into(), black.into());

        //White pawn is now in (0,1)
        let a2 = get!(world, (game_id, 0, 1), (Square));
        match a2.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhitePawn, "should be White Pawn in (0,1)");
            },
            Option::None(_) => assert(false, 'should have piece in (0,1)),
        };

        //Black pawn is now in (1,6)
        let b7 = get!(world, (game_id, 1, 6), (Square));
        match b7.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::BlackPawn, "should be Black Pawn in (1,6)");
            },
            Option::None(_) => assert(false, 'should have piece in (1,6)),
        };

        //Move White Pawn to (0,3)
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(0);
        move_calldata.append(1);
        move_calldata.append(0);
        move_calldata.append(3);
        move_calldata.append(white.into());
        move_calldata.append(game_id);
        world.execute('move_system'.into(), move_calldata);

        //White pawn is now in (0,3)
        let a4 = get!(world, (game_id, 0, 3), (Square));
        match a4.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhitePawn, "should be White Pawn in (0,3)");
            },
            Option::None(_) => assert(false, 'should have piece in (0,3)),
        };

        //Move black Pawn to (1,4)
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(1);
        move_calldata.append(6);
        move_calldata.append(1);
        move_calldata.append(4);
        move_calldata.append(black.into());
        move_calldata.append(game_id);
        world.execute('move_system'.into(), move_calldata);

        //Black pawn is now in (1,4)
        let b5 = get!(world, (game_id, 1, 4), (Square));
        match b5.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::BlackPawn, "should be Black Pawn  in (1,4)");
            },
            Option::None(_) => assert(false, ‘should have piece  in (1,4)),
        };

        // Move White Pawn to (1,4)
        // Capture black pawn
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(0);
        move_calldata.append(3);
        move_calldata.append(1);
        move_calldata.append(4);
        move_calldata.append(white.into());
        move_calldata.append(game_id);
        world.execute(‘move_system’.into(), move_calldata);

        let b5 = get!(world, (game_id, 1, 4), (Square));
        match b5.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhitePawn, “should be WhitePawn  in (1,4)”);
            },
            Option::None(_) => assert(false, ‘should have piece in (1,4)),
        };
    }
}

深入代码

首先,我们将设置球员及其阵营。

   let white = starknet::contract_address_const::<0x01>();
   let black = starknet::contract_address_const::<0x02>();

我们应以数组形式列出 Components and Systems ,每个数组都以 CLASS_HASH 作为元素。

// components
let mut components = array::ArrayTrait::new();
components.append(game::TEST_CLASS_HASH);
components.append(game_turn::TEST_CLASS_HASH);
components.append(square::TEST_CLASS_HASH);

//systems
let mut systems = array::ArrayTrait::new();
systems.append(initiate_system::TEST_CLASS_HASH);
systems.append(move_system::TEST_CLASS_HASH);

接下来,我们将创建游戏世界。

     let world = spawn_test_world(components, systems);

我们使用 initiate_system 将方块棋子放到棋盘上。每个方格放置一个棋子。系统的执行函数需要一些输入,我们将其作为 calldata 提供给它。

        // initiate
        let mut calldata = array::ArrayTrait::<core::felt252>::new();
        calldata.append(white.into());
        calldata.append(black.into());
        world.execute(‘initiate_system’.into(), calldata);

让我们检查白方棋子是否位于 (0,1)。请记住,要获取存在于该方格上的棋子,需要使用 Square 组件的键,即 game_idxy。对黑方进行同样的检查。

        //White pawn is now in (0,1)
        let a2 = get!(world, (game_id, 0, 1), (Square));
        match a2.piece {
            Option::Some(piece) => {
                assert(piece == PieceType::WhitePawn, “should be White Pawn in (0,1)”);
            },
            Option::None(_) => assert(false, ‘should have piece in (0,1)),
        };

设置好棋盘后,使用 move_system 下棋。提供当前位置、下一个位置、玩家地址和游戏 ID。

 //Move White Pawn to (0,3)
        let mut move_calldata = array::ArrayTrait::<core::felt252>::new();
        move_calldata.append(0);
        move_calldata.append(1);
        move_calldata.append(0);
        move_calldata.append(3);
        move_calldata.append(white.into());
        move_calldata.append(game_id);
        world.execute(‘move_system’.into(), move_calldata);

不断移动棋子,检查它们的位置是否正确。

恭喜!

您已经使用 Dojo 引擎制作了国际象棋游戏的基本合约!本教程只是一个开始。有很多方法可以让游戏变得更好,例如优化部分、添加检查或考虑特殊情况。如果您想在这个国际象棋游戏中做得更多,请尝试这些挑战:

  • 创建一个使用 lazy init 的initiate_system。如果你对 lazy init 不确定,阅读一下。这有助于提高游戏操作的效率。
  • 添加将死功能。我们的游戏现在还没有结束,因此请决定何时应该结束!
  • 加入特殊的棋步,如投子、吃子或提卒。
  • 制定自己的国际象棋规则!您甚至可以创建自己版本的 不朽游戏

最后,与 Dojo 社区 中的其他人分享您的项目!

为《Dojo Book》做贡献

随着 Dojo 引擎的进步和发展,Dojo 书籍需要跟上这些进展的步伐。对本书进行更新和完善,可以确保它对于那些有兴趣了解和使用最新 Dojo 引擎特性和功能的人来说,本书仍然是一个相关和有价值的资源。欢迎大家提供帮助!

本书的目的

Dojo 一书旨在为不同经验水平的用户提供全面的资源。它既可作为 Dojo 及其辅助软件包新手的入门指南,也可作为经验丰富的用户加深对引擎特性和功能的理解的参考资料。

本书分为几个主要章节:

  • 框架理论
  • 入门
  • 构建世界

代码标准

本书遵循 Rust Code of Conduct

贡献方式

Issues

如果您认为某些内容缺失或过时,请随时在git上提出issue。如果您发现有多个内容缺失,请为每个内容单独开立一个issue。

你提出issue在之后将被贴上相关的标签,以便其他贡献者更容易找到他们感兴趣的任务。

Issue应包含哪些地方缺失了,或哪些地方可以改进,只要你认为有必要,都可以详细说明。

Pull requests

欢迎随时通过提交PR请求对本书进行修改--从改写一个句子、修正一个错别字到添加新的段落或章节,我们都欢迎。

当您的PR开放时,其他贡献者会查看并可能要求你进行一些修正。请不要气馁!

写作风格

本节记录了一些贯穿全书的写作标准。

章节从二级标题开始(译者注:请参考markdown语法)

我们使用:

## Some Page

我们不使用(译者注:此为一级大标题语法,只应该作为页面标题存在而不应该作为页面内的内容标题而存在):

# Some Page